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 * Query logs
* API: Set querylog parameters * API: Set querylog parameters
* API: Get 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 ## Relations between subsystems
@ -1019,3 +1024,76 @@ Response:
"enabled": true | false "enabled": true | false
"interval": 1 | 7 | 30 | 90 "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_retention_desc": "If you decrease the interval value, some data will be lost",
"statistics_clear": " Clear statistics", "statistics_clear": " Clear statistics",
"statistics_clear_confirm": "Are you sure you want to 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": "Настройки клиентов", "client_settings": "Настройки клиентов",
"example_upstream_reserved": "вы можете указать DNS-сервер <0>для конкретного домена(ов)</0>", "example_upstream_reserved": "вы можете указать DNS-сервер <0>для конкретного домена(ов)</0>",
"upstream_parallel": "Использовать одновременные запроссы ко всем серверам для ускорения обработки запроса", "upstream_parallel": "Использовать одновременные запросы ко всем серверам для ускорения обработки запроса",
"bootstrap_dns": "Bootstrap DNS-серверы", "bootstrap_dns": "Bootstrap DNS-серверы",
"bootstrap_dns_desc": "Bootstrap DNS-серверы используются для поиска IP-адресов DoH/DoT серверов, которые вы указали.", "bootstrap_dns_desc": "Bootstrap DNS-серверы используются для поиска IP-адресов DoH/DoT серверов, которые вы указали.",
"url_added_successfully": "URL успешно добавлен",
"check_dhcp_servers": "Проверить DHCP-серверы", "check_dhcp_servers": "Проверить DHCP-серверы",
"save_config": "Сохранить конфигурацию", "save_config": "Сохранить конфигурацию",
"enabled_dhcp": "DHCP-сервер включен", "enabled_dhcp": "DHCP-сервер включен",
@ -67,7 +66,6 @@
"disabled_protection": "Защита выкл.", "disabled_protection": "Защита выкл.",
"refresh_statics": "Обновить статистику", "refresh_statics": "Обновить статистику",
"dns_query": "DNS-запросы", "dns_query": "DNS-запросы",
"blocked_by": "Заблокировано фильтрами",
"stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты", "stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты",
"stats_adult": "Заблокированные \"взрослые\" сайты", "stats_adult": "Заблокированные \"взрослые\" сайты",
"stats_query_domain": "Часто запрашиваемые домены", "stats_query_domain": "Часто запрашиваемые домены",
@ -78,7 +76,6 @@
"top_clients": "Частые клиенты", "top_clients": "Частые клиенты",
"no_clients_found": "Клиентов не найдено", "no_clients_found": "Клиентов не найдено",
"general_statistics": "Общая статистика", "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": "Количество DNS-запросов, заблокированных фильтрами и блок-списками",
"number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard", "number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Количество заблокированных \"сайтов для взрослых\"", "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_2": "Найдите настройки DHCP или DNS. Найдите буквы \"DNS\" рядом с текстовым полем, в которое можно ввести два или три ряда цифр, разделенных на 4 группы от одной до трёх цифр.",
"install_devices_router_list_3": "Введите туда адрес вашего AdGuard Home.", "install_devices_router_list_3": "Введите туда адрес вашего AdGuard Home.",
"install_devices_windows_list_1": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.", "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_3": "В левой стороне экрана найдите \"Изменение параметров адаптера\" и кликните по нему.",
"install_devices_windows_list_4": "Выделите ваше активное подключение, затем кликните по нему правой клавишей мыши и выберите \"Свойства\".", "install_devices_windows_list_4": "Выделите ваше активное подключение, затем кликните по нему правой клавишей мыши и выберите \"Свойства\".",
"install_devices_windows_list_5": "Найдите в списке пункт \"IP версии 4 (TCP/IP)\", выделите его и затем снова нажмите \"Свойства\".", "install_devices_windows_list_5": "Найдите в списке пункт \"IP версии 4 (TCP/IP)\", выделите его и затем снова нажмите \"Свойства\".",
@ -298,7 +295,6 @@
"client_deleted": "Клиент \"{{key}}\" успешно удален", "client_deleted": "Клиент \"{{key}}\" успешно удален",
"client_added": "Клиент \"{{key}}\" успешно добавлен", "client_added": "Клиент \"{{key}}\" успешно добавлен",
"client_updated": "Клиент \"{{key}}\" успешно обновлен", "client_updated": "Клиент \"{{key}}\" успешно обновлен",
"table_statistics": "Количество запросов (последние 24 часа)",
"clients_not_found": "Клиентов не найдено", "clients_not_found": "Клиентов не найдено",
"client_confirm_delete": "Вы уверены, что хотите удалить клиента \"{{key}}\"?", "client_confirm_delete": "Вы уверены, что хотите удалить клиента \"{{key}}\"?",
"filter_confirm_delete": "Вы уверены, что хотите удалить фильтр?", "filter_confirm_delete": "Вы уверены, что хотите удалить фильтр?",
@ -309,7 +305,7 @@
"access_allowed_title": "Разрешенные клиенты", "access_allowed_title": "Разрешенные клиенты",
"access_allowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет принимать запросы только с этих IP-адресов.", "access_allowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет принимать запросы только с этих IP-адресов.",
"access_disallowed_title": "Запрещенные клиенты", "access_disallowed_title": "Запрещенные клиенты",
"access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроек, AdGuard Home будет игнорировать запросы с этих IP-адресов.", "access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет игнорировать запросы с этих IP-адресов.",
"access_blocked_title": "Заблокированные домены", "access_blocked_title": "Заблокированные домены",
"access_blocked_desc": "Не путайте это с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами.", "access_blocked_desc": "Не путайте это с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами.",
"access_settings_saved": "Настройки доступа успешно сохранены", "access_settings_saved": "Настройки доступа успешно сохранены",
@ -353,5 +349,13 @@
"blocked_services_global": "Использовать глобальные заблокированные сервисы", "blocked_services_global": "Использовать глобальные заблокированные сервисы",
"blocked_service": "Заблокированный сервис", "blocked_service": "Заблокированный сервис",
"block_all": "Заблокировать все", "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 { createAction } from 'redux-actions';
import { t } from 'i18next'; import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import axios from 'axios'; import axios from 'axios';
import versionCompare from '../helpers/versionCompare'; 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 { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
import { getTlsStatus } from './encryption'; import { getTlsStatus } from './encryption';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
@ -21,16 +20,6 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
let successMessage = ''; let successMessage = '';
try { try {
switch (settingKey) { 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: case SETTINGS_NAMES.safebrowsing:
if (status) { if (status) {
successMessage = 'disabled_safe_browsing_toast'; successMessage = 'disabled_safe_browsing_toast';
@ -77,18 +66,15 @@ export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
export const initSettings = settingsList => async (dispatch) => { export const initSettings = settingsList => async (dispatch) => {
dispatch(initSettingsRequest()); dispatch(initSettingsRequest());
try { try {
const filteringStatus = await apiClient.getFilteringStatus();
const safebrowsingStatus = await apiClient.getSafebrowsingStatus(); const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
const parentalStatus = await apiClient.getParentalStatus(); const parentalStatus = await apiClient.getParentalStatus();
const safesearchStatus = await apiClient.getSafesearchStatus(); const safesearchStatus = await apiClient.getSafesearchStatus();
const { const {
filtering,
safebrowsing, safebrowsing,
parental, parental,
safesearch, safesearch,
} = settingsList; } = settingsList;
const newSettingsList = { const newSettingsList = {
filtering: { ...filtering, enabled: filteringStatus.enabled },
safebrowsing: { ...safebrowsing, enabled: safebrowsingStatus.enabled }, safebrowsing: { ...safebrowsing, enabled: safebrowsingStatus.enabled },
parental: { ...parental, enabled: parentalStatus.enabled }, parental: { ...parental, enabled: parentalStatus.enabled },
safesearch: { ...safesearch, enabled: safesearchStatus.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 toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE'); export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS'); 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 handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE');
export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST'); export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE'); export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import endsWith from 'lodash/endsWith';
import { Trans, withNamespaces } from 'react-i18next'; 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, formatDateTime, getClientName } from '../../helpers/helpers';
import { SERVICES, FILTERED_STATUS } from '../../helpers/constants'; import { SERVICES, FILTERED_STATUS } 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';
@ -114,7 +114,7 @@ class Logs extends Component {
getTimeCell = ({ value }) => ( getTimeCell = ({ value }) => (
<div className="logs__row"> <div className="logs__row">
<span className="logs__text" title={value}> <span className="logs__text" title={formatDateTime(value)}>
{formatTime(value)} {formatTime(value)}
</span> </span>
</div> </div>
@ -227,7 +227,7 @@ class Logs extends Component {
{ {
Header: t('time_table_header'), Header: t('time_table_header'),
accessor: 'time', accessor: 'time',
maxWidth: 90, maxWidth: 100,
filterable: false, filterable: false,
Cell: this.getTimeCell, 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; 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 { .form__inline {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@ -109,3 +120,11 @@
.custom-control-label:before { .custom-control-label:before {
transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color; 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 Services from './Services';
import StatsConfig from './StatsConfig'; import StatsConfig from './StatsConfig';
import LogsConfig from './LogsConfig'; import LogsConfig from './LogsConfig';
import FiltersConfig from './FiltersConfig';
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';
@ -14,11 +15,6 @@ import './Settings.css';
class Settings extends Component { class Settings extends Component {
settings = { settings = {
filtering: {
enabled: false,
title: 'block_domain_use_filters_and_hosts',
subtitle: 'filters_block_toggle_hint',
},
safebrowsing: { safebrowsing: {
enabled: false, enabled: false,
title: 'use_adguard_browsing_sec', title: 'use_adguard_browsing_sec',
@ -41,17 +37,20 @@ class Settings extends Component {
this.props.getBlockedServices(); this.props.getBlockedServices();
this.props.getStatsConfig(); this.props.getStatsConfig();
this.props.getLogsConfig(); this.props.getLogsConfig();
this.props.getFilteringStatus();
} }
renderSettings = (settings) => { renderSettings = (settings) => {
if (Object.keys(settings).length > 0) { const settingsKeys = Object.keys(settings);
return Object.keys(settings).map((key) => {
if (settingsKeys.length > 0) {
return settingsKeys.map((key) => {
const setting = settings[key]; const setting = settings[key];
const { enabled } = setting; const { enabled } = setting;
return ( return (
<Checkbox <Checkbox
key={key}
{...settings[key]} {...settings[key]}
key={key}
handleChange={() => this.props.toggleSetting(key, enabled)} handleChange={() => this.props.toggleSetting(key, enabled)}
/> />
); );
@ -71,6 +70,8 @@ class Settings extends Component {
queryLogs, queryLogs,
setLogsConfig, setLogsConfig,
clearLogs, clearLogs,
filtering,
setFiltersConfig,
t, t,
} = this.props; } = this.props;
@ -90,6 +91,12 @@ class Settings extends Component {
<div className="col-md-12"> <div className="col-md-12">
<Card bodyType="card-body box-body--settings"> <Card bodyType="card-body box-body--settings">
<div className="form"> <div className="form">
<FiltersConfig
interval={filtering.interval}
enabled={filtering.enabled}
processing={filtering.processingSetConfig}
setFiltersConfig={setFiltersConfig}
/>
{this.renderSettings(settings.settingsList)} {this.renderSettings(settings.settingsList)}
</div> </div>
</Card> </Card>
@ -134,6 +141,8 @@ Settings.propTypes = {
getStatsConfig: PropTypes.func.isRequired, getStatsConfig: PropTypes.func.isRequired,
setStatsConfig: PropTypes.func.isRequired, setStatsConfig: PropTypes.func.isRequired,
resetStats: PropTypes.func.isRequired, resetStats: PropTypes.func.isRequired,
setFiltersConfig: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func.isRequired,
t: 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 { .checkbox--settings .checkbox__label-title {
margin-bottom: 5px; margin-bottom: 2px;
font-weight: 600; font-weight: 600;
} }
@ -53,7 +53,7 @@
background-position: center center; background-position: center center;
background-size: 12px 10px; background-size: 12px 10px;
border-radius: 3px; 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 { .checkbox__label .checkbox__label-text {
@ -82,10 +82,13 @@
} }
.checkbox__input:disabled + .checkbox__label { .checkbox__input:disabled + .checkbox__label {
opacity: 0.6;
cursor: default; cursor: default;
} }
.checkbox__input:disabled + .checkbox__label:before {
opacity: 0.6;
}
.checkbox__label-text { .checkbox__label-text {
max-width: 515px; max-width: 515px;
line-height: 1.5; line-height: 1.5;

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { connect } from 'react-redux'; 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 { getLogs, getLogsConfig } from '../actions/queryLogs';
import Logs from '../components/Logs'; import Logs from '../components/Logs';

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import addDays from 'date-fns/add_days';
import subDays from 'date-fns/sub_days'; import subDays from 'date-fns/sub_days';
import round from 'lodash/round'; import round from 'lodash/round';
import axios from 'axios'; import axios from 'axios';
import i18n from 'i18next';
import { import {
STANDARD_DNS_PORT, STANDARD_DNS_PORT,
@ -19,6 +20,21 @@ export const formatTime = (time) => {
return dateFormat(parsedTime, 'HH:mm:ss'); 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) => { export const normalizeLogs = logs => logs.map((log) => {
const { const {
time, time,
@ -74,18 +90,38 @@ export const normalizeTopStats = stats => (
); );
export const normalizeFilteringStatus = (filteringStatus) => { export const normalizeFilteringStatus = (filteringStatus) => {
const { enabled, filters, user_rules: userRules } = filteringStatus; const {
const newFilters = filters ? filters.map((filter) => { enabled, filters, user_rules: userRules, interval,
const { } = filteringStatus;
id, url, enabled, lastUpdated: lastUpdated = Date.now(), name = 'Default name', rulesCount: rulesCount = 0, const newFilters = filters
} = filter; ? filters.map((filter) => {
const {
id,
url,
enabled,
last_updated,
name = 'Default name',
rules_count: rules_count = 0,
} = filter;
return { 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') : ''; 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) => { export const getPercent = (amount, number) => {
@ -241,3 +277,5 @@ export const secondsToMilliseconds = (seconds) => {
return 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 services from './services';
import stats from './stats'; import stats from './stats';
import queryLogs from './queryLogs'; import queryLogs from './queryLogs';
import filtering from './filtering';
const settings = handleActions({ const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }), [actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@ -130,13 +131,6 @@ const dashboard = handleActions({
return newState; 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.toggleProtectionRequest]: state => ({ ...state, processingProtection: true }),
[actions.toggleProtectionFailure]: state => ({ ...state, processingProtection: false }), [actions.toggleProtectionFailure]: state => ({ ...state, processingProtection: false }),
[actions.toggleProtectionSuccess]: (state) => { [actions.toggleProtectionSuccess]: (state) => {
@ -189,62 +183,6 @@ const dashboard = handleActions({
autoClients: [], 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({ const dhcp = handleActions({
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }), [actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }), [actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),

View File

@ -114,9 +114,9 @@ type Dnsfilter struct {
// Filter represents a filter list // Filter represents a filter list
type Filter struct { 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 ID int64 // auto-assigned when filter is added (see nextFilterID)
Data []byte `json:"-" yaml:"-"` // List of rules divided by '\n' Data []byte `yaml:"-"` // List of rules divided by '\n'
FilePath string `json:"-" yaml:"-"` // Path to a filtering rules file FilePath string `yaml:"-"` // Path to a filtering rules file
} }
//go:generate stringer -type=Reason //go:generate stringer -type=Reason

View File

@ -66,8 +66,10 @@ func NewServer(stats stats.Stats, queryLog querylog.QueryLog) *Server {
// FilteringConfig represents the DNS filtering configuration of AdGuard Home // FilteringConfig represents the DNS filtering configuration of AdGuard Home
// The zero FilteringConfig is empty and ready for use. // The zero FilteringConfig is empty and ready for use.
type FilteringConfig struct { type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists 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 BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600) BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled 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/golibs v0.2.1
github.com/AdguardTeam/urlfilter v0.5.0 github.com/AdguardTeam/urlfilter v0.5.0
github.com/NYTimes/gziphandler v1.1.1 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/bluele/gcache v0.0.0-20190518031135-bc40bd653833
github.com/etcd-io/bbolt v1.3.3 github.com/etcd-io/bbolt v1.3.3
github.com/go-test/deep v1.0.1 github.com/go-test/deep v1.0.1

View File

@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/AdGuardHome/querylog" "github.com/AdguardTeam/AdGuardHome/querylog"
"github.com/AdguardTeam/AdGuardHome/stats" "github.com/AdguardTeam/AdGuardHome/stats"
@ -72,6 +71,7 @@ type configuration struct {
client *http.Client client *http.Client
stats stats.Stats stats stats.Stats
queryLog querylog.QueryLog queryLog querylog.QueryLog
filteringStarted bool
// cached version.json to avoid hammering github.io for each page reload // cached version.json to avoid hammering github.io for each page reload
versionCheckJSON []byte versionCheckJSON []byte
@ -172,16 +172,17 @@ var config = configuration{
Port: 53, Port: 53,
StatsInterval: 1, StatsInterval: 1,
FilteringConfig: dnsforward.FilteringConfig{ FilteringConfig: dnsforward.FilteringConfig{
ProtectionEnabled: true, // whether or not use any of dnsfilter features ProtectionEnabled: true, // whether or not use any of dnsfilter features
FilteringEnabled: true, // whether or not use filter lists FilteringEnabled: true, // whether or not use filter lists
BlockingMode: "nxdomain", // mode how to answer filtered requests FiltersUpdateIntervalHours: 24,
BlockedResponseTTL: 10, // in seconds BlockingMode: "nxdomain", // mode how to answer filtered requests
QueryLogEnabled: true, BlockedResponseTTL: 10, // in seconds
QueryLogInterval: 1, QueryLogEnabled: true,
Ratelimit: 20, QueryLogInterval: 1,
RefuseAny: true, Ratelimit: 20,
BootstrapDNS: defaultBootstrap, RefuseAny: true,
AllServers: false, BootstrapDNS: defaultBootstrap,
AllServers: false,
}, },
UpstreamDNS: defaultDNS, UpstreamDNS: defaultDNS,
}, },
@ -191,12 +192,6 @@ var config = configuration{
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy 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{ DHCP: dhcpd.ServerConfig{
LeaseDuration: 86400, LeaseDuration: 86400,
ICMPTimeout: 1000, ICMPTimeout: 1000,
@ -226,6 +221,7 @@ func initConfig() {
config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024 config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024
config.DNS.ParentalCacheSize = 1 * 1024 * 1024 config.DNS.ParentalCacheSize = 1 * 1024 * 1024
config.DNS.CacheTime = 30 config.DNS.CacheTime = 30
config.Filters = defaultFilters()
} }
// getConfigFilename returns path to the current config file // getConfigFilename returns path to the current config file
@ -276,6 +272,9 @@ func parseConfig() error {
if !checkStatsInterval(config.DNS.StatsInterval) { if !checkStatsInterval(config.DNS.StatsInterval) {
config.DNS.StatsInterval = 1 config.DNS.StatsInterval = 1
} }
if !checkFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) {
config.DNS.FiltersUpdateIntervalHours = 24
}
if !checkQueryLogInterval(config.DNS.QueryLogInterval) { if !checkQueryLogInterval(config.DNS.QueryLogInterval) {
config.DNS.QueryLogInterval = 1 config.DNS.QueryLogInterval = 1
@ -308,11 +307,6 @@ func parseConfig() error {
return err return err
} }
// Deduplicate filters
deduplicateFilters()
updateUniqueFilterID(config.Filters)
return nil return nil
} }

View File

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
@ -17,8 +16,6 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
const updatePeriod = time.Hour * 24
var protocols = []string{"tls://", "https://", "tcp://", "sdns://"} var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
// ---------------- // ----------------
@ -547,15 +544,6 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage) httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
httpRegister(http.MethodPost, "/control/update", handleUpdate) 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/enable", handleSafeBrowsingEnable)
httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable) httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable)
httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus) httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus)
@ -575,6 +563,7 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/access/list", handleAccessList) httpRegister(http.MethodGet, "/control/access/list", handleAccessList)
httpRegister(http.MethodPost, "/control/access/set", handleAccessSet) httpRegister(http.MethodPost, "/control/access/set", handleAccessSet)
RegisterFilteringHandlers()
RegisterTLSHandlers() RegisterTLSHandlers()
RegisterClientsHandlers() RegisterClientsHandlers()
registerRewritesHandlers() registerRewritesHandlers()

View File

@ -5,74 +5,57 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"time"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/asaskevich/govalidator"
) )
func handleFilteringEnable(w http.ResponseWriter, r *http.Request) { // IsValidURL - return TRUE if URL is valid
config.DNS.FilteringEnabled = true func IsValidURL(rawurl string) bool {
httpUpdateConfigReloadDNSReturnOK(w, r) 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) { type filterAddJSON struct {
config.DNS.FilteringEnabled = false Name string `json:"name"`
httpUpdateConfigReloadDNSReturnOK(w, r) URL string `json:"url"`
}
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
}
} }
func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
f := filter{} fj := filterAddJSON{}
err := json.NewDecoder(r.Body).Decode(&f) err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err) httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
return return
} }
if len(f.URL) == 0 { if !IsValidURL(fj.URL) {
http.Error(w, "URL parameter was not specified", http.StatusBadRequest) http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
if valid := govalidator.IsRequestURL(f.URL); !valid {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return return
} }
// Check for duplicates // Check for duplicates
if filterExists(f.URL) { if filterExists(fj.URL) {
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
return return
} }
// Set necessary properties // Set necessary properties
f := filter{
Enabled: true,
URL: fj.URL,
Name: fj.Name,
}
f.ID = assignUniqueFilterID() f.ID = assignUniqueFilterID()
f.Enabled = true
// Download the filter contents // Download the filter contents
ok, err := f.update() ok, err := f.update()
@ -133,7 +116,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
return return
} }
if valid := govalidator.IsRequestURL(req.URL); !valid { if !IsValidURL(req.URL) {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest) http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return return
} }
@ -166,54 +149,27 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
httpUpdateConfigReloadDNSReturnOK(w, r) httpUpdateConfigReloadDNSReturnOK(w, r)
} }
func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) { type filterURLJSON struct {
parameters, err := parseParametersFromBody(r.Body) URL string `json:"url"`
if err != nil { Enabled bool `json:"enabled"`
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)
} }
func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) { func handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
parameters, err := parseParametersFromBody(r.Body) fj := filterURLJSON{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err) httpError(w, http.StatusBadRequest, "json decode: %s", err)
return return
} }
url, ok := parameters["url"] if !IsValidURL(fj.URL) {
if !ok { http.Error(w, "invalid URL", http.StatusBadRequest)
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
return return
} }
if valid := govalidator.IsRequestURL(url); !valid { found := filterEnable(fj.URL, fj.Enabled)
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return
}
found := filterEnable(url, false)
if !found { if !found {
http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) http.Error(w, "URL doesn't exist", http.StatusBadRequest)
return return
} }
@ -235,3 +191,91 @@ func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
updated := refreshFiltersIfNecessary(true) updated := refreshFiltersIfNecessary(true)
fmt.Fprintf(w, "OK %d filters updated\n", updated) 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"
"net/http" "net/http"
"os/exec" "os/exec"
"path/filepath"
"strconv" "strconv"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
@ -239,6 +240,9 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
config.AuthName = newSettings.Username config.AuthName = newSettings.Username
config.AuthPass = newSettings.Password config.AuthPass = newSettings.Password
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
initDNSServer(dnsBaseDir)
err = startDNSServer() err = startDNSServer()
if err != nil { if err != nil {
config.firstRun = true config.firstRun = true
@ -255,8 +259,6 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return return
} }
go refreshFiltersIfNecessary(false)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block // 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 // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTP { if restartHTTP {

View File

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

View File

@ -21,13 +21,35 @@ var (
filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`) 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 // field ordering is important -- yaml fields will mirror ordering from here
type filter struct { type filter struct {
Enabled bool `json:"enabled"` Enabled bool
URL string `json:"url"` URL string
Name string `json:"name" yaml:"name"` Name string `yaml:"name"`
RulesCount int `json:"rulesCount" yaml:"-"` RulesCount int `yaml:"-"`
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"` LastUpdated time.Time `yaml:"-"`
checksum uint32 // checksum of the file data checksum uint32 // checksum of the file data
dnsfilter.Filter `yaml:",inline"` dnsfilter.Filter `yaml:",inline"`
@ -119,8 +141,7 @@ func loadFilters() {
err := filter.load() err := filter.load()
if err != nil { if err != nil {
// This is okay for the first start, the filter will be loaded later log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
log.Debug("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 // Sets up a timer that will be checking for filters updates periodically
func periodicallyRefreshFilters() { func periodicallyRefreshFilters() {
for range time.Tick(time.Minute) { for {
time.Sleep(1 * time.Hour)
if config.DNS.FiltersUpdateIntervalHours == 0 {
continue
}
refreshFiltersIfNecessary(false) refreshFiltersIfNecessary(false)
} }
} }
@ -180,10 +206,7 @@ func refreshFiltersIfNecessary(force bool) int {
var updateFilters []filter var updateFilters []filter
var updateFlags []bool // 'true' if filter data has changed var updateFlags []bool // 'true' if filter data has changed
if config.firstRun { now := time.Now()
return 0
}
config.RLock() config.RLock()
for i := range config.Filters { for i := range config.Filters {
f := &config.Filters[i] // otherwise we will be operating on a copy f := &config.Filters[i] // otherwise we will be operating on a copy
@ -192,7 +215,8 @@ func refreshFiltersIfNecessary(force bool) int {
continue continue
} }
if !force && time.Since(f.LastUpdated) <= updatePeriod { expireTime := f.LastUpdated.Unix() + int64(config.DNS.FiltersUpdateIntervalHours)*60*60
if !force && expireTime > now.Unix() {
continue continue
} }
@ -214,7 +238,7 @@ func refreshFiltersIfNecessary(force bool) int {
log.Printf("Failed to update filter %s: %s\n", uf.URL, err) log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
continue continue
} }
uf.LastUpdated = time.Now() uf.LastUpdated = now
if updated { if updated {
updateCount++ 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 config.BindPort = args.bindPort
} }
loadFilters()
if !config.firstRun { if !config.firstRun {
// Save the updated config // Save the updated config
err := config.write() err := config.write()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
}
// Init the DNS server instance before registering HTTP handlers dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir) initDNSServer(dnsBaseDir)
initDNSServer(dnsBaseDir)
if !config.firstRun { err = startDNSServer()
err := startDNSServer()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -165,13 +160,6 @@ func run(args options) {
config.pidFileName = args.pidFile 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 // Initialize and run the admin Web interface
box := packr.NewBox("../build/static") box := packr.NewBox("../build/static")

View File

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

View File

@ -35,7 +35,7 @@ type queryLog struct {
lock sync.RWMutex 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 { func newQueryLog(conf Config) *queryLog {
l := queryLog{} l := queryLog{}
l.logFile = filepath.Join(conf.BaseDir, queryLogFileName) l.logFile = filepath.Join(conf.BaseDir, queryLogFileName)
@ -53,7 +53,6 @@ func (l *queryLog) Configure(conf Config) {
l.conf = conf l.conf = conf
} }
// Clear memory buffer and remove the file
func (l *queryLog) Clear() { func (l *queryLog) Clear() {
l.fileFlushLock.Lock() l.fileFlushLock.Lock()
defer l.fileFlushLock.Unlock() 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{} { func (l *queryLog) GetData() []map[string]interface{} {
l.lock.RLock() l.lock.RLock()
values := make([]*logEntry, len(l.cache)) values := make([]*logEntry, len(l.cache))

View File

@ -10,14 +10,20 @@ import (
// QueryLog - main interface // QueryLog - main interface
type QueryLog interface { type QueryLog interface {
// Close query log object
Close() Close()
// Set new configuration at runtime // Set new configuration at runtime
// Currently only 'Interval' field is supported. // Currently only 'Interval' field is supported.
Configure(conf Config) 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) 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{} GetData() []map[string]interface{}
// Clear memory buffer and remove log files
Clear() Clear()
} }
@ -27,7 +33,7 @@ type Config struct {
Interval uint32 // interval to rotate logs (in hours) 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 { func New(conf Config) QueryLog {
return newQueryLog(conf) return newQueryLog(conf)
} }