Merge: block access to specific services
Close #819 * commit 'e7001c3bc4a7e1ec53d810cafbc90d7f2a624af5': * config upgrade: sequential processing * config: upgrade schema version: 3 -> 4 + client: add prefix to the service icons + client: handle blocked services + openapi: add /blocked_services/* + openapi: clients: add "blocked_services" field * refactor + /control/blocked_services/* API + dnsfilter: use global and per-client BlockedServices array + clients: add BlockedServices field + config: store/load "blocked_services" per-client setting + doc: add "Services Filter" section
This commit is contained in:
commit
a59b6b3054
@ -31,6 +31,9 @@ Contents:
|
|||||||
* API: List rewrite entries
|
* API: List rewrite entries
|
||||||
* API: Add a rewrite entry
|
* API: Add a rewrite entry
|
||||||
* API: Remove a rewrite entry
|
* API: Remove a rewrite entry
|
||||||
|
* Services Filter
|
||||||
|
* API: Get blocked services list
|
||||||
|
* API: Set blocked services list
|
||||||
|
|
||||||
|
|
||||||
## First startup
|
## First startup
|
||||||
@ -536,6 +539,8 @@ Notes:
|
|||||||
|
|
||||||
* If `use_global_settings` is false, then the client-specific settings are used to override (enable or disable) global settings.
|
* If `use_global_settings` is false, then the client-specific settings are used to override (enable or disable) global settings.
|
||||||
|
|
||||||
|
* If `use_global_blocked_services` is false, then the client-specific settings are used to override (enable or disable) global Blocked Services settings.
|
||||||
|
|
||||||
|
|
||||||
### Get list of clients
|
### Get list of clients
|
||||||
|
|
||||||
@ -558,6 +563,8 @@ Response:
|
|||||||
parental_enabled: false
|
parental_enabled: false
|
||||||
safebrowsing_enabled: false
|
safebrowsing_enabled: false
|
||||||
safesearch_enabled: false
|
safesearch_enabled: false
|
||||||
|
use_global_blocked_services: true
|
||||||
|
blocked_services: [ "name1", ... ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
auto_clients: [
|
auto_clients: [
|
||||||
@ -585,6 +592,8 @@ Request:
|
|||||||
parental_enabled: false
|
parental_enabled: false
|
||||||
safebrowsing_enabled: false
|
safebrowsing_enabled: false
|
||||||
safesearch_enabled: false
|
safesearch_enabled: false
|
||||||
|
use_global_blocked_services: true
|
||||||
|
blocked_services: [ "name1", ... ]
|
||||||
}
|
}
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
@ -613,6 +622,8 @@ Request:
|
|||||||
parental_enabled: false
|
parental_enabled: false
|
||||||
safebrowsing_enabled: false
|
safebrowsing_enabled: false
|
||||||
safesearch_enabled: false
|
safesearch_enabled: false
|
||||||
|
use_global_blocked_services: true
|
||||||
|
blocked_services: [ "name1", ... ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -743,3 +754,49 @@ Request:
|
|||||||
Response:
|
Response:
|
||||||
|
|
||||||
200 OK
|
200 OK
|
||||||
|
|
||||||
|
|
||||||
|
## Services Filter
|
||||||
|
|
||||||
|
Allows to quickly block popular sites globally or for specific client only.
|
||||||
|
UI manages these settings via global or per-client API.
|
||||||
|
UI and server have the same list of the services supported and this list must always be in synchronization.
|
||||||
|
UI code also contains icons for each service: `client/src/components/ui/Icons.js`.
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
* UI presents the list of services which user may want to block
|
||||||
|
* Admin clicks on the checkboxes in front of the services to block and presses Save
|
||||||
|
* UI sends `Set blocked services list` or `Update client` message
|
||||||
|
* Server updates the internal configuration
|
||||||
|
* When a user sends a DNS request for a host which is blocked by these settings, he won't receive its IP address
|
||||||
|
* Query log will show that this request was blocked by "Blocked services"
|
||||||
|
|
||||||
|
Internally, all supported services are stored as a map:
|
||||||
|
|
||||||
|
service name -> list of rules
|
||||||
|
|
||||||
|
|
||||||
|
### API: Get blocked services list
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
GET /control/blocked_services/list
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
[ "name1", ... ]
|
||||||
|
|
||||||
|
|
||||||
|
### API: Set blocked services list
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/blocked_services/set
|
||||||
|
|
||||||
|
[ "name1", ... ]
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
6
client/package-lock.json
generated
vendored
6
client/package-lock.json
generated
vendored
@ -945,9 +945,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.18.1",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
|
||||||
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "1.5.10",
|
"follow-redirects": "1.5.10",
|
||||||
"is-buffer": "^2.0.2"
|
"is-buffer": "^2.0.2"
|
||||||
|
2
client/package.json
vendored
2
client/package.json
vendored
@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nivo/line": "^0.49.1",
|
"@nivo/line": "^0.49.1",
|
||||||
"axios": "^0.18.1",
|
"axios": "^0.19.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"date-fns": "^1.29.0",
|
"date-fns": "^1.29.0",
|
||||||
"file-saver": "^1.3.8",
|
"file-saver": "^1.3.8",
|
||||||
|
@ -344,5 +344,14 @@
|
|||||||
"form_answer": "Enter IP address or domain name",
|
"form_answer": "Enter IP address or domain name",
|
||||||
"form_error_domain_format": "Invalid domain format",
|
"form_error_domain_format": "Invalid domain format",
|
||||||
"form_error_answer_format": "Invalid answer format",
|
"form_error_answer_format": "Invalid answer format",
|
||||||
"configure": "Configure"
|
"configure": "Configure",
|
||||||
|
"main_settings": "Main settings",
|
||||||
|
"block_services": "Block specific services",
|
||||||
|
"blocked_services": "Blocked services",
|
||||||
|
"blocked_services_desc": "Allows to quickly block popular sites and services.",
|
||||||
|
"blocked_services_saved": "Blocked services successfully saved",
|
||||||
|
"blocked_services_global": "Use global blocked services",
|
||||||
|
"blocked_service": "Blocked service",
|
||||||
|
"block_all": "Block all",
|
||||||
|
"unblock_all": "Unblock all"
|
||||||
}
|
}
|
||||||
|
37
client/src/actions/services.js
Normal file
37
client/src/actions/services.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import Api from '../api/Api';
|
||||||
|
import { addErrorToast, addSuccessToast } from './index';
|
||||||
|
|
||||||
|
const apiClient = new Api();
|
||||||
|
|
||||||
|
export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST');
|
||||||
|
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
|
||||||
|
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');
|
||||||
|
|
||||||
|
export const getBlockedServices = () => async (dispatch) => {
|
||||||
|
dispatch(getBlockedServicesRequest());
|
||||||
|
try {
|
||||||
|
const data = await apiClient.getBlockedServices();
|
||||||
|
dispatch(getBlockedServicesSuccess(data));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(getBlockedServicesFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST');
|
||||||
|
export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE');
|
||||||
|
export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS');
|
||||||
|
|
||||||
|
export const setBlockedServices = values => async (dispatch) => {
|
||||||
|
dispatch(setBlockedServicesRequest());
|
||||||
|
try {
|
||||||
|
await apiClient.setBlockedServices(values);
|
||||||
|
dispatch(setBlockedServicesSuccess());
|
||||||
|
dispatch(getBlockedServices());
|
||||||
|
dispatch(addSuccessToast('blocked_services_saved'));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(setBlockedServicesFailure());
|
||||||
|
}
|
||||||
|
};
|
@ -509,4 +509,22 @@ export default class Api {
|
|||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blocked services
|
||||||
|
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
|
||||||
|
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
|
||||||
|
|
||||||
|
getBlockedServices() {
|
||||||
|
const { path, method } = this.BLOCKED_SERVICES_LIST;
|
||||||
|
return this.makeRequest(path, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlockedServices(config) {
|
||||||
|
const { path, method } = this.BLOCKED_SERVICES_SET;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs__row--icons {
|
||||||
|
max-width: 180px;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.logs__row .list-unstyled {
|
.logs__row .list-unstyled {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -26,6 +31,7 @@
|
|||||||
|
|
||||||
.logs__text,
|
.logs__text,
|
||||||
.logs__row .list-unstyled li {
|
.logs__row .list-unstyled li {
|
||||||
|
padding: 0 1px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -8,6 +8,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
|||||||
import { HashLink as Link } from 'react-router-hash-link';
|
import { HashLink as Link } from 'react-router-hash-link';
|
||||||
|
|
||||||
import { formatTime, getClientName } from '../../helpers/helpers';
|
import { formatTime, getClientName } from '../../helpers/helpers';
|
||||||
|
import { SERVICES } from '../../helpers/constants';
|
||||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
@ -39,12 +40,8 @@ class Logs extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTooltip(isFiltered, rule, filter) {
|
renderTooltip = (isFiltered, rule, filter, service) =>
|
||||||
if (rule) {
|
isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />;
|
||||||
return (isFiltered && <PopoverFiltered rule={rule} filter={filter}/>);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleBlocking = (type, domain) => {
|
toggleBlocking = (type, domain) => {
|
||||||
const { userRules } = this.props.filtering;
|
const { userRules } = this.props.filtering;
|
||||||
@ -146,6 +143,21 @@ class Logs extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reason === 'FilteredBlockedService') {
|
||||||
|
const getService = SERVICES
|
||||||
|
.find(service => service.id === row.original.serviceName);
|
||||||
|
const serviceName = getService && getService.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs__row">
|
||||||
|
<span className="logs__text" title={parsedFilteredReason}>
|
||||||
|
{parsedFilteredReason}
|
||||||
|
</span>
|
||||||
|
{this.renderTooltip(isFiltered, '', '', serviceName)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isFiltered) {
|
if (isFiltered) {
|
||||||
return (
|
return (
|
||||||
<div className="logs__row">
|
<div className="logs__row">
|
||||||
|
@ -17,10 +17,19 @@ class ClientsTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = (values) => {
|
handleSubmit = (values) => {
|
||||||
|
let config = values;
|
||||||
|
|
||||||
|
if (values && values.blocked_services) {
|
||||||
|
const blocked_services = Object
|
||||||
|
.keys(values.blocked_services)
|
||||||
|
.filter(service => values.blocked_services[service]);
|
||||||
|
config = { ...values, blocked_services };
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.modalType === MODAL_TYPE.EDIT) {
|
if (this.props.modalType === MODAL_TYPE.EDIT) {
|
||||||
this.handleFormUpdate(values, this.props.modalClientName);
|
this.handleFormUpdate(config, this.props.modalClientName);
|
||||||
} else {
|
} else {
|
||||||
this.handleFormAdd(values);
|
this.handleFormAdd(config);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,6 +50,7 @@ class ClientsTable extends Component {
|
|||||||
return {
|
return {
|
||||||
identifier,
|
identifier,
|
||||||
use_global_settings: true,
|
use_global_settings: true,
|
||||||
|
use_global_blocked_services: true,
|
||||||
...client,
|
...client,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -48,6 +58,7 @@ class ClientsTable extends Component {
|
|||||||
return {
|
return {
|
||||||
identifier: CLIENT_ID.IP,
|
identifier: CLIENT_ID.IP,
|
||||||
use_global_settings: true,
|
use_global_settings: true,
|
||||||
|
use_global_blocked_services: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -116,6 +127,27 @@ class ClientsTable extends Component {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('blocked_services'),
|
||||||
|
accessor: 'blocked_services',
|
||||||
|
Cell: (row) => {
|
||||||
|
const { value, original } = row;
|
||||||
|
|
||||||
|
if (original.use_global_blocked_services) {
|
||||||
|
return <Trans>settings_global</Trans>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--icons">
|
||||||
|
{value && value.length > 0 ? value.map(service => (
|
||||||
|
<svg className="service__icon service__icon--table" title={service} key={service}>
|
||||||
|
<use xlinkHref={`#service_${service}`} />
|
||||||
|
</svg>
|
||||||
|
)) : '–'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: this.props.t('table_statistics'),
|
Header: this.props.t('table_statistics'),
|
||||||
accessor: 'statistics',
|
accessor: 'statistics',
|
||||||
|
@ -5,18 +5,46 @@ import { Field, 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 { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form';
|
import Tabs from '../../ui/Tabs';
|
||||||
import { CLIENT_ID } from '../../../helpers/constants';
|
import { toggleAllServices } from '../../../helpers/helpers';
|
||||||
|
import { renderField, renderRadioField, renderSelectField, renderServiceField, ipv4, mac, required } from '../../../helpers/form';
|
||||||
|
import { CLIENT_ID, SERVICES } from '../../../helpers/constants';
|
||||||
|
import './Service.css';
|
||||||
|
|
||||||
|
const settingsCheckboxes = [
|
||||||
|
{
|
||||||
|
name: 'use_global_settings',
|
||||||
|
placeholder: 'client_global_settings',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filtering_enabled',
|
||||||
|
placeholder: 'block_domain_use_filters_and_hosts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'safebrowsing_enabled',
|
||||||
|
placeholder: 'use_adguard_browsing_sec',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parental_enabled',
|
||||||
|
placeholder: 'use_adguard_parental',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'safesearch_enabled',
|
||||||
|
placeholder: 'enforce_safe_search',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
let Form = (props) => {
|
let Form = (props) => {
|
||||||
const {
|
const {
|
||||||
t,
|
t,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
|
change,
|
||||||
pristine,
|
pristine,
|
||||||
submitting,
|
submitting,
|
||||||
clientIdentifier,
|
clientIdentifier,
|
||||||
useGlobalSettings,
|
useGlobalSettings,
|
||||||
|
useGlobalServices,
|
||||||
toggleClientModal,
|
toggleClientModal,
|
||||||
processingAdding,
|
processingAdding,
|
||||||
processingUpdating,
|
processingUpdating,
|
||||||
@ -26,57 +54,70 @@ let Form = (props) => {
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<div className="form-inline mb-3">
|
<div className="form__inline mb-2">
|
||||||
<strong className="mr-3">
|
<strong className="mr-3">
|
||||||
<Trans>client_identifier</Trans>
|
<Trans>client_identifier</Trans>
|
||||||
</strong>
|
</strong>
|
||||||
<label className="mr-3">
|
<div className="custom-controls-stacked">
|
||||||
<Field
|
<Field
|
||||||
name="identifier"
|
name="identifier"
|
||||||
component={renderField}
|
component={renderRadioField}
|
||||||
type="radio"
|
type="radio"
|
||||||
className="form-control mr-2"
|
className="form-control mr-2"
|
||||||
value="ip"
|
value="ip"
|
||||||
/>{' '}
|
placeholder={t('ip_address')}
|
||||||
<Trans>ip_address</Trans>
|
/>
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<Field
|
<Field
|
||||||
name="identifier"
|
name="identifier"
|
||||||
component={renderField}
|
component={renderRadioField}
|
||||||
type="radio"
|
type="radio"
|
||||||
className="form-control mr-2"
|
className="form-control mr-2"
|
||||||
value="mac"
|
value="mac"
|
||||||
/>{' '}
|
placeholder="MAC"
|
||||||
MAC
|
/>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{clientIdentifier === CLIENT_ID.IP && (
|
<div className="row">
|
||||||
<div className="form__group">
|
<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={[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>
|
||||||
|
<div className="col col-sm-6">
|
||||||
<Field
|
<Field
|
||||||
id="ip"
|
id="name"
|
||||||
name="ip"
|
name="name"
|
||||||
component={renderField}
|
component={renderField}
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder={t('form_enter_ip')}
|
placeholder={t('form_client_name')}
|
||||||
validate={[ipv4, required]}
|
validate={[required]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</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">
|
<div className="form__desc">
|
||||||
<Trans
|
<Trans
|
||||||
components={[
|
components={[
|
||||||
@ -90,72 +131,67 @@ let Form = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group">
|
<Tabs controlClass="form">
|
||||||
<Field
|
<div label="settings" title={props.t('main_settings')}>
|
||||||
id="name"
|
{settingsCheckboxes.map(setting => (
|
||||||
name="name"
|
<div className="form__group" key={setting.name}>
|
||||||
component={renderField}
|
<Field
|
||||||
type="text"
|
name={setting.name}
|
||||||
className="form-control"
|
type="checkbox"
|
||||||
placeholder={t('form_client_name')}
|
component={renderSelectField}
|
||||||
validate={[required]}
|
placeholder={t(setting.placeholder)}
|
||||||
/>
|
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
<div className="mb-4">
|
))}
|
||||||
<strong>
|
</div>
|
||||||
<Trans>settings</Trans>
|
<div label="services" title={props.t('block_services')}>
|
||||||
</strong>
|
<div className="form__group">
|
||||||
</div>
|
<Field
|
||||||
|
name="use_global_blocked_services"
|
||||||
<div className="form__group">
|
type="checkbox"
|
||||||
<Field
|
component={renderServiceField}
|
||||||
name="use_global_settings"
|
placeholder={t('blocked_services_global')}
|
||||||
type="checkbox"
|
modifier="service--global"
|
||||||
component={renderSelectField}
|
/>
|
||||||
placeholder={t('client_global_settings')}
|
<div className="row mb-4">
|
||||||
/>
|
<div className="col-6">
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
<div className="form__group">
|
className="btn btn-secondary btn-block"
|
||||||
<Field
|
disabled={useGlobalServices}
|
||||||
name="filtering_enabled"
|
onClick={() => toggleAllServices(SERVICES, change, true)}
|
||||||
type="checkbox"
|
>
|
||||||
component={renderSelectField}
|
<Trans>block_all</Trans>
|
||||||
placeholder={t('block_domain_use_filters_and_hosts')}
|
</button>
|
||||||
disabled={useGlobalSettings}
|
</div>
|
||||||
/>
|
<div className="col-6">
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
<div className="form__group">
|
className="btn btn-secondary btn-block"
|
||||||
<Field
|
disabled={useGlobalServices}
|
||||||
name="safebrowsing_enabled"
|
onClick={() => toggleAllServices(SERVICES, change, false)}
|
||||||
type="checkbox"
|
>
|
||||||
component={renderSelectField}
|
<Trans>unblock_all</Trans>
|
||||||
placeholder={t('use_adguard_browsing_sec')}
|
</button>
|
||||||
disabled={useGlobalSettings}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className="services">
|
||||||
|
{SERVICES.map(service => (
|
||||||
<div className="form__group">
|
<Field
|
||||||
<Field
|
key={service.id}
|
||||||
name="parental_enabled"
|
icon={`service_${service.id}`}
|
||||||
type="checkbox"
|
name={`blocked_services.${service.id}`}
|
||||||
component={renderSelectField}
|
type="checkbox"
|
||||||
placeholder={t('use_adguard_parental')}
|
component={renderServiceField}
|
||||||
disabled={useGlobalSettings}
|
placeholder={service.name}
|
||||||
/>
|
disabled={useGlobalServices}
|
||||||
</div>
|
/>
|
||||||
|
))}
|
||||||
<div className="form__group">
|
</div>
|
||||||
<Field
|
</div>
|
||||||
name="safesearch_enabled"
|
</div>
|
||||||
type="checkbox"
|
</Tabs>
|
||||||
component={renderSelectField}
|
|
||||||
placeholder={t('enforce_safe_search')}
|
|
||||||
disabled={useGlobalSettings}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
@ -188,10 +224,12 @@ Form.propTypes = {
|
|||||||
pristine: PropTypes.bool.isRequired,
|
pristine: PropTypes.bool.isRequired,
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
reset: PropTypes.func.isRequired,
|
reset: 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,
|
clientIdentifier: PropTypes.string,
|
||||||
useGlobalSettings: PropTypes.bool,
|
useGlobalSettings: 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,
|
||||||
@ -202,9 +240,11 @@ const selector = formValueSelector('clientForm');
|
|||||||
Form = connect((state) => {
|
Form = connect((state) => {
|
||||||
const clientIdentifier = selector(state, 'identifier');
|
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');
|
||||||
return {
|
return {
|
||||||
clientIdentifier,
|
clientIdentifier,
|
||||||
useGlobalSettings,
|
useGlobalSettings,
|
||||||
|
useGlobalServices,
|
||||||
};
|
};
|
||||||
})(Form);
|
})(Form);
|
||||||
|
|
||||||
|
@ -6,6 +6,24 @@ import ReactModal from 'react-modal';
|
|||||||
import { MODAL_TYPE } from '../../../helpers/constants';
|
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
|
|
||||||
|
const getInitialData = (initial) => {
|
||||||
|
if (initial && initial.blocked_services) {
|
||||||
|
const { blocked_services } = initial;
|
||||||
|
const blocked = {};
|
||||||
|
|
||||||
|
blocked_services.forEach((service) => {
|
||||||
|
blocked[service] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initial,
|
||||||
|
blocked_services: blocked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return initial;
|
||||||
|
};
|
||||||
|
|
||||||
const Modal = (props) => {
|
const Modal = (props) => {
|
||||||
const {
|
const {
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
@ -16,6 +34,7 @@ const Modal = (props) => {
|
|||||||
processingAdding,
|
processingAdding,
|
||||||
processingUpdating,
|
processingUpdating,
|
||||||
} = props;
|
} = props;
|
||||||
|
const initialData = getInitialData(currentClientData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
@ -38,9 +57,7 @@ const Modal = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={{ ...initialData }}
|
||||||
...currentClientData,
|
|
||||||
}}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
toggleClientModal={toggleClientModal}
|
toggleClientModal={toggleClientModal}
|
||||||
processingAdding={processingAdding}
|
processingAdding={processingAdding}
|
||||||
|
79
client/src/components/Settings/Clients/Service.css
Normal file
79
client/src/components/Settings/Clients/Service.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
.service {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.services {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: calc(99.9% * 4/12 - (30px - 30px * 4/12));
|
||||||
|
max-width: calc(99.9% * 4/12 - (30px - 30px * 4/12));
|
||||||
|
width: calc(99.9% * 4/12 - (30px - 30px * 4/12));
|
||||||
|
}
|
||||||
|
|
||||||
|
.service--global {
|
||||||
|
flex-basis: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service:nth-child(1n) {
|
||||||
|
margin-right: 30px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service:nth-child(3n) {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service__icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service--global .service__icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service__icon--table {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
color: #9aa0ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service__switch {
|
||||||
|
margin-left: auto;
|
||||||
|
border: 1px solid rgba(150, 150, 150, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-switch-input:checked ~ .service__switch {
|
||||||
|
background-color: #cd201f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-switch-input:focus ~ .service__switch {
|
||||||
|
box-shadow: 0 0 0 2px #cd201f3b;
|
||||||
|
border-color: #ec4241;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-switch-input:disabled ~ .service__switch,
|
||||||
|
.custom-switch-input:disabled ~ .service__text,
|
||||||
|
.custom-switch-input:disabled ~ .service__icon {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
90
client/src/components/Settings/Services/Form.js
Normal file
90
client/src/components/Settings/Services/Form.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Field, reduxForm } from 'redux-form';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
|
import { toggleAllServices } from '../../../helpers/helpers';
|
||||||
|
import { renderServiceField } from '../../../helpers/form';
|
||||||
|
import { SERVICES } from '../../../helpers/constants';
|
||||||
|
|
||||||
|
const Form = (props) => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
change,
|
||||||
|
pristine,
|
||||||
|
submitting,
|
||||||
|
processing,
|
||||||
|
processingSet,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form__group">
|
||||||
|
<div className="row mb-4">
|
||||||
|
<div className="col-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-block"
|
||||||
|
disabled={processing || processingSet}
|
||||||
|
onClick={() => toggleAllServices(SERVICES, change, true)}
|
||||||
|
>
|
||||||
|
<Trans>block_all</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="col-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-block"
|
||||||
|
disabled={processing || processingSet}
|
||||||
|
onClick={() => toggleAllServices(SERVICES, change, false)}
|
||||||
|
>
|
||||||
|
<Trans>unblock_all</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="services">
|
||||||
|
{SERVICES.map(service => (
|
||||||
|
<Field
|
||||||
|
key={service.id}
|
||||||
|
icon={`service_${service.id}`}
|
||||||
|
name={`blocked_services.${service.id}`}
|
||||||
|
type="checkbox"
|
||||||
|
component={renderServiceField}
|
||||||
|
placeholder={service.name}
|
||||||
|
disabled={processing || processingSet}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard btn-large"
|
||||||
|
disabled={submitting || pristine || processing || processingSet}
|
||||||
|
>
|
||||||
|
<Trans>save_btn</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Form.propTypes = {
|
||||||
|
pristine: PropTypes.bool.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
change: PropTypes.func.isRequired,
|
||||||
|
submitting: PropTypes.bool.isRequired,
|
||||||
|
processing: PropTypes.bool.isRequired,
|
||||||
|
processingSet: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default flow([
|
||||||
|
withNamespaces(),
|
||||||
|
reduxForm({
|
||||||
|
form: 'servicesForm',
|
||||||
|
enableReinitialize: true,
|
||||||
|
}),
|
||||||
|
])(Form);
|
69
client/src/components/Settings/Services/index.js
Normal file
69
client/src/components/Settings/Services/index.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
|
import Form from './Form';
|
||||||
|
import Card from '../../ui/Card';
|
||||||
|
|
||||||
|
class Services extends Component {
|
||||||
|
handleSubmit = (values) => {
|
||||||
|
let config = values;
|
||||||
|
|
||||||
|
if (values && values.blocked_services) {
|
||||||
|
const blocked_services = Object
|
||||||
|
.keys(values.blocked_services)
|
||||||
|
.filter(service => values.blocked_services[service]);
|
||||||
|
config = blocked_services;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.setBlockedServices(config);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
getInitialDataForServices = (initial) => {
|
||||||
|
if (initial) {
|
||||||
|
const blocked = {};
|
||||||
|
|
||||||
|
initial.forEach((service) => {
|
||||||
|
blocked[service] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
blocked_services: blocked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return initial;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { services, t } = this.props;
|
||||||
|
const initialData = this.getInitialDataForServices(services.list);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t('blocked_services')}
|
||||||
|
subtitle={t('Allows to quickly block popular sites.')}
|
||||||
|
bodyType="card-body box-body--settings"
|
||||||
|
>
|
||||||
|
<div className="form">
|
||||||
|
<Form
|
||||||
|
initialValues={{ ...initialData }}
|
||||||
|
processing={services.processing}
|
||||||
|
processingSet={services.processingSet}
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Services.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
services: PropTypes.object.isRequired,
|
||||||
|
setBlockedServices: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(Services);
|
@ -11,11 +11,20 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__inline {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-standard {
|
.btn-standard {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-control--textarea {
|
.form-control--textarea {
|
||||||
min-height: 110px;
|
min-height: 110px;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withNamespaces, Trans } from 'react-i18next';
|
import { withNamespaces, Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
import Services from './Services';
|
||||||
import Checkbox from '../ui/Checkbox';
|
import Checkbox from '../ui/Checkbox';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
@ -35,6 +36,7 @@ class Settings extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.initSettings(this.settings);
|
this.props.initSettings(this.settings);
|
||||||
|
this.props.getBlockedServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSettings = (settings) => {
|
renderSettings = (settings) => {
|
||||||
@ -59,7 +61,9 @@ class Settings extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { settings, t } = this.props;
|
const {
|
||||||
|
settings, services, setBlockedServices, t,
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageTitle title={t('general_settings')} />
|
<PageTitle title={t('general_settings')} />
|
||||||
@ -74,6 +78,12 @@ class Settings extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-md-12">
|
||||||
|
<Services
|
||||||
|
services={services}
|
||||||
|
setBlockedServices={setBlockedServices}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
Binary file not shown.
@ -109,9 +109,11 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
stroke: #9aa0ac;
|
stroke: #9aa0ac;
|
||||||
|
color: #9aa0ac;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover__icon--green {
|
.popover__icon--green {
|
||||||
|
color: #66b574;
|
||||||
stroke: #66b574;
|
stroke: #66b574;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,19 +6,36 @@ import './Popover.css';
|
|||||||
|
|
||||||
class PopoverFilter extends Component {
|
class PopoverFilter extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
const { rule, filter, service } = this.props;
|
||||||
|
|
||||||
|
if (!rule && !service) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="popover-wrap">
|
<div className="popover-wrap">
|
||||||
<div className="popover__trigger popover__trigger--filter">
|
<div className="popover__trigger popover__trigger--filter">
|
||||||
<svg className="popover__icon popover__icon--green" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
|
<svg className="popover__icon popover__icon--green">
|
||||||
|
<use xlinkHref="#question" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="popover__body popover__body--filter">
|
<div className="popover__body popover__body--filter">
|
||||||
<div className="popover__list">
|
<div className="popover__list">
|
||||||
<div className="popover__list-item popover__list-item--nowrap">
|
{rule && (
|
||||||
<Trans>rule_label</Trans>: <strong>{this.props.rule}</strong>
|
<div className="popover__list-item popover__list-item--nowrap">
|
||||||
</div>
|
<Trans>rule_label</Trans>: <strong>{rule}</strong>
|
||||||
{this.props.filter && <div className="popover__list-item popover__list-item--nowrap">
|
</div>
|
||||||
<Trans>filter_label</Trans>: <strong>{this.props.filter}</strong>
|
)}
|
||||||
</div>}
|
{filter && (
|
||||||
|
<div className="popover__list-item popover__list-item--nowrap">
|
||||||
|
<Trans>filter_label</Trans>: <strong>{filter}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{service && (
|
||||||
|
<div className="popover__list-item popover__list-item--nowrap">
|
||||||
|
<Trans>blocked_service</Trans>: <strong>{service}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -27,8 +44,9 @@ class PopoverFilter extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PopoverFilter.propTypes = {
|
PopoverFilter.propTypes = {
|
||||||
rule: PropTypes.string.isRequired,
|
rule: PropTypes.string,
|
||||||
filter: PropTypes.string,
|
filter: PropTypes.string,
|
||||||
|
service: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNamespaces()(PopoverFilter);
|
export default withNamespaces()(PopoverFilter);
|
||||||
|
@ -6,6 +6,20 @@
|
|||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid #e8e8e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs__controls--form {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__controls--form .tab__control {
|
||||||
|
min-width: initial;
|
||||||
|
margin-right: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__controls--form .tab__icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tab__control {
|
.tab__control {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import Tab from './Tab';
|
import Tab from './Tab';
|
||||||
import './Tabs.css';
|
import './Tabs.css';
|
||||||
@ -16,6 +17,7 @@ class Tabs extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
props: {
|
props: {
|
||||||
|
controlClass,
|
||||||
children,
|
children,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
@ -23,9 +25,14 @@ class Tabs extends Component {
|
|||||||
},
|
},
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
|
const getControlClass = classnames({
|
||||||
|
tabs__controls: true,
|
||||||
|
[`tabs__controls--${controlClass}`]: controlClass,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
<div className="tabs__controls">
|
<div className={getControlClass}>
|
||||||
{children.map((child) => {
|
{children.map((child) => {
|
||||||
const { label, title } = child.props;
|
const { label, title } = child.props;
|
||||||
|
|
||||||
@ -54,6 +61,7 @@ class Tabs extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Tabs.propTypes = {
|
Tabs.propTypes = {
|
||||||
|
controlClass: PropTypes.string,
|
||||||
children: PropTypes.array.isRequired,
|
children: PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { initSettings, toggleSetting } from '../actions';
|
import { initSettings, toggleSetting } from '../actions';
|
||||||
|
import { getBlockedServices, setBlockedServices } from '../actions/services';
|
||||||
import Settings from '../components/Settings';
|
import Settings from '../components/Settings';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
const { settings } = state;
|
const { settings, services } = state;
|
||||||
const props = {
|
const props = {
|
||||||
settings,
|
settings,
|
||||||
|
services,
|
||||||
};
|
};
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
@ -13,6 +15,8 @@ const mapStateToProps = (state) => {
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
initSettings,
|
initSettings,
|
||||||
toggleSetting,
|
toggleSetting,
|
||||||
|
getBlockedServices,
|
||||||
|
setBlockedServices,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -181,3 +181,66 @@ export const CLIENT_ID = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients'];
|
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients'];
|
||||||
|
|
||||||
|
export const SERVICES = [
|
||||||
|
{
|
||||||
|
id: 'facebook',
|
||||||
|
name: 'Facebook',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'whatsapp',
|
||||||
|
name: 'WhatsApp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'instagram',
|
||||||
|
name: 'Instagram',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'twitter',
|
||||||
|
name: 'Twitter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'youtube',
|
||||||
|
name: 'YouTube',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'netflix',
|
||||||
|
name: 'Netflix',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'snapchat',
|
||||||
|
name: 'Snapchat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'messenger',
|
||||||
|
name: 'Messenger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'twitch',
|
||||||
|
name: 'Twitch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'discord',
|
||||||
|
name: 'Discord',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'skype',
|
||||||
|
name: 'Skype',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'steam',
|
||||||
|
name: 'Steam',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ok',
|
||||||
|
name: 'OK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vk',
|
||||||
|
name: 'VK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mail_ru',
|
||||||
|
name: 'mail.ru',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -27,6 +27,23 @@ export const renderField = ({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const renderRadioField = ({
|
||||||
|
input, placeholder, disabled, meta: { touched, error },
|
||||||
|
}) => (
|
||||||
|
<Fragment>
|
||||||
|
<label className="custom-control custom-radio custom-control-inline">
|
||||||
|
<input
|
||||||
|
{...input}
|
||||||
|
type="radio"
|
||||||
|
className="custom-control-input"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<span className="custom-control-label">{placeholder}</span>
|
||||||
|
</label>
|
||||||
|
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
export const renderSelectField = ({
|
export const renderSelectField = ({
|
||||||
input, placeholder, disabled, meta: { touched, error },
|
input, placeholder, disabled, meta: { touched, error },
|
||||||
}) => (
|
}) => (
|
||||||
@ -46,6 +63,28 @@ export const renderSelectField = ({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const renderServiceField = ({
|
||||||
|
input, placeholder, disabled, modifier, icon, meta: { touched, error },
|
||||||
|
}) => (
|
||||||
|
<Fragment>
|
||||||
|
<label className={`service custom-switch ${modifier}`}>
|
||||||
|
<input
|
||||||
|
{...input}
|
||||||
|
type="checkbox"
|
||||||
|
className="custom-switch-input"
|
||||||
|
value={placeholder.toLowerCase()}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<span className="service__switch custom-switch-indicator"></span>
|
||||||
|
<span className="service__text">{placeholder}</span>
|
||||||
|
<svg className="service__icon">
|
||||||
|
<use xlinkHref={`#${icon}`} />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
export const required = (value) => {
|
export const required = (value) => {
|
||||||
if (value || value === 0) {
|
if (value || value === 0) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -27,6 +27,7 @@ export const normalizeLogs = logs => logs.map((log) => {
|
|||||||
client,
|
client,
|
||||||
filterId,
|
filterId,
|
||||||
rule,
|
rule,
|
||||||
|
service_name,
|
||||||
} = log;
|
} = log;
|
||||||
const { host: domain, type } = question;
|
const { host: domain, type } = question;
|
||||||
const responsesArray = response ? response.map((response) => {
|
const responsesArray = response ? response.map((response) => {
|
||||||
@ -42,6 +43,7 @@ export const normalizeLogs = logs => logs.map((log) => {
|
|||||||
client,
|
client,
|
||||||
filterId,
|
filterId,
|
||||||
rule,
|
rule,
|
||||||
|
serviceName: service_name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -225,3 +227,7 @@ export const sortClients = (clients) => {
|
|||||||
|
|
||||||
return clients.sort(compare);
|
return clients.sort(compare);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toggleAllServices = (services, change, isSelected) => {
|
||||||
|
services.forEach(service => change(`blocked_services.${service.id}`, isSelected));
|
||||||
|
};
|
||||||
|
@ -10,6 +10,7 @@ import encryption from './encryption';
|
|||||||
import clients from './clients';
|
import clients from './clients';
|
||||||
import access from './access';
|
import access from './access';
|
||||||
import rewrites from './rewrites';
|
import rewrites from './rewrites';
|
||||||
|
import services from './services';
|
||||||
|
|
||||||
const settings = handleActions({
|
const settings = handleActions({
|
||||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||||
@ -424,6 +425,7 @@ export default combineReducers({
|
|||||||
clients,
|
clients,
|
||||||
access,
|
access,
|
||||||
rewrites,
|
rewrites,
|
||||||
|
services,
|
||||||
loadingBar: loadingBarReducer,
|
loadingBar: loadingBarReducer,
|
||||||
form: formReducer,
|
form: formReducer,
|
||||||
});
|
});
|
||||||
|
29
client/src/reducers/services.js
Normal file
29
client/src/reducers/services.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
|
import * as actions from '../actions/services';
|
||||||
|
|
||||||
|
const services = handleActions(
|
||||||
|
{
|
||||||
|
[actions.getBlockedServicesRequest]: state => ({ ...state, processing: true }),
|
||||||
|
[actions.getBlockedServicesFailure]: state => ({ ...state, processing: false }),
|
||||||
|
[actions.getBlockedServicesSuccess]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
list: payload,
|
||||||
|
processing: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
[actions.setBlockedServicesRequest]: state => ({ ...state, processingSet: true }),
|
||||||
|
[actions.setBlockedServicesFailure]: state => ({ ...state, processingSet: false }),
|
||||||
|
[actions.setBlockedServicesSuccess]: state => ({
|
||||||
|
...state,
|
||||||
|
processingSet: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
processing: true,
|
||||||
|
processingSet: false,
|
||||||
|
list: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default services;
|
@ -39,12 +39,19 @@ const defaultParentalURL = "%s://%s/check-parental-control-hash?prefixes=%s&sens
|
|||||||
const defaultParentalSensitivity = 13 // use "TEEN" by default
|
const defaultParentalSensitivity = 13 // use "TEEN" by default
|
||||||
const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control
|
const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control
|
||||||
|
|
||||||
|
// ServiceEntry - blocked service array element
|
||||||
|
type ServiceEntry struct {
|
||||||
|
Name string
|
||||||
|
Rules []*urlfilter.NetworkRule
|
||||||
|
}
|
||||||
|
|
||||||
// RequestFilteringSettings is custom filtering settings
|
// RequestFilteringSettings is custom filtering settings
|
||||||
type RequestFilteringSettings struct {
|
type RequestFilteringSettings struct {
|
||||||
FilteringEnabled bool
|
FilteringEnabled bool
|
||||||
SafeSearchEnabled bool
|
SafeSearchEnabled bool
|
||||||
SafeBrowsingEnabled bool
|
SafeBrowsingEnabled bool
|
||||||
ParentalEnabled bool
|
ParentalEnabled bool
|
||||||
|
ServicesRules []ServiceEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
// RewriteEntry is a rewrite array element
|
// RewriteEntry is a rewrite array element
|
||||||
@ -139,6 +146,8 @@ const (
|
|||||||
FilteredInvalid
|
FilteredInvalid
|
||||||
// FilteredSafeSearch - the host was replaced with safesearch variant
|
// FilteredSafeSearch - the host was replaced with safesearch variant
|
||||||
FilteredSafeSearch
|
FilteredSafeSearch
|
||||||
|
// FilteredBlockedService - the host is blocked by "blocked services" settings
|
||||||
|
FilteredBlockedService
|
||||||
|
|
||||||
// ReasonRewrite - rewrite rule was applied
|
// ReasonRewrite - rewrite rule was applied
|
||||||
ReasonRewrite
|
ReasonRewrite
|
||||||
@ -155,6 +164,7 @@ func (i Reason) String() string {
|
|||||||
"FilteredParental",
|
"FilteredParental",
|
||||||
"FilteredInvalid",
|
"FilteredInvalid",
|
||||||
"FilteredSafeSearch",
|
"FilteredSafeSearch",
|
||||||
|
"FilteredBlockedService",
|
||||||
|
|
||||||
"Rewrite",
|
"Rewrite",
|
||||||
}
|
}
|
||||||
@ -185,6 +195,9 @@ type Result struct {
|
|||||||
// for ReasonRewrite:
|
// for ReasonRewrite:
|
||||||
CanonName string `json:",omitempty"` // CNAME value
|
CanonName string `json:",omitempty"` // CNAME value
|
||||||
IPList []net.IP `json:",omitempty"` // list of IP addresses
|
IPList []net.IP `json:",omitempty"` // list of IP addresses
|
||||||
|
|
||||||
|
// for FilteredBlockedService:
|
||||||
|
ServiceName string `json:",omitempty"` // Name of the blocked service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matched can be used to see if any match at all was found, no matter filtered or not
|
// Matched can be used to see if any match at all was found, no matter filtered or not
|
||||||
@ -193,7 +206,7 @@ func (r Reason) Matched() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
|
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
|
||||||
func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Result, error) {
|
func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) {
|
||||||
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
|
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
|
||||||
if host == "" {
|
if host == "" {
|
||||||
return Result{Reason: NotFilteredNotFound}, nil
|
return Result{Reason: NotFilteredNotFound}, nil
|
||||||
@ -204,15 +217,6 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res
|
|||||||
return Result{}, nil
|
return Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result Result
|
var result Result
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -232,6 +236,13 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(setts.ServicesRules) != 0 {
|
||||||
|
result = matchBlockedServicesRules(host, setts.ServicesRules)
|
||||||
|
if result.Reason.Matched() {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check safeSearch if no match
|
// check safeSearch if no match
|
||||||
if setts.SafeSearchEnabled {
|
if setts.SafeSearchEnabled {
|
||||||
result, err = d.checkSafeSearch(host)
|
result, err = d.checkSafeSearch(host)
|
||||||
@ -326,6 +337,26 @@ func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func matchBlockedServicesRules(host string, svcs []ServiceEntry) Result {
|
||||||
|
req := urlfilter.NewRequestForHostname(host)
|
||||||
|
res := Result{}
|
||||||
|
|
||||||
|
for _, s := range svcs {
|
||||||
|
for _, rule := range s.Rules {
|
||||||
|
if rule.Match(req) {
|
||||||
|
res.Reason = FilteredBlockedService
|
||||||
|
res.IsFiltered = true
|
||||||
|
res.ServiceName = s.Name
|
||||||
|
res.Rule = rule.Text()
|
||||||
|
log.Debug("Blocked Services: matched rule: %s host: %s service: %s",
|
||||||
|
res.Rule, host, s.Name)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
func setCacheResult(cache *fastcache.Cache, host string, res Result) {
|
func setCacheResult(cache *fastcache.Cache, host string, res Result) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
enc := gob.NewEncoder(&buf)
|
enc := gob.NewEncoder(&buf)
|
||||||
|
@ -10,10 +10,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/urlfilter"
|
||||||
"github.com/bluele/gcache"
|
"github.com/bluele/gcache"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var setts RequestFilteringSettings
|
||||||
|
|
||||||
// HELPERS
|
// HELPERS
|
||||||
// SAFE BROWSING
|
// SAFE BROWSING
|
||||||
// SAFE SEARCH
|
// SAFE SEARCH
|
||||||
@ -44,10 +48,16 @@ func _Func() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewForTest(c *Config, filters map[int]string) *Dnsfilter {
|
func NewForTest(c *Config, filters map[int]string) *Dnsfilter {
|
||||||
|
setts = RequestFilteringSettings{}
|
||||||
|
setts.FilteringEnabled = true
|
||||||
if c != nil {
|
if c != nil {
|
||||||
c.SafeBrowsingCacheSize = 1024
|
c.SafeBrowsingCacheSize = 1024
|
||||||
c.SafeSearchCacheSize = 1024
|
c.SafeSearchCacheSize = 1024
|
||||||
c.ParentalCacheSize = 1024
|
c.ParentalCacheSize = 1024
|
||||||
|
|
||||||
|
setts.SafeSearchEnabled = c.SafeSearchEnabled
|
||||||
|
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
|
||||||
|
setts.ParentalEnabled = c.ParentalEnabled
|
||||||
}
|
}
|
||||||
d := New(c, filters)
|
d := New(c, filters)
|
||||||
purgeCaches()
|
purgeCaches()
|
||||||
@ -56,7 +66,7 @@ func NewForTest(c *Config, filters map[int]string) *Dnsfilter {
|
|||||||
|
|
||||||
func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
ret, err := d.CheckHost(hostname, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
@ -67,7 +77,7 @@ func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
|||||||
|
|
||||||
func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype uint16) {
|
func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype uint16) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ret, err := d.CheckHost(hostname, qtype, "")
|
ret, err := d.CheckHost(hostname, qtype, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
@ -81,7 +91,7 @@ func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype
|
|||||||
|
|
||||||
func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) {
|
func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
ret, err := d.CheckHost(hostname, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
@ -212,7 +222,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
|
|||||||
|
|
||||||
// Check host for each domain
|
// Check host for each domain
|
||||||
for _, host := range yandex {
|
for _, host := range yandex {
|
||||||
result, err := d.CheckHost(host, dns.TypeA, "")
|
result, err := d.CheckHost(host, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err)
|
t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err)
|
||||||
}
|
}
|
||||||
@ -232,7 +242,7 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
|||||||
|
|
||||||
// Check host for each domain
|
// Check host for each domain
|
||||||
for _, host := range googleDomains {
|
for _, host := range googleDomains {
|
||||||
result, err := d.CheckHost(host, dns.TypeA, "")
|
result, err := d.CheckHost(host, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("SafeSearch doesn't work for %s cause %s", host, err)
|
t.Errorf("SafeSearch doesn't work for %s cause %s", host, err)
|
||||||
}
|
}
|
||||||
@ -252,7 +262,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Check host with disabled safesearch
|
// Check host with disabled safesearch
|
||||||
result, err = d.CheckHost(domain, dns.TypeA, "")
|
result, err = d.CheckHost(domain, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Cannot check host due to %s", err)
|
t.Fatalf("Cannot check host due to %s", err)
|
||||||
}
|
}
|
||||||
@ -263,7 +273,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
|||||||
d = NewForTest(&Config{SafeSearchEnabled: true}, nil)
|
d = NewForTest(&Config{SafeSearchEnabled: true}, nil)
|
||||||
defer d.Destroy()
|
defer d.Destroy()
|
||||||
|
|
||||||
result, err = d.CheckHost(domain, dns.TypeA, "")
|
result, err = d.CheckHost(domain, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
||||||
}
|
}
|
||||||
@ -289,7 +299,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
|
|||||||
d := NewForTest(nil, nil)
|
d := NewForTest(nil, nil)
|
||||||
defer d.Destroy()
|
defer d.Destroy()
|
||||||
domain := "www.google.ru"
|
domain := "www.google.ru"
|
||||||
result, err := d.CheckHost(domain, dns.TypeA, "")
|
result, err := d.CheckHost(domain, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Cannot check host due to %s", err)
|
t.Fatalf("Cannot check host due to %s", err)
|
||||||
}
|
}
|
||||||
@ -320,7 +330,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err = d.CheckHost(domain, dns.TypeA, "")
|
result, err = d.CheckHost(domain, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err)
|
||||||
}
|
}
|
||||||
@ -433,7 +443,7 @@ func TestMatching(t *testing.T) {
|
|||||||
d := NewForTest(nil, filters)
|
d := NewForTest(nil, filters)
|
||||||
defer d.Destroy()
|
defer d.Destroy()
|
||||||
|
|
||||||
ret, err := d.CheckHost(test.hostname, dns.TypeA, "")
|
ret, err := d.CheckHost(test.hostname, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error while matching host %s: %s", test.hostname, err)
|
t.Errorf("Error while matching host %s: %s", test.hostname, err)
|
||||||
}
|
}
|
||||||
@ -449,10 +459,16 @@ func TestMatching(t *testing.T) {
|
|||||||
|
|
||||||
// CLIENT SETTINGS
|
// CLIENT SETTINGS
|
||||||
|
|
||||||
func applyClientSettings(clientAddr string, setts *RequestFilteringSettings) {
|
func applyClientSettings(setts *RequestFilteringSettings) {
|
||||||
setts.FilteringEnabled = false
|
setts.FilteringEnabled = false
|
||||||
setts.ParentalEnabled = false
|
setts.ParentalEnabled = false
|
||||||
setts.SafeBrowsingEnabled = true
|
setts.SafeBrowsingEnabled = true
|
||||||
|
|
||||||
|
rule, _ := urlfilter.NewNetworkRule("||facebook.com^", 0)
|
||||||
|
s := ServiceEntry{}
|
||||||
|
s.Name = "facebook"
|
||||||
|
s.Rules = []*urlfilter.NetworkRule{rule}
|
||||||
|
setts.ServicesRules = append(setts.ServicesRules, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check behaviour without any per-client settings,
|
// Check behaviour without any per-client settings,
|
||||||
@ -468,43 +484,51 @@ func TestClientSettings(t *testing.T) {
|
|||||||
// no client settings:
|
// no client settings:
|
||||||
|
|
||||||
// blocked by filters
|
// blocked by filters
|
||||||
r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1")
|
r, _ = d.CheckHost("example.org", dns.TypeA, &setts)
|
||||||
if !r.IsFiltered || r.Reason != FilteredBlackList {
|
if !r.IsFiltered || r.Reason != FilteredBlackList {
|
||||||
t.Fatalf("CheckHost FilteredBlackList")
|
t.Fatalf("CheckHost FilteredBlackList")
|
||||||
}
|
}
|
||||||
|
|
||||||
// blocked by parental
|
// blocked by parental
|
||||||
r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1")
|
r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts)
|
||||||
if !r.IsFiltered || r.Reason != FilteredParental {
|
if !r.IsFiltered || r.Reason != FilteredParental {
|
||||||
t.Fatalf("CheckHost FilteredParental")
|
t.Fatalf("CheckHost FilteredParental")
|
||||||
}
|
}
|
||||||
|
|
||||||
// safesearch is disabled
|
// safesearch is disabled
|
||||||
r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, "1.1.1.1")
|
r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts)
|
||||||
if r.IsFiltered {
|
if r.IsFiltered {
|
||||||
t.Fatalf("CheckHost safesearch")
|
t.Fatalf("CheckHost safesearch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not blocked
|
||||||
|
r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts)
|
||||||
|
assert.True(t, !r.IsFiltered)
|
||||||
|
|
||||||
// override client settings:
|
// override client settings:
|
||||||
d.FilterHandler = applyClientSettings
|
applyClientSettings(&setts)
|
||||||
|
|
||||||
// override filtering settings
|
// override filtering settings
|
||||||
r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1")
|
r, _ = d.CheckHost("example.org", dns.TypeA, &setts)
|
||||||
if r.IsFiltered {
|
if r.IsFiltered {
|
||||||
t.Fatalf("CheckHost")
|
t.Fatalf("CheckHost")
|
||||||
}
|
}
|
||||||
|
|
||||||
// override parental settings (force disable parental)
|
// override parental settings (force disable parental)
|
||||||
r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1")
|
r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts)
|
||||||
if r.IsFiltered {
|
if r.IsFiltered {
|
||||||
t.Fatalf("CheckHost")
|
t.Fatalf("CheckHost")
|
||||||
}
|
}
|
||||||
|
|
||||||
// override safesearch settings (force enable safesearch)
|
// override safesearch settings (force enable safesearch)
|
||||||
r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, "1.1.1.1")
|
r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts)
|
||||||
if !r.IsFiltered || r.Reason != FilteredSafeBrowsing {
|
if !r.IsFiltered || r.Reason != FilteredSafeBrowsing {
|
||||||
t.Fatalf("CheckHost FilteredSafeBrowsing")
|
t.Fatalf("CheckHost FilteredSafeBrowsing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// blocked by additional rules
|
||||||
|
r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts)
|
||||||
|
assert.True(t, r.IsFiltered && r.Reason == FilteredBlockedService)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BENCHMARKS
|
// BENCHMARKS
|
||||||
@ -514,7 +538,7 @@ func BenchmarkSafeBrowsing(b *testing.B) {
|
|||||||
defer d.Destroy()
|
defer d.Destroy()
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
hostname := "wmconvirus.narod.ru"
|
hostname := "wmconvirus.narod.ru"
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
ret, err := d.CheckHost(hostname, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Errorf("Error while matching host %s: %s", hostname, err)
|
b.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
@ -530,7 +554,7 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
|
|||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
hostname := "wmconvirus.narod.ru"
|
hostname := "wmconvirus.narod.ru"
|
||||||
ret, err := d.CheckHost(hostname, dns.TypeA, "")
|
ret, err := d.CheckHost(hostname, dns.TypeA, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Errorf("Error while matching host %s: %s", hostname, err)
|
b.Errorf("Error while matching host %s: %s", hostname, err)
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,10 @@ type FilteringConfig struct {
|
|||||||
ParentalBlockHost string `yaml:"parental_block_host"`
|
ParentalBlockHost string `yaml:"parental_block_host"`
|
||||||
SafeBrowsingBlockHost string `yaml:"safebrowsing_block_host"`
|
SafeBrowsingBlockHost string `yaml:"safebrowsing_block_host"`
|
||||||
|
|
||||||
|
// Names of services to block (globally).
|
||||||
|
// Per-client settings can override this configuration.
|
||||||
|
BlockedServices []string `json:"blocked_services"`
|
||||||
|
|
||||||
dnsfilter.Config `yaml:",inline"`
|
dnsfilter.Config `yaml:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,7 +533,17 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
|
|||||||
if d.Addr != nil {
|
if d.Addr != nil {
|
||||||
clientAddr, _, _ = net.SplitHostPort(d.Addr.String())
|
clientAddr, _, _ = net.SplitHostPort(d.Addr.String())
|
||||||
}
|
}
|
||||||
res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, clientAddr)
|
|
||||||
|
var setts dnsfilter.RequestFilteringSettings
|
||||||
|
setts.FilteringEnabled = true
|
||||||
|
setts.SafeSearchEnabled = s.conf.SafeSearchEnabled
|
||||||
|
setts.SafeBrowsingEnabled = s.conf.SafeBrowsingEnabled
|
||||||
|
setts.ParentalEnabled = s.conf.ParentalEnabled
|
||||||
|
if s.conf.FilterHandler != nil {
|
||||||
|
s.conf.FilterHandler(clientAddr, &setts)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, &setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Return immediately if there's an error
|
// Return immediately if there's an error
|
||||||
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
||||||
|
@ -186,6 +186,10 @@ func (l *queryLog) getQueryLog() []map[string]interface{} {
|
|||||||
jsonEntry["filterId"] = entry.Result.FilterID
|
jsonEntry["filterId"] = entry.Result.FilterID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(entry.Result.ServiceName) != 0 {
|
||||||
|
jsonEntry["service_name"] = entry.Result.ServiceName
|
||||||
|
}
|
||||||
|
|
||||||
answers := answerToMap(a)
|
answers := answerToMap(a)
|
||||||
if answers != nil {
|
if answers != nil {
|
||||||
jsonEntry["answer"] = answers
|
jsonEntry["answer"] = answers
|
||||||
|
119
home/blocked_services.go
Normal file
119
home/blocked_services.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/urlfilter"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serviceRules map[string][]*urlfilter.NetworkRule // service name -> filtering rules
|
||||||
|
|
||||||
|
type svc struct {
|
||||||
|
name string
|
||||||
|
rules []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep in sync with:
|
||||||
|
// client/src/helpers/constants.js
|
||||||
|
// client/src/components/ui/Icons.js
|
||||||
|
var serviceRulesArray = []svc{
|
||||||
|
{"whatsapp", []string{"||whatsapp.net^"}},
|
||||||
|
{"facebook", []string{"||facebook.com^"}},
|
||||||
|
{"twitter", []string{"||twitter.com^", "||t.co^", "||twimg.com^"}},
|
||||||
|
{"youtube", []string{"||youtube.com^", "||ytimg.com^"}},
|
||||||
|
{"messenger", []string{"||fb.com^", "||facebook.com^"}},
|
||||||
|
{"twitch", []string{"||twitch.tv^", "||ttvnw.net^"}},
|
||||||
|
{"netflix", []string{"||nflxext.com^", "||netflix.com^"}},
|
||||||
|
{"instagram", []string{"||instagram.com^"}},
|
||||||
|
{"snapchat", []string{"||snapchat.com^"}},
|
||||||
|
{"discord", []string{"||discord.gg^", "||discordapp.net^", "||discordapp.com^"}},
|
||||||
|
{"ok", []string{"||ok.ru^"}},
|
||||||
|
{"skype", []string{"||skype.com^"}},
|
||||||
|
{"vk", []string{"||vk.com^"}},
|
||||||
|
{"steam", []string{"||steam.com^"}},
|
||||||
|
{"mail_ru", []string{"||mail.ru^"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert array to map
|
||||||
|
func initServices() {
|
||||||
|
serviceRules = make(map[string][]*urlfilter.NetworkRule)
|
||||||
|
for _, s := range serviceRulesArray {
|
||||||
|
rules := []*urlfilter.NetworkRule{}
|
||||||
|
for _, text := range s.rules {
|
||||||
|
rule, err := urlfilter.NewNetworkRule(text, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("urlfilter.NewNetworkRule: %s rule: %s", err, text)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rules = append(rules, rule)
|
||||||
|
}
|
||||||
|
serviceRules[s.name] = rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyBlockedServices - set blocked services settings for this DNS request
|
||||||
|
func ApplyBlockedServices(setts *dnsfilter.RequestFilteringSettings, list []string) {
|
||||||
|
setts.ServicesRules = []dnsfilter.ServiceEntry{}
|
||||||
|
for _, name := range list {
|
||||||
|
rules, ok := serviceRules[name]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Error("unknown service name: %s", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s := dnsfilter.ServiceEntry{}
|
||||||
|
s.Name = name
|
||||||
|
s.Rules = rules
|
||||||
|
setts.ServicesRules = append(setts.ServicesRules, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
config.RLock()
|
||||||
|
list := config.DNS.BlockedServices
|
||||||
|
config.RUnlock()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(list)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "json.Encode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
list := []string{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&list)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Lock()
|
||||||
|
config.DNS.BlockedServices = list
|
||||||
|
config.Unlock()
|
||||||
|
|
||||||
|
log.Debug("Updated blocked services list: %d", len(list))
|
||||||
|
|
||||||
|
err = writeAllConfigsAndReloadDNS()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterBlockedServicesHandlers - register HTTP handlers
|
||||||
|
func RegisterBlockedServicesHandlers() {
|
||||||
|
http.HandleFunc("/control/blocked_services/list", postInstall(optionalAuth(ensureGET(handleBlockedServicesList))))
|
||||||
|
http.HandleFunc("/control/blocked_services/set", postInstall(optionalAuth(ensurePOST(handleBlockedServicesSet))))
|
||||||
|
}
|
@ -31,6 +31,9 @@ type Client struct {
|
|||||||
SafeSearchEnabled bool
|
SafeSearchEnabled bool
|
||||||
SafeBrowsingEnabled bool
|
SafeBrowsingEnabled bool
|
||||||
ParentalEnabled bool
|
ParentalEnabled bool
|
||||||
|
|
||||||
|
UseOwnBlockedServices bool // false: use global settings
|
||||||
|
BlockedServices []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientJSON struct {
|
type clientJSON struct {
|
||||||
@ -42,6 +45,9 @@ type clientJSON struct {
|
|||||||
ParentalEnabled bool `json:"parental_enabled"`
|
ParentalEnabled bool `json:"parental_enabled"`
|
||||||
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
|
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
|
||||||
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
|
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
|
||||||
|
|
||||||
|
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
||||||
|
BlockedServices []string `json:"blocked_services"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientSource uint
|
type clientSource uint
|
||||||
@ -391,6 +397,9 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
|||||||
ParentalEnabled: c.ParentalEnabled,
|
ParentalEnabled: c.ParentalEnabled,
|
||||||
SafeSearchEnabled: c.SafeSearchEnabled,
|
SafeSearchEnabled: c.SafeSearchEnabled,
|
||||||
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
|
||||||
|
BlockedServices: c.BlockedServices,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.MAC) != 0 {
|
if len(c.MAC) != 0 {
|
||||||
@ -438,6 +447,9 @@ func jsonToClient(cj clientJSON) (*Client, error) {
|
|||||||
ParentalEnabled: cj.ParentalEnabled,
|
ParentalEnabled: cj.ParentalEnabled,
|
||||||
SafeSearchEnabled: cj.SafeSearchEnabled,
|
SafeSearchEnabled: cj.SafeSearchEnabled,
|
||||||
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
||||||
|
BlockedServices: cj.BlockedServices,
|
||||||
}
|
}
|
||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,9 @@ type clientObject struct {
|
|||||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||||
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
|
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
|
||||||
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
|
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
|
||||||
|
|
||||||
|
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
|
||||||
|
BlockedServices []string `yaml:"blocked_services"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPSServer struct {
|
type HTTPSServer struct {
|
||||||
@ -266,6 +269,9 @@ func parseConfig() error {
|
|||||||
ParentalEnabled: cy.ParentalEnabled,
|
ParentalEnabled: cy.ParentalEnabled,
|
||||||
SafeSearchEnabled: cy.SafeSearchEnabled,
|
SafeSearchEnabled: cy.SafeSearchEnabled,
|
||||||
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseOwnBlockedServices: !cy.UseGlobalBlockedServices,
|
||||||
|
BlockedServices: cy.BlockedServices,
|
||||||
}
|
}
|
||||||
_, err = config.clients.Add(cli)
|
_, err = config.clients.Add(cli)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -317,6 +323,9 @@ func (c *configuration) write() error {
|
|||||||
ParentalEnabled: cli.ParentalEnabled,
|
ParentalEnabled: cli.ParentalEnabled,
|
||||||
SafeSearchEnabled: cli.SafeSearchEnabled,
|
SafeSearchEnabled: cli.SafeSearchEnabled,
|
||||||
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
||||||
|
|
||||||
|
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
|
||||||
|
BlockedServices: cli.BlockedServices,
|
||||||
}
|
}
|
||||||
config.Clients = append(config.Clients, cy)
|
config.Clients = append(config.Clients, cy)
|
||||||
}
|
}
|
||||||
|
@ -1022,6 +1022,7 @@ func registerControlHandlers() {
|
|||||||
RegisterTLSHandlers()
|
RegisterTLSHandlers()
|
||||||
RegisterClientsHandlers()
|
RegisterClientsHandlers()
|
||||||
registerRewritesHandlers()
|
registerRewritesHandlers()
|
||||||
|
RegisterBlockedServicesHandlers()
|
||||||
|
|
||||||
http.HandleFunc("/dns-query", postInstall(handleDOH))
|
http.HandleFunc("/dns-query", postInstall(handleDOH))
|
||||||
}
|
}
|
||||||
|
23
home/dns.go
23
home/dns.go
@ -54,6 +54,7 @@ func initDNSServer(baseDir string) {
|
|||||||
log.Error("upstream.AddressToUpstream: %s", err)
|
log.Error("upstream.AddressToUpstream: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config.dnsctx.rdnsIP = make(map[string]bool)
|
config.dnsctx.rdnsIP = make(map[string]bool)
|
||||||
config.dnsctx.rdnsChannel = make(chan string, 256)
|
config.dnsctx.rdnsChannel = make(chan string, 256)
|
||||||
go asyncRDNSLoop()
|
go asyncRDNSLoop()
|
||||||
@ -210,19 +211,35 @@ func generateServerConfig() (dnsforward.ServerConfig, error) {
|
|||||||
newconfig.Upstreams = upstreamConfig.Upstreams
|
newconfig.Upstreams = upstreamConfig.Upstreams
|
||||||
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
|
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
|
||||||
newconfig.AllServers = config.DNS.AllServers
|
newconfig.AllServers = config.DNS.AllServers
|
||||||
newconfig.FilterHandler = applyClientSettings
|
newconfig.FilterHandler = applyAdditionalFiltering
|
||||||
newconfig.OnDNSRequest = onDNSRequest
|
newconfig.OnDNSRequest = onDNSRequest
|
||||||
return newconfig, nil
|
return newconfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a client has his own settings, apply them
|
// If a client has his own settings, apply them
|
||||||
func applyClientSettings(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
||||||
|
|
||||||
|
ApplyBlockedServices(setts, config.DNS.BlockedServices)
|
||||||
|
|
||||||
|
if len(clientAddr) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c, ok := config.clients.Find(clientAddr)
|
c, ok := config.clients.Find(clientAddr)
|
||||||
if !ok || !c.UseOwnSettings {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Using settings for client with IP %s", clientAddr)
|
log.Debug("Using settings for client with IP %s", clientAddr)
|
||||||
|
|
||||||
|
if c.UseOwnBlockedServices {
|
||||||
|
ApplyBlockedServices(setts, c.BlockedServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.UseOwnSettings {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setts.FilteringEnabled = c.FilteringEnabled
|
setts.FilteringEnabled = c.FilteringEnabled
|
||||||
setts.SafeSearchEnabled = c.SafeSearchEnabled
|
setts.SafeSearchEnabled = c.SafeSearchEnabled
|
||||||
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
|
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
|
||||||
|
@ -101,6 +101,7 @@ func run(args options) {
|
|||||||
|
|
||||||
initConfig()
|
initConfig()
|
||||||
config.clients.Init()
|
config.clients.Init()
|
||||||
|
initServices()
|
||||||
|
|
||||||
if !config.firstRun {
|
if !config.firstRun {
|
||||||
// Do the upgrade if necessary
|
// Do the upgrade if necessary
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentSchemaVersion = 3 // used for upgrading from old configs to new config
|
const currentSchemaVersion = 4 // 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 {
|
||||||
@ -53,20 +53,28 @@ func upgradeConfig() error {
|
|||||||
func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error {
|
func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error {
|
||||||
switch oldVersion {
|
switch oldVersion {
|
||||||
case 0:
|
case 0:
|
||||||
err := upgradeSchema0to3(diskConfig)
|
err := upgradeSchema0to1(diskConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fallthrough
|
||||||
case 1:
|
case 1:
|
||||||
err := upgradeSchema1to3(diskConfig)
|
err := upgradeSchema1to2(diskConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fallthrough
|
||||||
case 2:
|
case 2:
|
||||||
err := upgradeSchema2to3(diskConfig)
|
err := upgradeSchema2to3(diskConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fallthrough
|
||||||
|
case 3:
|
||||||
|
err := upgradeSchema3to4(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)
|
||||||
@ -173,22 +181,35 @@ func upgradeSchema2to3(diskConfig *map[string]interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// jump three schemas at once -- this time we just do it sequentially
|
// Add use_global_blocked_services=true setting for existing "clients" array
|
||||||
func upgradeSchema0to3(diskConfig *map[string]interface{}) error {
|
func upgradeSchema3to4(diskConfig *map[string]interface{}) error {
|
||||||
err := upgradeSchema0to1(diskConfig)
|
log.Printf("%s(): called", _Func())
|
||||||
if err != nil {
|
|
||||||
return err
|
(*diskConfig)["schema_version"] = 4
|
||||||
|
|
||||||
|
clients, ok := (*diskConfig)["clients"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return upgradeSchema1to3(diskConfig)
|
switch arr := clients.(type) {
|
||||||
}
|
case []interface{}:
|
||||||
|
|
||||||
// jump two schemas at once -- this time we just do it sequentially
|
for i := range arr {
|
||||||
func upgradeSchema1to3(diskConfig *map[string]interface{}) error {
|
|
||||||
err := upgradeSchema1to2(diskConfig)
|
switch c := arr[i].(type) {
|
||||||
if err != nil {
|
|
||||||
return err
|
case map[interface{}]interface{}:
|
||||||
|
c["use_global_blocked_services"] = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return upgradeSchema2to3(diskConfig)
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -786,6 +786,35 @@ paths:
|
|||||||
200:
|
200:
|
||||||
description: OK
|
description: OK
|
||||||
|
|
||||||
|
|
||||||
|
/blocked_services/list:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- blocked_services
|
||||||
|
operationId: blockedServicesList
|
||||||
|
summary: 'Get blocked services list'
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/BlockedServicesArray"
|
||||||
|
|
||||||
|
/blocked_services/set:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- blocked_services
|
||||||
|
operationId: blockedServicesSet
|
||||||
|
summary: 'Set blocked services list'
|
||||||
|
parameters:
|
||||||
|
- in: body
|
||||||
|
name: "body"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/BlockedServicesArray"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Rewrite methods
|
# Rewrite methods
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@ -1572,6 +1601,12 @@ definitions:
|
|||||||
type: "boolean"
|
type: "boolean"
|
||||||
safesearch_enabled:
|
safesearch_enabled:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
|
use_global_blocked_services:
|
||||||
|
type: "boolean"
|
||||||
|
blocked_services:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
type: "string"
|
||||||
ClientAuto:
|
ClientAuto:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Auto-Client information"
|
description: "Auto-Client information"
|
||||||
@ -1638,6 +1673,11 @@ definitions:
|
|||||||
description: "value of A, AAAA or CNAME DNS record"
|
description: "value of A, AAAA or CNAME DNS record"
|
||||||
example: "127.0.0.1"
|
example: "127.0.0.1"
|
||||||
|
|
||||||
|
BlockedServicesArray:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
type: "string"
|
||||||
|
|
||||||
CheckConfigRequest:
|
CheckConfigRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Configuration to be checked"
|
description: "Configuration to be checked"
|
||||||
|
Loading…
Reference in New Issue
Block a user