diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index ad63ed16..aa8626a9 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -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());
+ }
+};
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index 17df1221..e16a419e 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -39,6 +39,7 @@ export default class Api {
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
+ GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
restartGlobalFiltering() {
const { path, method } = this.GLOBAL_RESTART;
@@ -139,6 +140,11 @@ export default class Api {
return this.makeRequest(path, method);
}
+ getGlobalClients() {
+ const { path, method } = this.GLOBAL_CLIENTS;
+ return this.makeRequest(path, method);
+ }
+
// Filtering
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js
index a2676dd0..fdc4cc2a 100644
--- a/client/src/components/App/index.js
+++ b/client/src/components/App/index.js
@@ -26,6 +26,7 @@ class App extends Component {
componentDidMount() {
this.props.getDnsStatus();
this.props.getVersion();
+ this.props.getClients();
}
componentDidUpdate(prevProps) {
@@ -108,6 +109,7 @@ App.propTypes = {
getVersion: PropTypes.func,
changeLanguage: PropTypes.func,
encryption: PropTypes.object,
+ getClients: PropTypes.func,
};
export default withNamespaces()(App);
diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js
index bd9bb4c4..d7081e38 100644
--- a/client/src/components/Dashboard/Clients.js
+++ b/client/src/components/Dashboard/Clients.js
@@ -7,7 +7,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
-import { getPercent } from '../../helpers/helpers';
+import { getPercent, getClientName } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class Clients extends Component {
@@ -23,7 +23,24 @@ class Clients extends Component {
columns = [{
Header: 'IP',
accessor: 'ip',
- Cell: ({ value }) => (
{value}
),
+ Cell: ({ value }) => {
+ const clientName = getClientName(this.props.clients, value);
+ let client;
+
+ if (clientName) {
+ client = {clientName} ({value});
+ } else {
+ client = value;
+ }
+
+ return (
+
+
+ {client}
+
+
+ );
+ },
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
}, {
Header: requests_count,
@@ -61,6 +78,7 @@ Clients.propTypes = {
topClients: PropTypes.object.isRequired,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
+ clients: PropTypes.array.isRequired,
t: PropTypes.func,
};
diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js
index dbd9901c..84215d56 100644
--- a/client/src/components/Dashboard/index.js
+++ b/client/src/components/Dashboard/index.js
@@ -46,6 +46,7 @@ class Dashboard extends Component {
dashboard.processing ||
dashboard.processingStats ||
dashboard.processingStatsHistory ||
+ dashboard.processingClients ||
dashboard.processingTopStats;
const refreshFullButton = ;
@@ -94,6 +95,7 @@ class Dashboard extends Component {
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topClients={dashboard.topStats.top_clients}
+ clients={dashboard.clients}
/>
diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js
index 2499c1f1..7791025a 100644
--- a/client/src/components/Logs/index.js
+++ b/client/src/components/Logs/index.js
@@ -6,7 +6,7 @@ import escapeRegExp from 'lodash/escapeRegExp';
import endsWith from 'lodash/endsWith';
import { Trans, withNamespaces } from 'react-i18next';
-import { formatTime } from '../../helpers/helpers';
+import { formatTime, getClientName } from '../../helpers/helpers';
import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
@@ -86,7 +86,7 @@ class Logs extends Component {
}
renderLogs(logs) {
- const { t } = this.props;
+ const { t, dashboard } = this.props;
const columns = [{
Header: t('time_table_header'),
accessor: 'time',
@@ -196,11 +196,19 @@ class Logs extends Component {
Cell: (row) => {
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
+ const clientName = getClientName(dashboard.clients, row.value);
+ let client;
+
+ if (clientName) {
+ client =
{clientName} ({row.value});
+ } else {
+ client = row.value;
+ }
return (
- {row.value}
+ {client}
{this.renderBlockingButton(isFiltered, row.original.domain)}
@@ -315,9 +323,18 @@ class Logs extends Component {
- {queryLogEnabled && queryLogs.getLogsProcessing && }
- {queryLogEnabled && !queryLogs.getLogsProcessing &&
- this.renderLogs(queryLogs.logs)}
+ {
+ queryLogEnabled
+ && queryLogs.getLogsProcessing
+ && dashboard.processingClients
+ &&
+ }
+ {
+ queryLogEnabled
+ && !queryLogs.getLogsProcessing
+ && !dashboard.processingClients
+ && this.renderLogs(queryLogs.logs)
+ }
);
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index fbfa3b23..d3128f5d 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -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 getClientName = (clients, ip) => {
+ const client = clients.find(item => ip === item.ip);
+ return (client && client.name) || '';
+};
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index a6daef82..404679eb 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -167,6 +167,17 @@ const dashboard = handleActions({
const newState = { ...state, language: payload };
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,
isCoreRunning: false,
@@ -175,6 +186,7 @@ const dashboard = handleActions({
logStatusProcessing: false,
processingVersion: true,
processingFiltering: true,
+ processingClients: true,
upstreamDns: '',
bootstrapDns: '',
allServers: false,
@@ -184,6 +196,7 @@ const dashboard = handleActions({
dnsPort: 53,
dnsAddresses: [],
dnsVersion: '',
+ clients: [],
});
const queryLogs = handleActions({
diff --git a/clients.go b/clients.go
new file mode 100644
index 00000000..87a5c3fe
--- /dev/null
+++ b/clients.go
@@ -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))))
+}
diff --git a/control.go b/control.go
index 0169f4d5..9a0e7fa0 100644
--- a/control.go
+++ b/control.go
@@ -1136,6 +1136,7 @@ func registerControlHandlers() {
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
RegisterTLSHandlers()
+ RegisterClientsHandlers()
http.HandleFunc("/dns-query", postInstall(handleDOH))
}
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index d8b1072c..ae9f0505 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -39,6 +39,9 @@ tags:
-
name: dhcp
description: 'Built-in DHCP server controls'
+ -
+ name: clients
+ description: 'Clients list operations'
-
name: install
description: 'First-time install configuration handlers'
@@ -668,6 +671,22 @@ paths:
application/json:
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
# --------------------------------------------------
@@ -1296,7 +1315,7 @@ definitions:
properties:
ip:
type: "string"
- example: "127.0.01"
+ example: "127.0.0.1"
port:
type: "integer"
format: "int32"
@@ -1317,6 +1336,23 @@ definitions:
description: "Network interfaces dictionary (key is the interface name)"
additionalProperties:
$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:
type: "object"
description: "AdGuard Home initial configuration (for the first-install wizard)"