Merge: Add Filters Update Interval setting; refactor

Close #641

* commit 'd0fc1dc54dfbc017f28c6c0afa4623c6259af557':
  + client: handle filters configuration
  * openapi: update /filtering
  filtering: refactor;  change API;  add "filters_update_interval" setting
This commit is contained in:
Simon Zolin 2019-09-12 19:06:39 +03:00
commit 30ca77303b
40 changed files with 1113 additions and 673 deletions

View File

@ -45,6 +45,11 @@ Contents:
* Query logs
* API: Set querylog parameters
* API: Get querylog parameters
* Filtering
* Filters update mechanism
* API: Get filtering parameters
* API: Set filtering parameters
* API: Set URL parameters
## Relations between subsystems
@ -1019,3 +1024,76 @@ Response:
"enabled": true | false
"interval": 1 | 7 | 30 | 90
}
## Filtering
### Filters update mechanism
Filters can be updated either manually by request from UI or automatically.
Auto-update interval can be configured in UI. If it is 0, auto-update is disabled.
When the last modification date of filter files is older than auto-update interval, auto-update procedure is started.
If an enabled filter file doesn't exist, it's downloaded on application startup. This includes the case when installation wizard is completed and there are no filter files yet.
When auto-update time comes, server starts the update procedure by downloading filter files. After new filter files are in place, it restarts DNS filtering module with new rules.
Only filters that are enabled by configuration can be updated.
As a result of the update procedure, all enabled filter files are written to disk, refreshed (their last modification date is equal to the current time) and loaded.
### API: Get filtering parameters
Request:
GET /control/filtering_info
Response:
200 OK
{
"enabled": true | false
"interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24
"filters":[
{
"id":1
"enabled":true,
"url":"https://...",
"name":"...",
"rules_count":1234,
"last_updated":"2019-09-04T18:29:30+00:00",
}
...
],
"user_rules":["...", ...]
}
### API: Set filtering parameters
Request:
POST /control/filtering_config
{
"enabled": true | false
"interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24
}
Response:
200 OK
### API: Set URL parameters
Request:
POST /control/filtering/set_url
{
"url": "..."
"enabled": true | false
}
Response:
200 OK

View File

@ -378,5 +378,11 @@
"statistics_retention_desc": "If you decrease the interval value, some data will be lost",
"statistics_clear": " Clear statistics",
"statistics_clear_confirm": "Are you sure you want to clear statistics?",
"statistics_cleared": "Statistics successfully cleared"
"statistics_cleared": "Statistics successfully cleared",
"interval_hours": "{{count}} hour",
"interval_hours_plural": "{{count}} hours",
"filters_configuration": "Filters configuration",
"filters_enable": "Enable filters",
"filters_interval": "Filters update interval",
"disabled": "Disabled"
}

View File

@ -1,10 +1,9 @@
{
"client_settings": "Настройки клиентов",
"example_upstream_reserved": "вы можете указать DNS-сервер <0>для конкретного домена(ов)</0>",
"upstream_parallel": "Использовать одновременные запроссы ко всем серверам для ускорения обработки запроса",
"upstream_parallel": "Использовать одновременные запросы ко всем серверам для ускорения обработки запроса",
"bootstrap_dns": "Bootstrap DNS-серверы",
"bootstrap_dns_desc": "Bootstrap DNS-серверы используются для поиска IP-адресов DoH/DoT серверов, которые вы указали.",
"url_added_successfully": "URL успешно добавлен",
"check_dhcp_servers": "Проверить DHCP-серверы",
"save_config": "Сохранить конфигурацию",
"enabled_dhcp": "DHCP-сервер включен",
@ -67,7 +66,6 @@
"disabled_protection": "Защита выкл.",
"refresh_statics": "Обновить статистику",
"dns_query": "DNS-запросы",
"blocked_by": "Заблокировано фильтрами",
"stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты",
"stats_adult": "Заблокированные \"взрослые\" сайты",
"stats_query_domain": "Часто запрашиваемые домены",
@ -78,7 +76,6 @@
"top_clients": "Частые клиенты",
"no_clients_found": "Клиентов не найдено",
"general_statistics": "Общая статистика",
"number_of_dns_query_24_hours": "Количество DNS-запросов за 24 часа",
"number_of_dns_query_blocked_24_hours": "Количество DNS-запросов, заблокированных фильтрами и блок-списками",
"number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Количество заблокированных \"сайтов для взрослых\"",
@ -211,7 +208,7 @@
"install_devices_router_list_2": "Найдите настройки DHCP или DNS. Найдите буквы \"DNS\" рядом с текстовым полем, в которое можно ввести два или три ряда цифр, разделенных на 4 группы от одной до трёх цифр.",
"install_devices_router_list_3": "Введите туда адрес вашего AdGuard Home.",
"install_devices_windows_list_1": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.",
"install_devices_windows_list_2": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.",
"install_devices_windows_list_2": "Перейдите в \"Сеть и интернет\", а затем в \"Центр управления сетями и общим доступом\"",
"install_devices_windows_list_3": "В левой стороне экрана найдите \"Изменение параметров адаптера\" и кликните по нему.",
"install_devices_windows_list_4": "Выделите ваше активное подключение, затем кликните по нему правой клавишей мыши и выберите \"Свойства\".",
"install_devices_windows_list_5": "Найдите в списке пункт \"IP версии 4 (TCP/IP)\", выделите его и затем снова нажмите \"Свойства\".",
@ -298,7 +295,6 @@
"client_deleted": "Клиент \"{{key}}\" успешно удален",
"client_added": "Клиент \"{{key}}\" успешно добавлен",
"client_updated": "Клиент \"{{key}}\" успешно обновлен",
"table_statistics": "Количество запросов (последние 24 часа)",
"clients_not_found": "Клиентов не найдено",
"client_confirm_delete": "Вы уверены, что хотите удалить клиента \"{{key}}\"?",
"filter_confirm_delete": "Вы уверены, что хотите удалить фильтр?",
@ -309,7 +305,7 @@
"access_allowed_title": "Разрешенные клиенты",
"access_allowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет принимать запросы только с этих IP-адресов.",
"access_disallowed_title": "Запрещенные клиенты",
"access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроек, AdGuard Home будет игнорировать запросы с этих IP-адресов.",
"access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет игнорировать запросы с этих IP-адресов.",
"access_blocked_title": "Заблокированные домены",
"access_blocked_desc": "Не путайте это с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами.",
"access_settings_saved": "Настройки доступа успешно сохранены",
@ -353,5 +349,13 @@
"blocked_services_global": "Использовать глобальные заблокированные сервисы",
"blocked_service": "Заблокированный сервис",
"block_all": "Заблокировать все",
"unblock_all": "Разблокировать все"
"unblock_all": "Разблокировать все",
"domain": "Домен",
"answer": "Ответ",
"interval_hours_0": "{{count}} час",
"interval_hours_1": "{{count}} часа",
"interval_hours_2": "{{count}} часов",
"interval_days_0": "{{count}} день",
"interval_days_1": "{{count}} дня",
"interval_days_2": "{{count}} дней"
}

View File

@ -0,0 +1,145 @@
import { createAction } from 'redux-actions';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { normalizeFilteringStatus, normalizeRulesTextarea } from '../helpers/helpers';
import { addErrorToast, addSuccessToast } from './index';
import apiClient from '../api/Api';
export const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE');
export const handleRulesChange = createAction('HANDLE_RULES_CHANGE');
export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST');
export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE');
export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS');
export const getFilteringStatus = () => async (dispatch) => {
dispatch(getFilteringStatusRequest());
try {
const status = await apiClient.getFilteringStatus();
dispatch(getFilteringStatusSuccess({ ...normalizeFilteringStatus(status) }));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getFilteringStatusFailure());
}
};
export const setRulesRequest = createAction('SET_RULES_REQUEST');
export const setRulesFailure = createAction('SET_RULES_FAILURE');
export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
export const setRules = rules => async (dispatch) => {
dispatch(setRulesRequest());
try {
const normalizedRules = normalizeRulesTextarea(rules);
await apiClient.setRules(normalizedRules);
dispatch(addSuccessToast('updated_custom_filtering_toast'));
dispatch(setRulesSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setRulesFailure());
}
};
export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
export const addFilter = (url, name) => async (dispatch) => {
dispatch(addFilterRequest());
try {
await apiClient.addFilter(url, name);
dispatch(addFilterSuccess(url));
dispatch(toggleFilteringModal());
dispatch(addSuccessToast('filter_added_successfully'));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(addFilterFailure());
}
};
export const removeFilterRequest = createAction('REMOVE_FILTER_REQUEST');
export const removeFilterFailure = createAction('REMOVE_FILTER_FAILURE');
export const removeFilterSuccess = createAction('REMOVE_FILTER_SUCCESS');
export const removeFilter = url => async (dispatch) => {
dispatch(removeFilterRequest());
try {
await apiClient.removeFilter(url);
dispatch(removeFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure());
}
};
export const toggleFilterRequest = createAction('FILTER_TOGGLE_REQUEST');
export const toggleFilterFailure = createAction('FILTER_TOGGLE_FAILURE');
export const toggleFilterSuccess = createAction('FILTER_TOGGLE_SUCCESS');
export const toggleFilterStatus = (url, enabled) => async (dispatch) => {
dispatch(toggleFilterRequest());
try {
await apiClient.setFilterUrl({ url, enabled: !enabled });
dispatch(toggleFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleFilterFailure());
}
};
export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST');
export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');
export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
export const refreshFilters = () => async (dispatch) => {
dispatch(refreshFiltersRequest());
dispatch(showLoading());
try {
const refreshText = await apiClient.refreshFilters();
dispatch(refreshFiltersSuccess());
if (refreshText.includes('OK')) {
if (refreshText.includes('OK 0')) {
dispatch(addSuccessToast('all_filters_up_to_date_toast'));
} else {
dispatch(addSuccessToast(refreshText.replace(/OK /g, '')));
}
} else {
dispatch(addErrorToast({ error: refreshText }));
}
dispatch(getFilteringStatus());
dispatch(hideLoading());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(refreshFiltersFailure());
dispatch(hideLoading());
}
};
export const setFiltersConfigRequest = createAction('SET_FILTERS_CONFIG_REQUEST');
export const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE');
export const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS');
export const setFiltersConfig = config => async (dispatch, getState) => {
dispatch(setFiltersConfigRequest());
try {
const { enabled } = config;
const prevEnabled = getState().filtering.enabled;
let successToastMessage = 'config_successfully_saved';
if (prevEnabled !== enabled) {
successToastMessage = enabled ? 'enabled_filtering_toast' : 'disabled_filtering_toast';
}
await apiClient.setFiltersConfig(config);
dispatch(addSuccessToast(successToastMessage));
dispatch(setFiltersConfigSuccess(config));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setFiltersConfigFailure());
}
};

View File

@ -1,10 +1,9 @@
import { createAction } from 'redux-actions';
import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import axios from 'axios';
import versionCompare from '../helpers/versionCompare';
import { normalizeFilteringStatus, normalizeTextarea, sortClients } from '../helpers/helpers';
import { normalizeTextarea, sortClients } from '../helpers/helpers';
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
import { getTlsStatus } from './encryption';
import apiClient from '../api/Api';
@ -21,16 +20,6 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
let successMessage = '';
try {
switch (settingKey) {
case SETTINGS_NAMES.filtering:
if (status) {
successMessage = 'disabled_filtering_toast';
await apiClient.disableFiltering();
} else {
successMessage = 'enabled_filtering_toast';
await apiClient.enableFiltering();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case SETTINGS_NAMES.safebrowsing:
if (status) {
successMessage = 'disabled_safe_browsing_toast';
@ -77,18 +66,15 @@ export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
export const initSettings = settingsList => async (dispatch) => {
dispatch(initSettingsRequest());
try {
const filteringStatus = await apiClient.getFilteringStatus();
const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
const parentalStatus = await apiClient.getParentalStatus();
const safesearchStatus = await apiClient.getSafesearchStatus();
const {
filtering,
safebrowsing,
parental,
safesearch,
} = settingsList;
const newSettingsList = {
filtering: { ...filtering, enabled: filteringStatus.enabled },
safebrowsing: { ...safebrowsing, enabled: safebrowsingStatus.enabled },
parental: { ...parental, enabled: parentalStatus.enabled },
safesearch: { ...safesearch, enabled: safesearchStatus.enabled },
@ -100,21 +86,6 @@ export const initSettings = settingsList => async (dispatch) => {
}
};
export const getFilteringRequest = createAction('GET_FILTERING_REQUEST');
export const getFilteringFailure = createAction('GET_FILTERING_FAILURE');
export const getFilteringSuccess = createAction('GET_FILTERING_SUCCESS');
export const getFiltering = () => async (dispatch) => {
dispatch(getFilteringRequest());
try {
const filteringStatus = await apiClient.getFilteringStatus();
dispatch(getFilteringSuccess(filteringStatus.enabled));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getFilteringFailure());
}
};
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
@ -290,133 +261,6 @@ export const disableDns = () => async (dispatch) => {
}
};
export const setRulesRequest = createAction('SET_RULES_REQUEST');
export const setRulesFailure = createAction('SET_RULES_FAILURE');
export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
export const setRules = rules => async (dispatch) => {
dispatch(setRulesRequest());
try {
const replacedLineEndings = rules
.replace(/^\n/g, '')
.replace(/\n\s*\n/g, '\n');
await apiClient.setRules(replacedLineEndings);
dispatch(addSuccessToast('updated_custom_filtering_toast'));
dispatch(setRulesSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setRulesFailure());
}
};
export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST');
export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE');
export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS');
export const getFilteringStatus = () => async (dispatch) => {
dispatch(getFilteringStatusRequest());
try {
const status = await apiClient.getFilteringStatus();
dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) }));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getFilteringStatusFailure());
}
};
export const toggleFilterRequest = createAction('FILTER_ENABLE_REQUEST');
export const toggleFilterFailure = createAction('FILTER_ENABLE_FAILURE');
export const toggleFilterSuccess = createAction('FILTER_ENABLE_SUCCESS');
export const toggleFilterStatus = url => async (dispatch, getState) => {
dispatch(toggleFilterRequest());
const state = getState();
const { filters } = state.filtering;
const filter = filters.filter(filter => filter.url === url)[0];
const { enabled } = filter;
let toggleStatusMethod;
if (enabled) {
toggleStatusMethod = apiClient.disableFilter.bind(apiClient);
} else {
toggleStatusMethod = apiClient.enableFilter.bind(apiClient);
}
try {
await toggleStatusMethod(url);
dispatch(toggleFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleFilterFailure());
}
};
export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST');
export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');
export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
export const refreshFilters = () => async (dispatch) => {
dispatch(refreshFiltersRequest());
dispatch(showLoading());
try {
const refreshText = await apiClient.refreshFilters();
dispatch(refreshFiltersSuccess());
if (refreshText.includes('OK')) {
if (refreshText.includes('OK 0')) {
dispatch(addSuccessToast('all_filters_up_to_date_toast'));
} else {
dispatch(addSuccessToast(refreshText.replace(/OK /g, '')));
}
} else {
dispatch(addErrorToast({ error: refreshText }));
}
dispatch(getFilteringStatus());
dispatch(hideLoading());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(refreshFiltersFailure());
dispatch(hideLoading());
}
};
export const handleRulesChange = createAction('HANDLE_RULES_CHANGE');
export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
export const addFilter = (url, name) => async (dispatch) => {
dispatch(addFilterRequest());
try {
await apiClient.addFilter(url, name);
dispatch(addFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(addFilterFailure());
}
};
export const removeFilterRequest = createAction('ADD_FILTER_REQUEST');
export const removeFilterFailure = createAction('ADD_FILTER_FAILURE');
export const removeFilterSuccess = createAction('ADD_FILTER_SUCCESS');
export const removeFilter = url => async (dispatch) => {
dispatch(removeFilterRequest());
try {
await apiClient.removeFilter(url);
dispatch(removeFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure());
}
};
export const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE');
export const handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE');
export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');

View File

@ -90,32 +90,19 @@ class Api {
}
// Filtering
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
FILTERING_DISABLE = { path: 'filtering/disable', method: 'POST' };
FILTERING_INFO = { path: 'filtering_info', method: 'GET' };
FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' };
FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' };
FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' };
FILTERING_ENABLE_FILTER = { path: 'filtering/enable_url', method: 'POST' };
FILTERING_DISABLE_FILTER = { path: 'filtering/disable_url', method: 'POST' };
FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };
FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' };
FILTERING_CONFIG = { path: 'filtering_config', method: 'POST' };
getFilteringStatus() {
const { path, method } = this.FILTERING_STATUS;
const { path, method } = this.FILTERING_INFO;
return this.makeRequest(path, method);
}
enableFiltering() {
const { path, method } = this.FILTERING_ENABLE;
return this.makeRequest(path, method);
}
disableFiltering() {
const { path, method } = this.FILTERING_DISABLE;
return this.makeRequest(path, method);
}
// TODO find out when to use force parameter
refreshFilters() {
const { path, method } = this.FILTERING_REFRESH;
return this.makeRequest(path, method);
@ -151,26 +138,22 @@ class Api {
return this.makeRequest(path, method, parameters);
}
enableFilter(url) {
const { path, method } = this.FILTERING_ENABLE_FILTER;
const parameter = 'url';
const requestBody = `${parameter}=${url}`;
const config = {
data: requestBody,
header: { 'Content-Type': 'text/plain' },
setFiltersConfig(config) {
const { path, method } = this.FILTERING_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
return this.makeRequest(path, method, parameters);
}
disableFilter(url) {
const { path, method } = this.FILTERING_DISABLE_FILTER;
const parameter = 'url';
const requestBody = `${parameter}=${url}`;
const config = {
data: requestBody,
header: { 'Content-Type': 'text/plain' },
setFilterUrl(config) {
const { path, method } = this.FILTERING_SET_URL;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
return this.makeRequest(path, method, parameters);
}
// Parental

View File

@ -33,27 +33,13 @@ class Modal extends Component {
this.setState({ ...this.state, name });
};
handleNext = () => {
this.props.addFilter(this.state.url, this.state.name);
setTimeout(() => {
if (this.props.isFilterAdded) {
this.closeModal();
}
}, 2000);
};
closeModal = () => {
this.props.toggleModal();
this.setState({ ...this.state, ...initialState });
}
};
render() {
const {
isOpen,
title,
inputDescription,
processingAddFilter,
} = this.props;
const { isOpen, processingAddFilter } = this.props;
const { isUrlValid, url, name } = this.state;
const inputUrlClass = classnames({
'form-control mb-2': true,
@ -64,28 +50,7 @@ class Modal extends Component {
'form-control mb-2': true,
'is-valid': name.length > 0,
});
const renderBody = () => {
if (!this.props.isFilterAdded) {
return (
<React.Fragment>
<input type="text" className={inputNameClass} placeholder={this.props.t('enter_name_hint')} onChange={this.handleNameChange} />
<input type="text" className={inputUrlClass} placeholder={this.props.t('enter_url_hint')} onChange={this.handleUrlChange} />
{inputDescription &&
<div className="description">
{inputDescription}
</div>}
</React.Fragment>
);
}
return (
<div className="description">
<Trans>filter_added_successfully</Trans>
</div>
);
};
const isValidForSubmit = !(url.length > 0 && isUrlValid && name.length > 0);
const isValidForSubmit = url.length > 0 && isUrlValid && name.length > 0;
return (
<ReactModal
@ -97,16 +62,29 @@ class Modal extends Component {
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{title}
<Trans>new_filter_btn</Trans>
</h4>
<button type="button" className="close" onClick={this.closeModal}>
<span className="sr-only">Close</span>
</button>
</div>
<div className="modal-body">
{renderBody()}
<input
type="text"
className={inputNameClass}
placeholder={this.props.t('enter_name_hint')}
onChange={this.handleNameChange}
/>
<input
type="text"
className={inputUrlClass}
placeholder={this.props.t('enter_url_hint')}
onChange={this.handleUrlChange}
/>
<div className="description">
<Trans>enter_valid_filter_url</Trans>
</div>
</div>
{!this.props.isFilterAdded &&
<div className="modal-footer">
<button
type="button"
@ -118,13 +96,12 @@ class Modal extends Component {
<button
type="button"
className="btn btn-success"
onClick={this.handleNext}
disabled={isValidForSubmit || processingAddFilter}
onClick={() => this.props.addFilter(url, name)}
disabled={!isValidForSubmit || processingAddFilter}
>
<Trans>add_filter_btn</Trans>
</button>
</div>
}
</div>
</ReactModal>
);
@ -134,12 +111,10 @@ class Modal extends Component {
Modal.propTypes = {
toggleModal: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
inputDescription: PropTypes.string,
addFilter: PropTypes.func.isRequired,
isFilterAdded: PropTypes.bool,
processingAddFilter: PropTypes.bool,
t: PropTypes.func,
isFilterAdded: PropTypes.bool.isRequired,
processingAddFilter: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Modal);

View File

@ -15,13 +15,13 @@ class UserRules extends Component {
};
render() {
const { t } = this.props;
const { t, userRules } = this.props;
return (
<Card title={t('custom_filter_rules')} subtitle={t('custom_filter_rules_hint')}>
<form onSubmit={this.handleSubmit}>
<textarea
className="form-control form-control--textarea-large"
value={this.props.userRules}
value={userRules}
onChange={this.handleChange}
/>
<div className="card-actions">
@ -79,10 +79,10 @@ class UserRules extends Component {
}
UserRules.propTypes = {
userRules: PropTypes.string,
handleRulesChange: PropTypes.func,
handleRulesSubmit: PropTypes.func,
t: PropTypes.func,
userRules: PropTypes.string.isRequired,
handleRulesChange: PropTypes.func.isRequired,
handleRulesSubmit: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(UserRules);

View File

@ -1,11 +1,13 @@
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Modal from './Modal';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import CellWrap from '../ui/CellWrap';
import UserRules from './UserRules';
import Modal from './Modal';
class Filters extends Component {
componentDidMount() {
@ -20,14 +22,19 @@ class Filters extends Component {
this.props.setRules(this.props.filtering.userRules);
};
renderCheckbox = (row) => {
const { url } = row.original;
const { filters } = this.props.filtering;
const filter = filters.filter(filter => filter.url === url)[0];
renderCheckbox = ({ original }) => {
const { processingConfigFilter } = this.props.filtering;
const { url, enabled } = original;
return (
<label className="checkbox">
<input type="checkbox" className="checkbox__input" onChange={() => this.props.toggleFilterStatus(filter.url)} checked={filter.enabled}/>
<span className="checkbox__label"/>
<input
type="checkbox"
className="checkbox__input"
onChange={() => this.props.toggleFilterStatus(url, enabled)}
checked={enabled}
disabled={processingConfigFilter}
/>
<span className="checkbox__label" />
</label>
);
};
@ -37,32 +44,54 @@ class Filters extends Component {
if (window.confirm(this.props.t('filter_confirm_delete'))) {
this.props.removeFilter({ url });
}
}
};
columns = [{
columns = [
{
Header: <Trans>enabled_table_header</Trans>,
accessor: 'enabled',
Cell: this.renderCheckbox,
width: 90,
className: 'text-center',
}, {
},
{
Header: <Trans>name_table_header</Trans>,
accessor: 'name',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
}, {
minWidth: 200,
Cell: CellWrap,
},
{
Header: <Trans>filter_url_table_header</Trans>,
accessor: 'url',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><a href={value} target='_blank' rel='noopener noreferrer' className="link logs__text">{value}</a></div>),
}, {
minWidth: 200,
Cell: ({ value }) => (
<div className="logs__row logs__row--overflow">
<a
href={value}
target="_blank"
rel="noopener noreferrer"
className="link logs__text"
>
{value}
</a>
</div>
),
},
{
Header: <Trans>rules_count_table_header</Trans>,
accessor: 'rulesCount',
className: 'text-center',
minWidth: 100,
Cell: props => props.value.toLocaleString(),
}, {
},
{
Header: <Trans>last_time_updated_table_header</Trans>,
accessor: 'lastUpdated',
className: 'text-center',
}, {
minWidth: 150,
Cell: CellWrap,
},
{
Header: <Trans>actions_table_header</Trans>,
accessor: 'url',
Cell: ({ value }) => (
@ -84,45 +113,62 @@ class Filters extends Component {
];
render() {
const { t } = this.props;
const { filters, userRules, processingRefreshFilters } = this.props.filtering;
const {
filtering, t, toggleFilteringModal, refreshFilters, addFilter,
} = this.props;
const {
filters,
userRules,
isModalOpen,
isFilterAdded,
processingRefreshFilters,
processingRemoveFilter,
processingAddFilter,
processingFilters,
} = filtering;
return (
<div>
<PageTitle title={ t('filters') } />
<Fragment>
<PageTitle title={t('filters')} />
<div className="content">
<div className="row">
<div className="col-md-12">
<Card
title={ t('filters_and_hosts') }
subtitle={ t('filters_and_hosts_hint') }
title={t('filters_and_hosts')}
subtitle={t('filters_and_hosts_hint')}
>
<ReactTable
data={filters}
columns={this.columns}
showPagination={true}
defaultPageSize={10}
loading={
processingFilters ||
processingAddFilter ||
processingRemoveFilter ||
processingRefreshFilters
}
minRows={4}
// Text
previousText={ t('previous_btn') }
nextText={ t('next_btn') }
loadingText={ t('loading_table_status') }
pageText={ t('page_table_footer_text') }
ofText={ t('of_table_footer_text') }
rowsText={ t('rows_table_footer_text') }
noDataText={ t('no_filters_added') }
previousText={t('previous_btn')}
nextText={t('next_btn')}
loadingText={t('loading_table_status')}
pageText={t('page_table_footer_text')}
ofText={t('of_table_footer_text')}
rowsText={t('rows_table_footer_text')}
noDataText={t('no_filters_added')}
/>
<div className="card-actions">
<button
className="btn btn-success btn-standard mr-2"
type="submit"
onClick={this.props.toggleFilteringModal}
onClick={toggleFilteringModal}
>
<Trans>add_filter_btn</Trans>
</button>
<button
className="btn btn-primary btn-standard"
type="submit"
onClick={this.props.refreshFilters}
onClick={refreshFilters}
disabled={processingRefreshFilters}
>
<Trans>check_updates_btn</Trans>
@ -140,15 +186,13 @@ class Filters extends Component {
</div>
</div>
<Modal
isOpen={this.props.filtering.isFilteringModalOpen}
toggleModal={this.props.toggleFilteringModal}
addFilter={this.props.addFilter}
isFilterAdded={this.props.filtering.isFilterAdded}
processingAddFilter={this.props.filtering.processingAddFilter}
title={ t('new_filter_btn') }
inputDescription={ t('enter_valid_filter_url') }
isOpen={isModalOpen}
toggleModal={toggleFilteringModal}
addFilter={addFilter}
isFilterAdded={isFilterAdded}
processingAddFilter={processingAddFilter}
/>
</div>
</Fragment>
);
}
}
@ -157,12 +201,15 @@ Filters.propTypes = {
setRules: PropTypes.func,
getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.shape({
userRules: PropTypes.string,
filters: PropTypes.array,
isFilteringModalOpen: PropTypes.bool.isRequired,
isFilterAdded: PropTypes.bool,
processingAddFilter: PropTypes.bool,
processingRefreshFilters: PropTypes.bool,
userRules: PropTypes.string.isRequired,
filters: PropTypes.array.isRequired,
isModalOpen: PropTypes.bool.isRequired,
isFilterAdded: PropTypes.bool.isRequired,
processingFilters: PropTypes.bool.isRequired,
processingAddFilter: PropTypes.bool.isRequired,
processingRefreshFilters: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
processingRemoveFilter: PropTypes.bool.isRequired,
}),
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,
@ -170,8 +217,7 @@ Filters.propTypes = {
toggleFilteringModal: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
t: PropTypes.func,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Filters);

View File

@ -36,6 +36,10 @@
overflow: hidden;
}
.logs__text--full {
width: 100%;
}
.logs__row .tooltip-custom {
top: 0;
margin-left: 0;

View File

@ -6,7 +6,7 @@ import endsWith from 'lodash/endsWith';
import { Trans, withNamespaces } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link';
import { formatTime, getClientName } from '../../helpers/helpers';
import { formatTime, formatDateTime, getClientName } from '../../helpers/helpers';
import { SERVICES, FILTERED_STATUS } from '../../helpers/constants';
import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle';
@ -114,7 +114,7 @@ class Logs extends Component {
getTimeCell = ({ value }) => (
<div className="logs__row">
<span className="logs__text" title={value}>
<span className="logs__text" title={formatDateTime(value)}>
{formatTime(value)}
</span>
</div>
@ -227,7 +227,7 @@ class Logs extends Component {
{
Header: t('time_table_header'),
accessor: 'time',
maxWidth: 90,
maxWidth: 100,
filterable: false,
Cell: this.getTimeCell,
},

View File

@ -0,0 +1,88 @@
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 { renderSelectField, toNumber } from '../../../helpers/form';
import { FILTERS_INTERVALS_HOURS } from '../../../helpers/constants';
const getTitleForInterval = (interval, t) => {
if (interval === 0) {
return t('disabled');
} else if (interval === 72 || interval === 168) {
return t('interval_days', { count: interval / 24 });
}
return t('interval_hours', { count: interval });
};
const getIntervalSelect = (processing, t, handleChange, toNumber) => (
<Field
name="interval"
className="custom-select"
component="select"
onChange={handleChange}
normalize={toNumber}
disabled={processing}
>
{FILTERS_INTERVALS_HOURS.map(interval => (
<option value={interval} key={interval}>
{getTitleForInterval(interval, t)}
</option>
))}
</Field>
);
const Form = (props) => {
const {
handleSubmit, handleChange, processing, t,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="enabled"
type="checkbox"
modifier="checkbox--settings"
component={renderSelectField}
placeholder={t('block_domain_use_filters_and_hosts')}
subtitle={t('filters_block_toggle_hint')}
onChange={handleChange}
disabled={processing}
/>
</div>
</div>
<div className="col-12 col-md-5">
<div className="form__group form__group--inner mb-5">
<label className="form__label">
<Trans>filters_interval</Trans>
</label>
{getIntervalSelect(processing, t, handleChange, toNumber)}
</div>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleChange: PropTypes.func,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default flow([
withNamespaces(),
reduxForm({
form: 'filterConfigForm',
}),
])(Form);

View File

@ -0,0 +1,36 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import debounce from 'lodash/debounce';
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
import Form from './Form';
class FiltersConfig extends Component {
handleFormChange = debounce((values) => {
this.props.setFiltersConfig(values);
}, DEBOUNCE_TIMEOUT);
render() {
const { interval, enabled, processing } = this.props;
return (
<Form
initialValues={{ interval, enabled }}
onSubmit={this.handleFormChange}
onChange={this.handleFormChange}
processing={processing}
/>
);
}
}
FiltersConfig.propTypes = {
interval: PropTypes.number.isRequired,
enabled: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
setFiltersConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(FiltersConfig);

View File

@ -11,6 +11,17 @@
margin-bottom: 20px;
}
.form__group--inner {
max-width: 300px;
margin-top: -10px;
margin-left: 40px;
font-size: 14px;
}
.form__group--checkbox {
margin-bottom: 25px;
}
.form__inline {
display: flex;
justify-content: flex-start;
@ -109,3 +120,11 @@
.custom-control-label:before {
transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color;
}
.custom-select:disabled {
background-color: #f9f9f9;
}
.custom-select {
transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color;
}

View File

@ -5,6 +5,7 @@ import { withNamespaces } from 'react-i18next';
import Services from './Services';
import StatsConfig from './StatsConfig';
import LogsConfig from './LogsConfig';
import FiltersConfig from './FiltersConfig';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
@ -14,11 +15,6 @@ import './Settings.css';
class Settings extends Component {
settings = {
filtering: {
enabled: false,
title: 'block_domain_use_filters_and_hosts',
subtitle: 'filters_block_toggle_hint',
},
safebrowsing: {
enabled: false,
title: 'use_adguard_browsing_sec',
@ -41,17 +37,20 @@ class Settings extends Component {
this.props.getBlockedServices();
this.props.getStatsConfig();
this.props.getLogsConfig();
this.props.getFilteringStatus();
}
renderSettings = (settings) => {
if (Object.keys(settings).length > 0) {
return Object.keys(settings).map((key) => {
const settingsKeys = Object.keys(settings);
if (settingsKeys.length > 0) {
return settingsKeys.map((key) => {
const setting = settings[key];
const { enabled } = setting;
return (
<Checkbox
key={key}
{...settings[key]}
key={key}
handleChange={() => this.props.toggleSetting(key, enabled)}
/>
);
@ -71,6 +70,8 @@ class Settings extends Component {
queryLogs,
setLogsConfig,
clearLogs,
filtering,
setFiltersConfig,
t,
} = this.props;
@ -90,6 +91,12 @@ class Settings extends Component {
<div className="col-md-12">
<Card bodyType="card-body box-body--settings">
<div className="form">
<FiltersConfig
interval={filtering.interval}
enabled={filtering.enabled}
processing={filtering.processingSetConfig}
setFiltersConfig={setFiltersConfig}
/>
{this.renderSettings(settings.settingsList)}
</div>
</Card>
@ -134,6 +141,8 @@ Settings.propTypes = {
getStatsConfig: PropTypes.func.isRequired,
setStatsConfig: PropTypes.func.isRequired,
resetStats: PropTypes.func.isRequired,
setFiltersConfig: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
const CellWrap = ({ value }) => (
<div className="logs__row logs__row--overflow">
<span className="logs__text logs__text--full" title={value}>
{value}
</span>
</div>
);
CellWrap.propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
export default CellWrap;

View File

@ -18,7 +18,7 @@
}
.checkbox--settings .checkbox__label-title {
margin-bottom: 5px;
margin-bottom: 2px;
font-weight: 600;
}
@ -53,7 +53,7 @@
background-position: center center;
background-size: 12px 10px;
border-radius: 3px;
transition: 0.3s ease box-shadow;
transition: 0.3s ease-in-out box-shadow, 0.3s ease-in-out opacity;
}
.checkbox__label .checkbox__label-text {
@ -82,10 +82,13 @@
}
.checkbox__input:disabled + .checkbox__label {
opacity: 0.6;
cursor: default;
}
.checkbox__input:disabled + .checkbox__label:before {
opacity: 0.6;
}
.checkbox__label-text {
max-width: 515px;
line-height: 1.5;

View File

@ -14,7 +14,7 @@ class Checkbox extends Component {
t,
} = this.props;
return (
<div className="form__group">
<div className="form__group form__group--checkbox">
<label className="checkbox checkbox--settings">
<span className="checkbox__marker"/>
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled}/>

View File

@ -1,5 +1,14 @@
import { connect } from 'react-redux';
import * as actionCreators from '../actions';
import {
setRules,
getFilteringStatus,
addFilter,
removeFilter,
toggleFilterStatus,
toggleFilteringModal,
refreshFilters,
handleRulesChange,
} from '../actions/filtering';
import Filters from '../components/Filters';
const mapStateToProps = (state) => {
@ -8,7 +17,18 @@ const mapStateToProps = (state) => {
return props;
};
const mapDispatchToProps = {
setRules,
getFilteringStatus,
addFilter,
removeFilter,
toggleFilterStatus,
toggleFilteringModal,
refreshFilters,
handleRulesChange,
};
export default connect(
mapStateToProps,
actionCreators,
mapDispatchToProps,
)(Filters);

View File

@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { getFilteringStatus, setRules, addSuccessToast, getClients } from '../actions';
import { addSuccessToast, getClients } from '../actions';
import { getFilteringStatus, setRules } from '../actions/filtering';
import { getLogs, getLogsConfig } from '../actions/queryLogs';
import Logs from '../components/Logs';

View File

@ -3,17 +3,19 @@ import { initSettings, toggleSetting } from '../actions';
import { getBlockedServices, setBlockedServices } from '../actions/services';
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
const {
settings, services, stats, queryLogs,
settings, services, stats, queryLogs, filtering,
} = state;
const props = {
settings,
services,
stats,
queryLogs,
filtering,
};
return props;
};
@ -29,6 +31,8 @@ const mapDispatchToProps = {
clearLogs,
getLogsConfig,
setLogsConfig,
getFilteringStatus,
setFiltersConfig,
};
export default connect(

View File

@ -265,3 +265,5 @@ export const FILTERED_STATUS = {
export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];

View File

@ -32,28 +32,36 @@ export const renderRadioField = ({
}) => (
<Fragment>
<label className="custom-control custom-radio custom-control-inline">
<input
{...input}
type="radio"
className="custom-control-input"
disabled={disabled}
/>
<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>)}
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export const renderSelectField = ({
input, placeholder, disabled, meta: { touched, error },
input,
placeholder,
subtitle,
disabled,
modifier = 'checkbox--form',
meta: { touched, error },
}) => (
<Fragment>
<label className="checkbox checkbox--form">
<label className={`checkbox ${modifier}`}>
<span className="checkbox__marker" />
<input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
<span className="checkbox__label">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{placeholder}</span>
{subtitle && (
<span
className="checkbox__label-subtitle"
dangerouslySetInnerHTML={{ __html: subtitle }}
/>
)}
</span>
</span>
</label>
@ -64,7 +72,12 @@ export const renderSelectField = ({
);
export const renderServiceField = ({
input, placeholder, disabled, modifier, icon, meta: { touched, error },
input,
placeholder,
disabled,
modifier,
icon,
meta: { touched, error },
}) => (
<Fragment>
<label className={`service custom-switch ${modifier}`}>
@ -81,7 +94,9 @@ export const renderServiceField = ({
<use xlinkHref={`#${icon}`} />
</svg>
</label>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);

View File

@ -6,6 +6,7 @@ import addDays from 'date-fns/add_days';
import subDays from 'date-fns/sub_days';
import round from 'lodash/round';
import axios from 'axios';
import i18n from 'i18next';
import {
STANDARD_DNS_PORT,
@ -19,6 +20,21 @@ export const formatTime = (time) => {
return dateFormat(parsedTime, 'HH:mm:ss');
};
export const formatDateTime = (dateTime) => {
const currentLanguage = i18n.languages[0] || 'en';
const parsedTime = dateParse(dateTime);
const options = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: false,
};
return parsedTime.toLocaleString(currentLanguage, options);
};
export const normalizeLogs = logs => logs.map((log) => {
const {
time,
@ -74,18 +90,38 @@ export const normalizeTopStats = stats => (
);
export const normalizeFilteringStatus = (filteringStatus) => {
const { enabled, filters, user_rules: userRules } = filteringStatus;
const newFilters = filters ? filters.map((filter) => {
const {
id, url, enabled, lastUpdated: lastUpdated = Date.now(), name = 'Default name', rulesCount: rulesCount = 0,
enabled, filters, user_rules: userRules, interval,
} = filteringStatus;
const newFilters = filters
? filters.map((filter) => {
const {
id,
url,
enabled,
last_updated,
name = 'Default name',
rules_count: rules_count = 0,
} = filter;
return {
id, url, enabled, lastUpdated: formatTime(lastUpdated), name, rulesCount,
id,
url,
enabled,
lastUpdated: last_updated ? formatDateTime(last_updated) : '',
name,
rulesCount: rules_count,
};
}) : [];
})
: [];
const newUserRules = Array.isArray(userRules) ? userRules.join('\n') : '';
return { enabled, userRules: newUserRules, filters: newFilters };
return {
enabled,
userRules: newUserRules,
filters: newFilters,
interval,
};
};
export const getPercent = (amount, number) => {
@ -241,3 +277,5 @@ export const secondsToMilliseconds = (seconds) => {
return seconds;
};
export const normalizeRulesTextarea = text => text && text.replace(/^\n/g, '').replace(/\n\s*\n/g, '\n');

View File

@ -0,0 +1,86 @@
import { handleActions } from 'redux-actions';
import * as actions from '../actions/filtering';
const filtering = handleActions(
{
[actions.setRulesRequest]: state => ({ ...state, processingRules: true }),
[actions.setRulesFailure]: state => ({ ...state, processingRules: false }),
[actions.setRulesSuccess]: state => ({ ...state, processingRules: false }),
[actions.handleRulesChange]: (state, { payload }) => {
const { userRules } = payload;
return { ...state, userRules };
},
[actions.getFilteringStatusRequest]: state => ({ ...state, processingFilters: true }),
[actions.getFilteringStatusFailure]: state => ({ ...state, processingFilters: false }),
[actions.getFilteringStatusSuccess]: (state, { payload }) => ({
...state,
...payload,
processingFilters: false,
}),
[actions.addFilterRequest]: state => ({
...state,
processingAddFilter: true,
isFilterAdded: false,
}),
[actions.addFilterFailure]: state => ({
...state,
processingAddFilter: false,
isFilterAdded: false,
}),
[actions.addFilterSuccess]: state => ({
...state,
processingAddFilter: false,
isFilterAdded: true,
}),
[actions.toggleFilteringModal]: (state) => {
const newState = {
...state,
isModalOpen: !state.isModalOpen,
isFilterAdded: false,
};
return newState;
},
[actions.toggleFilterRequest]: state => ({ ...state, processingConfigFilter: true }),
[actions.toggleFilterFailure]: state => ({ ...state, processingConfigFilter: false }),
[actions.toggleFilterSuccess]: state => ({ ...state, processingConfigFilter: false }),
[actions.refreshFiltersRequest]: state => ({ ...state, processingRefreshFilters: true }),
[actions.refreshFiltersFailure]: state => ({ ...state, processingRefreshFilters: false }),
[actions.refreshFiltersSuccess]: state => ({ ...state, processingRefreshFilters: false }),
[actions.removeFilterRequest]: state => ({ ...state, processingRemoveFilter: true }),
[actions.removeFilterFailure]: state => ({ ...state, processingRemoveFilter: false }),
[actions.removeFilterSuccess]: state => ({ ...state, processingRemoveFilter: false }),
[actions.setFiltersConfigRequest]: state => ({ ...state, processingSetConfig: true }),
[actions.setFiltersConfigFailure]: state => ({ ...state, processingSetConfig: false }),
[actions.setFiltersConfigSuccess]: (state, { payload }) => ({
...state,
...payload,
processingSetConfig: false,
}),
},
{
isModalOpen: false,
processingFilters: false,
processingRules: false,
processingAddFilter: false,
processingRefreshFilters: false,
processingConfigFilter: false,
processingRemoveFilter: false,
processingSetConfig: false,
isFilterAdded: false,
filters: [],
userRules: '',
interval: 24,
enabled: true,
},
);
export default filtering;

View File

@ -13,6 +13,7 @@ import rewrites from './rewrites';
import services from './services';
import stats from './stats';
import queryLogs from './queryLogs';
import filtering from './filtering';
const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@ -130,13 +131,6 @@ const dashboard = handleActions({
return newState;
},
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
[actions.getFilteringSuccess]: (state, { payload }) => {
const newState = { ...state, isFilteringEnabled: payload, processingFiltering: false };
return newState;
},
[actions.toggleProtectionRequest]: state => ({ ...state, processingProtection: true }),
[actions.toggleProtectionFailure]: state => ({ ...state, processingProtection: false }),
[actions.toggleProtectionSuccess]: (state) => {
@ -189,62 +183,6 @@ const dashboard = handleActions({
autoClients: [],
});
const filtering = handleActions({
[actions.setRulesRequest]: state => ({ ...state, processingRules: true }),
[actions.setRulesFailure]: state => ({ ...state, processingRules: false }),
[actions.setRulesSuccess]: state => ({ ...state, processingRules: false }),
[actions.handleRulesChange]: (state, { payload }) => {
const { userRules } = payload;
return { ...state, userRules };
},
[actions.getFilteringStatusRequest]: state => ({ ...state, processingFilters: true }),
[actions.getFilteringStatusFailure]: state => ({ ...state, processingFilters: false }),
[actions.getFilteringStatusSuccess]: (state, { payload }) => {
const { status } = payload;
const { filters, userRules } = status;
const newState = {
...state, filters, userRules, processingFilters: false,
};
return newState;
},
[actions.addFilterRequest]: state =>
({ ...state, processingAddFilter: true, isFilterAdded: false }),
[actions.addFilterFailure]: (state) => {
const newState = { ...state, processingAddFilter: false, isFilterAdded: false };
return newState;
},
[actions.addFilterSuccess]: state =>
({ ...state, processingAddFilter: false, isFilterAdded: true }),
[actions.toggleFilteringModal]: (state) => {
const newState = {
...state,
isFilteringModalOpen: !state.isFilteringModalOpen,
isFilterAdded: false,
};
return newState;
},
[actions.toggleFilterRequest]: state => ({ ...state, processingFilters: true }),
[actions.toggleFilterFailure]: state => ({ ...state, processingFilters: false }),
[actions.toggleFilterSuccess]: state => ({ ...state, processingFilters: false }),
[actions.refreshFiltersRequest]: state => ({ ...state, processingRefreshFilters: true }),
[actions.refreshFiltersFailure]: state => ({ ...state, processingRefreshFilters: false }),
[actions.refreshFiltersSuccess]: state => ({ ...state, processingRefreshFilters: false }),
}, {
isFilteringModalOpen: false,
processingFilters: false,
processingRules: false,
processingAddFilter: false,
processingRefreshFilters: false,
filters: [],
userRules: '',
});
const dhcp = handleActions({
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),

View File

@ -114,9 +114,9 @@ type Dnsfilter struct {
// Filter represents a filter list
type Filter struct {
ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase
Data []byte `json:"-" yaml:"-"` // List of rules divided by '\n'
FilePath string `json:"-" yaml:"-"` // Path to a filtering rules file
ID int64 // auto-assigned when filter is added (see nextFilterID)
Data []byte `yaml:"-"` // List of rules divided by '\n'
FilePath string `yaml:"-"` // Path to a filtering rules file
}
//go:generate stringer -type=Reason

View File

@ -68,6 +68,8 @@ func NewServer(stats stats.Stats, queryLog querylog.QueryLog) *Server {
type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled

1
go.mod
View File

@ -7,7 +7,6 @@ require (
github.com/AdguardTeam/golibs v0.2.1
github.com/AdguardTeam/urlfilter v0.5.0
github.com/NYTimes/gziphandler v1.1.1
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
github.com/etcd-io/bbolt v1.3.3
github.com/go-test/deep v1.0.1

View File

@ -10,7 +10,6 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/AdGuardHome/querylog"
"github.com/AdguardTeam/AdGuardHome/stats"
@ -72,6 +71,7 @@ type configuration struct {
client *http.Client
stats stats.Stats
queryLog querylog.QueryLog
filteringStarted bool
// cached version.json to avoid hammering github.io for each page reload
versionCheckJSON []byte
@ -174,6 +174,7 @@ var config = configuration{
FilteringConfig: dnsforward.FilteringConfig{
ProtectionEnabled: true, // whether or not use any of dnsfilter features
FilteringEnabled: true, // whether or not use filter lists
FiltersUpdateIntervalHours: 24,
BlockingMode: "nxdomain", // mode how to answer filtered requests
BlockedResponseTTL: 10, // in seconds
QueryLogEnabled: true,
@ -191,12 +192,6 @@ var config = configuration{
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
},
},
Filters: []filter{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
DHCP: dhcpd.ServerConfig{
LeaseDuration: 86400,
ICMPTimeout: 1000,
@ -226,6 +221,7 @@ func initConfig() {
config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024
config.DNS.ParentalCacheSize = 1 * 1024 * 1024
config.DNS.CacheTime = 30
config.Filters = defaultFilters()
}
// getConfigFilename returns path to the current config file
@ -276,6 +272,9 @@ func parseConfig() error {
if !checkStatsInterval(config.DNS.StatsInterval) {
config.DNS.StatsInterval = 1
}
if !checkFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) {
config.DNS.FiltersUpdateIntervalHours = 24
}
if !checkQueryLogInterval(config.DNS.QueryLogInterval) {
config.DNS.QueryLogInterval = 1
@ -308,11 +307,6 @@ func parseConfig() error {
return err
}
// Deduplicate filters
deduplicateFilters()
updateUniqueFilterID(config.Filters)
return nil
}

View File

@ -7,7 +7,6 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream"
@ -17,8 +16,6 @@ import (
"github.com/miekg/dns"
)
const updatePeriod = time.Hour * 24
var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
// ----------------
@ -547,15 +544,6 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
httpRegister(http.MethodPost, "/control/update", handleUpdate)
httpRegister(http.MethodPost, "/control/filtering/enable", handleFilteringEnable)
httpRegister(http.MethodPost, "/control/filtering/disable", handleFilteringDisable)
httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL)
httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL)
httpRegister(http.MethodPost, "/control/filtering/enable_url", handleFilteringEnableURL)
httpRegister(http.MethodPost, "/control/filtering/disable_url", handleFilteringDisableURL)
httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh)
httpRegister(http.MethodGet, "/control/filtering/status", handleFilteringStatus)
httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules)
httpRegister(http.MethodPost, "/control/safebrowsing/enable", handleSafeBrowsingEnable)
httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable)
httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus)
@ -575,6 +563,7 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/access/list", handleAccessList)
httpRegister(http.MethodPost, "/control/access/set", handleAccessSet)
RegisterFilteringHandlers()
RegisterTLSHandlers()
RegisterClientsHandlers()
registerRewritesHandlers()

View File

@ -5,74 +5,57 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/asaskevich/govalidator"
)
func handleFilteringEnable(w http.ResponseWriter, r *http.Request) {
config.DNS.FilteringEnabled = true
httpUpdateConfigReloadDNSReturnOK(w, r)
// IsValidURL - return TRUE if URL is valid
func IsValidURL(rawurl string) bool {
url, err := url.ParseRequestURI(rawurl)
if err != nil {
return false //Couldn't even parse the rawurl
}
if len(url.Scheme) == 0 {
return false //No Scheme found
}
return true
}
func handleFilteringDisable(w http.ResponseWriter, r *http.Request) {
config.DNS.FilteringEnabled = false
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"enabled": config.DNS.FilteringEnabled,
}
config.RLock()
data["filters"] = config.Filters
data["user_rules"] = config.UserRules
jsonVal, err := json.Marshal(data)
config.RUnlock()
if err != nil {
httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err)
return
}
type filterAddJSON struct {
Name string `json:"name"`
URL string `json:"url"`
}
func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
f := filter{}
err := json.NewDecoder(r.Body).Decode(&f)
fj := filterAddJSON{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
return
}
if len(f.URL) == 0 {
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
return
}
if valid := govalidator.IsRequestURL(f.URL); !valid {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
if !IsValidURL(fj.URL) {
http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
// Check for duplicates
if filterExists(f.URL) {
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL)
if filterExists(fj.URL) {
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
return
}
// Set necessary properties
f := filter{
Enabled: true,
URL: fj.URL,
Name: fj.Name,
}
f.ID = assignUniqueFilterID()
f.Enabled = true
// Download the filter contents
ok, err := f.update()
@ -133,7 +116,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
return
}
if valid := govalidator.IsRequestURL(req.URL); !valid {
if !IsValidURL(req.URL) {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return
}
@ -166,54 +149,27 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) {
parameters, err := parseParametersFromBody(r.Body)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
return
}
url, ok := parameters["url"]
if !ok {
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
return
}
if valid := govalidator.IsRequestURL(url); !valid {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return
}
found := filterEnable(url, true)
if !found {
http.Error(w, "URL parameter was not previously added", http.StatusBadRequest)
return
}
httpUpdateConfigReloadDNSReturnOK(w, r)
type filterURLJSON struct {
URL string `json:"url"`
Enabled bool `json:"enabled"`
}
func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) {
parameters, err := parseParametersFromBody(r.Body)
func handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
fj := filterURLJSON{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil {
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
httpError(w, http.StatusBadRequest, "json decode: %s", err)
return
}
url, ok := parameters["url"]
if !ok {
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
if !IsValidURL(fj.URL) {
http.Error(w, "invalid URL", http.StatusBadRequest)
return
}
if valid := govalidator.IsRequestURL(url); !valid {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return
}
found := filterEnable(url, false)
found := filterEnable(fj.URL, fj.Enabled)
if !found {
http.Error(w, "URL parameter was not previously added", http.StatusBadRequest)
http.Error(w, "URL doesn't exist", http.StatusBadRequest)
return
}
@ -235,3 +191,91 @@ func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
updated := refreshFiltersIfNecessary(true)
fmt.Fprintf(w, "OK %d filters updated\n", updated)
}
type filterJSON struct {
ID int64 `json:"id"`
Enabled bool `json:"enabled"`
URL string `json:"url"`
Name string `json:"name"`
RulesCount uint32 `json:"rules_count"`
LastUpdated string `json:"last_updated"`
}
type filteringConfig struct {
Enabled bool `json:"enabled"`
Interval uint32 `json:"interval"` // in hours
Filters []filterJSON `json:"filters"`
UserRules []string `json:"user_rules"`
}
// Get filtering configuration
func handleFilteringInfo(w http.ResponseWriter, r *http.Request) {
resp := filteringConfig{}
config.RLock()
resp.Enabled = config.DNS.FilteringEnabled
resp.Interval = config.DNS.FiltersUpdateIntervalHours
for _, f := range config.Filters {
fj := filterJSON{
ID: f.ID,
Enabled: f.Enabled,
URL: f.URL,
Name: f.Name,
RulesCount: uint32(f.RulesCount),
}
if f.LastUpdated.Second() != 0 {
fj.LastUpdated = f.LastUpdated.Format(time.RFC3339)
}
resp.Filters = append(resp.Filters, fj)
}
resp.UserRules = config.UserRules
config.RUnlock()
jsonVal, err := json.Marshal(resp)
if err != nil {
httpError(w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
httpError(w, http.StatusInternalServerError, "http write: %s", err)
}
}
// Set filtering configuration
func handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
req := filteringConfig{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(w, http.StatusBadRequest, "json decode: %s", err)
return
}
if !checkFiltersUpdateIntervalHours(req.Interval) {
httpError(w, http.StatusBadRequest, "Unsupported interval")
return
}
config.DNS.FilteringEnabled = req.Enabled
config.DNS.FiltersUpdateIntervalHours = req.Interval
httpUpdateConfigReloadDNSReturnOK(w, r)
returnOK(w)
}
// RegisterFilteringHandlers - register handlers
func RegisterFilteringHandlers() {
httpRegister(http.MethodGet, "/control/filtering_info", handleFilteringInfo)
httpRegister(http.MethodPost, "/control/filtering_config", handleFilteringConfig)
httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL)
httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL)
httpRegister(http.MethodPost, "/control/filtering/set_url", handleFilteringSetURL)
httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh)
httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules)
}
func checkFiltersUpdateIntervalHours(i uint32) bool {
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
}

View File

@ -7,6 +7,7 @@ import (
"net"
"net/http"
"os/exec"
"path/filepath"
"strconv"
"github.com/AdguardTeam/golibs/log"
@ -239,6 +240,9 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
config.AuthName = newSettings.Username
config.AuthPass = newSettings.Password
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
initDNSServer(dnsBaseDir)
err = startDNSServer()
if err != nil {
config.firstRun = true
@ -255,8 +259,6 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return
}
go refreshFiltersIfNecessary(false)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTP {

View File

@ -49,6 +49,7 @@ func initDNSServer(baseDir string) {
config.dnsServer = dnsforward.NewServer(config.stats, config.queryLog)
initRDNS()
initFiltering()
}
func isRunning() bool {
@ -165,6 +166,11 @@ func startDNSServer() error {
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
}
if !config.filteringStarted {
config.filteringStarted = true
startRefreshFilters()
}
return nil
}

View File

@ -21,13 +21,35 @@ var (
filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
)
func initFiltering() {
loadFilters()
deduplicateFilters()
updateUniqueFilterID(config.Filters)
}
func startRefreshFilters() {
go func() {
_ = refreshFiltersIfNecessary(false)
}()
go periodicallyRefreshFilters()
}
func defaultFilters() []filter {
return []filter{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
}
}
// field ordering is important -- yaml fields will mirror ordering from here
type filter struct {
Enabled bool `json:"enabled"`
URL string `json:"url"`
Name string `json:"name" yaml:"name"`
RulesCount int `json:"rulesCount" yaml:"-"`
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"`
Enabled bool
URL string
Name string `yaml:"name"`
RulesCount int `yaml:"-"`
LastUpdated time.Time `yaml:"-"`
checksum uint32 // checksum of the file data
dnsfilter.Filter `yaml:",inline"`
@ -119,8 +141,7 @@ func loadFilters() {
err := filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err)
log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
}
}
}
@ -159,7 +180,12 @@ func assignUniqueFilterID() int64 {
// Sets up a timer that will be checking for filters updates periodically
func periodicallyRefreshFilters() {
for range time.Tick(time.Minute) {
for {
time.Sleep(1 * time.Hour)
if config.DNS.FiltersUpdateIntervalHours == 0 {
continue
}
refreshFiltersIfNecessary(false)
}
}
@ -180,10 +206,7 @@ func refreshFiltersIfNecessary(force bool) int {
var updateFilters []filter
var updateFlags []bool // 'true' if filter data has changed
if config.firstRun {
return 0
}
now := time.Now()
config.RLock()
for i := range config.Filters {
f := &config.Filters[i] // otherwise we will be operating on a copy
@ -192,7 +215,8 @@ func refreshFiltersIfNecessary(force bool) int {
continue
}
if !force && time.Since(f.LastUpdated) <= updatePeriod {
expireTime := f.LastUpdated.Unix() + int64(config.DNS.FiltersUpdateIntervalHours)*60*60
if !force && expireTime > now.Unix() {
continue
}
@ -214,7 +238,7 @@ func refreshFiltersIfNecessary(force bool) int {
log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
continue
}
uf.LastUpdated = time.Now()
uf.LastUpdated = now
if updated {
updateCount++
}

37
home/filter_test.go Normal file
View File

@ -0,0 +1,37 @@
package home
import (
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestFilters(t *testing.T) {
config.client = &http.Client{
Timeout: time.Minute * 5,
}
f := filter{
URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt",
}
// download
ok, err := f.update()
assert.True(t, ok && err == nil)
// refresh
ok, err = f.update()
assert.True(t, !ok && err == nil)
err = f.save()
assert.True(t, err == nil)
err = f.load()
assert.True(t, err == nil)
f.unload()
os.Remove(f.Path())
}

View File

@ -135,22 +135,17 @@ func run(args options) {
config.BindPort = args.bindPort
}
loadFilters()
if !config.firstRun {
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
}
// Init the DNS server instance before registering HTTP handlers
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
initDNSServer(dnsBaseDir)
if !config.firstRun {
err := startDNSServer()
err = startDNSServer()
if err != nil {
log.Fatal(err)
}
@ -165,13 +160,6 @@ func run(args options) {
config.pidFileName = args.pidFile
}
// Update filters we've just loaded right away, don't wait for periodic update timer
go func() {
refreshFiltersIfNecessary(false)
}()
// Schedule automatic filters updates
go periodicallyRefreshFilters()
// Initialize and run the admin Web interface
box := packr.NewBox("../build/static")

View File

@ -438,34 +438,30 @@ paths:
# Filtering status methods
# --------------------------------------------------
/filtering/status:
/filtering_info:
get:
tags:
- filtering
operationId: filteringStatus
summary: 'Get status of rules-based filter'
operationId: filteringInfo
summary: 'Get filtering parameters'
responses:
200:
description: OK
schema:
$ref: "#/definitions/FilteringStatus"
$ref: "#/definitions/FilterInfo"
/filtering/enable:
/filtering_config:
post:
tags:
- filtering
operationId: filteringEnable
summary: 'Enable filtering'
responses:
200:
description: OK
/filtering/disable:
post:
tags:
- filtering
operationId: filteringDisable
summary: 'Disable filtering'
operationId: filteringConfig
summary: 'Set filtering parameters'
parameters:
- in: "body"
name: "body"
required: true
schema:
$ref: "#/definitions/FilterConfig"
responses:
200:
description: OK
@ -506,42 +502,19 @@ paths:
200:
description: OK
/filtering/enable_url:
/filtering/set_url:
post:
tags:
- filtering
operationId: filteringEnableURL
summary: 'Enable filter URL'
operationId: filteringSetURL
summary: 'Set URL parameters'
consumes:
- text/plain
- application/json
parameters:
- in: body
name: url
description: 'Previously added URL containing filtering rules'
required: true
- in: "body"
name: "body"
schema:
type: string
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
responses:
200:
description: OK
/filtering/disable_url:
post:
tags:
- filtering
operationId: filteringDisableURL
summary: 'Disable filter URL'
consumes:
- text/plain
parameters:
- in: body
name: url
description: 'Previously added URL containing filtering rules'
required: true
schema:
type: string
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
$ref: "#/definitions/FilterSetUrl"
responses:
200:
description: OK
@ -1051,7 +1024,7 @@ definitions:
lastUpdated:
type: "string"
format: "date-time"
example: "2018-10-30T12:18:57.223101822+03:00"
example: "2018-10-30T12:18:57+03:00"
name:
type: "string"
example: "AdGuard Simplified Domain Names filter"
@ -1061,16 +1034,15 @@ definitions:
url:
type: "string"
example: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"
FilteringStatus:
FilterInfo:
type: "object"
description: "Filtering settings"
required:
- "enabled"
- "filters"
- "user_rules"
properties:
enabled:
type: "boolean"
interval:
type: "integer"
filters:
type: "array"
items:
@ -1079,9 +1051,25 @@ definitions:
type: "array"
items:
type: "string"
example:
- '||example.org^'
- '||example.com^'
FilterConfig:
type: "object"
description: "Filtering settings"
properties:
enabled:
type: "boolean"
interval:
type: "integer"
FilterSetUrl:
type: "object"
description: "Filtering URL settings"
properties:
url:
type: "string"
enabled:
type: "boolean"
GetVersionRequest:
type: "object"
description: "/version.json request data"

View File

@ -35,7 +35,7 @@ type queryLog struct {
lock sync.RWMutex
}
// newQueryLog creates a new instance of the query log
// create a new instance of the query log
func newQueryLog(conf Config) *queryLog {
l := queryLog{}
l.logFile = filepath.Join(conf.BaseDir, queryLogFileName)
@ -53,7 +53,6 @@ func (l *queryLog) Configure(conf Config) {
l.conf = conf
}
// Clear memory buffer and remove the file
func (l *queryLog) Clear() {
l.fileFlushLock.Lock()
defer l.fileFlushLock.Unlock()
@ -164,7 +163,6 @@ func (l *queryLog) Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Res
}
}
// getQueryLogJson returns a map with the current query log ready to be converted to a JSON
func (l *queryLog) GetData() []map[string]interface{} {
l.lock.RLock()
values := make([]*logEntry, len(l.cache))

View File

@ -10,14 +10,20 @@ import (
// QueryLog - main interface
type QueryLog interface {
// Close query log object
Close()
// Set new configuration at runtime
// Currently only 'Interval' field is supported.
Configure(conf Config)
// Add a log entry
Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Result, elapsed time.Duration, addr net.Addr, upstream string)
// Get log entries
GetData() []map[string]interface{}
// Clear memory buffer and remove log files
Clear()
}
@ -27,7 +33,7 @@ type Config struct {
Interval uint32 // interval to rotate logs (in hours)
}
// New - create instance
// New - create a new instance of the query log
func New(conf Config) QueryLog {
return newQueryLog(conf)
}