Merge pull request #179 in DNS/adguard-dns from feature/637 to master
* commit 'bc0b0af06b71197d75c8a7f8c3a401d4c8a49d61': + client: added name for client - openapi: fix typo + control: /clients: get the list of clients' IP addresses and names from /etc/hosts
This commit is contained in:
commit
800cb177f3
|
@ -665,3 +665,18 @@ export const toggleDhcp = config => async (dispatch) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
|
||||||
|
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
|
||||||
|
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||||
|
|
||||||
|
export const getClients = () => async (dispatch) => {
|
||||||
|
dispatch(getClientsRequest());
|
||||||
|
try {
|
||||||
|
const clients = await apiClient.getGlobalClients();
|
||||||
|
dispatch(getClientsSuccess(clients));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(getClientsFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -39,6 +39,7 @@ export default class Api {
|
||||||
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
|
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
|
||||||
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
|
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
|
||||||
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
||||||
|
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
|
||||||
|
|
||||||
restartGlobalFiltering() {
|
restartGlobalFiltering() {
|
||||||
const { path, method } = this.GLOBAL_RESTART;
|
const { path, method } = this.GLOBAL_RESTART;
|
||||||
|
@ -139,6 +140,11 @@ export default class Api {
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGlobalClients() {
|
||||||
|
const { path, method } = this.GLOBAL_CLIENTS;
|
||||||
|
return this.makeRequest(path, method);
|
||||||
|
}
|
||||||
|
|
||||||
// Filtering
|
// Filtering
|
||||||
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
||||||
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
|
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
|
||||||
|
|
|
@ -26,6 +26,7 @@ class App extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.getDnsStatus();
|
this.props.getDnsStatus();
|
||||||
this.props.getVersion();
|
this.props.getVersion();
|
||||||
|
this.props.getClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -108,6 +109,7 @@ App.propTypes = {
|
||||||
getVersion: PropTypes.func,
|
getVersion: PropTypes.func,
|
||||||
changeLanguage: PropTypes.func,
|
changeLanguage: PropTypes.func,
|
||||||
encryption: PropTypes.object,
|
encryption: PropTypes.object,
|
||||||
|
getClients: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNamespaces()(App);
|
export default withNamespaces()(App);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
import Cell from '../ui/Cell';
|
import Cell from '../ui/Cell';
|
||||||
|
|
||||||
import { getPercent } from '../../helpers/helpers';
|
import { getPercent, getClientName } from '../../helpers/helpers';
|
||||||
import { STATUS_COLORS } from '../../helpers/constants';
|
import { STATUS_COLORS } from '../../helpers/constants';
|
||||||
|
|
||||||
class Clients extends Component {
|
class Clients extends Component {
|
||||||
|
@ -23,7 +23,24 @@ class Clients extends Component {
|
||||||
columns = [{
|
columns = [{
|
||||||
Header: 'IP',
|
Header: 'IP',
|
||||||
accessor: 'ip',
|
accessor: 'ip',
|
||||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
|
Cell: ({ value }) => {
|
||||||
|
const clientName = getClientName(this.props.clients, value);
|
||||||
|
let client;
|
||||||
|
|
||||||
|
if (clientName) {
|
||||||
|
client = <span>{clientName} <small>({value})</small></span>;
|
||||||
|
} else {
|
||||||
|
client = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text" title={value}>
|
||||||
|
{client}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
|
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
|
||||||
}, {
|
}, {
|
||||||
Header: <Trans>requests_count</Trans>,
|
Header: <Trans>requests_count</Trans>,
|
||||||
|
@ -61,6 +78,7 @@ Clients.propTypes = {
|
||||||
topClients: PropTypes.object.isRequired,
|
topClients: PropTypes.object.isRequired,
|
||||||
dnsQueries: PropTypes.number.isRequired,
|
dnsQueries: PropTypes.number.isRequired,
|
||||||
refreshButton: PropTypes.node.isRequired,
|
refreshButton: PropTypes.node.isRequired,
|
||||||
|
clients: PropTypes.array.isRequired,
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ class Dashboard extends Component {
|
||||||
dashboard.processing ||
|
dashboard.processing ||
|
||||||
dashboard.processingStats ||
|
dashboard.processingStats ||
|
||||||
dashboard.processingStatsHistory ||
|
dashboard.processingStatsHistory ||
|
||||||
|
dashboard.processingClients ||
|
||||||
dashboard.processingTopStats;
|
dashboard.processingTopStats;
|
||||||
|
|
||||||
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}><Trans>refresh_statics</Trans></button>;
|
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}><Trans>refresh_statics</Trans></button>;
|
||||||
|
@ -94,6 +95,7 @@ class Dashboard extends Component {
|
||||||
dnsQueries={dashboard.stats.dns_queries}
|
dnsQueries={dashboard.stats.dns_queries}
|
||||||
refreshButton={refreshButton}
|
refreshButton={refreshButton}
|
||||||
topClients={dashboard.topStats.top_clients}
|
topClients={dashboard.topStats.top_clients}
|
||||||
|
clients={dashboard.clients}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
|
|
|
@ -6,7 +6,7 @@ import escapeRegExp from 'lodash/escapeRegExp';
|
||||||
import endsWith from 'lodash/endsWith';
|
import endsWith from 'lodash/endsWith';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
import { formatTime } from '../../helpers/helpers';
|
import { formatTime, getClientName } from '../../helpers/helpers';
|
||||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
|
@ -86,7 +86,7 @@ class Logs extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLogs(logs) {
|
renderLogs(logs) {
|
||||||
const { t } = this.props;
|
const { t, dashboard } = this.props;
|
||||||
const columns = [{
|
const columns = [{
|
||||||
Header: t('time_table_header'),
|
Header: t('time_table_header'),
|
||||||
accessor: 'time',
|
accessor: 'time',
|
||||||
|
@ -196,11 +196,19 @@ class Logs extends Component {
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const { reason } = row.original;
|
const { reason } = row.original;
|
||||||
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
||||||
|
const clientName = getClientName(dashboard.clients, row.value);
|
||||||
|
let client;
|
||||||
|
|
||||||
|
if (clientName) {
|
||||||
|
client = <span>{clientName} <small>({row.value})</small></span>;
|
||||||
|
} else {
|
||||||
|
client = row.value;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="logs__row">
|
<div className="logs__row">
|
||||||
{row.value}
|
{client}
|
||||||
</div>
|
</div>
|
||||||
{this.renderBlockingButton(isFiltered, row.original.domain)}
|
{this.renderBlockingButton(isFiltered, row.original.domain)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -315,9 +323,18 @@ class Logs extends Component {
|
||||||
</div>
|
</div>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<Card>
|
<Card>
|
||||||
{queryLogEnabled && queryLogs.getLogsProcessing && <Loading />}
|
{
|
||||||
{queryLogEnabled && !queryLogs.getLogsProcessing &&
|
queryLogEnabled
|
||||||
this.renderLogs(queryLogs.logs)}
|
&& queryLogs.getLogsProcessing
|
||||||
|
&& dashboard.processingClients
|
||||||
|
&& <Loading />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
queryLogEnabled
|
||||||
|
&& !queryLogs.getLogsProcessing
|
||||||
|
&& !dashboard.processingClients
|
||||||
|
&& this.renderLogs(queryLogs.logs)
|
||||||
|
}
|
||||||
</Card>
|
</Card>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -203,3 +203,8 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
|
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
|
||||||
|
|
||||||
|
export const getClientName = (clients, ip) => {
|
||||||
|
const client = clients.find(item => ip === item.ip);
|
||||||
|
return (client && client.name) || '';
|
||||||
|
};
|
||||||
|
|
|
@ -167,6 +167,17 @@ const dashboard = handleActions({
|
||||||
const newState = { ...state, language: payload };
|
const newState = { ...state, language: payload };
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[actions.getClientsRequest]: state => ({ ...state, processingClients: true }),
|
||||||
|
[actions.getClientsFailure]: state => ({ ...state, processingClients: false }),
|
||||||
|
[actions.getClientsSuccess]: (state, { payload }) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
clients: payload,
|
||||||
|
processingClients: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
processing: true,
|
processing: true,
|
||||||
isCoreRunning: false,
|
isCoreRunning: false,
|
||||||
|
@ -175,6 +186,7 @@ const dashboard = handleActions({
|
||||||
logStatusProcessing: false,
|
logStatusProcessing: false,
|
||||||
processingVersion: true,
|
processingVersion: true,
|
||||||
processingFiltering: true,
|
processingFiltering: true,
|
||||||
|
processingClients: true,
|
||||||
upstreamDns: '',
|
upstreamDns: '',
|
||||||
bootstrapDns: '',
|
bootstrapDns: '',
|
||||||
allServers: false,
|
allServers: false,
|
||||||
|
@ -184,6 +196,7 @@ const dashboard = handleActions({
|
||||||
dnsPort: 53,
|
dnsPort: 53,
|
||||||
dnsAddresses: [],
|
dnsAddresses: [],
|
||||||
dnsVersion: '',
|
dnsVersion: '',
|
||||||
|
clients: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryLogs = handleActions({
|
const queryLogs = handleActions({
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client information
|
||||||
|
type Client struct {
|
||||||
|
IP string
|
||||||
|
Name string
|
||||||
|
//Source source // Hosts file / User settings / DHCP
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientJSON struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var clients []Client
|
||||||
|
var clientsFilled bool
|
||||||
|
|
||||||
|
// Parse system 'hosts' file and fill clients array
|
||||||
|
func fillClientInfo() {
|
||||||
|
hostsFn := "/etc/hosts"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
|
||||||
|
}
|
||||||
|
|
||||||
|
d, e := ioutil.ReadFile(hostsFn)
|
||||||
|
if e != nil {
|
||||||
|
log.Info("Can't read file %s: %v", hostsFn, e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(d), "\n")
|
||||||
|
for _, ln := range lines {
|
||||||
|
ln = strings.TrimSpace(ln)
|
||||||
|
if len(ln) == 0 || ln[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(ln)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var c Client
|
||||||
|
c.IP = fields[0]
|
||||||
|
c.Name = fields[1]
|
||||||
|
clients = append(clients, c)
|
||||||
|
log.Tracef("%s -> %s", c.IP, c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Added %d client aliases from %s", len(clients), hostsFn)
|
||||||
|
clientsFilled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// respond with information about configured clients
|
||||||
|
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
if !clientsFilled {
|
||||||
|
fillClientInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
data := []clientJSON{}
|
||||||
|
for _, c := range clients {
|
||||||
|
cj := clientJSON{
|
||||||
|
IP: c.IP,
|
||||||
|
Name: c.Name,
|
||||||
|
}
|
||||||
|
data = append(data, cj)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
e := json.NewEncoder(w).Encode(data)
|
||||||
|
if e != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterClientsHandlers registers HTTP handlers
|
||||||
|
func RegisterClientsHandlers() {
|
||||||
|
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
|
||||||
|
}
|
|
@ -1136,6 +1136,7 @@ func registerControlHandlers() {
|
||||||
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
|
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
|
||||||
|
|
||||||
RegisterTLSHandlers()
|
RegisterTLSHandlers()
|
||||||
|
RegisterClientsHandlers()
|
||||||
|
|
||||||
http.HandleFunc("/dns-query", postInstall(handleDOH))
|
http.HandleFunc("/dns-query", postInstall(handleDOH))
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@ tags:
|
||||||
-
|
-
|
||||||
name: dhcp
|
name: dhcp
|
||||||
description: 'Built-in DHCP server controls'
|
description: 'Built-in DHCP server controls'
|
||||||
|
-
|
||||||
|
name: clients
|
||||||
|
description: 'Clients list operations'
|
||||||
-
|
-
|
||||||
name: install
|
name: install
|
||||||
description: 'First-time install configuration handlers'
|
description: 'First-time install configuration handlers'
|
||||||
|
@ -668,6 +671,22 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Clients list methods
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
/clients:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- clients
|
||||||
|
operationId: clientsStatus
|
||||||
|
summary: 'Get information about configured clients'
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Clients"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# I18N methods
|
# I18N methods
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
@ -1296,7 +1315,7 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
ip:
|
ip:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "127.0.01"
|
example: "127.0.0.1"
|
||||||
port:
|
port:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
format: "int32"
|
format: "int32"
|
||||||
|
@ -1317,6 +1336,23 @@ definitions:
|
||||||
description: "Network interfaces dictionary (key is the interface name)"
|
description: "Network interfaces dictionary (key is the interface name)"
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
$ref: "#/definitions/NetInterface"
|
$ref: "#/definitions/NetInterface"
|
||||||
|
Client:
|
||||||
|
type: "object"
|
||||||
|
description: "Client information"
|
||||||
|
properties:
|
||||||
|
ip:
|
||||||
|
type: "string"
|
||||||
|
description: "IP address"
|
||||||
|
example: "127.0.0.1"
|
||||||
|
name:
|
||||||
|
type: "string"
|
||||||
|
description: "Name"
|
||||||
|
example: "localhost"
|
||||||
|
Clients:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/Client"
|
||||||
|
description: "Clients array"
|
||||||
InitialConfiguration:
|
InitialConfiguration:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "AdGuard Home initial configuration (for the first-install wizard)"
|
description: "AdGuard Home initial configuration (for the first-install wizard)"
|
||||||
|
|
Loading…
Reference in New Issue