mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-16 11:52:58 +03:00
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
|
||||
* Get version 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
|
||||
* "Check 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.
|
||||
|
||||
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",
|
||||
"form_error_required": "Required field",
|
||||
"form_error_ip_format": "Invalid IPv4 format",
|
||||
"form_error_mac_format": "Invalid MAC format",
|
||||
"form_error_positive": "Must be greater than 0",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Subnet mask",
|
||||
@ -105,6 +106,7 @@
|
||||
"rules_count_table_header": "Rules count",
|
||||
"last_time_updated_table_header": "Last time updated",
|
||||
"actions_table_header": "Actions",
|
||||
"edit_table_action": "Edit",
|
||||
"delete_table_action": "Delete",
|
||||
"filters_and_hosts": "Filters and hosts blocklists",
|
||||
"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.",
|
||||
"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.",
|
||||
"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"
|
||||
}
|
84
client/src/actions/clients.js
Normal file
84
client/src/actions/clients.js
Normal file
@ -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 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 Api from '../api/Api';
|
||||
|
||||
@ -213,14 +213,41 @@ export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||
export const getClients = () => async (dispatch) => {
|
||||
dispatch(getClientsRequest());
|
||||
try {
|
||||
const clients = await apiClient.getGlobalClients();
|
||||
dispatch(getClientsSuccess(clients));
|
||||
const data = await apiClient.getClients();
|
||||
const sortedClients = data.clients && sortClients(data.clients);
|
||||
const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);
|
||||
|
||||
dispatch(getClientsSuccess({
|
||||
clients: sortedClients || [],
|
||||
autoClients: sortedAutoClients || [],
|
||||
}));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
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 dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||
@ -232,6 +259,7 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||
dispatch(dnsStatusSuccess(dnsStatus));
|
||||
dispatch(getVersion());
|
||||
dispatch(getClients());
|
||||
dispatch(getTopStats());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
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 getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||
|
@ -39,8 +39,6 @@ export default class Api {
|
||||
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
|
||||
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
|
||||
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
||||
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
|
||||
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' };
|
||||
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
|
||||
|
||||
restartGlobalFiltering() {
|
||||
@ -142,11 +140,6 @@ export default class Api {
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getGlobalClients() {
|
||||
const { path, method } = this.GLOBAL_CLIENTS;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getUpdate() {
|
||||
const { path, method } = this.GLOBAL_UPDATE;
|
||||
return this.makeRequest(path, method);
|
||||
@ -409,4 +402,42 @@ export default class Api {
|
||||
};
|
||||
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 UpdateOverlay from '../ui/UpdateOverlay';
|
||||
import EncryptionTopline from '../ui/EncryptionTopline';
|
||||
import Icons from '../ui/Icons';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
class App extends Component {
|
||||
@ -103,6 +104,7 @@ class App extends Component {
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
<Icons />
|
||||
</Fragment>
|
||||
</HashRouter>
|
||||
);
|
||||
|
@ -24,7 +24,8 @@ class Clients extends Component {
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: ({ value }) => {
|
||||
const clientName = getClientName(this.props.clients, value);
|
||||
const clientName = getClientName(this.props.clients, value)
|
||||
|| getClientName(this.props.autoClients, value);
|
||||
let client;
|
||||
|
||||
if (clientName) {
|
||||
@ -79,6 +80,7 @@ Clients.propTypes = {
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
clients: PropTypes.array.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
@ -96,6 +96,7 @@ class Dashboard extends Component {
|
||||
refreshButton={refreshButton}
|
||||
topClients={dashboard.topStats.top_clients}
|
||||
clients={dashboard.clients}
|
||||
autoClients={dashboard.autoClients}
|
||||
/>
|
||||
</div>
|
||||
<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 Card from '../ui/Card';
|
||||
import UserRules from './UserRules';
|
||||
import './Filters.css';
|
||||
|
||||
class Filters extends Component {
|
||||
componentDidMount() {
|
||||
@ -59,7 +58,18 @@ class Filters extends Component {
|
||||
}, {
|
||||
Header: <Trans>actions_table_header</Trans>,
|
||||
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',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
|
@ -5,6 +5,10 @@
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.logs__row--center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs__row--overflow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -196,7 +196,8 @@ class Logs extends Component {
|
||||
Cell: (row) => {
|
||||
const { reason } = row.original;
|
||||
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
||||
const clientName = getClientName(dashboard.clients, row.value);
|
||||
const clientName = getClientName(dashboard.clients, row.value)
|
||||
|| getClientName(dashboard.autoClients, row.value);
|
||||
let client;
|
||||
|
||||
if (clientName) {
|
||||
|
118
client/src/components/Settings/Clients/AutoClients.js
Normal file
118
client/src/components/Settings/Clients/AutoClients.js
Normal file
@ -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);
|
217
client/src/components/Settings/Clients/Form.js
Normal file
217
client/src/components/Settings/Clients/Form.js
Normal file
@ -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);
|
64
client/src/components/Settings/Clients/Modal.js
Normal file
64
client/src/components/Settings/Clients/Modal.js
Normal file
@ -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);
|
263
client/src/components/Settings/Clients/index.js
Normal file
263
client/src/components/Settings/Clients/index.js
Normal file
@ -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 {
|
||||
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 PropTypes from 'prop-types';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
|
||||
import Upstream from './Upstream';
|
||||
import Dhcp from './Dhcp';
|
||||
import Encryption from './Encryption';
|
||||
import Clients from './Clients';
|
||||
import AutoClients from './Clients/AutoClients';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import Loading from '../ui/Loading';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import './Settings.css';
|
||||
|
||||
class Settings extends Component {
|
||||
@ -46,29 +50,38 @@ class Settings extends Component {
|
||||
return Object.keys(settings).map((key) => {
|
||||
const setting = settings[key];
|
||||
const { enabled } = setting;
|
||||
return (<Checkbox
|
||||
key={key}
|
||||
{...settings[key]}
|
||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||
/>);
|
||||
return (
|
||||
<Checkbox
|
||||
key={key}
|
||||
{...settings[key]}
|
||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div><Trans>no_settings</Trans></div>
|
||||
<div>
|
||||
<Trans>no_settings</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, dashboard, t } = this.props;
|
||||
const {
|
||||
settings, dashboard, clients, t,
|
||||
} = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={ t('settings') } />
|
||||
<PageTitle title={t('settings')} />
|
||||
{settings.processing && <Loading />}
|
||||
{!settings.processing &&
|
||||
{!settings.processing && (
|
||||
<div className="content">
|
||||
<div className="row">
|
||||
<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">
|
||||
{this.renderSettings(settings.settingsList)}
|
||||
</div>
|
||||
@ -82,6 +95,28 @@ class Settings extends Component {
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
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={this.props.encryption}
|
||||
setTlsConfig={this.props.setTlsConfig}
|
||||
@ -97,7 +132,7 @@ class Settings extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 103;
|
||||
z-index: 105;
|
||||
width: 345px;
|
||||
}
|
||||
|
||||
|
5
client/src/components/ui/Icons.css
Normal file
5
client/src/components/ui/Icons.css
Normal file
@ -0,0 +1,5 @@
|
||||
.icons {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import './Icons.css';
|
||||
|
||||
const Icons = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="hidden">
|
||||
<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">
|
||||
<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 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>
|
||||
);
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
z-index: 104;
|
||||
}
|
||||
|
||||
.ReactModal__Overlay--after-open {
|
||||
@ -38,3 +38,9 @@
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.modal-dialog--clients {
|
||||
max-width: 650px;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,12 @@ import {
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
} from '../actions/encryption';
|
||||
import {
|
||||
addClient,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
toggleClientModal,
|
||||
} from '../actions/clients';
|
||||
import Settings from '../components/Settings';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@ -25,12 +31,14 @@ const mapStateToProps = (state) => {
|
||||
dashboard,
|
||||
dhcp,
|
||||
encryption,
|
||||
clients,
|
||||
} = state;
|
||||
const props = {
|
||||
settings,
|
||||
dashboard,
|
||||
dhcp,
|
||||
encryption,
|
||||
clients,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
@ -50,6 +58,10 @@ const mapDispatchToProps = {
|
||||
getTlsStatus,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
addClient,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
toggleClientModal,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
@ -1,5 +1,6 @@
|
||||
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_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 = {
|
||||
avg_processing_time: 'average_processing_time',
|
||||
@ -19,7 +20,8 @@ export const STATUS_COLORS = {
|
||||
|
||||
export const REPOSITORY = {
|
||||
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';
|
||||
@ -165,3 +167,13 @@ export const DHCP_STATUS_RESPONSE = {
|
||||
NO: 'no',
|
||||
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 { 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 = ({
|
||||
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
||||
@ -55,6 +55,13 @@ export const ipv4 = (value) => {
|
||||
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) => {
|
||||
if ((value || value === 0) && (value <= 0)) {
|
||||
return <Trans>form_error_positive</Trans>;
|
||||
|
@ -208,3 +208,20 @@ export const getClientName = (clients, ip) => {
|
||||
const client = clients.find(item => ip === item.ip);
|
||||
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);
|
||||
};
|
||||
|
63
client/src/reducers/clients.js
Normal file
63
client/src/reducers/clients.js
Normal file
@ -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 toasts from './toasts';
|
||||
import encryption from './encryption';
|
||||
import clients from './clients';
|
||||
|
||||
const settings = handleActions({
|
||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||
@ -184,7 +185,8 @@ const dashboard = handleActions({
|
||||
[actions.getClientsSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
clients: payload,
|
||||
clients: payload.clients,
|
||||
autoClients: payload.autoClients,
|
||||
processingClients: false,
|
||||
};
|
||||
return newState;
|
||||
@ -209,6 +211,8 @@ const dashboard = handleActions({
|
||||
dnsAddresses: [],
|
||||
dnsVersion: '',
|
||||
clients: [],
|
||||
autoClients: [],
|
||||
topStats: [],
|
||||
});
|
||||
|
||||
const queryLogs = handleActions({
|
||||
@ -361,6 +365,7 @@ export default combineReducers({
|
||||
toasts,
|
||||
dhcp,
|
||||
encryption,
|
||||
clients,
|
||||
loadingBar: loadingBarReducer,
|
||||
form: formReducer,
|
||||
});
|
||||
|
436
clients.go
436
clients.go
@ -2,32 +2,264 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Client information
|
||||
type Client struct {
|
||||
IP string
|
||||
Name string
|
||||
//Source source // Hosts file / User settings / DHCP
|
||||
IP string
|
||||
MAC string
|
||||
Name string
|
||||
UseOwnSettings bool // false: use global settings
|
||||
FilteringEnabled bool
|
||||
SafeSearchEnabled bool
|
||||
SafeBrowsingEnabled bool
|
||||
ParentalEnabled bool
|
||||
}
|
||||
|
||||
type clientJSON struct {
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
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
|
||||
var clientsFilled bool
|
||||
type clientSource uint
|
||||
|
||||
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
|
||||
func fillClientInfo() {
|
||||
func clientsAddFromHostsFile() {
|
||||
hostsFn := "/etc/hosts"
|
||||
if runtime.GOOS == "windows" {
|
||||
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
|
||||
@ -40,6 +272,7 @@ func fillClientInfo() {
|
||||
}
|
||||
|
||||
lines := strings.Split(string(d), "\n")
|
||||
n := 0
|
||||
for _, ln := range lines {
|
||||
ln = strings.TrimSpace(ln)
|
||||
if len(ln) == 0 || ln[0] == '#' {
|
||||
@ -51,33 +284,71 @@ func fillClientInfo() {
|
||||
continue
|
||||
}
|
||||
|
||||
var c Client
|
||||
c.IP = fields[0]
|
||||
c.Name = fields[1]
|
||||
clients = append(clients, c)
|
||||
log.Tracef("%s -> %s", c.IP, c.Name)
|
||||
ok, e := clientAddHost(fields[0], fields[1], ClientSourceHostsFile)
|
||||
if e != nil {
|
||||
log.Tracef("%s", e)
|
||||
}
|
||||
if ok {
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Added %d client aliases from %s", len(clients), hostsFn)
|
||||
clientsFilled = true
|
||||
log.Info("Added %d client aliases from %s", n, hostsFn)
|
||||
}
|
||||
|
||||
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
|
||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
if !clientsFilled {
|
||||
fillClientInfo()
|
||||
}
|
||||
data := clientListJSON{}
|
||||
|
||||
data := []clientJSON{}
|
||||
for _, c := range clients {
|
||||
clients.lock.Lock()
|
||||
for _, c := range clients.list {
|
||||
cj := clientJSON{
|
||||
IP: c.IP,
|
||||
Name: c.Name,
|
||||
IP: c.IP,
|
||||
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")
|
||||
e := json.NewEncoder(w).Encode(data)
|
||||
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
|
||||
func RegisterClientsHandlers() {
|
||||
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))))
|
||||
}
|
||||
|
122
clients_test.go
Normal file
122
clients_test.go
Normal file
@ -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
|
||||
}
|
||||
|
||||
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
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type configuration struct {
|
||||
@ -54,6 +65,9 @@ type configuration struct {
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
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"`
|
||||
|
||||
sync.RWMutex `yaml:"-"`
|
||||
@ -206,6 +220,25 @@ func parseConfig() error {
|
||||
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
|
||||
deduplicateFilters()
|
||||
|
||||
@ -233,9 +266,30 @@ func readConfigFile() ([]byte, error) {
|
||||
func (c *configuration) write() error {
|
||||
c.Lock()
|
||||
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()
|
||||
log.Debug("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
config.Clients = nil
|
||||
if err != nil {
|
||||
log.Error("Couldn't generate YAML file: %s", 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
|
||||
func (s *Server) reset() {
|
||||
s.Lock()
|
||||
|
23
dns.go
23
dns.go
@ -70,9 +70,32 @@ func generateServerConfig() dnsforward.ServerConfig {
|
||||
newconfig.Upstreams = upstreamConfig.Upstreams
|
||||
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
|
||||
newconfig.AllServers = config.DNS.AllServers
|
||||
newconfig.FilterHandler = applyClientSettings
|
||||
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 {
|
||||
if isRunning() {
|
||||
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 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.
|
||||
type Config struct {
|
||||
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"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
ResolverAddress string // DNS server address
|
||||
|
||||
// Filtering callback function
|
||||
FilterHandler func(clientAddr string, settings *RequestFilteringSettings) `yaml:"-"`
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
if host == "" {
|
||||
return Result{Reason: NotFilteredNotFound}, nil
|
||||
@ -160,17 +171,30 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16) (Result, error) {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
// try filter lists first
|
||||
result, err := d.matchHost(host, qtype)
|
||||
if err != nil {
|
||||
return result, err
|
||||
var setts RequestFilteringSettings
|
||||
setts.FilteringEnabled = true
|
||||
setts.SafeSearchEnabled = d.SafeSearchEnabled
|
||||
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
|
||||
if d.SafeSearchEnabled {
|
||||
if setts.SafeSearchEnabled {
|
||||
result, err = d.checkSafeSearch(host)
|
||||
if err != nil {
|
||||
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
|
||||
if d.SafeBrowsingEnabled {
|
||||
if setts.SafeBrowsingEnabled {
|
||||
result, err = d.checkSafeBrowsing(host)
|
||||
if err != nil {
|
||||
// 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
|
||||
if d.ParentalEnabled {
|
||||
if setts.ParentalEnabled {
|
||||
result, err = d.checkParental(host)
|
||||
if err != nil {
|
||||
// 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
|
||||
// PARENTAL
|
||||
// FILTERING
|
||||
// CLIENTS SETTINGS
|
||||
// BENCHMARKS
|
||||
|
||||
// HELPERS
|
||||
@ -52,7 +53,7 @@ func NewForTestFilters(filters map[int]string) *Dnsfilter {
|
||||
|
||||
func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
||||
t.Helper()
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||
if err != nil {
|
||||
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) {
|
||||
t.Helper()
|
||||
ret, err := d.CheckHost(hostname, qtype)
|
||||
ret, err := d.CheckHost(hostname, qtype, "")
|
||||
if err != nil {
|
||||
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) {
|
||||
t.Helper()
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||
if err != nil {
|
||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||
}
|
||||
@ -212,7 +213,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
|
||||
|
||||
// Check host for each domain
|
||||
for _, host := range yandex {
|
||||
result, err := d.CheckHost(host, dns.TypeA)
|
||||
result, err := d.CheckHost(host, dns.TypeA, "")
|
||||
if err != nil {
|
||||
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
|
||||
for _, host := range googleDomains {
|
||||
result, err := d.CheckHost(host, dns.TypeA)
|
||||
result, err := d.CheckHost(host, dns.TypeA, "")
|
||||
if err != nil {
|
||||
t.Errorf("SafeSearch doesn't work for %s cause %s", host, err)
|
||||
}
|
||||
@ -255,7 +256,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
||||
var err error
|
||||
|
||||
// Check host with disabled safesearch
|
||||
result, err = d.CheckHost(domain, dns.TypeA)
|
||||
result, err = d.CheckHost(domain, dns.TypeA, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot check host due to %s", err)
|
||||
}
|
||||
@ -265,7 +266,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
||||
|
||||
// Enable safesearch
|
||||
d.SafeSearchEnabled = true
|
||||
result, err = d.CheckHost(domain, dns.TypeA)
|
||||
result, err = d.CheckHost(domain, dns.TypeA, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
||||
}
|
||||
@ -295,7 +296,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
domain := "www.google.ru"
|
||||
result, err := d.CheckHost(domain, dns.TypeA)
|
||||
result, err := d.CheckHost(domain, dns.TypeA, "")
|
||||
if err != nil {
|
||||
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 {
|
||||
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
||||
}
|
||||
@ -441,7 +442,7 @@ func TestMatching(t *testing.T) {
|
||||
d := NewForTestFilters(filters)
|
||||
defer d.Destroy()
|
||||
|
||||
ret, err := d.CheckHost(test.hostname, dns.TypeA)
|
||||
ret, err := d.CheckHost(test.hostname, dns.TypeA, "")
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
func BenchmarkSafeBrowsing(b *testing.B) {
|
||||
@ -463,7 +510,7 @@ func BenchmarkSafeBrowsing(b *testing.B) {
|
||||
d.SafeBrowsingEnabled = true
|
||||
for n := 0; n < b.N; n++ {
|
||||
hostname := "wmconvirus.narod.ru"
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||
if err != nil {
|
||||
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) {
|
||||
for pb.Next() {
|
||||
hostname := "wmconvirus.narod.ru"
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA)
|
||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
||||
if err != nil {
|
||||
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 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 {
|
||||
// Return immediately if there's an error
|
||||
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
||||
|
@ -2,7 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: 'AdGuard Home'
|
||||
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:
|
||||
- http
|
||||
basePath: /control
|
||||
@ -424,7 +424,7 @@ paths:
|
||||
description: OK
|
||||
|
||||
/filtering/add_url:
|
||||
put:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringAddURL
|
||||
@ -444,7 +444,7 @@ paths:
|
||||
description: OK
|
||||
|
||||
/filtering/remove_url:
|
||||
delete:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringRemoveURL
|
||||
@ -530,7 +530,7 @@ paths:
|
||||
description: OK with how many filters were actually updated
|
||||
|
||||
/filtering/set_rules:
|
||||
put:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringSetRules
|
||||
@ -698,6 +698,54 @@ paths:
|
||||
schema:
|
||||
$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
|
||||
# --------------------------------------------------
|
||||
@ -1377,11 +1425,65 @@ definitions:
|
||||
type: "string"
|
||||
description: "Name"
|
||||
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:
|
||||
type: "object"
|
||||
properties:
|
||||
clients:
|
||||
$ref: "#/definitions/ClientsArray"
|
||||
auto_clients:
|
||||
$ref: "#/definitions/ClientsAutoArray"
|
||||
ClientsArray:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Client"
|
||||
description: "Clients array"
|
||||
ClientsAutoArray:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/ClientAuto"
|
||||
description: "Auto-Clients array"
|
||||
CheckConfigRequest:
|
||||
type: "object"
|
||||
description: "Configuration to be checked"
|
||||
|
Loading…
Reference in New Issue
Block a user