Merge: + Clients: multiple IP, CIDR, MAC
Close #809 * commit 'f8202a74bd55b97cdd95c33d8f8ed97412ba43b1': + client: handle clients find + client: add multiple fields client form * minor * openapi: update 'Client' object; add /clients/find * config: upgrade scheme to v6 * clients: multiple IP, CIDR, MAC addresses
This commit is contained in:
commit
127a68a39f
|
@ -21,6 +21,7 @@ Contents:
|
||||||
* Add client
|
* Add client
|
||||||
* Update client
|
* Update client
|
||||||
* Delete client
|
* Delete client
|
||||||
|
* API: Find clients by IP
|
||||||
* Enable DHCP server
|
* Enable DHCP server
|
||||||
* "Show DHCP status" command
|
* "Show DHCP status" command
|
||||||
* "Check DHCP" command
|
* "Check DHCP" command
|
||||||
|
@ -618,8 +619,6 @@ Notes:
|
||||||
|
|
||||||
* `name`, `ip` and `mac` values are unique.
|
* `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 `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 true, then DNS responses for this client are processed and filtered using global settings.
|
||||||
|
@ -643,8 +642,7 @@ Response:
|
||||||
clients: [
|
clients: [
|
||||||
{
|
{
|
||||||
name: "client1"
|
name: "client1"
|
||||||
ip: "..."
|
ids: ["...", ...] // IP, CIDR or MAC
|
||||||
mac: "..."
|
|
||||||
use_global_settings: true
|
use_global_settings: true
|
||||||
filtering_enabled: false
|
filtering_enabled: false
|
||||||
parental_enabled: false
|
parental_enabled: false
|
||||||
|
@ -682,8 +680,7 @@ Request:
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "client1"
|
name: "client1"
|
||||||
ip: "..."
|
ids: ["...", ...] // IP, CIDR or MAC
|
||||||
mac: "..."
|
|
||||||
use_global_settings: true
|
use_global_settings: true
|
||||||
filtering_enabled: false
|
filtering_enabled: false
|
||||||
parental_enabled: false
|
parental_enabled: false
|
||||||
|
@ -712,8 +709,7 @@ Request:
|
||||||
name: "client1"
|
name: "client1"
|
||||||
data: {
|
data: {
|
||||||
name: "client1"
|
name: "client1"
|
||||||
ip: "..."
|
ids: ["...", ...] // IP, CIDR or MAC
|
||||||
mac: "..."
|
|
||||||
use_global_settings: true
|
use_global_settings: true
|
||||||
filtering_enabled: false
|
filtering_enabled: false
|
||||||
parental_enabled: false
|
parental_enabled: false
|
||||||
|
@ -752,6 +748,41 @@ Error response (Client not found):
|
||||||
400
|
400
|
||||||
|
|
||||||
|
|
||||||
|
### API: Find clients by IP
|
||||||
|
|
||||||
|
This method returns the list of clients (manual and auto-clients) matching the IP list.
|
||||||
|
For auto-clients only `name`, `ids` and `whois_info` fields are set. Other fields are empty.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
GET /control/clients/find?ip0=...&ip1=...&ip2=...
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"1.2.3.4": {
|
||||||
|
name: "client1"
|
||||||
|
ids: ["...", ...] // IP, CIDR or MAC
|
||||||
|
use_global_settings: true
|
||||||
|
filtering_enabled: false
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
safesearch_enabled: false
|
||||||
|
use_global_blocked_services: true
|
||||||
|
blocked_services: [ "name1", ... ]
|
||||||
|
whois_info: {
|
||||||
|
key: "value"
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
## DNS access settings
|
## DNS access settings
|
||||||
|
|
||||||
There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.
|
There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.
|
||||||
|
|
|
@ -299,9 +299,11 @@
|
||||||
"client_edit": "Edit Client",
|
"client_edit": "Edit Client",
|
||||||
"client_identifier": "Identifier",
|
"client_identifier": "Identifier",
|
||||||
"ip_address": "IP address",
|
"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>",
|
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, 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_ip": "Enter IP",
|
||||||
"form_enter_mac": "Enter MAC",
|
"form_enter_mac": "Enter MAC",
|
||||||
|
"form_enter_id": "Enter identifier",
|
||||||
|
"form_add_id": "Add identifier",
|
||||||
"form_client_name": "Enter client name",
|
"form_client_name": "Enter client name",
|
||||||
"client_global_settings": "Use global settings",
|
"client_global_settings": "Use global settings",
|
||||||
"client_deleted": "Client \"{{key}}\" successfully deleted",
|
"client_deleted": "Client \"{{key}}\" successfully deleted",
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { createAction } from 'redux-actions';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
import { addErrorToast, addSuccessToast, getClients } from './index';
|
import { addErrorToast, addSuccessToast, getClients } from './index';
|
||||||
import { CLIENT_ID } from '../helpers/constants';
|
|
||||||
|
|
||||||
export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');
|
export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');
|
||||||
|
|
||||||
|
@ -13,18 +12,7 @@ export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
|
||||||
export const addClient = config => async (dispatch) => {
|
export const addClient = config => async (dispatch) => {
|
||||||
dispatch(addClientRequest());
|
dispatch(addClientRequest());
|
||||||
try {
|
try {
|
||||||
let data;
|
await apiClient.addClient(config);
|
||||||
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(addClientSuccess());
|
||||||
dispatch(toggleClientModal());
|
dispatch(toggleClientModal());
|
||||||
dispatch(addSuccessToast(t('client_added', { key: config.name })));
|
dispatch(addSuccessToast(t('client_added', { key: config.name })));
|
||||||
|
@ -59,16 +47,7 @@ export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
|
||||||
export const updateClient = (config, name) => async (dispatch) => {
|
export const updateClient = (config, name) => async (dispatch) => {
|
||||||
dispatch(updateClientRequest());
|
dispatch(updateClientRequest());
|
||||||
try {
|
try {
|
||||||
let data;
|
const data = { name, data: { ...config } };
|
||||||
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);
|
await apiClient.updateClient(data);
|
||||||
dispatch(updateClientSuccess());
|
dispatch(updateClientSuccess());
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { createAction } from 'redux-actions';
|
||||||
|
|
||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
import { addErrorToast, addSuccessToast } from './index';
|
import { addErrorToast, addSuccessToast } from './index';
|
||||||
import { normalizeLogs } from '../helpers/helpers';
|
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
||||||
import { TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants';
|
import { TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants';
|
||||||
|
|
||||||
const getLogsWithParams = async (config) => {
|
const getLogsWithParams = async (config) => {
|
||||||
|
@ -10,9 +10,12 @@ const getLogsWithParams = async (config) => {
|
||||||
const rawLogs = await apiClient.getQueryLog({ ...filter, older_than });
|
const rawLogs = await apiClient.getQueryLog({ ...filter, older_than });
|
||||||
const { data, oldest } = rawLogs;
|
const { data, oldest } = rawLogs;
|
||||||
const logs = normalizeLogs(data);
|
const logs = normalizeLogs(data);
|
||||||
|
const clientsParams = getParamsForClientsSearch(logs, 'client');
|
||||||
|
const clients = await apiClient.findClients(clientsParams);
|
||||||
|
const logsWithClientInfo = addClientInfo(logs, clients, 'client');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logs, oldest, older_than, filter, ...values,
|
logs: logsWithClientInfo, oldest, older_than, filter, ...values,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { createAction } from 'redux-actions';
|
||||||
|
|
||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
import { addErrorToast, addSuccessToast } from './index';
|
import { addErrorToast, addSuccessToast } from './index';
|
||||||
import { normalizeTopStats, secondsToMilliseconds } from '../helpers/helpers';
|
import { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
||||||
|
|
||||||
export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');
|
export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');
|
||||||
export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');
|
export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');
|
||||||
|
@ -43,11 +43,15 @@ export const getStats = () => async (dispatch) => {
|
||||||
dispatch(getStatsRequest());
|
dispatch(getStatsRequest());
|
||||||
try {
|
try {
|
||||||
const stats = await apiClient.getStats();
|
const stats = await apiClient.getStats();
|
||||||
|
const normalizedTopClients = normalizeTopStats(stats.top_clients);
|
||||||
|
const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
|
||||||
|
const clients = await apiClient.findClients(clientsParams);
|
||||||
|
const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
|
||||||
|
|
||||||
const normalizedStats = {
|
const normalizedStats = {
|
||||||
...stats,
|
...stats,
|
||||||
top_blocked_domains: normalizeTopStats(stats.top_blocked_domains),
|
top_blocked_domains: normalizeTopStats(stats.top_blocked_domains),
|
||||||
top_clients: normalizeTopStats(stats.top_clients),
|
top_clients: topClientsWithInfo,
|
||||||
top_queried_domains: normalizeTopStats(stats.top_queried_domains),
|
top_queried_domains: normalizeTopStats(stats.top_queried_domains),
|
||||||
avg_processing_time: secondsToMilliseconds(stats.avg_processing_time),
|
avg_processing_time: secondsToMilliseconds(stats.avg_processing_time),
|
||||||
};
|
};
|
||||||
|
|
|
@ -353,6 +353,7 @@ class Api {
|
||||||
|
|
||||||
// Per-client settings
|
// Per-client settings
|
||||||
GET_CLIENTS = { path: 'clients', method: 'GET' };
|
GET_CLIENTS = { path: 'clients', method: 'GET' };
|
||||||
|
FIND_CLIENTS = { path: 'clients/find', method: 'GET' };
|
||||||
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
|
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
|
||||||
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };
|
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };
|
||||||
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };
|
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };
|
||||||
|
@ -389,6 +390,12 @@ class Api {
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findClients(params) {
|
||||||
|
const { path, method } = this.FIND_CLIENTS;
|
||||||
|
const url = getPathWithQueryString(path, params);
|
||||||
|
return this.makeRequest(url, method);
|
||||||
|
}
|
||||||
|
|
||||||
// DNS access settings
|
// DNS access settings
|
||||||
ACCESS_LIST = { path: 'access/list', method: 'GET' };
|
ACCESS_LIST = { path: 'access/list', method: 'GET' };
|
||||||
ACCESS_SET = { path: 'access/set', method: 'POST' };
|
ACCESS_SET = { path: 'access/set', method: 'POST' };
|
||||||
|
|
|
@ -28,19 +28,17 @@ const countCell = dnsQueries =>
|
||||||
return <Cell value={value} percent={percent} color={percentColor} />;
|
return <Cell value={value} percent={percent} color={percentColor} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clientCell = (clients, autoClients, t) =>
|
const clientCell = t =>
|
||||||
function cell(row) {
|
function cell(row) {
|
||||||
const { value } = row;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs__row logs__row--overflow logs__row--column">
|
<div className="logs__row logs__row--overflow logs__row--column">
|
||||||
{formatClientCell(value, clients, autoClients, t)}
|
{formatClientCell(row, t)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Clients = ({
|
const Clients = ({
|
||||||
t, refreshButton, topClients, subtitle, clients, autoClients, dnsQueries,
|
t, refreshButton, topClients, subtitle, dnsQueries,
|
||||||
}) => (
|
}) => (
|
||||||
<Card
|
<Card
|
||||||
title={t('top_clients')}
|
title={t('top_clients')}
|
||||||
|
@ -49,9 +47,10 @@ const Clients = ({
|
||||||
refresh={refreshButton}
|
refresh={refreshButton}
|
||||||
>
|
>
|
||||||
<ReactTable
|
<ReactTable
|
||||||
data={topClients.map(({ name: ip, count }) => ({
|
data={topClients.map(({ name: ip, count, info }) => ({
|
||||||
ip,
|
ip,
|
||||||
count,
|
count,
|
||||||
|
info,
|
||||||
}))}
|
}))}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
@ -59,7 +58,7 @@ const Clients = ({
|
||||||
accessor: 'ip',
|
accessor: 'ip',
|
||||||
sortMethod: (a, b) =>
|
sortMethod: (a, b) =>
|
||||||
parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
|
parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
|
||||||
Cell: clientCell(clients, autoClients, t),
|
Cell: clientCell(t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: <Trans>requests_count</Trans>,
|
Header: <Trans>requests_count</Trans>,
|
||||||
|
|
|
@ -20,7 +20,6 @@ class Dashboard extends Component {
|
||||||
getAllStats = () => {
|
getAllStats = () => {
|
||||||
this.props.getStats();
|
this.props.getStats();
|
||||||
this.props.getStatsConfig();
|
this.props.getStatsConfig();
|
||||||
this.props.getClients();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getToggleFilteringButton = () => {
|
getToggleFilteringButton = () => {
|
||||||
|
@ -44,7 +43,6 @@ class Dashboard extends Component {
|
||||||
const { dashboard, stats, t } = this.props;
|
const { dashboard, stats, t } = this.props;
|
||||||
const dashboardProcessing =
|
const dashboardProcessing =
|
||||||
dashboard.processing ||
|
dashboard.processing ||
|
||||||
dashboard.processingClients ||
|
|
||||||
stats.processingStats ||
|
stats.processingStats ||
|
||||||
stats.processingGetConfig;
|
stats.processingGetConfig;
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ class Logs extends Component {
|
||||||
this.props.setLogsPage(TABLE_FIRST_PAGE);
|
this.props.setLogsPage(TABLE_FIRST_PAGE);
|
||||||
this.getLogs(...INITIAL_REQUEST_DATA);
|
this.getLogs(...INITIAL_REQUEST_DATA);
|
||||||
this.props.getFilteringStatus();
|
this.props.getFilteringStatus();
|
||||||
this.props.getClients();
|
|
||||||
this.props.getLogsConfig();
|
this.props.getLogsConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,9 +190,9 @@ class Logs extends Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getClientCell = ({ original, value }) => {
|
getClientCell = (row) => {
|
||||||
const { dashboard, t } = this.props;
|
const { original } = row;
|
||||||
const { clients, autoClients } = dashboard;
|
const { t } = this.props;
|
||||||
const { reason, domain } = original;
|
const { reason, domain } = original;
|
||||||
const isFiltered = this.checkFiltered(reason);
|
const isFiltered = this.checkFiltered(reason);
|
||||||
const isRewrite = this.checkRewrite(reason);
|
const isRewrite = this.checkRewrite(reason);
|
||||||
|
@ -201,7 +200,7 @@ class Logs extends Component {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="logs__row logs__row--overflow logs__row--column">
|
<div className="logs__row logs__row--overflow logs__row--column">
|
||||||
{formatClientCell(value, clients, autoClients, t)}
|
{formatClientCell(row, t)}
|
||||||
</div>
|
</div>
|
||||||
{isRewrite ? (
|
{isRewrite ? (
|
||||||
<div className="logs__action">
|
<div className="logs__action">
|
||||||
|
@ -232,12 +231,11 @@ class Logs extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderLogs() {
|
renderLogs() {
|
||||||
const { queryLogs, dashboard, t } = this.props;
|
const { queryLogs, t } = this.props;
|
||||||
const { processingClients } = dashboard;
|
|
||||||
const {
|
const {
|
||||||
processingGetLogs, processingGetConfig, logs, pages, page,
|
processingGetLogs, processingGetConfig, logs, pages, page,
|
||||||
} = queryLogs;
|
} = queryLogs;
|
||||||
const isLoading = processingGetLogs || processingClients || processingGetConfig;
|
const isLoading = processingGetLogs || processingGetConfig;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
|
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import WrapCell from './WrapCell';
|
import WrapCell from './WrapCell';
|
||||||
|
@ -40,10 +40,7 @@ class ClientsTable extends Component {
|
||||||
const client = clients.find(item => name === item.name);
|
const client = clients.find(item => name === item.name);
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identifier,
|
|
||||||
use_global_settings: true,
|
use_global_settings: true,
|
||||||
use_global_blocked_services: true,
|
use_global_blocked_services: true,
|
||||||
...client,
|
...client,
|
||||||
|
@ -51,7 +48,7 @@ class ClientsTable extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identifier: CLIENT_ID.IP,
|
ids: [''],
|
||||||
use_global_settings: true,
|
use_global_settings: true,
|
||||||
use_global_blocked_services: true,
|
use_global_blocked_services: true,
|
||||||
};
|
};
|
||||||
|
@ -76,28 +73,22 @@ class ClientsTable extends Component {
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
Header: this.props.t('table_client'),
|
Header: this.props.t('table_client'),
|
||||||
accessor: 'ip',
|
accessor: 'ids',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
if (row.original && row.original.mac) {
|
const { value } = row;
|
||||||
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 '';
|
return (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text">
|
||||||
|
{value.map(address => (
|
||||||
|
<div key={address} title={address}>
|
||||||
|
{address}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -119,9 +110,7 @@ class ClientsTable extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs__row logs__row--overflow">
|
<div className="logs__row logs__row--overflow">
|
||||||
<div className="logs__text" title={title}>
|
<div className="logs__text">{title}</div>
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
import flow from 'lodash/flow';
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
|
import i18n from '../../../i18n';
|
||||||
import Tabs from '../../ui/Tabs';
|
import Tabs from '../../ui/Tabs';
|
||||||
import { toggleAllServices } from '../../../helpers/helpers';
|
import { toggleAllServices } from '../../../helpers/helpers';
|
||||||
import { renderField, renderRadioField, renderSelectField, renderServiceField, ip, mac, required } from '../../../helpers/form';
|
import {
|
||||||
import { CLIENT_ID, SERVICES } from '../../../helpers/constants';
|
renderField,
|
||||||
|
renderGroupField,
|
||||||
|
renderSelectField,
|
||||||
|
renderServiceField,
|
||||||
|
} from '../../../helpers/form';
|
||||||
|
import { SERVICES } from '../../../helpers/constants';
|
||||||
import './Service.css';
|
import './Service.css';
|
||||||
|
|
||||||
const settingsCheckboxes = [
|
const settingsCheckboxes = [
|
||||||
|
@ -34,6 +40,67 @@ const settingsCheckboxes = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const validate = (values) => {
|
||||||
|
const errors = {};
|
||||||
|
const { name, ids } = values;
|
||||||
|
|
||||||
|
if (!name || !name.length) {
|
||||||
|
errors.name = i18n.t('form_error_required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ids && ids.length) {
|
||||||
|
const idArrayErrors = [];
|
||||||
|
ids.forEach((id, idx) => {
|
||||||
|
if (!id || !id.length) {
|
||||||
|
idArrayErrors[idx] = i18n.t('form_error_required');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idArrayErrors.length) {
|
||||||
|
errors.ids = idArrayErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFields = (placeholder, buttonTitle) =>
|
||||||
|
function cell(row) {
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
meta: { error },
|
||||||
|
} = row;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form__group">
|
||||||
|
{fields.map((ip, index) => (
|
||||||
|
<div key={index} className="mb-1">
|
||||||
|
<Field
|
||||||
|
name={ip}
|
||||||
|
component={renderGroupField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={placeholder}
|
||||||
|
isActionAvailable={index !== 0}
|
||||||
|
removeField={() => fields.remove(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-link btn-block btn-sm"
|
||||||
|
onClick={() => fields.push()}
|
||||||
|
title={buttonTitle}
|
||||||
|
>
|
||||||
|
<svg className="icon icon--close">
|
||||||
|
<use xlinkHref="#plus" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let Form = (props) => {
|
let Form = (props) => {
|
||||||
const {
|
const {
|
||||||
t,
|
t,
|
||||||
|
@ -42,71 +109,19 @@ let Form = (props) => {
|
||||||
change,
|
change,
|
||||||
pristine,
|
pristine,
|
||||||
submitting,
|
submitting,
|
||||||
clientIdentifier,
|
|
||||||
useGlobalSettings,
|
useGlobalSettings,
|
||||||
useGlobalServices,
|
useGlobalServices,
|
||||||
toggleClientModal,
|
toggleClientModal,
|
||||||
processingAdding,
|
processingAdding,
|
||||||
processingUpdating,
|
processingUpdating,
|
||||||
|
invalid,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
|
<div className="form__group mb-0">
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<div className="form__inline mb-2">
|
|
||||||
<strong className="mr-3">
|
|
||||||
<Trans>client_identifier</Trans>
|
|
||||||
</strong>
|
|
||||||
<div className="custom-controls-stacked">
|
|
||||||
<Field
|
|
||||||
name="identifier"
|
|
||||||
component={renderRadioField}
|
|
||||||
type="radio"
|
|
||||||
className="form-control mr-2"
|
|
||||||
value="ip"
|
|
||||||
placeholder={t('ip_address')}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name="identifier"
|
|
||||||
component={renderRadioField}
|
|
||||||
type="radio"
|
|
||||||
className="form-control mr-2"
|
|
||||||
value="mac"
|
|
||||||
placeholder="MAC"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col col-sm-6">
|
|
||||||
{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={[ip, 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>
|
|
||||||
<div className="col col-sm-6">
|
|
||||||
<Field
|
<Field
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
|
@ -114,11 +129,16 @@ let Form = (props) => {
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t('form_client_name')}
|
placeholder={t('form_client_name')}
|
||||||
validate={[required]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<div className="form__label">
|
||||||
|
<strong className="mr-3">
|
||||||
|
<Trans>client_identifier</Trans>
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="form__desc">
|
<div className="form__desc mt-0">
|
||||||
<Trans
|
<Trans
|
||||||
components={[
|
components={[
|
||||||
<a href="#dhcp" key="0">
|
<a href="#dhcp" key="0">
|
||||||
|
@ -131,6 +151,14 @@ let Form = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<FieldArray
|
||||||
|
name="ids"
|
||||||
|
component={renderFields(t('form_enter_id'), t('form_add_id'))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs controlClass="form">
|
<Tabs controlClass="form">
|
||||||
<div label="settings" title={props.t('main_settings')}>
|
<div label="settings" title={props.t('main_settings')}>
|
||||||
{settingsCheckboxes.map(setting => (
|
{settingsCheckboxes.map(setting => (
|
||||||
|
@ -140,7 +168,11 @@ let Form = (props) => {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
component={renderSelectField}
|
component={renderSelectField}
|
||||||
placeholder={t(setting.placeholder)}
|
placeholder={t(setting.placeholder)}
|
||||||
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
|
disabled={
|
||||||
|
setting.name !== 'use_global_settings'
|
||||||
|
? useGlobalSettings
|
||||||
|
: false
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -210,7 +242,13 @@ let Form = (props) => {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={submitting || pristine || processingAdding || processingUpdating}
|
disabled={
|
||||||
|
submitting ||
|
||||||
|
invalid ||
|
||||||
|
pristine ||
|
||||||
|
processingAdding ||
|
||||||
|
processingUpdating
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
@ -227,22 +265,20 @@ Form.propTypes = {
|
||||||
change: PropTypes.func.isRequired,
|
change: PropTypes.func.isRequired,
|
||||||
submitting: PropTypes.bool.isRequired,
|
submitting: PropTypes.bool.isRequired,
|
||||||
toggleClientModal: PropTypes.func.isRequired,
|
toggleClientModal: PropTypes.func.isRequired,
|
||||||
clientIdentifier: PropTypes.string,
|
|
||||||
useGlobalSettings: PropTypes.bool,
|
useGlobalSettings: PropTypes.bool,
|
||||||
useGlobalServices: PropTypes.bool,
|
useGlobalServices: PropTypes.bool,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
processingAdding: PropTypes.bool.isRequired,
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
processingUpdating: PropTypes.bool.isRequired,
|
processingUpdating: PropTypes.bool.isRequired,
|
||||||
|
invalid: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = formValueSelector('clientForm');
|
const selector = formValueSelector('clientForm');
|
||||||
|
|
||||||
Form = connect((state) => {
|
Form = connect((state) => {
|
||||||
const clientIdentifier = selector(state, 'identifier');
|
|
||||||
const useGlobalSettings = selector(state, 'use_global_settings');
|
const useGlobalSettings = selector(state, 'use_global_settings');
|
||||||
const useGlobalServices = selector(state, 'use_global_blocked_services');
|
const useGlobalServices = selector(state, 'use_global_blocked_services');
|
||||||
return {
|
return {
|
||||||
clientIdentifier,
|
|
||||||
useGlobalSettings,
|
useGlobalSettings,
|
||||||
useGlobalServices,
|
useGlobalServices,
|
||||||
};
|
};
|
||||||
|
@ -253,5 +289,6 @@ export default flow([
|
||||||
reduxForm({
|
reduxForm({
|
||||||
form: 'clientForm',
|
form: 'clientForm',
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
|
validate,
|
||||||
}),
|
}),
|
||||||
])(Form);
|
])(Form);
|
||||||
|
|
|
@ -3,3 +3,8 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon--close {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
|
@ -167,6 +167,14 @@ const Icons = () => (
|
||||||
<symbol id="location" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round">
|
<symbol id="location" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M12,2C8.134,2,5,5.134,5,9c0,5,7,13,7,13s7-8,7-13C19,5.134,15.866,2,12,2z M12,11.5c-1.381,0-2.5-1.119-2.5-2.5 c0-1.381,1.119-2.5,2.5-2.5s2.5,1.119,2.5,2.5C14.5,10.381,13.381,11.5,12,11.5z"/>
|
<path d="M12,2C8.134,2,5,5.134,5,9c0,5,7,13,7,13s7-8,7-13C19,5.134,15.866,2,12,2z M12,11.5c-1.381,0-2.5-1.119-2.5-2.5 c0-1.381,1.119-2.5,2.5-2.5s2.5,1.119,2.5,2.5C14.5,10.381,13.381,11.5,12,11.5z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="cross" viewBox="0 0 24 24" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="plus" viewBox="0 0 24 24" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,50 @@ export const renderField = ({
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const renderGroupField = ({
|
||||||
|
input,
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
autoComplete,
|
||||||
|
isActionAvailable,
|
||||||
|
removeField,
|
||||||
|
meta: { touched, error },
|
||||||
|
}) => (
|
||||||
|
<Fragment>
|
||||||
|
<div className="input-group">
|
||||||
|
<input
|
||||||
|
{...input}
|
||||||
|
id={id}
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={type}
|
||||||
|
className={className}
|
||||||
|
disabled={disabled}
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
/>
|
||||||
|
{isActionAvailable &&
|
||||||
|
<span className="input-group-append">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-icon"
|
||||||
|
onClick={removeField}
|
||||||
|
>
|
||||||
|
<svg className="icon icon--close">
|
||||||
|
<use xlinkHref="#cross" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!disabled &&
|
||||||
|
touched &&
|
||||||
|
(error && <span className="form__message form__message--error">{error}</span>)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
export const renderRadioField = ({
|
export const renderRadioField = ({
|
||||||
input, placeholder, disabled, meta: { touched, error },
|
input, placeholder, disabled, meta: { touched, error },
|
||||||
}) => (
|
}) => (
|
||||||
|
@ -102,6 +146,7 @@ export const renderServiceField = ({
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Validation functions
|
||||||
export const required = (value) => {
|
export const required = (value) => {
|
||||||
if (value || value === 0) {
|
if (value || value === 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { getClientInfo, normalizeWhois } from './helpers';
|
import { normalizeWhois } from './helpers';
|
||||||
import { WHOIS_ICONS } from './constants';
|
import { WHOIS_ICONS } from './constants';
|
||||||
|
|
||||||
const getFormattedWhois = (whois, t) => {
|
const getFormattedWhois = (whois, t) => {
|
||||||
|
@ -22,12 +22,14 @@ const getFormattedWhois = (whois, t) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatClientCell = (value, clients, autoClients, t) => {
|
export const formatClientCell = (row, t) => {
|
||||||
const clientInfo = getClientInfo(clients, value) || getClientInfo(autoClients, value);
|
const { value, original: { info } } = row;
|
||||||
const { name, whois } = clientInfo;
|
|
||||||
let whoisContainer = '';
|
let whoisContainer = '';
|
||||||
let nameContainer = value;
|
let nameContainer = value;
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
const { name, whois } = info;
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
nameContainer = (
|
nameContainer = (
|
||||||
<span className="logs__text logs__text--wrap" title={`${name} (${value})`}>
|
<span className="logs__text logs__text--wrap" title={`${name} (${value})`}>
|
||||||
|
@ -43,6 +45,7 @@ export const formatClientCell = (value, clients, autoClients, t) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="logs__text">
|
<span className="logs__text">
|
||||||
|
|
|
@ -8,6 +8,7 @@ import subDays from 'date-fns/sub_days';
|
||||||
import round from 'lodash/round';
|
import round from 'lodash/round';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
import uniqBy from 'lodash/uniqBy';
|
||||||
import versionCompare from './versionCompare';
|
import versionCompare from './versionCompare';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -92,6 +93,17 @@ export const normalizeTopStats = stats => (
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const addClientInfo = (data, clients, param) => (
|
||||||
|
data.map((row) => {
|
||||||
|
const clientIp = row[param];
|
||||||
|
const info = clients.find(item => item[clientIp]) || '';
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
info: (info && info[clientIp]) || '',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const normalizeFilteringStatus = (filteringStatus) => {
|
export const normalizeFilteringStatus = (filteringStatus) => {
|
||||||
const {
|
const {
|
||||||
enabled, filters, user_rules: userRules, interval,
|
enabled, filters, user_rules: userRules, interval,
|
||||||
|
@ -248,6 +260,20 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
|
||||||
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
|
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
|
||||||
|
|
||||||
export const getClientInfo = (clients, ip) => {
|
export const getClientInfo = (clients, ip) => {
|
||||||
|
const client = clients
|
||||||
|
.find(item => item.ip_addrs && item.ip_addrs.find(clientIp => clientIp === ip));
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, whois_info } = client;
|
||||||
|
const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
|
||||||
|
|
||||||
|
return { name, whois };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAutoClientInfo = (clients, ip) => {
|
||||||
const client = clients.find(item => ip === item.ip);
|
const client = clients.find(item => ip === item.ip);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -328,3 +354,13 @@ export const getPathWithQueryString = (path, params) => {
|
||||||
|
|
||||||
return `${path}?${searchParams.toString()}`;
|
return `${path}?${searchParams.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getParamsForClientsSearch = (data, param) => {
|
||||||
|
const uniqueClients = uniqBy(data, param);
|
||||||
|
return uniqueClients
|
||||||
|
.reduce((acc, item, idx) => {
|
||||||
|
const key = `ip${idx}`;
|
||||||
|
acc[key] = item[param];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
|
@ -684,6 +684,21 @@ func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
|
||||||
|
func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
s.leasesLock.RLock()
|
||||||
|
defer s.leasesLock.RUnlock()
|
||||||
|
|
||||||
|
for _, l := range s.leases {
|
||||||
|
if l.Expiry.Unix() > now && l.IP.Equal(ip) {
|
||||||
|
return l.HWAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset internal state
|
// Reset internal state
|
||||||
func (s *Server) reset() {
|
func (s *Server) reset() {
|
||||||
s.leasesLock.Lock()
|
s.leasesLock.Lock()
|
||||||
|
|
447
home/clients.go
447
home/clients.go
|
@ -1,11 +1,10 @@
|
||||||
package home
|
package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -23,8 +22,7 @@ const (
|
||||||
|
|
||||||
// Client information
|
// Client information
|
||||||
type Client struct {
|
type Client struct {
|
||||||
IP string
|
IDs []string
|
||||||
MAC string
|
|
||||||
Name string
|
Name string
|
||||||
UseOwnSettings bool // false: use global settings
|
UseOwnSettings bool // false: use global settings
|
||||||
FilteringEnabled bool
|
FilteringEnabled bool
|
||||||
|
@ -37,22 +35,6 @@ type Client struct {
|
||||||
BlockedServices []string
|
BlockedServices []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientJSON struct {
|
|
||||||
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"`
|
|
||||||
|
|
||||||
WhoisInfo map[string]interface{} `json:"whois_info"`
|
|
||||||
|
|
||||||
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
|
||||||
BlockedServices []string `json:"blocked_services"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientSource uint
|
type clientSource uint
|
||||||
|
|
||||||
// Client sources
|
// Client sources
|
||||||
|
@ -74,24 +56,79 @@ type ClientHost struct {
|
||||||
|
|
||||||
type clientsContainer struct {
|
type clientsContainer struct {
|
||||||
list map[string]*Client // name -> client
|
list map[string]*Client // name -> client
|
||||||
ipIndex map[string]*Client // IP -> client
|
idIndex map[string]*Client // IP -> client
|
||||||
ipHost map[string]*ClientHost // IP -> Hostname
|
ipHost map[string]*ClientHost // IP -> Hostname
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes clients container
|
// Init initializes clients container
|
||||||
// Note: this function must be called only once
|
// Note: this function must be called only once
|
||||||
func (clients *clientsContainer) Init() {
|
func (clients *clientsContainer) Init(objects []clientObject) {
|
||||||
if clients.list != nil {
|
if clients.list != nil {
|
||||||
log.Fatal("clients.list != nil")
|
log.Fatal("clients.list != nil")
|
||||||
}
|
}
|
||||||
clients.list = make(map[string]*Client)
|
clients.list = make(map[string]*Client)
|
||||||
clients.ipIndex = make(map[string]*Client)
|
clients.idIndex = make(map[string]*Client)
|
||||||
clients.ipHost = make(map[string]*ClientHost)
|
clients.ipHost = make(map[string]*ClientHost)
|
||||||
|
clients.addFromConfig(objects)
|
||||||
|
|
||||||
go clients.periodicUpdate()
|
go clients.periodicUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type clientObject struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
IDs []string `yaml:"ids"`
|
||||||
|
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"`
|
||||||
|
|
||||||
|
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
|
||||||
|
BlockedServices []string `yaml:"blocked_services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clients *clientsContainer) addFromConfig(objects []clientObject) {
|
||||||
|
for _, cy := range objects {
|
||||||
|
cli := Client{
|
||||||
|
Name: cy.Name,
|
||||||
|
IDs: cy.IDs,
|
||||||
|
UseOwnSettings: !cy.UseGlobalSettings,
|
||||||
|
FilteringEnabled: cy.FilteringEnabled,
|
||||||
|
ParentalEnabled: cy.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: cy.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseOwnBlockedServices: !cy.UseGlobalBlockedServices,
|
||||||
|
BlockedServices: cy.BlockedServices,
|
||||||
|
}
|
||||||
|
_, err := clients.Add(cli)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("clientAdd: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteDiskConfig - write configuration
|
||||||
|
func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) {
|
||||||
|
clientsList := clients.GetList()
|
||||||
|
for _, cli := range clientsList {
|
||||||
|
cy := clientObject{
|
||||||
|
Name: cli.Name,
|
||||||
|
IDs: cli.IDs,
|
||||||
|
UseGlobalSettings: !cli.UseOwnSettings,
|
||||||
|
FilteringEnabled: cli.FilteringEnabled,
|
||||||
|
ParentalEnabled: cli.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: cli.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
|
||||||
|
BlockedServices: cli.BlockedServices,
|
||||||
|
}
|
||||||
|
*objects = append(*objects, cy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (clients *clientsContainer) periodicUpdate() {
|
func (clients *clientsContainer) periodicUpdate() {
|
||||||
for {
|
for {
|
||||||
clients.addFromHostsFile()
|
clients.addFromHostsFile()
|
||||||
|
@ -111,7 +148,7 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool {
|
||||||
clients.lock.Lock()
|
clients.lock.Lock()
|
||||||
defer clients.lock.Unlock()
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
_, ok := clients.ipIndex[ip]
|
_, ok := clients.idIndex[ip]
|
||||||
if ok {
|
if ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -128,25 +165,42 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool {
|
||||||
|
|
||||||
// Find searches for a client by IP
|
// Find searches for a client by IP
|
||||||
func (clients *clientsContainer) Find(ip string) (Client, bool) {
|
func (clients *clientsContainer) Find(ip string) (Client, bool) {
|
||||||
|
ipAddr := net.ParseIP(ip)
|
||||||
|
if ipAddr == nil {
|
||||||
|
return Client{}, false
|
||||||
|
}
|
||||||
|
|
||||||
clients.lock.Lock()
|
clients.lock.Lock()
|
||||||
defer clients.lock.Unlock()
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
c, ok := clients.ipIndex[ip]
|
c, ok := clients.idIndex[ip]
|
||||||
if ok {
|
if ok {
|
||||||
return *c, true
|
return *c, true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c = range clients.list {
|
for _, c = range clients.list {
|
||||||
if len(c.MAC) != 0 {
|
for _, id := range c.IDs {
|
||||||
mac, err := net.ParseMAC(c.MAC)
|
_, ipnet, err := net.ParseCIDR(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ipAddr := config.dhcpServer.FindIPbyMAC(mac)
|
if ipnet.Contains(ipAddr) {
|
||||||
if ipAddr == nil {
|
return *c, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macFound := config.dhcpServer.FindMACbyIP(ipAddr)
|
||||||
|
if macFound == nil {
|
||||||
|
return Client{}, false
|
||||||
|
}
|
||||||
|
for _, c = range clients.list {
|
||||||
|
for _, id := range c.IDs {
|
||||||
|
hwAddr, err := net.ParseMAC(id)
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ip == ipAddr.String() {
|
if bytes.Equal(hwAddr, macFound) {
|
||||||
return *c, true
|
return *c, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,28 +209,51 @@ func (clients *clientsContainer) Find(ip string) (Client, bool) {
|
||||||
return Client{}, false
|
return Client{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindAutoClient - search for an auto-client by IP
|
||||||
|
func (clients *clientsContainer) FindAutoClient(ip string) (ClientHost, bool) {
|
||||||
|
ipAddr := net.ParseIP(ip)
|
||||||
|
if ipAddr == nil {
|
||||||
|
return ClientHost{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
ch, ok := clients.ipHost[ip]
|
||||||
|
if ok {
|
||||||
|
return *ch, true
|
||||||
|
}
|
||||||
|
return ClientHost{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// Check if Client object's fields are correct
|
// Check if Client object's fields are correct
|
||||||
func (c *Client) check() error {
|
func (c *Client) check() error {
|
||||||
if len(c.Name) == 0 {
|
if len(c.Name) == 0 {
|
||||||
return fmt.Errorf("Invalid Name")
|
return fmt.Errorf("Invalid Name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len(c.IP) == 0 && len(c.MAC) == 0) ||
|
if len(c.IDs) == 0 {
|
||||||
(len(c.IP) != 0 && len(c.MAC) != 0) {
|
return fmt.Errorf("ID required")
|
||||||
return fmt.Errorf("IP or MAC required")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.IP) != 0 {
|
for i, id := range c.IDs {
|
||||||
ip := net.ParseIP(c.IP)
|
ip := net.ParseIP(id)
|
||||||
if ip == nil {
|
if ip != nil {
|
||||||
return fmt.Errorf("Invalid IP")
|
c.IDs[i] = ip.String() // normalize IP address
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
c.IP = ip.String()
|
|
||||||
} else {
|
_, _, err := net.ParseCIDR(id)
|
||||||
_, err := net.ParseMAC(c.MAC)
|
if err == nil {
|
||||||
if err != nil {
|
continue
|
||||||
return fmt.Errorf("Invalid MAC: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = net.ParseMAC(id)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Invalid ID: %s", id)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -198,26 +275,34 @@ func (clients *clientsContainer) Add(c Client) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check IP index
|
// check ID index
|
||||||
if len(c.IP) != 0 {
|
for _, id := range c.IDs {
|
||||||
c2, ok := clients.ipIndex[c.IP]
|
c2, ok := clients.idIndex[id]
|
||||||
if ok {
|
if ok {
|
||||||
return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
return false, fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, ok := clients.ipHost[c.IP]
|
// remove auto-clients with the same IP address, keeping WHOIS info if possible
|
||||||
|
for _, id := range c.IDs {
|
||||||
|
ch, ok := clients.ipHost[id]
|
||||||
if ok {
|
if ok {
|
||||||
|
if len(c.WhoisInfo) == 0 {
|
||||||
c.WhoisInfo = ch.WhoisInfo
|
c.WhoisInfo = ch.WhoisInfo
|
||||||
delete(clients.ipHost, c.IP)
|
}
|
||||||
|
delete(clients.ipHost, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update Name index
|
||||||
clients.list[c.Name] = &c
|
clients.list[c.Name] = &c
|
||||||
if len(c.IP) != 0 {
|
|
||||||
clients.ipIndex[c.IP] = &c
|
// update ID index
|
||||||
|
for _, id := range c.IDs {
|
||||||
|
clients.idIndex[id] = &c
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list))
|
log.Tracef("'%s': ID:%v [%d]", c.Name, c.IDs, len(clients.list))
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,8 +316,26 @@ func (clients *clientsContainer) Del(name string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update Name index
|
||||||
delete(clients.list, name)
|
delete(clients.list, name)
|
||||||
delete(clients.ipIndex, c.IP)
|
|
||||||
|
// update ID index
|
||||||
|
for _, id := range c.IDs {
|
||||||
|
delete(clients.idIndex, id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return TRUE if arrays are equal
|
||||||
|
func arraysEqual(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i != len(a); i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,27 +363,30 @@ func (clients *clientsContainer) Update(name string, c Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check IP index
|
// check IP index
|
||||||
if old.IP != c.IP && len(c.IP) != 0 {
|
if !arraysEqual(old.IDs, c.IDs) {
|
||||||
c2, ok := clients.ipIndex[c.IP]
|
for _, id := range c.IDs {
|
||||||
if ok {
|
c2, ok := clients.idIndex[id]
|
||||||
return fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
if ok && c2 != old {
|
||||||
|
return fmt.Errorf("Another client uses the same ID (%s): %s", id, c2.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update ID index
|
||||||
|
for _, id := range old.IDs {
|
||||||
|
delete(clients.idIndex, id)
|
||||||
|
}
|
||||||
|
for _, id := range c.IDs {
|
||||||
|
clients.idIndex[id] = old
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update Name index
|
// update Name index
|
||||||
if old.Name != c.Name {
|
if old.Name != c.Name {
|
||||||
delete(clients.list, old.Name)
|
delete(clients.list, old.Name)
|
||||||
}
|
clients.list[c.Name] = old
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*old = c
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +395,7 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) {
|
||||||
clients.lock.Lock()
|
clients.lock.Lock()
|
||||||
defer clients.lock.Unlock()
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
c, ok := clients.ipIndex[ip]
|
c, ok := clients.idIndex[ip]
|
||||||
if ok {
|
if ok {
|
||||||
c.WhoisInfo = info
|
c.WhoisInfo = info
|
||||||
log.Debug("Clients: set WHOIS info for client %s: %v", c.Name, c.WhoisInfo)
|
log.Debug("Clients: set WHOIS info for client %s: %v", c.Name, c.WhoisInfo)
|
||||||
|
@ -319,7 +425,7 @@ func (clients *clientsContainer) AddHost(ip, host string, source clientSource) (
|
||||||
defer clients.lock.Unlock()
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
// check index
|
// check index
|
||||||
_, ok := clients.ipIndex[ip]
|
_, ok := clients.idIndex[ip]
|
||||||
if ok {
|
if ok {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -440,210 +546,3 @@ func (clients *clientsContainer) addFromDHCP() {
|
||||||
}
|
}
|
||||||
log.Debug("Added %d client aliases from DHCP", n)
|
log.Debug("Added %d client aliases from DHCP", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientHostJSON struct {
|
|
||||||
IP string `json:"ip"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
|
|
||||||
WhoisInfo map[string]interface{} `json:"whois_info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
data := clientListJSON{}
|
|
||||||
|
|
||||||
config.clients.lock.Lock()
|
|
||||||
for _, c := range config.clients.list {
|
|
||||||
cj := clientJSON{
|
|
||||||
IP: c.IP,
|
|
||||||
MAC: c.MAC,
|
|
||||||
Name: c.Name,
|
|
||||||
UseGlobalSettings: !c.UseOwnSettings,
|
|
||||||
FilteringEnabled: c.FilteringEnabled,
|
|
||||||
ParentalEnabled: c.ParentalEnabled,
|
|
||||||
SafeSearchEnabled: c.SafeSearchEnabled,
|
|
||||||
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
|
||||||
|
|
||||||
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
|
|
||||||
BlockedServices: c.BlockedServices,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.MAC) != 0 {
|
|
||||||
hwAddr, _ := net.ParseMAC(c.MAC)
|
|
||||||
ipAddr := config.dhcpServer.FindIPbyMAC(hwAddr)
|
|
||||||
if ipAddr != nil {
|
|
||||||
cj.IP = ipAddr.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cj.WhoisInfo = make(map[string]interface{})
|
|
||||||
for _, wi := range c.WhoisInfo {
|
|
||||||
cj.WhoisInfo[wi[0]] = wi[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Clients = append(data.Clients, cj)
|
|
||||||
}
|
|
||||||
for ip, ch := range config.clients.ipHost {
|
|
||||||
cj := clientHostJSON{
|
|
||||||
IP: ip,
|
|
||||||
Name: ch.Host,
|
|
||||||
}
|
|
||||||
|
|
||||||
cj.Source = "etc/hosts"
|
|
||||||
switch ch.Source {
|
|
||||||
case ClientSourceDHCP:
|
|
||||||
cj.Source = "DHCP"
|
|
||||||
case ClientSourceRDNS:
|
|
||||||
cj.Source = "rDNS"
|
|
||||||
case ClientSourceARP:
|
|
||||||
cj.Source = "ARP"
|
|
||||||
case ClientSourceWHOIS:
|
|
||||||
cj.Source = "WHOIS"
|
|
||||||
}
|
|
||||||
|
|
||||||
cj.WhoisInfo = make(map[string]interface{})
|
|
||||||
for _, wi := range ch.WhoisInfo {
|
|
||||||
cj.WhoisInfo[wi[0]] = wi[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
data.AutoClients = append(data.AutoClients, cj)
|
|
||||||
}
|
|
||||||
config.clients.lock.Unlock()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
e := json.NewEncoder(w).Encode(data)
|
|
||||||
if e != nil {
|
|
||||||
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
|
|
||||||
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
|
||||||
BlockedServices: cj.BlockedServices,
|
|
||||||
}
|
|
||||||
return &c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new client
|
|
||||||
func handleAddClient(w http.ResponseWriter, r *http.Request) {
|
|
||||||
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 := config.clients.Add(*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) {
|
|
||||||
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 !config.clients.Del(cj.Name) {
|
|
||||||
httpError(w, http.StatusBadRequest, "Client not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = writeAllConfigsAndReloadDNS()
|
|
||||||
returnOK(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateJSON struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Data clientJSON `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update client's properties
|
|
||||||
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var dj updateJSON
|
|
||||||
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 = config.clients.Update(dj.Name, *c)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusBadRequest, "%s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = writeAllConfigsAndReloadDNS()
|
|
||||||
returnOK(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterClientsHandlers registers HTTP handlers
|
|
||||||
func RegisterClientsHandlers() {
|
|
||||||
httpRegister(http.MethodGet, "/control/clients", handleGetClients)
|
|
||||||
httpRegister(http.MethodPost, "/control/clients/add", handleAddClient)
|
|
||||||
httpRegister(http.MethodPost, "/control/clients/delete", handleDelClient)
|
|
||||||
httpRegister(http.MethodPost, "/control/clients/update", handleUpdateClient)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,286 @@
|
||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clientJSON struct {
|
||||||
|
IDs []string `json:"ids"`
|
||||||
|
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"`
|
||||||
|
|
||||||
|
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||||
|
|
||||||
|
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
||||||
|
BlockedServices []string `json:"blocked_services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientHostJSON struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
|
||||||
|
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
data := clientListJSON{}
|
||||||
|
|
||||||
|
config.clients.lock.Lock()
|
||||||
|
for _, c := range config.clients.list {
|
||||||
|
cj := clientToJSON(c)
|
||||||
|
data.Clients = append(data.Clients, cj)
|
||||||
|
}
|
||||||
|
for ip, ch := range config.clients.ipHost {
|
||||||
|
cj := clientHostJSON{
|
||||||
|
IP: ip,
|
||||||
|
Name: ch.Host,
|
||||||
|
}
|
||||||
|
|
||||||
|
cj.Source = "etc/hosts"
|
||||||
|
switch ch.Source {
|
||||||
|
case ClientSourceDHCP:
|
||||||
|
cj.Source = "DHCP"
|
||||||
|
case ClientSourceRDNS:
|
||||||
|
cj.Source = "rDNS"
|
||||||
|
case ClientSourceARP:
|
||||||
|
cj.Source = "ARP"
|
||||||
|
case ClientSourceWHOIS:
|
||||||
|
cj.Source = "WHOIS"
|
||||||
|
}
|
||||||
|
|
||||||
|
cj.WhoisInfo = make(map[string]interface{})
|
||||||
|
for _, wi := range ch.WhoisInfo {
|
||||||
|
cj.WhoisInfo[wi[0]] = wi[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
data.AutoClients = append(data.AutoClients, cj)
|
||||||
|
}
|
||||||
|
config.clients.lock.Unlock()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
e := json.NewEncoder(w).Encode(data)
|
||||||
|
if e != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert JSON object to Client object
|
||||||
|
func jsonToClient(cj clientJSON) (*Client, error) {
|
||||||
|
c := Client{
|
||||||
|
Name: cj.Name,
|
||||||
|
IDs: cj.IDs,
|
||||||
|
UseOwnSettings: !cj.UseGlobalSettings,
|
||||||
|
FilteringEnabled: cj.FilteringEnabled,
|
||||||
|
ParentalEnabled: cj.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: cj.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
||||||
|
BlockedServices: cj.BlockedServices,
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Client object to JSON
|
||||||
|
func clientToJSON(c *Client) clientJSON {
|
||||||
|
cj := clientJSON{
|
||||||
|
Name: c.Name,
|
||||||
|
IDs: c.IDs,
|
||||||
|
UseGlobalSettings: !c.UseOwnSettings,
|
||||||
|
FilteringEnabled: c.FilteringEnabled,
|
||||||
|
ParentalEnabled: c.ParentalEnabled,
|
||||||
|
SafeSearchEnabled: c.SafeSearchEnabled,
|
||||||
|
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
|
||||||
|
BlockedServices: c.BlockedServices,
|
||||||
|
}
|
||||||
|
|
||||||
|
cj.WhoisInfo = make(map[string]interface{})
|
||||||
|
for _, wi := range c.WhoisInfo {
|
||||||
|
cj.WhoisInfo[wi[0]] = wi[1]
|
||||||
|
}
|
||||||
|
return cj
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientHostJSONWithID struct {
|
||||||
|
IDs []string `json:"ids"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
WhoisInfo map[string]interface{} `json:"whois_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ClientHost object to JSON
|
||||||
|
func clientHostToJSON(ip string, ch ClientHost) clientHostJSONWithID {
|
||||||
|
cj := clientHostJSONWithID{
|
||||||
|
Name: ch.Host,
|
||||||
|
IDs: []string{ip},
|
||||||
|
}
|
||||||
|
|
||||||
|
cj.WhoisInfo = make(map[string]interface{})
|
||||||
|
for _, wi := range ch.WhoisInfo {
|
||||||
|
cj.WhoisInfo[wi[0]] = wi[1]
|
||||||
|
}
|
||||||
|
return cj
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new client
|
||||||
|
func handleAddClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
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 := config.clients.Add(*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) {
|
||||||
|
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 !config.clients.Del(cj.Name) {
|
||||||
|
httpError(w, http.StatusBadRequest, "Client not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = writeAllConfigsAndReloadDNS()
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateJSON struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data clientJSON `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update client's properties
|
||||||
|
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dj updateJSON
|
||||||
|
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 = config.clients.Update(dj.Name, *c)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = writeAllConfigsAndReloadDNS()
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list of clients by IP address list
|
||||||
|
func handleFindClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
data := []map[string]interface{}{}
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
ip := q.Get(fmt.Sprintf("ip%d", i))
|
||||||
|
if len(ip) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
el := map[string]interface{}{}
|
||||||
|
c, ok := config.clients.Find(ip)
|
||||||
|
if !ok {
|
||||||
|
ch, ok := config.clients.FindAutoClient(ip)
|
||||||
|
if !ok {
|
||||||
|
continue // a client with this IP isn't found
|
||||||
|
}
|
||||||
|
cj := clientHostToJSON(ip, ch)
|
||||||
|
el[ip] = cj
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cj := clientToJSON(&c)
|
||||||
|
el[ip] = cj
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, el)
|
||||||
|
}
|
||||||
|
|
||||||
|
js, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, err = w.Write(js)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "Couldn't write response: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterClientsHandlers registers HTTP handlers
|
||||||
|
func RegisterClientsHandlers() {
|
||||||
|
httpRegister("GET", "/control/clients", handleGetClients)
|
||||||
|
httpRegister("POST", "/control/clients/add", handleAddClient)
|
||||||
|
httpRegister("POST", "/control/clients/delete", handleDelClient)
|
||||||
|
httpRegister("POST", "/control/clients/update", handleUpdateClient)
|
||||||
|
httpRegister("GET", "/control/clients/find", handleFindClient)
|
||||||
|
}
|
|
@ -12,11 +12,11 @@ func TestClients(t *testing.T) {
|
||||||
var b bool
|
var b bool
|
||||||
clients := clientsContainer{}
|
clients := clientsContainer{}
|
||||||
|
|
||||||
clients.Init()
|
clients.Init(nil)
|
||||||
|
|
||||||
// add
|
// add
|
||||||
c = Client{
|
c = Client{
|
||||||
IP: "1.1.1.1",
|
IDs: []string{"1.1.1.1", "aa:aa:aa:aa:aa:aa"},
|
||||||
Name: "client1",
|
Name: "client1",
|
||||||
}
|
}
|
||||||
b, e = clients.Add(c)
|
b, e = clients.Add(c)
|
||||||
|
@ -26,7 +26,7 @@ func TestClients(t *testing.T) {
|
||||||
|
|
||||||
// add #2
|
// add #2
|
||||||
c = Client{
|
c = Client{
|
||||||
IP: "2.2.2.2",
|
IDs: []string{"2.2.2.2"},
|
||||||
Name: "client2",
|
Name: "client2",
|
||||||
}
|
}
|
||||||
b, e = clients.Add(c)
|
b, e = clients.Add(c)
|
||||||
|
@ -46,7 +46,7 @@ func TestClients(t *testing.T) {
|
||||||
|
|
||||||
// failed add - name in use
|
// failed add - name in use
|
||||||
c = Client{
|
c = Client{
|
||||||
IP: "1.2.3.5",
|
IDs: []string{"1.2.3.5"},
|
||||||
Name: "client1",
|
Name: "client1",
|
||||||
}
|
}
|
||||||
b, _ = clients.Add(c)
|
b, _ = clients.Add(c)
|
||||||
|
@ -56,7 +56,7 @@ func TestClients(t *testing.T) {
|
||||||
|
|
||||||
// failed add - ip in use
|
// failed add - ip in use
|
||||||
c = Client{
|
c = Client{
|
||||||
IP: "2.2.2.2",
|
IDs: []string{"2.2.2.2"},
|
||||||
Name: "client3",
|
Name: "client3",
|
||||||
}
|
}
|
||||||
b, e = clients.Add(c)
|
b, e = clients.Add(c)
|
||||||
|
@ -70,35 +70,45 @@ func TestClients(t *testing.T) {
|
||||||
assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile))
|
assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile))
|
||||||
|
|
||||||
// failed update - no such name
|
// failed update - no such name
|
||||||
c.IP = "1.2.3.0"
|
c.IDs = []string{"1.2.3.0"}
|
||||||
c.Name = "client3"
|
c.Name = "client3"
|
||||||
if clients.Update("client3", c) == nil {
|
if clients.Update("client3", c) == nil {
|
||||||
t.Fatalf("Update")
|
t.Fatalf("Update")
|
||||||
}
|
}
|
||||||
|
|
||||||
// failed update - name in use
|
// failed update - name in use
|
||||||
c.IP = "1.2.3.0"
|
c.IDs = []string{"1.2.3.0"}
|
||||||
c.Name = "client2"
|
c.Name = "client2"
|
||||||
if clients.Update("client1", c) == nil {
|
if clients.Update("client1", c) == nil {
|
||||||
t.Fatalf("Update - name in use")
|
t.Fatalf("Update - name in use")
|
||||||
}
|
}
|
||||||
|
|
||||||
// failed update - ip in use
|
// failed update - ip in use
|
||||||
c.IP = "2.2.2.2"
|
c.IDs = []string{"2.2.2.2"}
|
||||||
c.Name = "client1"
|
c.Name = "client1"
|
||||||
if clients.Update("client1", c) == nil {
|
if clients.Update("client1", c) == nil {
|
||||||
t.Fatalf("Update - ip in use")
|
t.Fatalf("Update - ip in use")
|
||||||
}
|
}
|
||||||
|
|
||||||
// update
|
// update
|
||||||
c.IP = "1.1.1.2"
|
c.IDs = []string{"1.1.1.2"}
|
||||||
c.Name = "client1"
|
c.Name = "client1"
|
||||||
if clients.Update("client1", c) != nil {
|
if clients.Update("client1", c) != nil {
|
||||||
t.Fatalf("Update")
|
t.Fatalf("Update")
|
||||||
}
|
}
|
||||||
|
|
||||||
// get after update
|
// get after update
|
||||||
assert.True(t, !(clients.Exists("1.1.1.1", ClientSourceHostsFile) || !clients.Exists("1.1.1.2", ClientSourceHostsFile)))
|
assert.True(t, !clients.Exists("1.1.1.1", ClientSourceHostsFile))
|
||||||
|
assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile))
|
||||||
|
|
||||||
|
// update - rename
|
||||||
|
c.IDs = []string{"1.1.1.2"}
|
||||||
|
c.Name = "client1-renamed"
|
||||||
|
c.UseOwnSettings = true
|
||||||
|
assert.True(t, clients.Update("client1", c) == nil)
|
||||||
|
c = Client{}
|
||||||
|
c, b = clients.Find("1.1.1.2")
|
||||||
|
assert.True(t, b && c.Name == "client1-renamed" && c.IDs[0] == "1.1.1.2" && c.UseOwnSettings)
|
||||||
|
|
||||||
// failed remove - no such name
|
// failed remove - no such name
|
||||||
if clients.Del("client3") {
|
if clients.Del("client3") {
|
||||||
|
@ -106,7 +116,7 @@ func TestClients(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove
|
// remove
|
||||||
assert.True(t, !(!clients.Del("client1") || clients.Exists("1.1.1.2", ClientSourceHostsFile)))
|
assert.True(t, !(!clients.Del("client1-renamed") || clients.Exists("1.1.1.2", ClientSourceHostsFile)))
|
||||||
|
|
||||||
// add host client
|
// add host client
|
||||||
b, e = clients.AddHost("1.1.1.1", "host", ClientSourceARP)
|
b, e = clients.AddHost("1.1.1.1", "host", ClientSourceARP)
|
||||||
|
@ -139,7 +149,7 @@ func TestClients(t *testing.T) {
|
||||||
func TestClientsWhois(t *testing.T) {
|
func TestClientsWhois(t *testing.T) {
|
||||||
var c Client
|
var c Client
|
||||||
clients := clientsContainer{}
|
clients := clientsContainer{}
|
||||||
clients.Init()
|
clients.Init(nil)
|
||||||
|
|
||||||
whois := [][]string{{"orgname", "orgname-val"}, {"country", "country-val"}}
|
whois := [][]string{{"orgname", "orgname-val"}, {"country", "country-val"}}
|
||||||
// set whois info on new client
|
// set whois info on new client
|
||||||
|
@ -153,11 +163,11 @@ func TestClientsWhois(t *testing.T) {
|
||||||
|
|
||||||
// set whois info on existing client
|
// set whois info on existing client
|
||||||
c = Client{
|
c = Client{
|
||||||
IP: "1.1.1.2",
|
IDs: []string{"1.1.1.2"},
|
||||||
Name: "client1",
|
Name: "client1",
|
||||||
}
|
}
|
||||||
_, _ = clients.Add(c)
|
_, _ = clients.Add(c)
|
||||||
clients.SetWhoisInfo("1.1.1.2", whois)
|
clients.SetWhoisInfo("1.1.1.2", whois)
|
||||||
assert.True(t, clients.ipIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val")
|
assert.True(t, clients.idIndex["1.1.1.2"].WhoisInfo[0][1] == "orgname-val")
|
||||||
_ = clients.Del("client1")
|
_ = clients.Del("client1")
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,20 +30,6 @@ 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"`
|
|
||||||
|
|
||||||
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
|
|
||||||
BlockedServices []string `yaml:"blocked_services"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPSServer struct {
|
type HTTPSServer struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
|
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
|
||||||
|
@ -285,27 +271,6 @@ func parseConfig() error {
|
||||||
config.DNS.FiltersUpdateIntervalHours = 24
|
config.DNS.FiltersUpdateIntervalHours = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
|
|
||||||
UseOwnBlockedServices: !cy.UseGlobalBlockedServices,
|
|
||||||
BlockedServices: cy.BlockedServices,
|
|
||||||
}
|
|
||||||
_, err = config.clients.Add(cli)
|
|
||||||
if err != nil {
|
|
||||||
log.Tracef("clientAdd: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.Clients = nil
|
|
||||||
|
|
||||||
status := tlsConfigStatus{}
|
status := tlsConfigStatus{}
|
||||||
if !tlsLoadConfig(&config.TLS, &status) {
|
if !tlsLoadConfig(&config.TLS, &status) {
|
||||||
log.Error("%s", status.WarningValidation)
|
log.Error("%s", status.WarningValidation)
|
||||||
|
@ -335,27 +300,7 @@ func (c *configuration) write() error {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
|
|
||||||
clientsList := config.clients.GetList()
|
config.clients.WriteDiskConfig(&config.Clients)
|
||||||
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,
|
|
||||||
|
|
||||||
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
|
|
||||||
BlockedServices: cli.BlockedServices,
|
|
||||||
}
|
|
||||||
config.Clients = append(config.Clients, cy)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.auth != nil {
|
if config.auth != nil {
|
||||||
config.Users = config.auth.GetUsers()
|
config.Users = config.auth.GetUsers()
|
||||||
|
|
|
@ -98,7 +98,6 @@ func run(args options) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
initConfig()
|
initConfig()
|
||||||
config.clients.Init()
|
|
||||||
initServices()
|
initServices()
|
||||||
|
|
||||||
if !config.firstRun {
|
if !config.firstRun {
|
||||||
|
@ -119,6 +118,9 @@ func run(args options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.clients.Init(config.Clients)
|
||||||
|
config.Clients = nil
|
||||||
|
|
||||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||||
config.RlimitNoFile != 0 {
|
config.RlimitNoFile != 0 {
|
||||||
setRlimit(config.RlimitNoFile)
|
setRlimit(config.RlimitNoFile)
|
||||||
|
@ -370,11 +372,13 @@ func cleanup() {
|
||||||
|
|
||||||
// Stop HTTP server, possibly waiting for all active connections to be closed
|
// Stop HTTP server, possibly waiting for all active connections to be closed
|
||||||
func stopHTTPServer() {
|
func stopHTTPServer() {
|
||||||
|
log.Info("Stopping HTTP server...")
|
||||||
config.httpsServer.shutdown = true
|
config.httpsServer.shutdown = true
|
||||||
if config.httpsServer.server != nil {
|
if config.httpsServer.server != nil {
|
||||||
config.httpsServer.server.Shutdown(context.TODO())
|
config.httpsServer.server.Shutdown(context.TODO())
|
||||||
}
|
}
|
||||||
config.httpServer.Shutdown(context.TODO())
|
config.httpServer.Shutdown(context.TODO())
|
||||||
|
log.Info("Stopped HTTP server")
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is called before application exits
|
// This function is called before application exits
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentSchemaVersion = 5 // used for upgrading from old configs to new config
|
const currentSchemaVersion = 6 // used for upgrading from old configs to new config
|
||||||
|
|
||||||
// Performs necessary upgrade operations if needed
|
// Performs necessary upgrade operations if needed
|
||||||
func upgradeConfig() error {
|
func upgradeConfig() error {
|
||||||
|
@ -82,6 +82,12 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fallthrough
|
||||||
|
case 5:
|
||||||
|
err := upgradeSchema5to6(diskConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err := fmt.Errorf("configuration file contains unknown schema_version, abort")
|
err := fmt.Errorf("configuration file contains unknown schema_version, abort")
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
@ -268,3 +274,72 @@ func upgradeSchema4to5(diskConfig *map[string]interface{}) error {
|
||||||
(*diskConfig)["users"] = users
|
(*diskConfig)["users"] = users
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clients:
|
||||||
|
// ...
|
||||||
|
// ip: 127.0.0.1
|
||||||
|
// mac: ...
|
||||||
|
//
|
||||||
|
// ->
|
||||||
|
//
|
||||||
|
// clients:
|
||||||
|
// ...
|
||||||
|
// ids:
|
||||||
|
// - 127.0.0.1
|
||||||
|
// - ...
|
||||||
|
func upgradeSchema5to6(diskConfig *map[string]interface{}) error {
|
||||||
|
log.Printf("%s(): called", _Func())
|
||||||
|
|
||||||
|
(*diskConfig)["schema_version"] = 6
|
||||||
|
|
||||||
|
clients, ok := (*diskConfig)["clients"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch arr := clients.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
|
||||||
|
for i := range arr {
|
||||||
|
|
||||||
|
switch c := arr[i].(type) {
|
||||||
|
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
_ip, ok := c["ip"]
|
||||||
|
ids := []string{}
|
||||||
|
if ok {
|
||||||
|
ip, ok := _ip.(string)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("client.ip is not a string: %v", _ip)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(ip) != 0 {
|
||||||
|
ids = append(ids, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mac, ok := c["mac"]
|
||||||
|
if ok {
|
||||||
|
mac, ok := _mac.(string)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("client.mac is not a string: %v", _mac)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(mac) != 0 {
|
||||||
|
ids = append(ids, mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c["ids"] = ids
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,95 @@
|
||||||
# AdGuard Home API Change Log
|
# AdGuard Home API Change Log
|
||||||
|
|
||||||
|
|
||||||
|
## v0.100: API changes
|
||||||
|
|
||||||
|
### API: Get list of clients: GET /control/clients
|
||||||
|
|
||||||
|
* "ip" and "mac" fields are removed
|
||||||
|
* "ids" and "ip_addrs" fields are added
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
clients: [
|
||||||
|
{
|
||||||
|
name: "client1"
|
||||||
|
ids: ["...", ...] // IP or MAC
|
||||||
|
ip_addrs: ["...", ...] // all IP addresses (set by user and resolved by MAC)
|
||||||
|
use_global_settings: true
|
||||||
|
filtering_enabled: false
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
safesearch_enabled: false
|
||||||
|
use_global_blocked_services: true
|
||||||
|
blocked_services: [ "name1", ... ]
|
||||||
|
whois_info: {
|
||||||
|
key: "value"
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
auto_clients: [
|
||||||
|
{
|
||||||
|
name: "host"
|
||||||
|
ip: "..."
|
||||||
|
source: "etc/hosts" || "rDNS"
|
||||||
|
whois_info: {
|
||||||
|
key: "value"
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
### API: Add client: POST /control/clients/add
|
||||||
|
|
||||||
|
* "ip" and "mac" fields are removed
|
||||||
|
* "ids" field is added
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/clients/add
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "client1"
|
||||||
|
ids: ["...", ...] // IP or MAC
|
||||||
|
use_global_settings: true
|
||||||
|
filtering_enabled: false
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
safesearch_enabled: false
|
||||||
|
use_global_blocked_services: true
|
||||||
|
blocked_services: [ "name1", ... ]
|
||||||
|
}
|
||||||
|
|
||||||
|
### API: Update client: POST /control/clients/update
|
||||||
|
|
||||||
|
* "ip" and "mac" fields are removed
|
||||||
|
* "ids" field is added
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/clients/update
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "client1"
|
||||||
|
data: {
|
||||||
|
name: "client1"
|
||||||
|
ids: ["...", ...] // IP or MAC
|
||||||
|
use_global_settings: true
|
||||||
|
filtering_enabled: false
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
safesearch_enabled: false
|
||||||
|
use_global_blocked_services: true
|
||||||
|
blocked_services: [ "name1", ... ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
## v0.99.3: API changes
|
## v0.99.3: API changes
|
||||||
|
|
||||||
### API: Get query log: GET /control/querylog
|
### API: Get query log: GET /control/querylog
|
||||||
|
|
|
@ -772,6 +772,22 @@ paths:
|
||||||
200:
|
200:
|
||||||
description: OK
|
description: OK
|
||||||
|
|
||||||
|
/clients/find:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- clients
|
||||||
|
operationId: clientsFind
|
||||||
|
summary: 'Get information about selected clients by their IP address'
|
||||||
|
parameters:
|
||||||
|
- name: ip0
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/ClientsFindResponse"
|
||||||
|
|
||||||
|
|
||||||
/blocked_services/list:
|
/blocked_services/list:
|
||||||
get:
|
get:
|
||||||
|
@ -1589,15 +1605,14 @@ definitions:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Client information"
|
description: "Client information"
|
||||||
properties:
|
properties:
|
||||||
ip:
|
|
||||||
type: "string"
|
|
||||||
description: "IP address"
|
|
||||||
example: "127.0.0.1"
|
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: "string"
|
||||||
description: "Name"
|
description: "Name"
|
||||||
example: "localhost"
|
example: "localhost"
|
||||||
mac:
|
ids:
|
||||||
|
type: "array"
|
||||||
|
description: "IP, CIDR or MAC address"
|
||||||
|
items:
|
||||||
type: "string"
|
type: "string"
|
||||||
use_global_settings:
|
use_global_settings:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
|
@ -1645,6 +1660,20 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: "string"
|
||||||
|
|
||||||
|
ClientsFindResponse:
|
||||||
|
type: "array"
|
||||||
|
description: "Response to clients find operation"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/ClientsFindEntry"
|
||||||
|
|
||||||
|
ClientsFindEntry:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
"1.2.3.4":
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/Client"
|
||||||
|
|
||||||
Clients:
|
Clients:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
|
Loading…
Reference in New Issue