Merge: + Per-client settings
Close #727 * commit 'a83bc5eeeb4107f2157443b7b40636036fe2a7cc': * client: add source column * client: remove redundant table formatting for runtime clients table * client: show MAC address as default + client: add runtime clients table * client: add icons for table buttons * client: remove unused api method * client: confirm before deleting * client: remove table column min-width * client: fix no data text * client: fix sort helper + client: handle per-client settings - openapi.yaml: fix HTTP methods + openapi.yaml: add /clients handlers + dnsfilter: use callback function for applying per-client settings + dhcp: FindIPbyMAC() + dns: use per-client filtering settings + clients: config: save/restore clients info array + clients API + doc: clients
This commit is contained in:
commit
c038e4cf14
138
AGHTechDoc.md
138
AGHTechDoc.md
|
@ -12,6 +12,12 @@ Contents:
|
||||||
* Updating
|
* Updating
|
||||||
* Get version command
|
* Get version command
|
||||||
* Update command
|
* Update command
|
||||||
|
* Device Names and Per-client Settings
|
||||||
|
* Per-client settings
|
||||||
|
* Get list of clients
|
||||||
|
* Add client
|
||||||
|
* Update client
|
||||||
|
* Delete client
|
||||||
* Enable DHCP server
|
* Enable DHCP server
|
||||||
* "Check DHCP" command
|
* "Check DHCP" command
|
||||||
* "Enable DHCP" command
|
* "Enable DHCP" command
|
||||||
|
@ -420,3 +426,135 @@ Step 2.
|
||||||
If we would set a different IP address, we'd need to replace the IP address for the current network configuration. But currently this step isn't necessary.
|
If we would set a different IP address, we'd need to replace the IP address for the current network configuration. But currently this step isn't necessary.
|
||||||
|
|
||||||
ip addr replace dev eth0 192.168.0.1/24
|
ip addr replace dev eth0 192.168.0.1/24
|
||||||
|
|
||||||
|
|
||||||
|
## Device Names and Per-client Settings
|
||||||
|
|
||||||
|
When a client requests information from DNS server, he's identified by IP address.
|
||||||
|
Administrator can set a name for a client with a known IP and also override global settings for this client. The name is used to improve readability of DNS logs: client's name is shown in UI next to its IP address. The names are loaded from 3 sources:
|
||||||
|
* automatically from "/etc/hosts" file. It's a list of `IP<->Name` entries which is loaded once on AGH startup from "/etc/hosts" file.
|
||||||
|
* automatically using rDNS. It's a list of `IP<->Name` entries which is added in runtime using rDNS mechanism when a client first makes a DNS request.
|
||||||
|
* manually configured via UI. It's a list of client's names and their settings which is loaded from configuration file and stored on disk.
|
||||||
|
|
||||||
|
### Per-client settings
|
||||||
|
|
||||||
|
UI provides means to manage the list of known clients (List/Add/Update/Delete) and their settings. These settings are stored in configuration file as an array of objects.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
* `name`, `ip` and `mac` values are unique.
|
||||||
|
|
||||||
|
* `ip` & `mac` values can't be set both at the same time.
|
||||||
|
|
||||||
|
* If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table.
|
||||||
|
|
||||||
|
* If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings.
|
||||||
|
|
||||||
|
* If `use_global_settings` is false, then the client-specific settings are used to override (disable) global settings. For example, if global setting `parental_enabled` is true, then per-client setting `parental_enabled:false` can disable Parental Control for this specific client.
|
||||||
|
|
||||||
|
|
||||||
|
### Get list of clients
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
GET /control/clients
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
clients: [
|
||||||
|
{
|
||||||
|
name: "client1"
|
||||||
|
ip: "..."
|
||||||
|
mac: "..."
|
||||||
|
use_global_settings: true
|
||||||
|
filtering_enabled: false
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
safesearch_enabled: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
auto_clients: [
|
||||||
|
{
|
||||||
|
name: "host"
|
||||||
|
ip: "..."
|
||||||
|
source: "etc/hosts" || "rDNS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Add client
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/clients/add
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "client1"
|
||||||
|
ip: "..."
|
||||||
|
mac: "..."
|
||||||
|
use_global_settings: true
|
||||||
|
filtering_enabled: false
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
safesearch_enabled: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
Error response (Client already exists):
|
||||||
|
|
||||||
|
400
|
||||||
|
|
||||||
|
|
||||||
|
### Update client
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/clients/update
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "client1"
|
||||||
|
data: {
|
||||||
|
name: "client1"
|
||||||
|
ip: "..."
|
||||||
|
mac: "..."
|
||||||
|
use_global_settings: true
|
||||||
|
filtering_enabled: false
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
safesearch_enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
Error response (Client not found):
|
||||||
|
|
||||||
|
400
|
||||||
|
|
||||||
|
|
||||||
|
### Delete client
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/clients/delete
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "client1"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
Error response (Client not found):
|
||||||
|
|
||||||
|
400
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"dhcp_config_saved": "Saved DHCP server config",
|
"dhcp_config_saved": "Saved DHCP server config",
|
||||||
"form_error_required": "Required field",
|
"form_error_required": "Required field",
|
||||||
"form_error_ip_format": "Invalid IPv4 format",
|
"form_error_ip_format": "Invalid IPv4 format",
|
||||||
|
"form_error_mac_format": "Invalid MAC format",
|
||||||
"form_error_positive": "Must be greater than 0",
|
"form_error_positive": "Must be greater than 0",
|
||||||
"dhcp_form_gateway_input": "Gateway IP",
|
"dhcp_form_gateway_input": "Gateway IP",
|
||||||
"dhcp_form_subnet_input": "Subnet mask",
|
"dhcp_form_subnet_input": "Subnet mask",
|
||||||
|
@ -105,6 +106,7 @@
|
||||||
"rules_count_table_header": "Rules count",
|
"rules_count_table_header": "Rules count",
|
||||||
"last_time_updated_table_header": "Last time updated",
|
"last_time_updated_table_header": "Last time updated",
|
||||||
"actions_table_header": "Actions",
|
"actions_table_header": "Actions",
|
||||||
|
"edit_table_action": "Edit",
|
||||||
"delete_table_action": "Delete",
|
"delete_table_action": "Delete",
|
||||||
"filters_and_hosts": "Filters and hosts blocklists",
|
"filters_and_hosts": "Filters and hosts blocklists",
|
||||||
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
|
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
|
||||||
|
@ -263,5 +265,30 @@
|
||||||
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
||||||
"update_now": "Update now",
|
"update_now": "Update now",
|
||||||
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
||||||
"processing_update": "Please wait, AdGuard Home is being updated"
|
"processing_update": "Please wait, AdGuard Home is being updated",
|
||||||
|
"clients_title": "Clients",
|
||||||
|
"clients_desc": "Configure devices connected to AdGuard Home",
|
||||||
|
"settings_global": "Global",
|
||||||
|
"settings_custom": "Custom",
|
||||||
|
"table_client": "Client",
|
||||||
|
"table_name": "Name",
|
||||||
|
"save_btn": "Save",
|
||||||
|
"client_add": "Add Client",
|
||||||
|
"client_new": "New Client",
|
||||||
|
"client_edit": "Edit Client",
|
||||||
|
"client_identifier": "Identifier",
|
||||||
|
"ip_address": "IP address",
|
||||||
|
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
|
||||||
|
"form_enter_ip": "Enter IP",
|
||||||
|
"form_enter_mac": "Enter MAC",
|
||||||
|
"form_client_name": "Enter client name",
|
||||||
|
"client_global_settings": "Use global settings",
|
||||||
|
"client_deleted": "Client \"{{key}}\" successfully deleted",
|
||||||
|
"client_added": "Client \"{{key}}\" successfully added",
|
||||||
|
"client_updated": "Client \"{{key}}\" successfully updated",
|
||||||
|
"table_statistics": "Requests count (last 24 hours)",
|
||||||
|
"clients_not_found": "No clients found",
|
||||||
|
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
|
||||||
|
"auto_clients_title": "Clients (runtime)",
|
||||||
|
"auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration"
|
||||||
}
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import Api from '../api/Api';
|
||||||
|
import { addErrorToast, addSuccessToast, getClients } from './index';
|
||||||
|
import { CLIENT_ID } from '../helpers/constants';
|
||||||
|
|
||||||
|
const apiClient = new Api();
|
||||||
|
|
||||||
|
export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');
|
||||||
|
|
||||||
|
export const addClientRequest = createAction('ADD_CLIENT_REQUEST');
|
||||||
|
export const addClientFailure = createAction('ADD_CLIENT_FAILURE');
|
||||||
|
export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
|
||||||
|
|
||||||
|
export const addClient = config => async (dispatch) => {
|
||||||
|
dispatch(addClientRequest());
|
||||||
|
try {
|
||||||
|
let data;
|
||||||
|
if (config.identifier === CLIENT_ID.MAC) {
|
||||||
|
const { ip, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { ...values };
|
||||||
|
} else {
|
||||||
|
const { mac, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { ...values };
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.addClient(data);
|
||||||
|
dispatch(addClientSuccess());
|
||||||
|
dispatch(toggleClientModal());
|
||||||
|
dispatch(addSuccessToast(t('client_added', { key: config.name })));
|
||||||
|
dispatch(getClients());
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(addClientFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST');
|
||||||
|
export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE');
|
||||||
|
export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS');
|
||||||
|
|
||||||
|
export const deleteClient = config => async (dispatch) => {
|
||||||
|
dispatch(deleteClientRequest());
|
||||||
|
try {
|
||||||
|
await apiClient.deleteClient(config);
|
||||||
|
dispatch(deleteClientSuccess());
|
||||||
|
dispatch(addSuccessToast(t('client_deleted', { key: config.name })));
|
||||||
|
dispatch(getClients());
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(deleteClientFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST');
|
||||||
|
export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE');
|
||||||
|
export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
|
||||||
|
|
||||||
|
export const updateClient = (config, name) => async (dispatch) => {
|
||||||
|
dispatch(updateClientRequest());
|
||||||
|
try {
|
||||||
|
let data;
|
||||||
|
if (config.identifier === CLIENT_ID.MAC) {
|
||||||
|
const { ip, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { name, data: { ...values } };
|
||||||
|
} else {
|
||||||
|
const { mac, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { name, data: { ...values } };
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.updateClient(data);
|
||||||
|
dispatch(updateClientSuccess());
|
||||||
|
dispatch(toggleClientModal());
|
||||||
|
dispatch(addSuccessToast(t('client_updated', { key: name })));
|
||||||
|
dispatch(getClients());
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(updateClientFailure());
|
||||||
|
}
|
||||||
|
};
|
|
@ -4,7 +4,7 @@ import { t } from 'i18next';
|
||||||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
|
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea, sortClients } from '../helpers/helpers';
|
||||||
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
|
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
|
||||||
import Api from '../api/Api';
|
import Api from '../api/Api';
|
||||||
|
|
||||||
|
@ -213,14 +213,41 @@ export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||||
export const getClients = () => async (dispatch) => {
|
export const getClients = () => async (dispatch) => {
|
||||||
dispatch(getClientsRequest());
|
dispatch(getClientsRequest());
|
||||||
try {
|
try {
|
||||||
const clients = await apiClient.getGlobalClients();
|
const data = await apiClient.getClients();
|
||||||
dispatch(getClientsSuccess(clients));
|
const sortedClients = data.clients && sortClients(data.clients);
|
||||||
|
const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);
|
||||||
|
|
||||||
|
dispatch(getClientsSuccess({
|
||||||
|
clients: sortedClients || [],
|
||||||
|
autoClients: sortedAutoClients || [],
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(getClientsFailure());
|
dispatch(getClientsFailure());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
||||||
|
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
||||||
|
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
||||||
|
|
||||||
|
export const getTopStats = () => async (dispatch, getState) => {
|
||||||
|
dispatch(getTopStatsRequest());
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
const state = getState();
|
||||||
|
if (state.dashboard.isCoreRunning) {
|
||||||
|
clearInterval(timer);
|
||||||
|
try {
|
||||||
|
const stats = await apiClient.getGlobalStatsTop();
|
||||||
|
dispatch(getTopStatsSuccess(stats));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(getTopStatsFailure(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
||||||
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||||
|
@ -232,6 +259,7 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||||
dispatch(dnsStatusSuccess(dnsStatus));
|
dispatch(dnsStatusSuccess(dnsStatus));
|
||||||
dispatch(getVersion());
|
dispatch(getVersion());
|
||||||
dispatch(getClients());
|
dispatch(getClients());
|
||||||
|
dispatch(getTopStats());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(initSettingsFailure());
|
dispatch(initSettingsFailure());
|
||||||
|
@ -289,27 +317,6 @@ export const getStats = () => async (dispatch) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
|
||||||
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
|
||||||
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
|
||||||
|
|
||||||
export const getTopStats = () => async (dispatch, getState) => {
|
|
||||||
dispatch(getTopStatsRequest());
|
|
||||||
const timer = setInterval(async () => {
|
|
||||||
const state = getState();
|
|
||||||
if (state.dashboard.isCoreRunning) {
|
|
||||||
clearInterval(timer);
|
|
||||||
try {
|
|
||||||
const stats = await apiClient.getGlobalStatsTop();
|
|
||||||
dispatch(getTopStatsSuccess(stats));
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(addErrorToast({ error }));
|
|
||||||
dispatch(getTopStatsFailure(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||||
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||||
|
|
|
@ -39,8 +39,6 @@ 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' }
|
|
||||||
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' };
|
|
||||||
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
|
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
|
||||||
|
|
||||||
restartGlobalFiltering() {
|
restartGlobalFiltering() {
|
||||||
|
@ -142,11 +140,6 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUpdate() {
|
getUpdate() {
|
||||||
const { path, method } = this.GLOBAL_UPDATE;
|
const { path, method } = this.GLOBAL_UPDATE;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
|
@ -409,4 +402,42 @@ export default class Api {
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-client settings
|
||||||
|
GET_CLIENTS = { path: 'clients', method: 'GET' }
|
||||||
|
ADD_CLIENT = { path: 'clients/add', method: 'POST' }
|
||||||
|
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' }
|
||||||
|
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' }
|
||||||
|
|
||||||
|
getClients() {
|
||||||
|
const { path, method } = this.GET_CLIENTS;
|
||||||
|
return this.makeRequest(path, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
addClient(config) {
|
||||||
|
const { path, method } = this.ADD_CLIENT;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteClient(config) {
|
||||||
|
const { path, method } = this.DELETE_CLIENT;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClient(config) {
|
||||||
|
const { path, method } = this.UPDATE_CLIENT;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import Status from '../ui/Status';
|
||||||
import UpdateTopline from '../ui/UpdateTopline';
|
import UpdateTopline from '../ui/UpdateTopline';
|
||||||
import UpdateOverlay from '../ui/UpdateOverlay';
|
import UpdateOverlay from '../ui/UpdateOverlay';
|
||||||
import EncryptionTopline from '../ui/EncryptionTopline';
|
import EncryptionTopline from '../ui/EncryptionTopline';
|
||||||
|
import Icons from '../ui/Icons';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
@ -103,6 +104,7 @@ class App extends Component {
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
<Toasts />
|
<Toasts />
|
||||||
|
<Icons />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,7 +24,8 @@ class Clients extends Component {
|
||||||
Header: 'IP',
|
Header: 'IP',
|
||||||
accessor: 'ip',
|
accessor: 'ip',
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const clientName = getClientName(this.props.clients, value);
|
const clientName = getClientName(this.props.clients, value)
|
||||||
|
|| getClientName(this.props.autoClients, value);
|
||||||
let client;
|
let client;
|
||||||
|
|
||||||
if (clientName) {
|
if (clientName) {
|
||||||
|
@ -79,6 +80,7 @@ Clients.propTypes = {
|
||||||
dnsQueries: PropTypes.number.isRequired,
|
dnsQueries: PropTypes.number.isRequired,
|
||||||
refreshButton: PropTypes.node.isRequired,
|
refreshButton: PropTypes.node.isRequired,
|
||||||
clients: PropTypes.array.isRequired,
|
clients: PropTypes.array.isRequired,
|
||||||
|
autoClients: PropTypes.array.isRequired,
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ class Dashboard extends Component {
|
||||||
refreshButton={refreshButton}
|
refreshButton={refreshButton}
|
||||||
topClients={dashboard.topStats.top_clients}
|
topClients={dashboard.topStats.top_clients}
|
||||||
clients={dashboard.clients}
|
clients={dashboard.clients}
|
||||||
|
autoClients={dashboard.autoClients}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
.remove-icon {
|
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
height: 18px;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-icon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ import Modal from '../ui/Modal';
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
import UserRules from './UserRules';
|
import UserRules from './UserRules';
|
||||||
import './Filters.css';
|
|
||||||
|
|
||||||
class Filters extends Component {
|
class Filters extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -59,7 +58,18 @@ class Filters extends Component {
|
||||||
}, {
|
}, {
|
||||||
Header: <Trans>actions_table_header</Trans>,
|
Header: <Trans>actions_table_header</Trans>,
|
||||||
accessor: 'url',
|
accessor: 'url',
|
||||||
Cell: ({ value }) => (<span title={ this.props.t('delete_table_action') } className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
|
Cell: ({ value }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||||
|
onClick={() => this.props.removeFilter(value)}
|
||||||
|
title={this.props.t('delete_table_action')}
|
||||||
|
>
|
||||||
|
<svg className="icons">
|
||||||
|
<use xlinkHref="#delete" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
),
|
||||||
className: 'text-center',
|
className: 'text-center',
|
||||||
width: 80,
|
width: 80,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
min-height: 26px;
|
min-height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs__row--center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.logs__row--overflow {
|
.logs__row--overflow {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,7 +196,8 @@ 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);
|
const clientName = getClientName(dashboard.clients, row.value)
|
||||||
|
|| getClientName(dashboard.autoClients, row.value);
|
||||||
let client;
|
let client;
|
||||||
|
|
||||||
if (clientName) {
|
if (clientName) {
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withNamespaces } from 'react-i18next';
|
||||||
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
|
import { CLIENT_ID } from '../../../helpers/constants';
|
||||||
|
import Card from '../../ui/Card';
|
||||||
|
|
||||||
|
class AutoClients extends Component {
|
||||||
|
getClient = (name, clients) => {
|
||||||
|
const client = clients.find(item => name === item.name);
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
|
||||||
|
|
||||||
|
return {
|
||||||
|
identifier,
|
||||||
|
use_global_settings: true,
|
||||||
|
...client,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
identifier: 'ip',
|
||||||
|
use_global_settings: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getStats = (ip, stats) => {
|
||||||
|
if (stats && stats.top_clients) {
|
||||||
|
return stats.top_clients[ip];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
cellWrap = ({ value }) => (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text" title={value}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
Header: this.props.t('table_client'),
|
||||||
|
accessor: 'ip',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('table_name'),
|
||||||
|
accessor: 'name',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('source_label'),
|
||||||
|
accessor: 'source',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('table_statistics'),
|
||||||
|
accessor: 'statistics',
|
||||||
|
Cell: (row) => {
|
||||||
|
const clientIP = row.original.ip;
|
||||||
|
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
|
||||||
|
|
||||||
|
if (clientStats) {
|
||||||
|
return (
|
||||||
|
<div className="logs__row">
|
||||||
|
<div className="logs__text" title={clientStats}>
|
||||||
|
{clientStats}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '–';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t, autoClients } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t('auto_clients_title')}
|
||||||
|
subtitle={t('auto_clients_desc')}
|
||||||
|
bodyType="card-body box-body--settings"
|
||||||
|
>
|
||||||
|
<ReactTable
|
||||||
|
data={autoClients || []}
|
||||||
|
columns={this.columns}
|
||||||
|
className="-striped -highlight card-table-overflow"
|
||||||
|
showPagination={true}
|
||||||
|
defaultPageSize={10}
|
||||||
|
minRows={5}
|
||||||
|
previousText={t('previous_btn')}
|
||||||
|
nextText={t('next_btn')}
|
||||||
|
loadingText={t('loading_table_status')}
|
||||||
|
pageText={t('page_table_footer_text')}
|
||||||
|
ofText={t('of_table_footer_text')}
|
||||||
|
rowsText={t('rows_table_footer_text')}
|
||||||
|
noDataText={t('clients_not_found')}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoClients.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
autoClients: PropTypes.array.isRequired,
|
||||||
|
topStats: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(AutoClients);
|
|
@ -0,0 +1,217 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
|
import { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form';
|
||||||
|
import { CLIENT_ID } from '../../../helpers/constants';
|
||||||
|
|
||||||
|
let Form = (props) => {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
pristine,
|
||||||
|
submitting,
|
||||||
|
clientIdentifier,
|
||||||
|
useGlobalSettings,
|
||||||
|
toggleClientModal,
|
||||||
|
processingAdding,
|
||||||
|
processingUpdating,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="form__group">
|
||||||
|
<div className="form-inline mb-3">
|
||||||
|
<strong className="mr-3">
|
||||||
|
<Trans>client_identifier</Trans>
|
||||||
|
</strong>
|
||||||
|
<label className="mr-3">
|
||||||
|
<Field
|
||||||
|
name="identifier"
|
||||||
|
component={renderField}
|
||||||
|
type="radio"
|
||||||
|
className="form-control mr-2"
|
||||||
|
value="ip"
|
||||||
|
/>{' '}
|
||||||
|
<Trans>ip_address</Trans>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<Field
|
||||||
|
name="identifier"
|
||||||
|
component={renderField}
|
||||||
|
type="radio"
|
||||||
|
className="form-control mr-2"
|
||||||
|
value="mac"
|
||||||
|
/>{' '}
|
||||||
|
MAC
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{clientIdentifier === CLIENT_ID.IP && (
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="ip"
|
||||||
|
name="ip"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_enter_ip')}
|
||||||
|
validate={[ipv4, required]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{clientIdentifier === CLIENT_ID.MAC && (
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="mac"
|
||||||
|
name="mac"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_enter_mac')}
|
||||||
|
validate={[mac, required]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="form__desc">
|
||||||
|
<Trans
|
||||||
|
components={[
|
||||||
|
<a href="#settings_dhcp" key="0">
|
||||||
|
link
|
||||||
|
</a>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
client_identifier_desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_client_name')}
|
||||||
|
validate={[required]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<strong>
|
||||||
|
<Trans>settings</Trans>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
name="use_global_settings"
|
||||||
|
type="checkbox"
|
||||||
|
component={renderSelectField}
|
||||||
|
placeholder={t('client_global_settings')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
name="filtering_enabled"
|
||||||
|
type="checkbox"
|
||||||
|
component={renderSelectField}
|
||||||
|
placeholder={t('block_domain_use_filters_and_hosts')}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
name="safebrowsing_enabled"
|
||||||
|
type="checkbox"
|
||||||
|
component={renderSelectField}
|
||||||
|
placeholder={t('use_adguard_browsing_sec')}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
name="parental_enabled"
|
||||||
|
type="checkbox"
|
||||||
|
component={renderSelectField}
|
||||||
|
placeholder={t('use_adguard_parental')}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
name="safesearch_enabled"
|
||||||
|
type="checkbox"
|
||||||
|
component={renderSelectField}
|
||||||
|
placeholder={t('enforce_safe_search')}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-standard"
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
toggleClientModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>cancel_btn</Trans>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={submitting || pristine || processingAdding || processingUpdating}
|
||||||
|
>
|
||||||
|
<Trans>save_btn</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Form.propTypes = {
|
||||||
|
pristine: PropTypes.bool.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
reset: PropTypes.func.isRequired,
|
||||||
|
submitting: PropTypes.bool.isRequired,
|
||||||
|
toggleClientModal: PropTypes.func.isRequired,
|
||||||
|
clientIdentifier: PropTypes.string,
|
||||||
|
useGlobalSettings: PropTypes.bool,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
|
processingUpdating: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const selector = formValueSelector('clientForm');
|
||||||
|
|
||||||
|
Form = connect((state) => {
|
||||||
|
const clientIdentifier = selector(state, 'identifier');
|
||||||
|
const useGlobalSettings = selector(state, 'use_global_settings');
|
||||||
|
return {
|
||||||
|
clientIdentifier,
|
||||||
|
useGlobalSettings,
|
||||||
|
};
|
||||||
|
})(Form);
|
||||||
|
|
||||||
|
export default flow([
|
||||||
|
withNamespaces(),
|
||||||
|
reduxForm({
|
||||||
|
form: 'clientForm',
|
||||||
|
enableReinitialize: true,
|
||||||
|
}),
|
||||||
|
])(Form);
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import ReactModal from 'react-modal';
|
||||||
|
|
||||||
|
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||||
|
import Form from './Form';
|
||||||
|
|
||||||
|
const Modal = (props) => {
|
||||||
|
const {
|
||||||
|
isModalOpen,
|
||||||
|
modalType,
|
||||||
|
currentClientData,
|
||||||
|
handleSubmit,
|
||||||
|
toggleClientModal,
|
||||||
|
processingAdding,
|
||||||
|
processingUpdating,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactModal
|
||||||
|
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
|
||||||
|
closeTimeoutMS={0}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onRequestClose={() => toggleClientModal()}
|
||||||
|
>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h4 className="modal-title">
|
||||||
|
{modalType === MODAL_TYPE.EDIT ? (
|
||||||
|
<Trans>client_edit</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>client_new</Trans>
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
|
<button type="button" className="close" onClick={() => toggleClientModal()}>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
initialValues={{
|
||||||
|
...currentClientData,
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
toggleClientModal={toggleClientModal}
|
||||||
|
processingAdding={processingAdding}
|
||||||
|
processingUpdating={processingUpdating}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ReactModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.propTypes = {
|
||||||
|
isModalOpen: PropTypes.bool.isRequired,
|
||||||
|
modalType: PropTypes.string.isRequired,
|
||||||
|
currentClientData: PropTypes.object.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
toggleClientModal: PropTypes.func.isRequired,
|
||||||
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
|
processingUpdating: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(Modal);
|
|
@ -0,0 +1,263 @@
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
|
import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
|
||||||
|
import Card from '../../ui/Card';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
|
class Clients extends Component {
|
||||||
|
handleFormAdd = (values) => {
|
||||||
|
this.props.addClient(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFormUpdate = (values, name) => {
|
||||||
|
this.props.updateClient(values, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = (values) => {
|
||||||
|
if (this.props.modalType === MODAL_TYPE.EDIT) {
|
||||||
|
this.handleFormUpdate(values, this.props.modalClientName);
|
||||||
|
} else {
|
||||||
|
this.handleFormAdd(values);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cellWrap = ({ value }) => (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text" title={value}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
getClient = (name, clients) => {
|
||||||
|
const client = clients.find(item => name === item.name);
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
|
||||||
|
|
||||||
|
return {
|
||||||
|
identifier,
|
||||||
|
use_global_settings: true,
|
||||||
|
...client,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
identifier: CLIENT_ID.IP,
|
||||||
|
use_global_settings: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getStats = (ip, stats) => {
|
||||||
|
if (stats && stats.top_clients) {
|
||||||
|
return stats.top_clients[ip];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDelete = (data) => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (window.confirm(this.props.t('client_confirm_delete', { key: data.name }))) {
|
||||||
|
this.props.deleteClient(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
Header: this.props.t('table_client'),
|
||||||
|
accessor: 'ip',
|
||||||
|
Cell: (row) => {
|
||||||
|
if (row.original && row.original.mac) {
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text" title={row.original.mac}>
|
||||||
|
{row.original.mac} <em>(MAC)</em>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (row.value) {
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text" title={row.value}>
|
||||||
|
{row.value} <em>(IP)</em>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('table_name'),
|
||||||
|
accessor: 'name',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('settings'),
|
||||||
|
accessor: 'use_global_settings',
|
||||||
|
Cell: ({ value }) => {
|
||||||
|
const title = value ? (
|
||||||
|
<Trans>settings_global</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>settings_custom</Trans>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<div className="logs__text" title={title}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('table_statistics'),
|
||||||
|
accessor: 'statistics',
|
||||||
|
Cell: (row) => {
|
||||||
|
const clientIP = row.original.ip;
|
||||||
|
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
|
||||||
|
|
||||||
|
if (clientStats) {
|
||||||
|
return (
|
||||||
|
<div className="logs__row">
|
||||||
|
<div className="logs__text" title={clientStats}>
|
||||||
|
{clientStats}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '–';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('actions_table_header'),
|
||||||
|
accessor: 'actions',
|
||||||
|
maxWidth: 150,
|
||||||
|
Cell: (row) => {
|
||||||
|
const clientName = row.original.name;
|
||||||
|
const {
|
||||||
|
toggleClientModal,
|
||||||
|
processingDeleting,
|
||||||
|
processingUpdating,
|
||||||
|
t,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-outline-primary btn-sm mr-2"
|
||||||
|
onClick={() =>
|
||||||
|
toggleClientModal({
|
||||||
|
type: MODAL_TYPE.EDIT,
|
||||||
|
name: clientName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={processingUpdating}
|
||||||
|
title={t('edit_table_action')}
|
||||||
|
>
|
||||||
|
<svg className="icons">
|
||||||
|
<use xlinkHref="#edit" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||||
|
onClick={() => this.handleDelete({ name: clientName })}
|
||||||
|
disabled={processingDeleting}
|
||||||
|
title={t('delete_table_action')}
|
||||||
|
>
|
||||||
|
<svg className="icons">
|
||||||
|
<use xlinkHref="#delete" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
clients,
|
||||||
|
isModalOpen,
|
||||||
|
modalType,
|
||||||
|
modalClientName,
|
||||||
|
toggleClientModal,
|
||||||
|
processingAdding,
|
||||||
|
processingUpdating,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const currentClientData = this.getClient(modalClientName, clients);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t('clients_title')}
|
||||||
|
subtitle={t('clients_desc')}
|
||||||
|
bodyType="card-body box-body--settings"
|
||||||
|
>
|
||||||
|
<Fragment>
|
||||||
|
<ReactTable
|
||||||
|
data={clients || []}
|
||||||
|
columns={this.columns}
|
||||||
|
className="-striped -highlight card-table-overflow"
|
||||||
|
showPagination={true}
|
||||||
|
defaultPageSize={10}
|
||||||
|
minRows={5}
|
||||||
|
previousText={t('previous_btn')}
|
||||||
|
nextText={t('next_btn')}
|
||||||
|
loadingText={t('loading_table_status')}
|
||||||
|
pageText={t('page_table_footer_text')}
|
||||||
|
ofText={t('of_table_footer_text')}
|
||||||
|
rowsText={t('rows_table_footer_text')}
|
||||||
|
noDataText={t('clients_not_found')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-success btn-standard mt-3"
|
||||||
|
onClick={() => toggleClientModal(MODAL_TYPE.ADD)}
|
||||||
|
disabled={processingAdding}
|
||||||
|
>
|
||||||
|
<Trans>client_add</Trans>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
modalType={modalType}
|
||||||
|
toggleClientModal={toggleClientModal}
|
||||||
|
currentClientData={currentClientData}
|
||||||
|
handleSubmit={this.handleSubmit}
|
||||||
|
processingAdding={processingAdding}
|
||||||
|
processingUpdating={processingUpdating}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Clients.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
clients: PropTypes.array.isRequired,
|
||||||
|
topStats: PropTypes.object.isRequired,
|
||||||
|
toggleClientModal: PropTypes.func.isRequired,
|
||||||
|
deleteClient: PropTypes.func.isRequired,
|
||||||
|
addClient: PropTypes.func.isRequired,
|
||||||
|
updateClient: PropTypes.func.isRequired,
|
||||||
|
isModalOpen: PropTypes.bool.isRequired,
|
||||||
|
modalType: PropTypes.string.isRequired,
|
||||||
|
modalClientName: PropTypes.string.isRequired,
|
||||||
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
|
processingDeleting: PropTypes.bool.isRequired,
|
||||||
|
processingUpdating: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(Clients);
|
|
@ -76,3 +76,11 @@
|
||||||
.encryption__list li {
|
.encryption__list li {
|
||||||
list-style: inside;
|
list-style: inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withNamespaces, Trans } from 'react-i18next';
|
import { withNamespaces, Trans } from 'react-i18next';
|
||||||
|
|
||||||
import Upstream from './Upstream';
|
import Upstream from './Upstream';
|
||||||
import Dhcp from './Dhcp';
|
import Dhcp from './Dhcp';
|
||||||
import Encryption from './Encryption';
|
import Encryption from './Encryption';
|
||||||
|
import Clients from './Clients';
|
||||||
|
import AutoClients from './Clients/AutoClients';
|
||||||
import Checkbox from '../ui/Checkbox';
|
import Checkbox from '../ui/Checkbox';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
|
|
||||||
import './Settings.css';
|
import './Settings.css';
|
||||||
|
|
||||||
class Settings extends Component {
|
class Settings extends Component {
|
||||||
|
@ -46,29 +50,38 @@ class Settings extends Component {
|
||||||
return Object.keys(settings).map((key) => {
|
return Object.keys(settings).map((key) => {
|
||||||
const setting = settings[key];
|
const setting = settings[key];
|
||||||
const { enabled } = setting;
|
const { enabled } = setting;
|
||||||
return (<Checkbox
|
return (
|
||||||
key={key}
|
<Checkbox
|
||||||
{...settings[key]}
|
key={key}
|
||||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
{...settings[key]}
|
||||||
/>);
|
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div><Trans>no_settings</Trans></div>
|
<div>
|
||||||
|
<Trans>no_settings</Trans>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { settings, dashboard, t } = this.props;
|
const {
|
||||||
|
settings, dashboard, clients, t,
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageTitle title={ t('settings') } />
|
<PageTitle title={t('settings')} />
|
||||||
{settings.processing && <Loading />}
|
{settings.processing && <Loading />}
|
||||||
{!settings.processing &&
|
{!settings.processing && (
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<Card title={ t('general_settings') } bodyType="card-body box-body--settings">
|
<Card
|
||||||
|
title={t('general_settings')}
|
||||||
|
bodyType="card-body box-body--settings"
|
||||||
|
>
|
||||||
<div className="form">
|
<div className="form">
|
||||||
{this.renderSettings(settings.settingsList)}
|
{this.renderSettings(settings.settingsList)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,6 +95,28 @@ class Settings extends Component {
|
||||||
processingTestUpstream={settings.processingTestUpstream}
|
processingTestUpstream={settings.processingTestUpstream}
|
||||||
processingSetUpstream={settings.processingSetUpstream}
|
processingSetUpstream={settings.processingSetUpstream}
|
||||||
/>
|
/>
|
||||||
|
{!dashboard.processingTopStats && !dashboard.processingClients && (
|
||||||
|
<Fragment>
|
||||||
|
<Clients
|
||||||
|
clients={dashboard.clients}
|
||||||
|
topStats={dashboard.topStats}
|
||||||
|
isModalOpen={clients.isModalOpen}
|
||||||
|
modalClientName={clients.modalClientName}
|
||||||
|
modalType={clients.modalType}
|
||||||
|
addClient={this.props.addClient}
|
||||||
|
updateClient={this.props.updateClient}
|
||||||
|
deleteClient={this.props.deleteClient}
|
||||||
|
toggleClientModal={this.props.toggleClientModal}
|
||||||
|
processingAdding={clients.processingAdding}
|
||||||
|
processingDeleting={clients.processingDeleting}
|
||||||
|
processingUpdating={clients.processingUpdating}
|
||||||
|
/>
|
||||||
|
<AutoClients
|
||||||
|
autoClients={dashboard.autoClients}
|
||||||
|
topStats={dashboard.topStats}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
<Encryption
|
<Encryption
|
||||||
encryption={this.props.encryption}
|
encryption={this.props.encryption}
|
||||||
setTlsConfig={this.props.setTlsConfig}
|
setTlsConfig={this.props.setTlsConfig}
|
||||||
|
@ -97,7 +132,7 @@ class Settings extends Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
z-index: 103;
|
z-index: 105;
|
||||||
width: 345px;
|
width: 345px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.icons {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 100%;
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import './Icons.css';
|
||||||
|
|
||||||
const Icons = () => (
|
const Icons = () => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="hidden">
|
<svg xmlns="http://www.w3.org/2000/svg" className="hidden">
|
||||||
<symbol id="android" viewBox="0 0 14 16" fill="currentColor">
|
<symbol id="android" viewBox="0 0 14 16" fill="currentColor">
|
||||||
|
@ -21,6 +23,14 @@ const Icons = () => (
|
||||||
<symbol id="router" viewBox="0 0 30 30" fill="currentColor">
|
<symbol id="router" viewBox="0 0 30 30" fill="currentColor">
|
||||||
<path d="M17.646 2.332a1 1 0 0 0-.697 1.719 6.984 6.984 0 0 1 0 9.898 1 1 0 1 0 1.414 1.414c3.507-3.506 3.507-9.22 0-12.726a1 1 0 0 0-.717-.305zm-12.662.654A1 1 0 0 0 4 4v14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h22a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H12V9a1 1 0 0 0-1.016-1.014A1 1 0 0 0 10 9v9H6V4a1 1 0 0 0-1.016-1.014zm9.834 2.176a1 1 0 0 0-.697 1.717 2.985 2.985 0 0 1 0 4.242 1 1 0 1 0 1.414 1.414 5.014 5.014 0 0 0 0-7.07 1 1 0 0 0-.717-.303zM5 21a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm4 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
<path d="M17.646 2.332a1 1 0 0 0-.697 1.719 6.984 6.984 0 0 1 0 9.898 1 1 0 1 0 1.414 1.414c3.507-3.506 3.507-9.22 0-12.726a1 1 0 0 0-.717-.305zm-12.662.654A1 1 0 0 0 4 4v14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h22a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H12V9a1 1 0 0 0-1.016-1.014A1 1 0 0 0 10 9v9H6V4a1 1 0 0 0-1.016-1.014zm9.834 2.176a1 1 0 0 0-.697 1.717 2.985 2.985 0 0 1 0 4.242 1 1 0 1 0 1.414 1.414 5.014 5.014 0 0 0 0-7.07 1 1 0 0 0-.717-.303zM5 21a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm4 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="edit" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="delete" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||||
|
<path d="m3 6h2 16"/><path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m10 11v6"/><path d="m14 11v6"/>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 1;
|
z-index: 104;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Overlay--after-open {
|
.ReactModal__Overlay--after-open {
|
||||||
|
@ -38,3 +38,9 @@
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.modal-dialog--clients {
|
||||||
|
max-width: 650px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,12 @@ import {
|
||||||
setTlsConfig,
|
setTlsConfig,
|
||||||
validateTlsConfig,
|
validateTlsConfig,
|
||||||
} from '../actions/encryption';
|
} from '../actions/encryption';
|
||||||
|
import {
|
||||||
|
addClient,
|
||||||
|
updateClient,
|
||||||
|
deleteClient,
|
||||||
|
toggleClientModal,
|
||||||
|
} from '../actions/clients';
|
||||||
import Settings from '../components/Settings';
|
import Settings from '../components/Settings';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
|
@ -25,12 +31,14 @@ const mapStateToProps = (state) => {
|
||||||
dashboard,
|
dashboard,
|
||||||
dhcp,
|
dhcp,
|
||||||
encryption,
|
encryption,
|
||||||
|
clients,
|
||||||
} = state;
|
} = state;
|
||||||
const props = {
|
const props = {
|
||||||
settings,
|
settings,
|
||||||
dashboard,
|
dashboard,
|
||||||
dhcp,
|
dhcp,
|
||||||
encryption,
|
encryption,
|
||||||
|
clients,
|
||||||
};
|
};
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
|
@ -50,6 +58,10 @@ const mapDispatchToProps = {
|
||||||
getTlsStatus,
|
getTlsStatus,
|
||||||
setTlsConfig,
|
setTlsConfig,
|
||||||
validateTlsConfig,
|
validateTlsConfig,
|
||||||
|
addClient,
|
||||||
|
updateClient,
|
||||||
|
deleteClient,
|
||||||
|
toggleClientModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
|
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
|
||||||
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
|
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
|
||||||
|
export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]))$/g;
|
||||||
|
|
||||||
export const STATS_NAMES = {
|
export const STATS_NAMES = {
|
||||||
avg_processing_time: 'average_processing_time',
|
avg_processing_time: 'average_processing_time',
|
||||||
|
@ -19,7 +20,8 @@ export const STATUS_COLORS = {
|
||||||
|
|
||||||
export const REPOSITORY = {
|
export const REPOSITORY = {
|
||||||
URL: 'https://github.com/AdguardTeam/AdGuardHome',
|
URL: 'https://github.com/AdguardTeam/AdGuardHome',
|
||||||
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
TRACKERS_DB:
|
||||||
|
'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
||||||
|
@ -165,3 +167,13 @@ export const DHCP_STATUS_RESPONSE = {
|
||||||
NO: 'no',
|
NO: 'no',
|
||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MODAL_TYPE = {
|
||||||
|
ADD: 'add',
|
||||||
|
EDIT: 'edit',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CLIENT_ID = {
|
||||||
|
MAC: 'mac',
|
||||||
|
IP: 'ip',
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
|
import { R_IPV4, R_MAC, UNSAFE_PORTS } from '../helpers/constants';
|
||||||
|
|
||||||
export const renderField = ({
|
export const renderField = ({
|
||||||
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
||||||
|
@ -55,6 +55,13 @@ export const ipv4 = (value) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mac = (value) => {
|
||||||
|
if (value && !new RegExp(R_MAC).test(value)) {
|
||||||
|
return <Trans>form_error_mac_format</Trans>;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const isPositive = (value) => {
|
export const isPositive = (value) => {
|
||||||
if ((value || value === 0) && (value <= 0)) {
|
if ((value || value === 0) && (value <= 0)) {
|
||||||
return <Trans>form_error_positive</Trans>;
|
return <Trans>form_error_positive</Trans>;
|
||||||
|
|
|
@ -208,3 +208,20 @@ export const getClientName = (clients, ip) => {
|
||||||
const client = clients.find(item => ip === item.ip);
|
const client = clients.find(item => ip === item.ip);
|
||||||
return (client && client.name) || '';
|
return (client && client.name) || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sortClients = (clients) => {
|
||||||
|
const compare = (a, b) => {
|
||||||
|
const nameA = a.name.toUpperCase();
|
||||||
|
const nameB = b.name.toUpperCase();
|
||||||
|
|
||||||
|
if (nameA > nameB) {
|
||||||
|
return 1;
|
||||||
|
} else if (nameA < nameB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return clients.sort(compare);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
|
import * as actions from '../actions/clients';
|
||||||
|
|
||||||
|
const clients = handleActions({
|
||||||
|
[actions.addClientRequest]: state => ({ ...state, processingAdding: true }),
|
||||||
|
[actions.addClientFailure]: state => ({ ...state, processingAdding: false }),
|
||||||
|
[actions.addClientSuccess]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
processingAdding: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.deleteClientRequest]: state => ({ ...state, processingDeleting: true }),
|
||||||
|
[actions.deleteClientFailure]: state => ({ ...state, processingDeleting: false }),
|
||||||
|
[actions.deleteClientSuccess]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
processingDeleting: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.updateClientRequest]: state => ({ ...state, processingUpdating: true }),
|
||||||
|
[actions.updateClientFailure]: state => ({ ...state, processingUpdating: false }),
|
||||||
|
[actions.updateClientSuccess]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
processingUpdating: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.toggleClientModal]: (state, { payload }) => {
|
||||||
|
if (payload) {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
modalType: payload.type || '',
|
||||||
|
modalClientName: payload.name || '',
|
||||||
|
isModalOpen: !state.isModalOpen,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
isModalOpen: !state.isModalOpen,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
processing: true,
|
||||||
|
processingAdding: false,
|
||||||
|
processingDeleting: false,
|
||||||
|
processingUpdating: false,
|
||||||
|
isModalOpen: false,
|
||||||
|
modalClientName: '',
|
||||||
|
modalType: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default clients;
|
|
@ -7,6 +7,7 @@ import versionCompare from '../helpers/versionCompare';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import toasts from './toasts';
|
import toasts from './toasts';
|
||||||
import encryption from './encryption';
|
import encryption from './encryption';
|
||||||
|
import clients from './clients';
|
||||||
|
|
||||||
const settings = handleActions({
|
const settings = handleActions({
|
||||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||||
|
@ -184,7 +185,8 @@ const dashboard = handleActions({
|
||||||
[actions.getClientsSuccess]: (state, { payload }) => {
|
[actions.getClientsSuccess]: (state, { payload }) => {
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
clients: payload,
|
clients: payload.clients,
|
||||||
|
autoClients: payload.autoClients,
|
||||||
processingClients: false,
|
processingClients: false,
|
||||||
};
|
};
|
||||||
return newState;
|
return newState;
|
||||||
|
@ -209,6 +211,8 @@ const dashboard = handleActions({
|
||||||
dnsAddresses: [],
|
dnsAddresses: [],
|
||||||
dnsVersion: '',
|
dnsVersion: '',
|
||||||
clients: [],
|
clients: [],
|
||||||
|
autoClients: [],
|
||||||
|
topStats: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryLogs = handleActions({
|
const queryLogs = handleActions({
|
||||||
|
@ -361,6 +365,7 @@ export default combineReducers({
|
||||||
toasts,
|
toasts,
|
||||||
dhcp,
|
dhcp,
|
||||||
encryption,
|
encryption,
|
||||||
|
clients,
|
||||||
loadingBar: loadingBarReducer,
|
loadingBar: loadingBarReducer,
|
||||||
form: formReducer,
|
form: formReducer,
|
||||||
});
|
});
|
||||||
|
|
436
clients.go
436
clients.go
|
@ -2,32 +2,264 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client information
|
// Client information
|
||||||
type Client struct {
|
type Client struct {
|
||||||
IP string
|
IP string
|
||||||
Name string
|
MAC string
|
||||||
//Source source // Hosts file / User settings / DHCP
|
Name string
|
||||||
|
UseOwnSettings bool // false: use global settings
|
||||||
|
FilteringEnabled bool
|
||||||
|
SafeSearchEnabled bool
|
||||||
|
SafeBrowsingEnabled bool
|
||||||
|
ParentalEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientJSON struct {
|
type clientJSON struct {
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Name string `json:"name"`
|
MAC string `json:"mac"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
UseGlobalSettings bool `json:"use_global_settings"`
|
||||||
|
FilteringEnabled bool `json:"filtering_enabled"`
|
||||||
|
ParentalEnabled bool `json:"parental_enabled"`
|
||||||
|
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
|
||||||
|
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var clients []Client
|
type clientSource uint
|
||||||
var clientsFilled bool
|
|
||||||
|
const (
|
||||||
|
ClientSourceHostsFile clientSource = 0 // from /etc/hosts
|
||||||
|
ClientSourceRDNS clientSource = 1 // from rDNS
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientHost information
|
||||||
|
type ClientHost struct {
|
||||||
|
Host string
|
||||||
|
Source clientSource
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientsContainer struct {
|
||||||
|
list map[string]*Client
|
||||||
|
ipIndex map[string]*Client
|
||||||
|
ipHost map[string]ClientHost // IP -> Hostname
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var clients clientsContainer
|
||||||
|
|
||||||
|
// Initialize clients container
|
||||||
|
func clientsInit() {
|
||||||
|
if clients.list != nil {
|
||||||
|
log.Fatal("clients.list != nil")
|
||||||
|
}
|
||||||
|
clients.list = make(map[string]*Client)
|
||||||
|
clients.ipIndex = make(map[string]*Client)
|
||||||
|
clients.ipHost = make(map[string]ClientHost)
|
||||||
|
|
||||||
|
clientsAddFromHostsFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientsGetList() map[string]*Client {
|
||||||
|
return clients.list
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientExists(ip string) bool {
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
_, ok := clients.ipIndex[ip]
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = clients.ipHost[ip]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for a client by IP
|
||||||
|
func clientFind(ip string) (Client, bool) {
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
c, ok := clients.ipIndex[ip]
|
||||||
|
if ok {
|
||||||
|
return *c, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c = range clients.list {
|
||||||
|
if len(c.MAC) != 0 {
|
||||||
|
mac, err := net.ParseMAC(c.MAC)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ipAddr := dhcpServer.FindIPbyMAC(mac)
|
||||||
|
if ipAddr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip == ipAddr.String() {
|
||||||
|
return *c, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Client{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Client object's fields are correct
|
||||||
|
func clientCheck(c *Client) error {
|
||||||
|
if len(c.Name) == 0 {
|
||||||
|
return fmt.Errorf("Invalid Name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(c.IP) == 0 && len(c.MAC) == 0) ||
|
||||||
|
(len(c.IP) != 0 && len(c.MAC) != 0) {
|
||||||
|
return fmt.Errorf("IP or MAC required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.IP) != 0 {
|
||||||
|
ip := net.ParseIP(c.IP)
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("Invalid IP")
|
||||||
|
}
|
||||||
|
c.IP = ip.String()
|
||||||
|
} else {
|
||||||
|
_, err := net.ParseMAC(c.MAC)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid MAC: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new client object
|
||||||
|
// Return true: success; false: client exists.
|
||||||
|
func clientAdd(c Client) (bool, error) {
|
||||||
|
e := clientCheck(&c)
|
||||||
|
if e != nil {
|
||||||
|
return false, e
|
||||||
|
}
|
||||||
|
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
// check Name index
|
||||||
|
_, ok := clients.list[c.Name]
|
||||||
|
if ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check IP index
|
||||||
|
if len(c.IP) != 0 {
|
||||||
|
c2, ok := clients.ipIndex[c.IP]
|
||||||
|
if ok {
|
||||||
|
return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clients.list[c.Name] = &c
|
||||||
|
if len(c.IP) != 0 {
|
||||||
|
clients.ipIndex[c.IP] = &c
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a client
|
||||||
|
func clientDel(name string) bool {
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
c, ok := clients.list[name]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(clients.list, name)
|
||||||
|
delete(clients.ipIndex, c.IP)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a client
|
||||||
|
func clientUpdate(name string, c Client) error {
|
||||||
|
err := clientCheck(&c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
old, ok := clients.list[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Client not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check Name index
|
||||||
|
if old.Name != c.Name {
|
||||||
|
_, ok = clients.list[c.Name]
|
||||||
|
if ok {
|
||||||
|
return fmt.Errorf("Client already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check IP index
|
||||||
|
if old.IP != c.IP && len(c.IP) != 0 {
|
||||||
|
c2, ok := clients.ipIndex[c.IP]
|
||||||
|
if ok {
|
||||||
|
return fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update Name index
|
||||||
|
if old.Name != c.Name {
|
||||||
|
delete(clients.list, old.Name)
|
||||||
|
}
|
||||||
|
clients.list[c.Name] = &c
|
||||||
|
|
||||||
|
// update IP index
|
||||||
|
if old.IP != c.IP {
|
||||||
|
delete(clients.ipIndex, old.IP)
|
||||||
|
}
|
||||||
|
if len(c.IP) != 0 {
|
||||||
|
clients.ipIndex[c.IP] = &c
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientAddHost(ip, host string, source clientSource) (bool, error) {
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
// check index
|
||||||
|
_, ok := clients.ipHost[ip]
|
||||||
|
if ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
clients.ipHost[ip] = ClientHost{
|
||||||
|
Host: host,
|
||||||
|
Source: source,
|
||||||
|
}
|
||||||
|
log.Tracef("'%s': '%s' -> [%d]", host, ip, len(clients.ipHost))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Parse system 'hosts' file and fill clients array
|
// Parse system 'hosts' file and fill clients array
|
||||||
func fillClientInfo() {
|
func clientsAddFromHostsFile() {
|
||||||
hostsFn := "/etc/hosts"
|
hostsFn := "/etc/hosts"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
|
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
|
||||||
|
@ -40,6 +272,7 @@ func fillClientInfo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(string(d), "\n")
|
lines := strings.Split(string(d), "\n")
|
||||||
|
n := 0
|
||||||
for _, ln := range lines {
|
for _, ln := range lines {
|
||||||
ln = strings.TrimSpace(ln)
|
ln = strings.TrimSpace(ln)
|
||||||
if len(ln) == 0 || ln[0] == '#' {
|
if len(ln) == 0 || ln[0] == '#' {
|
||||||
|
@ -51,33 +284,71 @@ func fillClientInfo() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var c Client
|
ok, e := clientAddHost(fields[0], fields[1], ClientSourceHostsFile)
|
||||||
c.IP = fields[0]
|
if e != nil {
|
||||||
c.Name = fields[1]
|
log.Tracef("%s", e)
|
||||||
clients = append(clients, c)
|
}
|
||||||
log.Tracef("%s -> %s", c.IP, c.Name)
|
if ok {
|
||||||
|
n++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Added %d client aliases from %s", len(clients), hostsFn)
|
log.Info("Added %d client aliases from %s", n, hostsFn)
|
||||||
clientsFilled = true
|
}
|
||||||
|
|
||||||
|
type clientHostJSON struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientListJSON struct {
|
||||||
|
Clients []clientJSON `json:"clients"`
|
||||||
|
AutoClients []clientHostJSON `json:"auto_clients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// respond with information about configured clients
|
// respond with information about configured clients
|
||||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Tracef("%s %v", r.Method, r.URL)
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
if !clientsFilled {
|
data := clientListJSON{}
|
||||||
fillClientInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
data := []clientJSON{}
|
clients.lock.Lock()
|
||||||
for _, c := range clients {
|
for _, c := range clients.list {
|
||||||
cj := clientJSON{
|
cj := clientJSON{
|
||||||
IP: c.IP,
|
IP: c.IP,
|
||||||
Name: c.Name,
|
MAC: c.MAC,
|
||||||
|
Name: c.Name,
|
||||||
|
UseGlobalSettings: !c.UseOwnSettings,
|
||||||
|
FilteringEnabled: c.FilteringEnabled,
|
||||||
|
ParentalEnabled: c.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: c.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
||||||
}
|
}
|
||||||
data = append(data, cj)
|
|
||||||
|
if len(c.MAC) != 0 {
|
||||||
|
hwAddr, _ := net.ParseMAC(c.MAC)
|
||||||
|
ipAddr := dhcpServer.FindIPbyMAC(hwAddr)
|
||||||
|
if ipAddr != nil {
|
||||||
|
cj.IP = ipAddr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Clients = append(data.Clients, cj)
|
||||||
}
|
}
|
||||||
|
for ip, ch := range clients.ipHost {
|
||||||
|
cj := clientHostJSON{
|
||||||
|
IP: ip,
|
||||||
|
Name: ch.Host,
|
||||||
|
}
|
||||||
|
cj.Source = "etc/hosts"
|
||||||
|
if ch.Source == ClientSourceRDNS {
|
||||||
|
cj.Source = "rDNS"
|
||||||
|
}
|
||||||
|
data.AutoClients = append(data.AutoClients, cj)
|
||||||
|
}
|
||||||
|
clients.lock.Unlock()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
e := json.NewEncoder(w).Encode(data)
|
e := json.NewEncoder(w).Encode(data)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
@ -86,7 +357,126 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert JSON object to Client object
|
||||||
|
func jsonToClient(cj clientJSON) (*Client, error) {
|
||||||
|
c := Client{
|
||||||
|
IP: cj.IP,
|
||||||
|
MAC: cj.MAC,
|
||||||
|
Name: cj.Name,
|
||||||
|
UseOwnSettings: !cj.UseGlobalSettings,
|
||||||
|
FilteringEnabled: cj.FilteringEnabled,
|
||||||
|
ParentalEnabled: cj.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: cj.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new client
|
||||||
|
func handleAddClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cj := clientJSON{}
|
||||||
|
err = json.Unmarshal(body, &cj)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := jsonToClient(cj)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ok, err := clientAdd(*c)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
httpError(w, http.StatusBadRequest, "Client already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = writeAllConfigsAndReloadDNS()
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove client
|
||||||
|
func handleDelClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cj := clientJSON{}
|
||||||
|
err = json.Unmarshal(body, &cj)
|
||||||
|
if err != nil || len(cj.Name) == 0 {
|
||||||
|
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !clientDel(cj.Name) {
|
||||||
|
httpError(w, http.StatusBadRequest, "Client not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = writeAllConfigsAndReloadDNS()
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientUpdateJSON struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data clientJSON `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update client's properties
|
||||||
|
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dj clientUpdateJSON
|
||||||
|
err = json.Unmarshal(body, &dj)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(dj.Name) == 0 {
|
||||||
|
httpError(w, http.StatusBadRequest, "Invalid request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := jsonToClient(dj.Data)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = clientUpdate(dj.Name, *c)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = writeAllConfigsAndReloadDNS()
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterClientsHandlers registers HTTP handlers
|
// RegisterClientsHandlers registers HTTP handlers
|
||||||
func RegisterClientsHandlers() {
|
func RegisterClientsHandlers() {
|
||||||
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
|
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
|
||||||
|
http.HandleFunc("/control/clients/add", postInstall(optionalAuth(ensurePOST(handleAddClient))))
|
||||||
|
http.HandleFunc("/control/clients/delete", postInstall(optionalAuth(ensurePOST(handleDelClient))))
|
||||||
|
http.HandleFunc("/control/clients/update", postInstall(optionalAuth(ensurePOST(handleUpdateClient))))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestClients(t *testing.T) {
|
||||||
|
var c Client
|
||||||
|
var e error
|
||||||
|
var b bool
|
||||||
|
|
||||||
|
clientsInit()
|
||||||
|
|
||||||
|
// add
|
||||||
|
c = Client{
|
||||||
|
IP: "1.1.1.1",
|
||||||
|
Name: "client1",
|
||||||
|
}
|
||||||
|
b, e = clientAdd(c)
|
||||||
|
if !b || e != nil {
|
||||||
|
t.Fatalf("clientAdd #1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add #2
|
||||||
|
c = Client{
|
||||||
|
IP: "2.2.2.2",
|
||||||
|
Name: "client2",
|
||||||
|
}
|
||||||
|
b, e = clientAdd(c)
|
||||||
|
if !b || e != nil {
|
||||||
|
t.Fatalf("clientAdd #2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed add - name in use
|
||||||
|
c = Client{
|
||||||
|
IP: "1.2.3.5",
|
||||||
|
Name: "client1",
|
||||||
|
}
|
||||||
|
b, e = clientAdd(c)
|
||||||
|
if b {
|
||||||
|
t.Fatalf("clientAdd - name in use")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed add - ip in use
|
||||||
|
c = Client{
|
||||||
|
IP: "2.2.2.2",
|
||||||
|
Name: "client3",
|
||||||
|
}
|
||||||
|
b, e = clientAdd(c)
|
||||||
|
if b || e == nil {
|
||||||
|
t.Fatalf("clientAdd - ip in use")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get
|
||||||
|
if clientExists("1.2.3.4") {
|
||||||
|
t.Fatalf("clientExists")
|
||||||
|
}
|
||||||
|
if !clientExists("1.1.1.1") {
|
||||||
|
t.Fatalf("clientExists #1")
|
||||||
|
}
|
||||||
|
if !clientExists("2.2.2.2") {
|
||||||
|
t.Fatalf("clientExists #2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed update - no such name
|
||||||
|
c.IP = "1.2.3.0"
|
||||||
|
c.Name = "client3"
|
||||||
|
if clientUpdate("client3", c) == nil {
|
||||||
|
t.Fatalf("clientUpdate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed update - name in use
|
||||||
|
c.IP = "1.2.3.0"
|
||||||
|
c.Name = "client2"
|
||||||
|
if clientUpdate("client1", c) == nil {
|
||||||
|
t.Fatalf("clientUpdate - name in use")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed update - ip in use
|
||||||
|
c.IP = "2.2.2.2"
|
||||||
|
c.Name = "client1"
|
||||||
|
if clientUpdate("client1", c) == nil {
|
||||||
|
t.Fatalf("clientUpdate - ip in use")
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
c.IP = "1.1.1.2"
|
||||||
|
c.Name = "client1"
|
||||||
|
if clientUpdate("client1", c) != nil {
|
||||||
|
t.Fatalf("clientUpdate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get after update
|
||||||
|
if clientExists("1.1.1.1") || !clientExists("1.1.1.2") {
|
||||||
|
t.Fatalf("clientExists - get after update")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed remove - no such name
|
||||||
|
if clientDel("client3") {
|
||||||
|
t.Fatalf("clientDel - no such name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
if !clientDel("client1") || clientExists("1.1.1.2") {
|
||||||
|
t.Fatalf("clientDel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add host client
|
||||||
|
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
|
||||||
|
if !b || e != nil {
|
||||||
|
t.Fatalf("clientAddHost")
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed add - ip exists
|
||||||
|
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
|
||||||
|
if b {
|
||||||
|
t.Fatalf("clientAddHost - ip exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get
|
||||||
|
if !clientExists("1.1.1.1") {
|
||||||
|
t.Fatalf("clientAddHost")
|
||||||
|
}
|
||||||
|
}
|
54
config.go
54
config.go
|
@ -27,6 +27,17 @@ type logSettings struct {
|
||||||
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
|
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type clientObject struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
IP string `yaml:"ip"`
|
||||||
|
MAC string `yaml:"mac"`
|
||||||
|
UseGlobalSettings bool `yaml:"use_global_settings"`
|
||||||
|
FilteringEnabled bool `yaml:"filtering_enabled"`
|
||||||
|
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||||
|
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
|
||||||
|
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
// configuration is loaded from YAML
|
// configuration is loaded from YAML
|
||||||
// field ordering is important -- yaml fields will mirror ordering from here
|
// field ordering is important -- yaml fields will mirror ordering from here
|
||||||
type configuration struct {
|
type configuration struct {
|
||||||
|
@ -54,6 +65,9 @@ type configuration struct {
|
||||||
UserRules []string `yaml:"user_rules"`
|
UserRules []string `yaml:"user_rules"`
|
||||||
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
|
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
|
||||||
|
|
||||||
|
// Note: this array is filled only before file read/write and then it's cleared
|
||||||
|
Clients []clientObject `yaml:"clients"`
|
||||||
|
|
||||||
logSettings `yaml:",inline"`
|
logSettings `yaml:",inline"`
|
||||||
|
|
||||||
sync.RWMutex `yaml:"-"`
|
sync.RWMutex `yaml:"-"`
|
||||||
|
@ -206,6 +220,25 @@ func parseConfig() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientsInit()
|
||||||
|
for _, cy := range config.Clients {
|
||||||
|
cli := Client{
|
||||||
|
Name: cy.Name,
|
||||||
|
IP: cy.IP,
|
||||||
|
MAC: cy.MAC,
|
||||||
|
UseOwnSettings: !cy.UseGlobalSettings,
|
||||||
|
FilteringEnabled: cy.FilteringEnabled,
|
||||||
|
ParentalEnabled: cy.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: cy.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
|
||||||
|
}
|
||||||
|
_, err = clientAdd(cli)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("clientAdd: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.Clients = nil
|
||||||
|
|
||||||
// Deduplicate filters
|
// Deduplicate filters
|
||||||
deduplicateFilters()
|
deduplicateFilters()
|
||||||
|
|
||||||
|
@ -233,9 +266,30 @@ func readConfigFile() ([]byte, error) {
|
||||||
func (c *configuration) write() error {
|
func (c *configuration) write() error {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
|
|
||||||
|
clientsList := clientsGetList()
|
||||||
|
for _, cli := range clientsList {
|
||||||
|
ip := cli.IP
|
||||||
|
if len(cli.MAC) != 0 {
|
||||||
|
ip = ""
|
||||||
|
}
|
||||||
|
cy := clientObject{
|
||||||
|
Name: cli.Name,
|
||||||
|
IP: ip,
|
||||||
|
MAC: cli.MAC,
|
||||||
|
UseGlobalSettings: !cli.UseOwnSettings,
|
||||||
|
FilteringEnabled: cli.FilteringEnabled,
|
||||||
|
ParentalEnabled: cli.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: cli.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
||||||
|
}
|
||||||
|
config.Clients = append(config.Clients, cy)
|
||||||
|
}
|
||||||
|
|
||||||
configFile := config.getConfigFilename()
|
configFile := config.getConfigFilename()
|
||||||
log.Debug("Writing YAML file: %s", configFile)
|
log.Debug("Writing YAML file: %s", configFile)
|
||||||
yamlText, err := yaml.Marshal(&config)
|
yamlText, err := yaml.Marshal(&config)
|
||||||
|
config.Clients = nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Couldn't generate YAML file: %s", err)
|
log.Error("Couldn't generate YAML file: %s", err)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -540,6 +540,19 @@ func (s *Server) printLeases() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases
|
||||||
|
func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
for _, l := range s.leases {
|
||||||
|
if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) {
|
||||||
|
return l.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset internal state
|
// Reset internal state
|
||||||
func (s *Server) reset() {
|
func (s *Server) reset() {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
|
|
23
dns.go
23
dns.go
|
@ -70,9 +70,32 @@ func generateServerConfig() dnsforward.ServerConfig {
|
||||||
newconfig.Upstreams = upstreamConfig.Upstreams
|
newconfig.Upstreams = upstreamConfig.Upstreams
|
||||||
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
|
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
|
||||||
newconfig.AllServers = config.DNS.AllServers
|
newconfig.AllServers = config.DNS.AllServers
|
||||||
|
newconfig.FilterHandler = applyClientSettings
|
||||||
return newconfig
|
return newconfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a client has his own settings, apply them
|
||||||
|
func applyClientSettings(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
||||||
|
c, ok := clientFind(clientAddr)
|
||||||
|
if !ok || !c.UseOwnSettings {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Using settings for client with IP %s", clientAddr)
|
||||||
|
if !c.FilteringEnabled {
|
||||||
|
setts.FilteringEnabled = false
|
||||||
|
}
|
||||||
|
if !c.SafeSearchEnabled {
|
||||||
|
setts.SafeSearchEnabled = false
|
||||||
|
}
|
||||||
|
if !c.SafeBrowsingEnabled {
|
||||||
|
setts.SafeBrowsingEnabled = false
|
||||||
|
}
|
||||||
|
if !c.ParentalEnabled {
|
||||||
|
setts.ParentalEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startDNSServer() error {
|
func startDNSServer() error {
|
||||||
if isRunning() {
|
if isRunning() {
|
||||||
return fmt.Errorf("unable to start forwarding DNS server: Already running")
|
return fmt.Errorf("unable to start forwarding DNS server: Already running")
|
||||||
|
|
|
@ -46,6 +46,14 @@ const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet
|
||||||
const enableFastLookup = true // flag for debugging, must be true in production for faster performance
|
const enableFastLookup = true // flag for debugging, must be true in production for faster performance
|
||||||
const enableDelayedCompilation = true // flag for debugging, must be true in production for faster performance
|
const enableDelayedCompilation = true // flag for debugging, must be true in production for faster performance
|
||||||
|
|
||||||
|
// Custom filtering settings
|
||||||
|
type RequestFilteringSettings struct {
|
||||||
|
FilteringEnabled bool
|
||||||
|
SafeSearchEnabled bool
|
||||||
|
SafeBrowsingEnabled bool
|
||||||
|
ParentalEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
// Config allows you to configure DNS filtering with New() or just change variables directly.
|
// Config allows you to configure DNS filtering with New() or just change variables directly.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
FilteringTempFilename string `yaml:"filtering_temp_filename"` // temporary file for storing unused filtering rules
|
FilteringTempFilename string `yaml:"filtering_temp_filename"` // temporary file for storing unused filtering rules
|
||||||
|
@ -55,6 +63,9 @@ type Config struct {
|
||||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||||
ResolverAddress string // DNS server address
|
ResolverAddress string // DNS server address
|
||||||
|
|
||||||
|
// Filtering callback function
|
||||||
|
FilterHandler func(clientAddr string, settings *RequestFilteringSettings) `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type privateConfig struct {
|
type privateConfig struct {
|
||||||
|
@ -149,7 +160,7 @@ func (r Reason) Matched() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
|
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
|
||||||
func (d *Dnsfilter) CheckHost(host string, qtype uint16) (Result, error) {
|
func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Result, error) {
|
||||||
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
|
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
|
||||||
if host == "" {
|
if host == "" {
|
||||||
return Result{Reason: NotFilteredNotFound}, nil
|
return Result{Reason: NotFilteredNotFound}, nil
|
||||||
|
@ -160,17 +171,30 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16) (Result, error) {
|
||||||
return Result{}, nil
|
return Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// try filter lists first
|
var setts RequestFilteringSettings
|
||||||
result, err := d.matchHost(host, qtype)
|
setts.FilteringEnabled = true
|
||||||
if err != nil {
|
setts.SafeSearchEnabled = d.SafeSearchEnabled
|
||||||
return result, err
|
setts.SafeBrowsingEnabled = d.SafeBrowsingEnabled
|
||||||
|
setts.ParentalEnabled = d.ParentalEnabled
|
||||||
|
if len(clientAddr) != 0 && d.FilterHandler != nil {
|
||||||
|
d.FilterHandler(clientAddr, &setts)
|
||||||
}
|
}
|
||||||
if result.Reason.Matched() {
|
|
||||||
return result, nil
|
var result Result
|
||||||
|
var err error
|
||||||
|
// try filter lists first
|
||||||
|
if setts.FilteringEnabled {
|
||||||
|
result, err = d.matchHost(host, qtype)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if result.Reason.Matched() {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check safeSearch if no match
|
// check safeSearch if no match
|
||||||
if d.SafeSearchEnabled {
|
if setts.SafeSearchEnabled {
|
||||||
result, err = d.checkSafeSearch(host)
|
result, err = d.checkSafeSearch(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to safesearch HTTP lookup, ignoring check: %v", err)
|
log.Printf("Failed to safesearch HTTP lookup, ignoring check: %v", err)
|
||||||
|
@ -183,7 +207,7 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16) (Result, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check safebrowsing if no match
|
// check safebrowsing if no match
|
||||||
if d.SafeBrowsingEnabled {
|
if setts.SafeBrowsingEnabled {
|
||||||
result, err = d.checkSafeBrowsing(host)
|
result, err = d.checkSafeBrowsing(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
|
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
|
||||||
|
@ -196,7 +220,7 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16) (Result, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check parental if no match
|
// check parental if no match
|
||||||
if d.ParentalEnabled {
|
if setts.ParentalEnabled {
|
||||||
result, err = d.checkParental(host)
|
result, err = d.checkParental(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
|
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
// SAFE SEARCH
|
// SAFE SEARCH
|
||||||
// PARENTAL
|
// PARENTAL
|
||||||
// FILTERING
|
// FILTERING
|
||||||
|
// CLIENTS SETTINGS
|
||||||
// BENCHMARKS
|
// BENCHMARKS
|
||||||
|
|
||||||
// HELPERS
|
// HELPERS
|
||||||
|
@ -52,7 +53,7 @@ func NewForTestFilters(filters map[int]string) *Dnsfilter {
|
||||||
|
|
||||||
func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +64,7 @@ func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
||||||
|
|
||||||
func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype uint16) {
|
func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype uint16) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ret, err := d.CheckHost(hostname, qtype)
|
ret, err := d.CheckHost(hostname, qtype, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +78,7 @@ func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype
|
||||||
|
|
||||||
func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) {
|
func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
|
@ -212,7 +213,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
|
||||||
|
|
||||||
// Check host for each domain
|
// Check host for each domain
|
||||||
for _, host := range yandex {
|
for _, host := range yandex {
|
||||||
result, err := d.CheckHost(host, dns.TypeA)
|
result, err := d.CheckHost(host, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err)
|
t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err)
|
||||||
}
|
}
|
||||||
|
@ -235,7 +236,7 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
||||||
|
|
||||||
// Check host for each domain
|
// Check host for each domain
|
||||||
for _, host := range googleDomains {
|
for _, host := range googleDomains {
|
||||||
result, err := d.CheckHost(host, dns.TypeA)
|
result, err := d.CheckHost(host, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("SafeSearch doesn't work for %s cause %s", host, err)
|
t.Errorf("SafeSearch doesn't work for %s cause %s", host, err)
|
||||||
}
|
}
|
||||||
|
@ -255,7 +256,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Check host with disabled safesearch
|
// Check host with disabled safesearch
|
||||||
result, err = d.CheckHost(domain, dns.TypeA)
|
result, err = d.CheckHost(domain, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Cannot check host due to %s", err)
|
t.Fatalf("Cannot check host due to %s", err)
|
||||||
}
|
}
|
||||||
|
@ -265,7 +266,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
||||||
|
|
||||||
// Enable safesearch
|
// Enable safesearch
|
||||||
d.SafeSearchEnabled = true
|
d.SafeSearchEnabled = true
|
||||||
result, err = d.CheckHost(domain, dns.TypeA)
|
result, err = d.CheckHost(domain, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
||||||
}
|
}
|
||||||
|
@ -295,7 +296,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||||
d := NewForTest()
|
d := NewForTest()
|
||||||
defer d.Destroy()
|
defer d.Destroy()
|
||||||
domain := "www.google.ru"
|
domain := "www.google.ru"
|
||||||
result, err := d.CheckHost(domain, dns.TypeA)
|
result, err := d.CheckHost(domain, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Cannot check host due to %s", err)
|
t.Fatalf("Cannot check host due to %s", err)
|
||||||
}
|
}
|
||||||
|
@ -324,7 +325,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err = d.CheckHost(domain, dns.TypeA)
|
result, err = d.CheckHost(domain, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
||||||
}
|
}
|
||||||
|
@ -441,7 +442,7 @@ func TestMatching(t *testing.T) {
|
||||||
d := NewForTestFilters(filters)
|
d := NewForTestFilters(filters)
|
||||||
defer d.Destroy()
|
defer d.Destroy()
|
||||||
|
|
||||||
ret, err := d.CheckHost(test.hostname, dns.TypeA)
|
ret, err := d.CheckHost(test.hostname, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", test.hostname, err)
|
t.Errorf("Error while matching host %s: %s", test.hostname, err)
|
||||||
}
|
}
|
||||||
|
@ -455,6 +456,52 @@ func TestMatching(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CLIENT SETTINGS
|
||||||
|
|
||||||
|
func applyClientSettings(clientAddr string, setts *RequestFilteringSettings) {
|
||||||
|
setts.FilteringEnabled = false
|
||||||
|
setts.ParentalEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientSettings(t *testing.T) {
|
||||||
|
var r Result
|
||||||
|
filters := make(map[int]string)
|
||||||
|
filters[0] = "||example.org^\n"
|
||||||
|
d := NewForTestFilters(filters)
|
||||||
|
defer d.Destroy()
|
||||||
|
d.ParentalEnabled = true
|
||||||
|
d.ParentalSensitivity = 3
|
||||||
|
|
||||||
|
// no client settings:
|
||||||
|
|
||||||
|
// blocked by filters
|
||||||
|
r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1")
|
||||||
|
if !r.IsFiltered || r.Reason != FilteredBlackList {
|
||||||
|
t.Fatalf("CheckHost FilteredBlackList")
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocked by parental
|
||||||
|
r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1")
|
||||||
|
if !r.IsFiltered || r.Reason != FilteredParental {
|
||||||
|
t.Fatalf("CheckHost FilteredParental")
|
||||||
|
}
|
||||||
|
|
||||||
|
// override client settings:
|
||||||
|
d.FilterHandler = applyClientSettings
|
||||||
|
|
||||||
|
// override filtering settings
|
||||||
|
r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1")
|
||||||
|
if r.IsFiltered {
|
||||||
|
t.Fatalf("CheckHost")
|
||||||
|
}
|
||||||
|
|
||||||
|
// override parental settings
|
||||||
|
r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1")
|
||||||
|
if r.IsFiltered {
|
||||||
|
t.Fatalf("CheckHost")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BENCHMARKS
|
// BENCHMARKS
|
||||||
|
|
||||||
func BenchmarkSafeBrowsing(b *testing.B) {
|
func BenchmarkSafeBrowsing(b *testing.B) {
|
||||||
|
@ -463,7 +510,7 @@ func BenchmarkSafeBrowsing(b *testing.B) {
|
||||||
d.SafeBrowsingEnabled = true
|
d.SafeBrowsingEnabled = true
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
hostname := "wmconvirus.narod.ru"
|
hostname := "wmconvirus.narod.ru"
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Errorf("Error while matching host %s: %s", hostname, err)
|
b.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
|
@ -480,7 +527,7 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
hostname := "wmconvirus.narod.ru"
|
hostname := "wmconvirus.narod.ru"
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Errorf("Error while matching host %s: %s", hostname, err)
|
b.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -378,7 +378,11 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
|
||||||
var res dnsfilter.Result
|
var res dnsfilter.Result
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype)
|
clientAddr := ""
|
||||||
|
if d.Addr != nil {
|
||||||
|
clientAddr, _, _ = net.SplitHostPort(d.Addr.String())
|
||||||
|
}
|
||||||
|
res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, clientAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Return immediately if there's an error
|
// Return immediately if there's an error
|
||||||
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
||||||
|
|
|
@ -2,7 +2,7 @@ swagger: '2.0'
|
||||||
info:
|
info:
|
||||||
title: 'AdGuard Home'
|
title: 'AdGuard Home'
|
||||||
description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
|
description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
|
||||||
version: 0.95.0
|
version: 0.96.0
|
||||||
schemes:
|
schemes:
|
||||||
- http
|
- http
|
||||||
basePath: /control
|
basePath: /control
|
||||||
|
@ -424,7 +424,7 @@ paths:
|
||||||
description: OK
|
description: OK
|
||||||
|
|
||||||
/filtering/add_url:
|
/filtering/add_url:
|
||||||
put:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- filtering
|
- filtering
|
||||||
operationId: filteringAddURL
|
operationId: filteringAddURL
|
||||||
|
@ -444,7 +444,7 @@ paths:
|
||||||
description: OK
|
description: OK
|
||||||
|
|
||||||
/filtering/remove_url:
|
/filtering/remove_url:
|
||||||
delete:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- filtering
|
- filtering
|
||||||
operationId: filteringRemoveURL
|
operationId: filteringRemoveURL
|
||||||
|
@ -530,7 +530,7 @@ paths:
|
||||||
description: OK with how many filters were actually updated
|
description: OK with how many filters were actually updated
|
||||||
|
|
||||||
/filtering/set_rules:
|
/filtering/set_rules:
|
||||||
put:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- filtering
|
- filtering
|
||||||
operationId: filteringSetRules
|
operationId: filteringSetRules
|
||||||
|
@ -698,6 +698,54 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/Clients"
|
$ref: "#/definitions/Clients"
|
||||||
|
|
||||||
|
/clients/add:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- clients
|
||||||
|
operationId: clientsAdd
|
||||||
|
summary: 'Add a new client'
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Client"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
|
/clients/delete:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- clients
|
||||||
|
operationId: clientsDelete
|
||||||
|
summary: 'Remove a client'
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/ClientDelete"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
|
/clients/update:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- clients
|
||||||
|
operationId: clientsUpdate
|
||||||
|
summary: 'Update client information'
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/ClientUpdate"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# I18N methods
|
# I18N methods
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
@ -1377,11 +1425,65 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
description: "Name"
|
description: "Name"
|
||||||
example: "localhost"
|
example: "localhost"
|
||||||
|
mac:
|
||||||
|
type: "string"
|
||||||
|
use_global_settings:
|
||||||
|
type: "boolean"
|
||||||
|
filtering_enabled:
|
||||||
|
type: "boolean"
|
||||||
|
parental_enabled:
|
||||||
|
type: "boolean"
|
||||||
|
safebrowsing_enabled:
|
||||||
|
type: "boolean"
|
||||||
|
safesearch_enabled:
|
||||||
|
type: "boolean"
|
||||||
|
ClientAuto:
|
||||||
|
type: "object"
|
||||||
|
description: "Auto-Client information"
|
||||||
|
properties:
|
||||||
|
ip:
|
||||||
|
type: "string"
|
||||||
|
description: "IP address"
|
||||||
|
example: "127.0.0.1"
|
||||||
|
name:
|
||||||
|
type: "string"
|
||||||
|
description: "Name"
|
||||||
|
example: "localhost"
|
||||||
|
source:
|
||||||
|
type: "string"
|
||||||
|
description: "The source of this information"
|
||||||
|
example: "etc/hosts"
|
||||||
|
ClientUpdate:
|
||||||
|
type: "object"
|
||||||
|
description: "Client update request"
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: "string"
|
||||||
|
data:
|
||||||
|
$ref: "#/definitions/Client"
|
||||||
|
ClientDelete:
|
||||||
|
type: "object"
|
||||||
|
description: "Client delete request"
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: "string"
|
||||||
Clients:
|
Clients:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
clients:
|
||||||
|
$ref: "#/definitions/ClientsArray"
|
||||||
|
auto_clients:
|
||||||
|
$ref: "#/definitions/ClientsAutoArray"
|
||||||
|
ClientsArray:
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/Client"
|
$ref: "#/definitions/Client"
|
||||||
description: "Clients array"
|
description: "Clients array"
|
||||||
|
ClientsAutoArray:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/ClientAuto"
|
||||||
|
description: "Auto-Clients array"
|
||||||
CheckConfigRequest:
|
CheckConfigRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Configuration to be checked"
|
description: "Configuration to be checked"
|
||||||
|
|
Loading…
Reference in New Issue