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)"