mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-26 23:28:08 +03:00
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:
commit
30ca77303b
40 changed files with 1113 additions and 673 deletions
|
@ -45,6 +45,11 @@ Contents:
|
|||
* Query logs
|
||||
* API: Set querylog parameters
|
||||
* API: Get querylog parameters
|
||||
* Filtering
|
||||
* Filters update mechanism
|
||||
* API: Get filtering parameters
|
||||
* API: Set filtering parameters
|
||||
* API: Set URL parameters
|
||||
|
||||
|
||||
## Relations between subsystems
|
||||
|
@ -1019,3 +1024,76 @@ Response:
|
|||
"enabled": true | false
|
||||
"interval": 1 | 7 | 30 | 90
|
||||
}
|
||||
|
||||
|
||||
## Filtering
|
||||
|
||||
### Filters update mechanism
|
||||
|
||||
Filters can be updated either manually by request from UI or automatically.
|
||||
Auto-update interval can be configured in UI. If it is 0, auto-update is disabled.
|
||||
When the last modification date of filter files is older than auto-update interval, auto-update procedure is started.
|
||||
If an enabled filter file doesn't exist, it's downloaded on application startup. This includes the case when installation wizard is completed and there are no filter files yet.
|
||||
When auto-update time comes, server starts the update procedure by downloading filter files. After new filter files are in place, it restarts DNS filtering module with new rules.
|
||||
Only filters that are enabled by configuration can be updated.
|
||||
As a result of the update procedure, all enabled filter files are written to disk, refreshed (their last modification date is equal to the current time) and loaded.
|
||||
|
||||
|
||||
### API: Get filtering parameters
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/filtering_info
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"enabled": true | false
|
||||
"interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24
|
||||
"filters":[
|
||||
{
|
||||
"id":1
|
||||
"enabled":true,
|
||||
"url":"https://...",
|
||||
"name":"...",
|
||||
"rules_count":1234,
|
||||
"last_updated":"2019-09-04T18:29:30+00:00",
|
||||
}
|
||||
...
|
||||
],
|
||||
"user_rules":["...", ...]
|
||||
}
|
||||
|
||||
|
||||
### API: Set filtering parameters
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/filtering_config
|
||||
|
||||
{
|
||||
"enabled": true | false
|
||||
"interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
|
||||
### API: Set URL parameters
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/filtering/set_url
|
||||
|
||||
{
|
||||
"url": "..."
|
||||
"enabled": true | false
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
|
|
@ -378,5 +378,11 @@
|
|||
"statistics_retention_desc": "If you decrease the interval value, some data will be lost",
|
||||
"statistics_clear": " Clear statistics",
|
||||
"statistics_clear_confirm": "Are you sure you want to clear statistics?",
|
||||
"statistics_cleared": "Statistics successfully cleared"
|
||||
"statistics_cleared": "Statistics successfully cleared",
|
||||
"interval_hours": "{{count}} hour",
|
||||
"interval_hours_plural": "{{count}} hours",
|
||||
"filters_configuration": "Filters configuration",
|
||||
"filters_enable": "Enable filters",
|
||||
"filters_interval": "Filters update interval",
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
{
|
||||
"client_settings": "Настройки клиентов",
|
||||
"example_upstream_reserved": "вы можете указать DNS-сервер <0>для конкретного домена(ов)</0>",
|
||||
"upstream_parallel": "Использовать одновременные запроссы ко всем серверам для ускорения обработки запроса",
|
||||
"upstream_parallel": "Использовать одновременные запросы ко всем серверам для ускорения обработки запроса",
|
||||
"bootstrap_dns": "Bootstrap DNS-серверы",
|
||||
"bootstrap_dns_desc": "Bootstrap DNS-серверы используются для поиска IP-адресов DoH/DoT серверов, которые вы указали.",
|
||||
"url_added_successfully": "URL успешно добавлен",
|
||||
"check_dhcp_servers": "Проверить DHCP-серверы",
|
||||
"save_config": "Сохранить конфигурацию",
|
||||
"enabled_dhcp": "DHCP-сервер включен",
|
||||
|
@ -67,7 +66,6 @@
|
|||
"disabled_protection": "Защита выкл.",
|
||||
"refresh_statics": "Обновить статистику",
|
||||
"dns_query": "DNS-запросы",
|
||||
"blocked_by": "Заблокировано фильтрами",
|
||||
"stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты",
|
||||
"stats_adult": "Заблокированные \"взрослые\" сайты",
|
||||
"stats_query_domain": "Часто запрашиваемые домены",
|
||||
|
@ -78,7 +76,6 @@
|
|||
"top_clients": "Частые клиенты",
|
||||
"no_clients_found": "Клиентов не найдено",
|
||||
"general_statistics": "Общая статистика",
|
||||
"number_of_dns_query_24_hours": "Количество DNS-запросов за 24 часа",
|
||||
"number_of_dns_query_blocked_24_hours": "Количество DNS-запросов, заблокированных фильтрами и блок-списками",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Количество заблокированных \"сайтов для взрослых\"",
|
||||
|
@ -211,7 +208,7 @@
|
|||
"install_devices_router_list_2": "Найдите настройки DHCP или DNS. Найдите буквы \"DNS\" рядом с текстовым полем, в которое можно ввести два или три ряда цифр, разделенных на 4 группы от одной до трёх цифр.",
|
||||
"install_devices_router_list_3": "Введите туда адрес вашего AdGuard Home.",
|
||||
"install_devices_windows_list_1": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.",
|
||||
"install_devices_windows_list_2": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.",
|
||||
"install_devices_windows_list_2": "Перейдите в \"Сеть и интернет\", а затем в \"Центр управления сетями и общим доступом\"",
|
||||
"install_devices_windows_list_3": "В левой стороне экрана найдите \"Изменение параметров адаптера\" и кликните по нему.",
|
||||
"install_devices_windows_list_4": "Выделите ваше активное подключение, затем кликните по нему правой клавишей мыши и выберите \"Свойства\".",
|
||||
"install_devices_windows_list_5": "Найдите в списке пункт \"IP версии 4 (TCP/IP)\", выделите его и затем снова нажмите \"Свойства\".",
|
||||
|
@ -298,7 +295,6 @@
|
|||
"client_deleted": "Клиент \"{{key}}\" успешно удален",
|
||||
"client_added": "Клиент \"{{key}}\" успешно добавлен",
|
||||
"client_updated": "Клиент \"{{key}}\" успешно обновлен",
|
||||
"table_statistics": "Количество запросов (последние 24 часа)",
|
||||
"clients_not_found": "Клиентов не найдено",
|
||||
"client_confirm_delete": "Вы уверены, что хотите удалить клиента \"{{key}}\"?",
|
||||
"filter_confirm_delete": "Вы уверены, что хотите удалить фильтр?",
|
||||
|
@ -309,7 +305,7 @@
|
|||
"access_allowed_title": "Разрешенные клиенты",
|
||||
"access_allowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет принимать запросы только с этих IP-адресов.",
|
||||
"access_disallowed_title": "Запрещенные клиенты",
|
||||
"access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроек, AdGuard Home будет игнорировать запросы с этих IP-адресов.",
|
||||
"access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет игнорировать запросы с этих IP-адресов.",
|
||||
"access_blocked_title": "Заблокированные домены",
|
||||
"access_blocked_desc": "Не путайте это с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами.",
|
||||
"access_settings_saved": "Настройки доступа успешно сохранены",
|
||||
|
@ -353,5 +349,13 @@
|
|||
"blocked_services_global": "Использовать глобальные заблокированные сервисы",
|
||||
"blocked_service": "Заблокированный сервис",
|
||||
"block_all": "Заблокировать все",
|
||||
"unblock_all": "Разблокировать все"
|
||||
"unblock_all": "Разблокировать все",
|
||||
"domain": "Домен",
|
||||
"answer": "Ответ",
|
||||
"interval_hours_0": "{{count}} час",
|
||||
"interval_hours_1": "{{count}} часа",
|
||||
"interval_hours_2": "{{count}} часов",
|
||||
"interval_days_0": "{{count}} день",
|
||||
"interval_days_1": "{{count}} дня",
|
||||
"interval_days_2": "{{count}} дней"
|
||||
}
|
145
client/src/actions/filtering.js
Normal file
145
client/src/actions/filtering.js
Normal 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());
|
||||
}
|
||||
};
|
|
@ -1,10 +1,9 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { t } from 'i18next';
|
||||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||
import axios from 'axios';
|
||||
|
||||
import versionCompare from '../helpers/versionCompare';
|
||||
import { normalizeFilteringStatus, normalizeTextarea, sortClients } from '../helpers/helpers';
|
||||
import { normalizeTextarea, sortClients } from '../helpers/helpers';
|
||||
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
|
||||
import { getTlsStatus } from './encryption';
|
||||
import apiClient from '../api/Api';
|
||||
|
@ -21,16 +20,6 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
|||
let successMessage = '';
|
||||
try {
|
||||
switch (settingKey) {
|
||||
case SETTINGS_NAMES.filtering:
|
||||
if (status) {
|
||||
successMessage = 'disabled_filtering_toast';
|
||||
await apiClient.disableFiltering();
|
||||
} else {
|
||||
successMessage = 'enabled_filtering_toast';
|
||||
await apiClient.enableFiltering();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case SETTINGS_NAMES.safebrowsing:
|
||||
if (status) {
|
||||
successMessage = 'disabled_safe_browsing_toast';
|
||||
|
@ -77,18 +66,15 @@ export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
|
|||
export const initSettings = settingsList => async (dispatch) => {
|
||||
dispatch(initSettingsRequest());
|
||||
try {
|
||||
const filteringStatus = await apiClient.getFilteringStatus();
|
||||
const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
|
||||
const parentalStatus = await apiClient.getParentalStatus();
|
||||
const safesearchStatus = await apiClient.getSafesearchStatus();
|
||||
const {
|
||||
filtering,
|
||||
safebrowsing,
|
||||
parental,
|
||||
safesearch,
|
||||
} = settingsList;
|
||||
const newSettingsList = {
|
||||
filtering: { ...filtering, enabled: filteringStatus.enabled },
|
||||
safebrowsing: { ...safebrowsing, enabled: safebrowsingStatus.enabled },
|
||||
parental: { ...parental, enabled: parentalStatus.enabled },
|
||||
safesearch: { ...safesearch, enabled: safesearchStatus.enabled },
|
||||
|
@ -100,21 +86,6 @@ export const initSettings = settingsList => async (dispatch) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getFilteringRequest = createAction('GET_FILTERING_REQUEST');
|
||||
export const getFilteringFailure = createAction('GET_FILTERING_FAILURE');
|
||||
export const getFilteringSuccess = createAction('GET_FILTERING_SUCCESS');
|
||||
|
||||
export const getFiltering = () => async (dispatch) => {
|
||||
dispatch(getFilteringRequest());
|
||||
try {
|
||||
const filteringStatus = await apiClient.getFilteringStatus();
|
||||
dispatch(getFilteringSuccess(filteringStatus.enabled));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getFilteringFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
|
||||
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
|
||||
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
|
||||
|
@ -290,133 +261,6 @@ export const disableDns = () => async (dispatch) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setRulesRequest = createAction('SET_RULES_REQUEST');
|
||||
export const setRulesFailure = createAction('SET_RULES_FAILURE');
|
||||
export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
|
||||
|
||||
export const setRules = rules => async (dispatch) => {
|
||||
dispatch(setRulesRequest());
|
||||
try {
|
||||
const replacedLineEndings = rules
|
||||
.replace(/^\n/g, '')
|
||||
.replace(/\n\s*\n/g, '\n');
|
||||
await apiClient.setRules(replacedLineEndings);
|
||||
dispatch(addSuccessToast('updated_custom_filtering_toast'));
|
||||
dispatch(setRulesSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setRulesFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST');
|
||||
export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE');
|
||||
export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS');
|
||||
|
||||
export const getFilteringStatus = () => async (dispatch) => {
|
||||
dispatch(getFilteringStatusRequest());
|
||||
try {
|
||||
const status = await apiClient.getFilteringStatus();
|
||||
dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getFilteringStatusFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleFilterRequest = createAction('FILTER_ENABLE_REQUEST');
|
||||
export const toggleFilterFailure = createAction('FILTER_ENABLE_FAILURE');
|
||||
export const toggleFilterSuccess = createAction('FILTER_ENABLE_SUCCESS');
|
||||
|
||||
export const toggleFilterStatus = url => async (dispatch, getState) => {
|
||||
dispatch(toggleFilterRequest());
|
||||
const state = getState();
|
||||
const { filters } = state.filtering;
|
||||
const filter = filters.filter(filter => filter.url === url)[0];
|
||||
const { enabled } = filter;
|
||||
let toggleStatusMethod;
|
||||
if (enabled) {
|
||||
toggleStatusMethod = apiClient.disableFilter.bind(apiClient);
|
||||
} else {
|
||||
toggleStatusMethod = apiClient.enableFilter.bind(apiClient);
|
||||
}
|
||||
try {
|
||||
await toggleStatusMethod(url);
|
||||
dispatch(toggleFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleFilterFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST');
|
||||
export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');
|
||||
export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
|
||||
|
||||
export const refreshFilters = () => async (dispatch) => {
|
||||
dispatch(refreshFiltersRequest());
|
||||
dispatch(showLoading());
|
||||
try {
|
||||
const refreshText = await apiClient.refreshFilters();
|
||||
dispatch(refreshFiltersSuccess());
|
||||
|
||||
if (refreshText.includes('OK')) {
|
||||
if (refreshText.includes('OK 0')) {
|
||||
dispatch(addSuccessToast('all_filters_up_to_date_toast'));
|
||||
} else {
|
||||
dispatch(addSuccessToast(refreshText.replace(/OK /g, '')));
|
||||
}
|
||||
} else {
|
||||
dispatch(addErrorToast({ error: refreshText }));
|
||||
}
|
||||
|
||||
dispatch(getFilteringStatus());
|
||||
dispatch(hideLoading());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(refreshFiltersFailure());
|
||||
dispatch(hideLoading());
|
||||
}
|
||||
};
|
||||
|
||||
export const handleRulesChange = createAction('HANDLE_RULES_CHANGE');
|
||||
|
||||
export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
|
||||
export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
|
||||
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
|
||||
|
||||
export const addFilter = (url, name) => async (dispatch) => {
|
||||
dispatch(addFilterRequest());
|
||||
try {
|
||||
await apiClient.addFilter(url, name);
|
||||
dispatch(addFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(addFilterFailure());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const removeFilterRequest = createAction('ADD_FILTER_REQUEST');
|
||||
export const removeFilterFailure = createAction('ADD_FILTER_FAILURE');
|
||||
export const removeFilterSuccess = createAction('ADD_FILTER_SUCCESS');
|
||||
|
||||
export const removeFilter = url => async (dispatch) => {
|
||||
dispatch(removeFilterRequest());
|
||||
try {
|
||||
await apiClient.removeFilter(url);
|
||||
dispatch(removeFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(removeFilterFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE');
|
||||
|
||||
export const handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE');
|
||||
export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
|
||||
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
|
||||
|
|
|
@ -90,32 +90,19 @@ class Api {
|
|||
}
|
||||
|
||||
// Filtering
|
||||
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
||||
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
|
||||
FILTERING_DISABLE = { path: 'filtering/disable', method: 'POST' };
|
||||
FILTERING_INFO = { path: 'filtering_info', method: 'GET' };
|
||||
FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' };
|
||||
FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' };
|
||||
FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' };
|
||||
FILTERING_ENABLE_FILTER = { path: 'filtering/enable_url', method: 'POST' };
|
||||
FILTERING_DISABLE_FILTER = { path: 'filtering/disable_url', method: 'POST' };
|
||||
FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };
|
||||
FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' };
|
||||
FILTERING_CONFIG = { path: 'filtering_config', method: 'POST' };
|
||||
|
||||
getFilteringStatus() {
|
||||
const { path, method } = this.FILTERING_STATUS;
|
||||
const { path, method } = this.FILTERING_INFO;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
enableFiltering() {
|
||||
const { path, method } = this.FILTERING_ENABLE;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
disableFiltering() {
|
||||
const { path, method } = this.FILTERING_DISABLE;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
// TODO find out when to use force parameter
|
||||
refreshFilters() {
|
||||
const { path, method } = this.FILTERING_REFRESH;
|
||||
return this.makeRequest(path, method);
|
||||
|
@ -151,26 +138,22 @@ class Api {
|
|||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
enableFilter(url) {
|
||||
const { path, method } = this.FILTERING_ENABLE_FILTER;
|
||||
const parameter = 'url';
|
||||
const requestBody = `${parameter}=${url}`;
|
||||
const config = {
|
||||
data: requestBody,
|
||||
header: { 'Content-Type': 'text/plain' },
|
||||
setFiltersConfig(config) {
|
||||
const { path, method } = this.FILTERING_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
disableFilter(url) {
|
||||
const { path, method } = this.FILTERING_DISABLE_FILTER;
|
||||
const parameter = 'url';
|
||||
const requestBody = `${parameter}=${url}`;
|
||||
const config = {
|
||||
data: requestBody,
|
||||
header: { 'Content-Type': 'text/plain' },
|
||||
setFilterUrl(config) {
|
||||
const { path, method } = this.FILTERING_SET_URL;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// Parental
|
||||
|
|
|
@ -33,27 +33,13 @@ class Modal extends Component {
|
|||
this.setState({ ...this.state, name });
|
||||
};
|
||||
|
||||
handleNext = () => {
|
||||
this.props.addFilter(this.state.url, this.state.name);
|
||||
setTimeout(() => {
|
||||
if (this.props.isFilterAdded) {
|
||||
this.closeModal();
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.props.toggleModal();
|
||||
this.setState({ ...this.state, ...initialState });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
title,
|
||||
inputDescription,
|
||||
processingAddFilter,
|
||||
} = this.props;
|
||||
const { isOpen, processingAddFilter } = this.props;
|
||||
const { isUrlValid, url, name } = this.state;
|
||||
const inputUrlClass = classnames({
|
||||
'form-control mb-2': true,
|
||||
|
@ -64,28 +50,7 @@ class Modal extends Component {
|
|||
'form-control mb-2': true,
|
||||
'is-valid': name.length > 0,
|
||||
});
|
||||
|
||||
const renderBody = () => {
|
||||
if (!this.props.isFilterAdded) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<input type="text" className={inputNameClass} placeholder={this.props.t('enter_name_hint')} onChange={this.handleNameChange} />
|
||||
<input type="text" className={inputUrlClass} placeholder={this.props.t('enter_url_hint')} onChange={this.handleUrlChange} />
|
||||
{inputDescription &&
|
||||
<div className="description">
|
||||
{inputDescription}
|
||||
</div>}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="description">
|
||||
<Trans>filter_added_successfully</Trans>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isValidForSubmit = !(url.length > 0 && isUrlValid && name.length > 0);
|
||||
const isValidForSubmit = url.length > 0 && isUrlValid && name.length > 0;
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
|
@ -96,35 +61,47 @@ class Modal extends Component {
|
|||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">
|
||||
{title}
|
||||
</h4>
|
||||
<button type="button" className="close" onClick={this.closeModal}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 className="modal-title">
|
||||
<Trans>new_filter_btn</Trans>
|
||||
</h4>
|
||||
<button type="button" className="close" onClick={this.closeModal}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{renderBody()}
|
||||
</div>
|
||||
{!this.props.isFilterAdded &&
|
||||
<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.handleNext}
|
||||
disabled={isValidForSubmit || processingAddFilter}
|
||||
>
|
||||
<Trans>add_filter_btn</Trans>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
className={inputNameClass}
|
||||
placeholder={this.props.t('enter_name_hint')}
|
||||
onChange={this.handleNameChange}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className={inputUrlClass}
|
||||
placeholder={this.props.t('enter_url_hint')}
|
||||
onChange={this.handleUrlChange}
|
||||
/>
|
||||
<div className="description">
|
||||
<Trans>enter_valid_filter_url</Trans>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<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>
|
||||
</ReactModal>
|
||||
);
|
||||
|
@ -134,12 +111,10 @@ class Modal extends Component {
|
|||
Modal.propTypes = {
|
||||
toggleModal: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
inputDescription: PropTypes.string,
|
||||
addFilter: PropTypes.func.isRequired,
|
||||
isFilterAdded: PropTypes.bool,
|
||||
processingAddFilter: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
isFilterAdded: PropTypes.bool.isRequired,
|
||||
processingAddFilter: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Modal);
|
||||
|
|
|
@ -15,13 +15,13 @@ class UserRules extends Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { t, userRules } = this.props;
|
||||
return (
|
||||
<Card title={t('custom_filter_rules')} subtitle={t('custom_filter_rules_hint')}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<textarea
|
||||
className="form-control form-control--textarea-large"
|
||||
value={this.props.userRules}
|
||||
value={userRules}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<div className="card-actions">
|
||||
|
@ -79,10 +79,10 @@ class UserRules extends Component {
|
|||
}
|
||||
|
||||
UserRules.propTypes = {
|
||||
userRules: PropTypes.string,
|
||||
handleRulesChange: PropTypes.func,
|
||||
handleRulesSubmit: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
userRules: PropTypes.string.isRequired,
|
||||
handleRulesChange: PropTypes.func.isRequired,
|
||||
handleRulesSubmit: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(UserRules);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import Modal from './Modal';
|
||||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
import CellWrap from '../ui/CellWrap';
|
||||
import UserRules from './UserRules';
|
||||
import Modal from './Modal';
|
||||
|
||||
class Filters extends Component {
|
||||
componentDidMount() {
|
||||
|
@ -20,14 +22,19 @@ class Filters extends Component {
|
|||
this.props.setRules(this.props.filtering.userRules);
|
||||
};
|
||||
|
||||
renderCheckbox = (row) => {
|
||||
const { url } = row.original;
|
||||
const { filters } = this.props.filtering;
|
||||
const filter = filters.filter(filter => filter.url === url)[0];
|
||||
renderCheckbox = ({ original }) => {
|
||||
const { processingConfigFilter } = this.props.filtering;
|
||||
const { url, enabled } = original;
|
||||
return (
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" className="checkbox__input" onChange={() => this.props.toggleFilterStatus(filter.url)} checked={filter.enabled}/>
|
||||
<span className="checkbox__label"/>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox__input"
|
||||
onChange={() => this.props.toggleFilterStatus(url, enabled)}
|
||||
checked={enabled}
|
||||
disabled={processingConfigFilter}
|
||||
/>
|
||||
<span className="checkbox__label" />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
@ -37,92 +44,131 @@ class Filters extends Component {
|
|||
if (window.confirm(this.props.t('filter_confirm_delete'))) {
|
||||
this.props.removeFilter({ url });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
columns = [{
|
||||
Header: <Trans>enabled_table_header</Trans>,
|
||||
accessor: 'enabled',
|
||||
Cell: this.renderCheckbox,
|
||||
width: 90,
|
||||
className: 'text-center',
|
||||
}, {
|
||||
Header: <Trans>name_table_header</Trans>,
|
||||
accessor: 'name',
|
||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
|
||||
}, {
|
||||
Header: <Trans>filter_url_table_header</Trans>,
|
||||
accessor: 'url',
|
||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><a href={value} target='_blank' rel='noopener noreferrer' className="link logs__text">{value}</a></div>),
|
||||
}, {
|
||||
Header: <Trans>rules_count_table_header</Trans>,
|
||||
accessor: 'rulesCount',
|
||||
className: 'text-center',
|
||||
Cell: props => props.value.toLocaleString(),
|
||||
}, {
|
||||
Header: <Trans>last_time_updated_table_header</Trans>,
|
||||
accessor: 'lastUpdated',
|
||||
className: 'text-center',
|
||||
}, {
|
||||
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,
|
||||
},
|
||||
columns = [
|
||||
{
|
||||
Header: <Trans>enabled_table_header</Trans>,
|
||||
accessor: 'enabled',
|
||||
Cell: this.renderCheckbox,
|
||||
width: 90,
|
||||
className: 'text-center',
|
||||
},
|
||||
{
|
||||
Header: <Trans>name_table_header</Trans>,
|
||||
accessor: 'name',
|
||||
minWidth: 200,
|
||||
Cell: CellWrap,
|
||||
},
|
||||
{
|
||||
Header: <Trans>filter_url_table_header</Trans>,
|
||||
accessor: 'url',
|
||||
minWidth: 200,
|
||||
Cell: ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<a
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link logs__text"
|
||||
>
|
||||
{value}
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: <Trans>rules_count_table_header</Trans>,
|
||||
accessor: 'rulesCount',
|
||||
className: 'text-center',
|
||||
minWidth: 100,
|
||||
Cell: props => props.value.toLocaleString(),
|
||||
},
|
||||
{
|
||||
Header: <Trans>last_time_updated_table_header</Trans>,
|
||||
accessor: 'lastUpdated',
|
||||
className: 'text-center',
|
||||
minWidth: 150,
|
||||
Cell: CellWrap,
|
||||
},
|
||||
{
|
||||
Header: <Trans>actions_table_header</Trans>,
|
||||
accessor: 'url',
|
||||
Cell: ({ value }) => (
|
||||
<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() {
|
||||
const { t } = this.props;
|
||||
const { filters, userRules, processingRefreshFilters } = this.props.filtering;
|
||||
const {
|
||||
filtering, t, toggleFilteringModal, refreshFilters, addFilter,
|
||||
} = this.props;
|
||||
const {
|
||||
filters,
|
||||
userRules,
|
||||
isModalOpen,
|
||||
isFilterAdded,
|
||||
processingRefreshFilters,
|
||||
processingRemoveFilter,
|
||||
processingAddFilter,
|
||||
processingFilters,
|
||||
} = filtering;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title={ t('filters') } />
|
||||
<Fragment>
|
||||
<PageTitle title={t('filters')} />
|
||||
<div className="content">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<Card
|
||||
title={ t('filters_and_hosts') }
|
||||
subtitle={ t('filters_and_hosts_hint') }
|
||||
title={t('filters_and_hosts')}
|
||||
subtitle={t('filters_and_hosts_hint')}
|
||||
>
|
||||
<ReactTable
|
||||
data={filters}
|
||||
columns={this.columns}
|
||||
showPagination={true}
|
||||
defaultPageSize={10}
|
||||
loading={
|
||||
processingFilters ||
|
||||
processingAddFilter ||
|
||||
processingRemoveFilter ||
|
||||
processingRefreshFilters
|
||||
}
|
||||
minRows={4}
|
||||
// Text
|
||||
previousText={ t('previous_btn') }
|
||||
nextText={ t('next_btn') }
|
||||
loadingText={ t('loading_table_status') }
|
||||
pageText={ t('page_table_footer_text') }
|
||||
ofText={ t('of_table_footer_text') }
|
||||
rowsText={ t('rows_table_footer_text') }
|
||||
noDataText={ t('no_filters_added') }
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
ofText={t('of_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={t('no_filters_added')}
|
||||
/>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-success btn-standard mr-2"
|
||||
type="submit"
|
||||
onClick={this.props.toggleFilteringModal}
|
||||
onClick={toggleFilteringModal}
|
||||
>
|
||||
<Trans>add_filter_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-standard"
|
||||
type="submit"
|
||||
onClick={this.props.refreshFilters}
|
||||
onClick={refreshFilters}
|
||||
disabled={processingRefreshFilters}
|
||||
>
|
||||
<Trans>check_updates_btn</Trans>
|
||||
|
@ -140,15 +186,13 @@ class Filters extends Component {
|
|||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={this.props.filtering.isFilteringModalOpen}
|
||||
toggleModal={this.props.toggleFilteringModal}
|
||||
addFilter={this.props.addFilter}
|
||||
isFilterAdded={this.props.filtering.isFilterAdded}
|
||||
processingAddFilter={this.props.filtering.processingAddFilter}
|
||||
title={ t('new_filter_btn') }
|
||||
inputDescription={ t('enter_valid_filter_url') }
|
||||
isOpen={isModalOpen}
|
||||
toggleModal={toggleFilteringModal}
|
||||
addFilter={addFilter}
|
||||
isFilterAdded={isFilterAdded}
|
||||
processingAddFilter={processingAddFilter}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -157,12 +201,15 @@ Filters.propTypes = {
|
|||
setRules: PropTypes.func,
|
||||
getFilteringStatus: PropTypes.func.isRequired,
|
||||
filtering: PropTypes.shape({
|
||||
userRules: PropTypes.string,
|
||||
filters: PropTypes.array,
|
||||
isFilteringModalOpen: PropTypes.bool.isRequired,
|
||||
isFilterAdded: PropTypes.bool,
|
||||
processingAddFilter: PropTypes.bool,
|
||||
processingRefreshFilters: PropTypes.bool,
|
||||
userRules: PropTypes.string.isRequired,
|
||||
filters: PropTypes.array.isRequired,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
isFilterAdded: PropTypes.bool.isRequired,
|
||||
processingFilters: PropTypes.bool.isRequired,
|
||||
processingAddFilter: PropTypes.bool.isRequired,
|
||||
processingRefreshFilters: PropTypes.bool.isRequired,
|
||||
processingConfigFilter: PropTypes.bool.isRequired,
|
||||
processingRemoveFilter: PropTypes.bool.isRequired,
|
||||
}),
|
||||
removeFilter: PropTypes.func.isRequired,
|
||||
toggleFilterStatus: PropTypes.func.isRequired,
|
||||
|
@ -170,8 +217,7 @@ Filters.propTypes = {
|
|||
toggleFilteringModal: PropTypes.func.isRequired,
|
||||
handleRulesChange: PropTypes.func.isRequired,
|
||||
refreshFilters: PropTypes.func.isRequired,
|
||||
t: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default withNamespaces()(Filters);
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__text--full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logs__row .tooltip-custom {
|
||||
top: 0;
|
||||
margin-left: 0;
|
||||
|
|
|
@ -6,7 +6,7 @@ import endsWith from 'lodash/endsWith';
|
|||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { HashLink as Link } from 'react-router-hash-link';
|
||||
|
||||
import { formatTime, getClientName } from '../../helpers/helpers';
|
||||
import { formatTime, formatDateTime, getClientName } from '../../helpers/helpers';
|
||||
import { SERVICES, FILTERED_STATUS } from '../../helpers/constants';
|
||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
|
@ -114,7 +114,7 @@ class Logs extends Component {
|
|||
|
||||
getTimeCell = ({ value }) => (
|
||||
<div className="logs__row">
|
||||
<span className="logs__text" title={value}>
|
||||
<span className="logs__text" title={formatDateTime(value)}>
|
||||
{formatTime(value)}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -227,7 +227,7 @@ class Logs extends Component {
|
|||
{
|
||||
Header: t('time_table_header'),
|
||||
accessor: 'time',
|
||||
maxWidth: 90,
|
||||
maxWidth: 100,
|
||||
filterable: false,
|
||||
Cell: this.getTimeCell,
|
||||
},
|
||||
|
|
88
client/src/components/Settings/FiltersConfig/Form.js
Normal file
88
client/src/components/Settings/FiltersConfig/Form.js
Normal 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);
|
36
client/src/components/Settings/FiltersConfig/index.js
Normal file
36
client/src/components/Settings/FiltersConfig/index.js
Normal 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);
|
|
@ -11,6 +11,17 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form__group--inner {
|
||||
max-width: 300px;
|
||||
margin-top: -10px;
|
||||
margin-left: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form__group--checkbox {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form__inline {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
@ -109,3 +120,11 @@
|
|||
.custom-control-label:before {
|
||||
transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color;
|
||||
}
|
||||
|
||||
.custom-select:disabled {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { withNamespaces } from 'react-i18next';
|
|||
import Services from './Services';
|
||||
import StatsConfig from './StatsConfig';
|
||||
import LogsConfig from './LogsConfig';
|
||||
import FiltersConfig from './FiltersConfig';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import Loading from '../ui/Loading';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
|
@ -14,11 +15,6 @@ import './Settings.css';
|
|||
|
||||
class Settings extends Component {
|
||||
settings = {
|
||||
filtering: {
|
||||
enabled: false,
|
||||
title: 'block_domain_use_filters_and_hosts',
|
||||
subtitle: 'filters_block_toggle_hint',
|
||||
},
|
||||
safebrowsing: {
|
||||
enabled: false,
|
||||
title: 'use_adguard_browsing_sec',
|
||||
|
@ -41,17 +37,20 @@ class Settings extends Component {
|
|||
this.props.getBlockedServices();
|
||||
this.props.getStatsConfig();
|
||||
this.props.getLogsConfig();
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
renderSettings = (settings) => {
|
||||
if (Object.keys(settings).length > 0) {
|
||||
return Object.keys(settings).map((key) => {
|
||||
const settingsKeys = Object.keys(settings);
|
||||
|
||||
if (settingsKeys.length > 0) {
|
||||
return settingsKeys.map((key) => {
|
||||
const setting = settings[key];
|
||||
const { enabled } = setting;
|
||||
return (
|
||||
<Checkbox
|
||||
key={key}
|
||||
{...settings[key]}
|
||||
key={key}
|
||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||
/>
|
||||
);
|
||||
|
@ -71,6 +70,8 @@ class Settings extends Component {
|
|||
queryLogs,
|
||||
setLogsConfig,
|
||||
clearLogs,
|
||||
filtering,
|
||||
setFiltersConfig,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
|
@ -90,6 +91,12 @@ class Settings extends Component {
|
|||
<div className="col-md-12">
|
||||
<Card bodyType="card-body box-body--settings">
|
||||
<div className="form">
|
||||
<FiltersConfig
|
||||
interval={filtering.interval}
|
||||
enabled={filtering.enabled}
|
||||
processing={filtering.processingSetConfig}
|
||||
setFiltersConfig={setFiltersConfig}
|
||||
/>
|
||||
{this.renderSettings(settings.settingsList)}
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -134,6 +141,8 @@ Settings.propTypes = {
|
|||
getStatsConfig: PropTypes.func.isRequired,
|
||||
setStatsConfig: PropTypes.func.isRequired,
|
||||
resetStats: PropTypes.func.isRequired,
|
||||
setFiltersConfig: PropTypes.func.isRequired,
|
||||
getFilteringStatus: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
19
client/src/components/ui/CellWrap.js
Normal file
19
client/src/components/ui/CellWrap.js
Normal 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;
|
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
|
||||
.checkbox--settings .checkbox__label-title {
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
|||
background-position: center center;
|
||||
background-size: 12px 10px;
|
||||
border-radius: 3px;
|
||||
transition: 0.3s ease box-shadow;
|
||||
transition: 0.3s ease-in-out box-shadow, 0.3s ease-in-out opacity;
|
||||
}
|
||||
|
||||
.checkbox__label .checkbox__label-text {
|
||||
|
@ -82,10 +82,13 @@
|
|||
}
|
||||
|
||||
.checkbox__input:disabled + .checkbox__label {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.checkbox__input:disabled + .checkbox__label:before {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.checkbox__label-text {
|
||||
max-width: 515px;
|
||||
line-height: 1.5;
|
||||
|
|
|
@ -14,7 +14,7 @@ class Checkbox extends Component {
|
|||
t,
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="form__group">
|
||||
<div className="form__group form__group--checkbox">
|
||||
<label className="checkbox checkbox--settings">
|
||||
<span className="checkbox__marker"/>
|
||||
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled}/>
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import * as actionCreators from '../actions';
|
||||
import {
|
||||
setRules,
|
||||
getFilteringStatus,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
toggleFilterStatus,
|
||||
toggleFilteringModal,
|
||||
refreshFilters,
|
||||
handleRulesChange,
|
||||
} from '../actions/filtering';
|
||||
import Filters from '../components/Filters';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
|
@ -8,7 +17,18 @@ const mapStateToProps = (state) => {
|
|||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setRules,
|
||||
getFilteringStatus,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
toggleFilterStatus,
|
||||
toggleFilteringModal,
|
||||
refreshFilters,
|
||||
handleRulesChange,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
mapDispatchToProps,
|
||||
)(Filters);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { getFilteringStatus, setRules, addSuccessToast, getClients } from '../actions';
|
||||
import { addSuccessToast, getClients } from '../actions';
|
||||
import { getFilteringStatus, setRules } from '../actions/filtering';
|
||||
import { getLogs, getLogsConfig } from '../actions/queryLogs';
|
||||
import Logs from '../components/Logs';
|
||||
|
||||
|
|
|
@ -3,17 +3,19 @@ import { initSettings, toggleSetting } from '../actions';
|
|||
import { getBlockedServices, setBlockedServices } from '../actions/services';
|
||||
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
|
||||
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
|
||||
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
|
||||
import Settings from '../components/Settings';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {
|
||||
settings, services, stats, queryLogs,
|
||||
settings, services, stats, queryLogs, filtering,
|
||||
} = state;
|
||||
const props = {
|
||||
settings,
|
||||
services,
|
||||
stats,
|
||||
queryLogs,
|
||||
filtering,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
@ -29,6 +31,8 @@ const mapDispatchToProps = {
|
|||
clearLogs,
|
||||
getLogsConfig,
|
||||
setLogsConfig,
|
||||
getFilteringStatus,
|
||||
setFiltersConfig,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -265,3 +265,5 @@ export const FILTERED_STATUS = {
|
|||
export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
|
||||
|
||||
export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
|
||||
|
||||
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
|
||||
|
|
|
@ -32,28 +32,36 @@ export const renderRadioField = ({
|
|||
}) => (
|
||||
<Fragment>
|
||||
<label className="custom-control custom-radio custom-control-inline">
|
||||
<input
|
||||
{...input}
|
||||
type="radio"
|
||||
className="custom-control-input"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<input {...input} type="radio" className="custom-control-input" disabled={disabled} />
|
||||
<span className="custom-control-label">{placeholder}</span>
|
||||
</label>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
{!disabled &&
|
||||
touched &&
|
||||
(error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const renderSelectField = ({
|
||||
input, placeholder, disabled, meta: { touched, error },
|
||||
input,
|
||||
placeholder,
|
||||
subtitle,
|
||||
disabled,
|
||||
modifier = 'checkbox--form',
|
||||
meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<label className="checkbox checkbox--form">
|
||||
<label className={`checkbox ${modifier}`}>
|
||||
<span className="checkbox__marker" />
|
||||
<input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
|
||||
<span className="checkbox__label">
|
||||
<span className="checkbox__label-text checkbox__label-text--long">
|
||||
<span className="checkbox__label-title">{placeholder}</span>
|
||||
{subtitle && (
|
||||
<span
|
||||
className="checkbox__label-subtitle"
|
||||
dangerouslySetInnerHTML={{ __html: subtitle }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
@ -64,7 +72,12 @@ export const renderSelectField = ({
|
|||
);
|
||||
|
||||
export const renderServiceField = ({
|
||||
input, placeholder, disabled, modifier, icon, meta: { touched, error },
|
||||
input,
|
||||
placeholder,
|
||||
disabled,
|
||||
modifier,
|
||||
icon,
|
||||
meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<label className={`service custom-switch ${modifier}`}>
|
||||
|
@ -81,7 +94,9 @@ export const renderServiceField = ({
|
|||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
</label>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
{!disabled &&
|
||||
touched &&
|
||||
(error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import addDays from 'date-fns/add_days';
|
|||
import subDays from 'date-fns/sub_days';
|
||||
import round from 'lodash/round';
|
||||
import axios from 'axios';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import {
|
||||
STANDARD_DNS_PORT,
|
||||
|
@ -19,6 +20,21 @@ export const formatTime = (time) => {
|
|||
return dateFormat(parsedTime, 'HH:mm:ss');
|
||||
};
|
||||
|
||||
export const formatDateTime = (dateTime) => {
|
||||
const currentLanguage = i18n.languages[0] || 'en';
|
||||
const parsedTime = dateParse(dateTime);
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
return parsedTime.toLocaleString(currentLanguage, options);
|
||||
};
|
||||
|
||||
export const normalizeLogs = logs => logs.map((log) => {
|
||||
const {
|
||||
time,
|
||||
|
@ -74,18 +90,38 @@ export const normalizeTopStats = stats => (
|
|||
);
|
||||
|
||||
export const normalizeFilteringStatus = (filteringStatus) => {
|
||||
const { enabled, filters, user_rules: userRules } = filteringStatus;
|
||||
const newFilters = filters ? filters.map((filter) => {
|
||||
const {
|
||||
id, url, enabled, lastUpdated: lastUpdated = Date.now(), name = 'Default name', rulesCount: rulesCount = 0,
|
||||
} = filter;
|
||||
const {
|
||||
enabled, filters, user_rules: userRules, interval,
|
||||
} = filteringStatus;
|
||||
const newFilters = filters
|
||||
? filters.map((filter) => {
|
||||
const {
|
||||
id,
|
||||
url,
|
||||
enabled,
|
||||
last_updated,
|
||||
name = 'Default name',
|
||||
rules_count: rules_count = 0,
|
||||
} = filter;
|
||||
|
||||
return {
|
||||
id, url, enabled, lastUpdated: formatTime(lastUpdated), name, rulesCount,
|
||||
};
|
||||
}) : [];
|
||||
return {
|
||||
id,
|
||||
url,
|
||||
enabled,
|
||||
lastUpdated: last_updated ? formatDateTime(last_updated) : '–',
|
||||
name,
|
||||
rulesCount: rules_count,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
const newUserRules = Array.isArray(userRules) ? userRules.join('\n') : '';
|
||||
return { enabled, userRules: newUserRules, filters: newFilters };
|
||||
|
||||
return {
|
||||
enabled,
|
||||
userRules: newUserRules,
|
||||
filters: newFilters,
|
||||
interval,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPercent = (amount, number) => {
|
||||
|
@ -241,3 +277,5 @@ export const secondsToMilliseconds = (seconds) => {
|
|||
|
||||
return seconds;
|
||||
};
|
||||
|
||||
export const normalizeRulesTextarea = text => text && text.replace(/^\n/g, '').replace(/\n\s*\n/g, '\n');
|
||||
|
|
86
client/src/reducers/filtering.js
Normal file
86
client/src/reducers/filtering.js
Normal 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;
|
|
@ -13,6 +13,7 @@ import rewrites from './rewrites';
|
|||
import services from './services';
|
||||
import stats from './stats';
|
||||
import queryLogs from './queryLogs';
|
||||
import filtering from './filtering';
|
||||
|
||||
const settings = handleActions({
|
||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||
|
@ -130,13 +131,6 @@ const dashboard = handleActions({
|
|||
return newState;
|
||||
},
|
||||
|
||||
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
|
||||
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
|
||||
[actions.getFilteringSuccess]: (state, { payload }) => {
|
||||
const newState = { ...state, isFilteringEnabled: payload, processingFiltering: false };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.toggleProtectionRequest]: state => ({ ...state, processingProtection: true }),
|
||||
[actions.toggleProtectionFailure]: state => ({ ...state, processingProtection: false }),
|
||||
[actions.toggleProtectionSuccess]: (state) => {
|
||||
|
@ -189,62 +183,6 @@ const dashboard = handleActions({
|
|||
autoClients: [],
|
||||
});
|
||||
|
||||
const filtering = handleActions({
|
||||
[actions.setRulesRequest]: state => ({ ...state, processingRules: true }),
|
||||
[actions.setRulesFailure]: state => ({ ...state, processingRules: false }),
|
||||
[actions.setRulesSuccess]: state => ({ ...state, processingRules: false }),
|
||||
|
||||
[actions.handleRulesChange]: (state, { payload }) => {
|
||||
const { userRules } = payload;
|
||||
return { ...state, userRules };
|
||||
},
|
||||
|
||||
[actions.getFilteringStatusRequest]: state => ({ ...state, processingFilters: true }),
|
||||
[actions.getFilteringStatusFailure]: state => ({ ...state, processingFilters: false }),
|
||||
[actions.getFilteringStatusSuccess]: (state, { payload }) => {
|
||||
const { status } = payload;
|
||||
const { filters, userRules } = status;
|
||||
const newState = {
|
||||
...state, filters, userRules, processingFilters: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.addFilterRequest]: state =>
|
||||
({ ...state, processingAddFilter: true, isFilterAdded: false }),
|
||||
[actions.addFilterFailure]: (state) => {
|
||||
const newState = { ...state, processingAddFilter: false, isFilterAdded: false };
|
||||
return newState;
|
||||
},
|
||||
[actions.addFilterSuccess]: state =>
|
||||
({ ...state, processingAddFilter: false, isFilterAdded: true }),
|
||||
|
||||
[actions.toggleFilteringModal]: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
isFilteringModalOpen: !state.isFilteringModalOpen,
|
||||
isFilterAdded: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.toggleFilterRequest]: state => ({ ...state, processingFilters: true }),
|
||||
[actions.toggleFilterFailure]: state => ({ ...state, processingFilters: false }),
|
||||
[actions.toggleFilterSuccess]: state => ({ ...state, processingFilters: false }),
|
||||
|
||||
[actions.refreshFiltersRequest]: state => ({ ...state, processingRefreshFilters: true }),
|
||||
[actions.refreshFiltersFailure]: state => ({ ...state, processingRefreshFilters: false }),
|
||||
[actions.refreshFiltersSuccess]: state => ({ ...state, processingRefreshFilters: false }),
|
||||
}, {
|
||||
isFilteringModalOpen: false,
|
||||
processingFilters: false,
|
||||
processingRules: false,
|
||||
processingAddFilter: false,
|
||||
processingRefreshFilters: false,
|
||||
filters: [],
|
||||
userRules: '',
|
||||
});
|
||||
|
||||
const dhcp = handleActions({
|
||||
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
|
||||
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
|
||||
|
|
|
@ -114,9 +114,9 @@ type Dnsfilter struct {
|
|||
|
||||
// Filter represents a filter list
|
||||
type Filter struct {
|
||||
ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase
|
||||
Data []byte `json:"-" yaml:"-"` // List of rules divided by '\n'
|
||||
FilePath string `json:"-" yaml:"-"` // Path to a filtering rules file
|
||||
ID int64 // auto-assigned when filter is added (see nextFilterID)
|
||||
Data []byte `yaml:"-"` // List of rules divided by '\n'
|
||||
FilePath string `yaml:"-"` // Path to a filtering rules file
|
||||
}
|
||||
|
||||
//go:generate stringer -type=Reason
|
||||
|
|
|
@ -66,8 +66,10 @@ func NewServer(stats stats.Stats, queryLog querylog.QueryLog) *Server {
|
|||
// FilteringConfig represents the DNS filtering configuration of AdGuard Home
|
||||
// The zero FilteringConfig is empty and ready for use.
|
||||
type FilteringConfig struct {
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
|
||||
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
|
||||
|
||||
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
|
||||
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
|
||||
|
|
1
go.mod
1
go.mod
|
@ -7,7 +7,6 @@ require (
|
|||
github.com/AdguardTeam/golibs v0.2.1
|
||||
github.com/AdguardTeam/urlfilter v0.5.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
|
||||
github.com/etcd-io/bbolt v1.3.3
|
||||
github.com/go-test/deep v1.0.1
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/stats"
|
||||
|
@ -72,6 +71,7 @@ type configuration struct {
|
|||
client *http.Client
|
||||
stats stats.Stats
|
||||
queryLog querylog.QueryLog
|
||||
filteringStarted bool
|
||||
|
||||
// cached version.json to avoid hammering github.io for each page reload
|
||||
versionCheckJSON []byte
|
||||
|
@ -172,16 +172,17 @@ var config = configuration{
|
|||
Port: 53,
|
||||
StatsInterval: 1,
|
||||
FilteringConfig: dnsforward.FilteringConfig{
|
||||
ProtectionEnabled: true, // whether or not use any of dnsfilter features
|
||||
FilteringEnabled: true, // whether or not use filter lists
|
||||
BlockingMode: "nxdomain", // mode how to answer filtered requests
|
||||
BlockedResponseTTL: 10, // in seconds
|
||||
QueryLogEnabled: true,
|
||||
QueryLogInterval: 1,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
BootstrapDNS: defaultBootstrap,
|
||||
AllServers: false,
|
||||
ProtectionEnabled: true, // whether or not use any of dnsfilter features
|
||||
FilteringEnabled: true, // whether or not use filter lists
|
||||
FiltersUpdateIntervalHours: 24,
|
||||
BlockingMode: "nxdomain", // mode how to answer filtered requests
|
||||
BlockedResponseTTL: 10, // in seconds
|
||||
QueryLogEnabled: true,
|
||||
QueryLogInterval: 1,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
BootstrapDNS: defaultBootstrap,
|
||||
AllServers: false,
|
||||
},
|
||||
UpstreamDNS: defaultDNS,
|
||||
},
|
||||
|
@ -191,12 +192,6 @@ var config = configuration{
|
|||
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
|
||||
},
|
||||
},
|
||||
Filters: []filter{
|
||||
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
|
||||
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
|
||||
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
|
||||
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
|
||||
},
|
||||
DHCP: dhcpd.ServerConfig{
|
||||
LeaseDuration: 86400,
|
||||
ICMPTimeout: 1000,
|
||||
|
@ -226,6 +221,7 @@ func initConfig() {
|
|||
config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024
|
||||
config.DNS.ParentalCacheSize = 1 * 1024 * 1024
|
||||
config.DNS.CacheTime = 30
|
||||
config.Filters = defaultFilters()
|
||||
}
|
||||
|
||||
// getConfigFilename returns path to the current config file
|
||||
|
@ -276,6 +272,9 @@ func parseConfig() error {
|
|||
if !checkStatsInterval(config.DNS.StatsInterval) {
|
||||
config.DNS.StatsInterval = 1
|
||||
}
|
||||
if !checkFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) {
|
||||
config.DNS.FiltersUpdateIntervalHours = 24
|
||||
}
|
||||
|
||||
if !checkQueryLogInterval(config.DNS.QueryLogInterval) {
|
||||
config.DNS.QueryLogInterval = 1
|
||||
|
@ -308,11 +307,6 @@ func parseConfig() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Deduplicate filters
|
||||
deduplicateFilters()
|
||||
|
||||
updateUniqueFilterID(config.Filters)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
|
@ -17,8 +16,6 @@ import (
|
|||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const updatePeriod = time.Hour * 24
|
||||
|
||||
var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
|
||||
|
||||
// ----------------
|
||||
|
@ -547,15 +544,6 @@ func registerControlHandlers() {
|
|||
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
|
||||
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
||||
httpRegister(http.MethodPost, "/control/update", handleUpdate)
|
||||
httpRegister(http.MethodPost, "/control/filtering/enable", handleFilteringEnable)
|
||||
httpRegister(http.MethodPost, "/control/filtering/disable", handleFilteringDisable)
|
||||
httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/enable_url", handleFilteringEnableURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/disable_url", handleFilteringDisableURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh)
|
||||
httpRegister(http.MethodGet, "/control/filtering/status", handleFilteringStatus)
|
||||
httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules)
|
||||
httpRegister(http.MethodPost, "/control/safebrowsing/enable", handleSafeBrowsingEnable)
|
||||
httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable)
|
||||
httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus)
|
||||
|
@ -575,6 +563,7 @@ func registerControlHandlers() {
|
|||
httpRegister(http.MethodGet, "/control/access/list", handleAccessList)
|
||||
httpRegister(http.MethodPost, "/control/access/set", handleAccessSet)
|
||||
|
||||
RegisterFilteringHandlers()
|
||||
RegisterTLSHandlers()
|
||||
RegisterClientsHandlers()
|
||||
registerRewritesHandlers()
|
||||
|
|
|
@ -5,74 +5,57 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
func handleFilteringEnable(w http.ResponseWriter, r *http.Request) {
|
||||
config.DNS.FilteringEnabled = true
|
||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||
// IsValidURL - return TRUE if URL is valid
|
||||
func IsValidURL(rawurl string) bool {
|
||||
url, err := url.ParseRequestURI(rawurl)
|
||||
if err != nil {
|
||||
return false //Couldn't even parse the rawurl
|
||||
}
|
||||
if len(url.Scheme) == 0 {
|
||||
return false //No Scheme found
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleFilteringDisable(w http.ResponseWriter, r *http.Request) {
|
||||
config.DNS.FilteringEnabled = false
|
||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||
}
|
||||
|
||||
func handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]interface{}{
|
||||
"enabled": config.DNS.FilteringEnabled,
|
||||
}
|
||||
|
||||
config.RLock()
|
||||
data["filters"] = config.Filters
|
||||
data["user_rules"] = config.UserRules
|
||||
jsonVal, err := json.Marshal(data)
|
||||
config.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||
return
|
||||
}
|
||||
type filterAddJSON struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
|
||||
f := filter{}
|
||||
err := json.NewDecoder(r.Body).Decode(&f)
|
||||
fj := filterAddJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&fj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(f.URL) == 0 {
|
||||
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if valid := govalidator.IsRequestURL(f.URL); !valid {
|
||||
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
|
||||
if !IsValidURL(fj.URL) {
|
||||
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if filterExists(f.URL) {
|
||||
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL)
|
||||
if filterExists(fj.URL) {
|
||||
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Set necessary properties
|
||||
f := filter{
|
||||
Enabled: true,
|
||||
URL: fj.URL,
|
||||
Name: fj.Name,
|
||||
}
|
||||
f.ID = assignUniqueFilterID()
|
||||
f.Enabled = true
|
||||
|
||||
// Download the filter contents
|
||||
ok, err := f.update()
|
||||
|
@ -133,7 +116,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if valid := govalidator.IsRequestURL(req.URL); !valid {
|
||||
if !IsValidURL(req.URL) {
|
||||
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
@ -166,54 +149,27 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
|
|||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||
}
|
||||
|
||||
func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) {
|
||||
parameters, err := parseParametersFromBody(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
url, ok := parameters["url"]
|
||||
if !ok {
|
||||
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if valid := govalidator.IsRequestURL(url); !valid {
|
||||
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
found := filterEnable(url, true)
|
||||
if !found {
|
||||
http.Error(w, "URL parameter was not previously added", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||
type filterURLJSON struct {
|
||||
URL string `json:"url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) {
|
||||
parameters, err := parseParametersFromBody(r.Body)
|
||||
func handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
|
||||
fj := filterURLJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&fj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
|
||||
httpError(w, http.StatusBadRequest, "json decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
url, ok := parameters["url"]
|
||||
if !ok {
|
||||
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
|
||||
if !IsValidURL(fj.URL) {
|
||||
http.Error(w, "invalid URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if valid := govalidator.IsRequestURL(url); !valid {
|
||||
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
found := filterEnable(url, false)
|
||||
found := filterEnable(fj.URL, fj.Enabled)
|
||||
if !found {
|
||||
http.Error(w, "URL parameter was not previously added", http.StatusBadRequest)
|
||||
http.Error(w, "URL doesn't exist", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -235,3 +191,91 @@ func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
|
|||
updated := refreshFiltersIfNecessary(true)
|
||||
fmt.Fprintf(w, "OK %d filters updated\n", updated)
|
||||
}
|
||||
|
||||
type filterJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
RulesCount uint32 `json:"rules_count"`
|
||||
LastUpdated string `json:"last_updated"`
|
||||
}
|
||||
|
||||
type filteringConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Interval uint32 `json:"interval"` // in hours
|
||||
Filters []filterJSON `json:"filters"`
|
||||
UserRules []string `json:"user_rules"`
|
||||
}
|
||||
|
||||
// Get filtering configuration
|
||||
func handleFilteringInfo(w http.ResponseWriter, r *http.Request) {
|
||||
resp := filteringConfig{}
|
||||
config.RLock()
|
||||
resp.Enabled = config.DNS.FilteringEnabled
|
||||
resp.Interval = config.DNS.FiltersUpdateIntervalHours
|
||||
for _, f := range config.Filters {
|
||||
fj := filterJSON{
|
||||
ID: f.ID,
|
||||
Enabled: f.Enabled,
|
||||
URL: f.URL,
|
||||
Name: f.Name,
|
||||
RulesCount: uint32(f.RulesCount),
|
||||
}
|
||||
|
||||
if f.LastUpdated.Second() != 0 {
|
||||
fj.LastUpdated = f.LastUpdated.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
resp.Filters = append(resp.Filters, fj)
|
||||
}
|
||||
resp.UserRules = config.UserRules
|
||||
config.RUnlock()
|
||||
|
||||
jsonVal, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "http write: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set filtering configuration
|
||||
func handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req := filteringConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !checkFiltersUpdateIntervalHours(req.Interval) {
|
||||
httpError(w, http.StatusBadRequest, "Unsupported interval")
|
||||
return
|
||||
}
|
||||
|
||||
config.DNS.FilteringEnabled = req.Enabled
|
||||
config.DNS.FiltersUpdateIntervalHours = req.Interval
|
||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
// RegisterFilteringHandlers - register handlers
|
||||
func RegisterFilteringHandlers() {
|
||||
httpRegister(http.MethodGet, "/control/filtering_info", handleFilteringInfo)
|
||||
httpRegister(http.MethodPost, "/control/filtering_config", handleFilteringConfig)
|
||||
httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/set_url", handleFilteringSetURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh)
|
||||
httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules)
|
||||
}
|
||||
|
||||
func checkFiltersUpdateIntervalHours(i uint32) bool {
|
||||
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
|
@ -239,6 +240,9 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
|||
config.AuthName = newSettings.Username
|
||||
config.AuthPass = newSettings.Password
|
||||
|
||||
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
|
||||
initDNSServer(dnsBaseDir)
|
||||
|
||||
err = startDNSServer()
|
||||
if err != nil {
|
||||
config.firstRun = true
|
||||
|
@ -255,8 +259,6 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
go refreshFiltersIfNecessary(false)
|
||||
|
||||
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
|
||||
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
|
||||
if restartHTTP {
|
||||
|
|
|
@ -49,6 +49,7 @@ func initDNSServer(baseDir string) {
|
|||
config.dnsServer = dnsforward.NewServer(config.stats, config.queryLog)
|
||||
|
||||
initRDNS()
|
||||
initFiltering()
|
||||
}
|
||||
|
||||
func isRunning() bool {
|
||||
|
@ -165,6 +166,11 @@ func startDNSServer() error {
|
|||
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
||||
}
|
||||
|
||||
if !config.filteringStarted {
|
||||
config.filteringStarted = true
|
||||
startRefreshFilters()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -21,13 +21,35 @@ var (
|
|||
filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
|
||||
)
|
||||
|
||||
func initFiltering() {
|
||||
loadFilters()
|
||||
deduplicateFilters()
|
||||
updateUniqueFilterID(config.Filters)
|
||||
}
|
||||
|
||||
func startRefreshFilters() {
|
||||
go func() {
|
||||
_ = refreshFiltersIfNecessary(false)
|
||||
}()
|
||||
go periodicallyRefreshFilters()
|
||||
}
|
||||
|
||||
func defaultFilters() []filter {
|
||||
return []filter{
|
||||
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
|
||||
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
|
||||
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
|
||||
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
|
||||
}
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type filter struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
RulesCount int `json:"rulesCount" yaml:"-"`
|
||||
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"`
|
||||
Enabled bool
|
||||
URL string
|
||||
Name string `yaml:"name"`
|
||||
RulesCount int `yaml:"-"`
|
||||
LastUpdated time.Time `yaml:"-"`
|
||||
checksum uint32 // checksum of the file data
|
||||
|
||||
dnsfilter.Filter `yaml:",inline"`
|
||||
|
@ -119,8 +141,7 @@ func loadFilters() {
|
|||
|
||||
err := filter.load()
|
||||
if err != nil {
|
||||
// This is okay for the first start, the filter will be loaded later
|
||||
log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err)
|
||||
log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +180,12 @@ func assignUniqueFilterID() int64 {
|
|||
|
||||
// Sets up a timer that will be checking for filters updates periodically
|
||||
func periodicallyRefreshFilters() {
|
||||
for range time.Tick(time.Minute) {
|
||||
for {
|
||||
time.Sleep(1 * time.Hour)
|
||||
if config.DNS.FiltersUpdateIntervalHours == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
refreshFiltersIfNecessary(false)
|
||||
}
|
||||
}
|
||||
|
@ -180,10 +206,7 @@ func refreshFiltersIfNecessary(force bool) int {
|
|||
var updateFilters []filter
|
||||
var updateFlags []bool // 'true' if filter data has changed
|
||||
|
||||
if config.firstRun {
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
config.RLock()
|
||||
for i := range config.Filters {
|
||||
f := &config.Filters[i] // otherwise we will be operating on a copy
|
||||
|
@ -192,7 +215,8 @@ func refreshFiltersIfNecessary(force bool) int {
|
|||
continue
|
||||
}
|
||||
|
||||
if !force && time.Since(f.LastUpdated) <= updatePeriod {
|
||||
expireTime := f.LastUpdated.Unix() + int64(config.DNS.FiltersUpdateIntervalHours)*60*60
|
||||
if !force && expireTime > now.Unix() {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -214,7 +238,7 @@ func refreshFiltersIfNecessary(force bool) int {
|
|||
log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
|
||||
continue
|
||||
}
|
||||
uf.LastUpdated = time.Now()
|
||||
uf.LastUpdated = now
|
||||
if updated {
|
||||
updateCount++
|
||||
}
|
||||
|
|
37
home/filter_test.go
Normal file
37
home/filter_test.go
Normal 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())
|
||||
}
|
18
home/home.go
18
home/home.go
|
@ -135,22 +135,17 @@ func run(args options) {
|
|||
config.BindPort = args.bindPort
|
||||
}
|
||||
|
||||
loadFilters()
|
||||
|
||||
if !config.firstRun {
|
||||
// Save the updated config
|
||||
err := config.write()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Init the DNS server instance before registering HTTP handlers
|
||||
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
|
||||
initDNSServer(dnsBaseDir)
|
||||
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
|
||||
initDNSServer(dnsBaseDir)
|
||||
|
||||
if !config.firstRun {
|
||||
err := startDNSServer()
|
||||
err = startDNSServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -165,13 +160,6 @@ func run(args options) {
|
|||
config.pidFileName = args.pidFile
|
||||
}
|
||||
|
||||
// Update filters we've just loaded right away, don't wait for periodic update timer
|
||||
go func() {
|
||||
refreshFiltersIfNecessary(false)
|
||||
}()
|
||||
// Schedule automatic filters updates
|
||||
go periodicallyRefreshFilters()
|
||||
|
||||
// Initialize and run the admin Web interface
|
||||
box := packr.NewBox("../build/static")
|
||||
|
||||
|
|
|
@ -438,34 +438,30 @@ paths:
|
|||
# Filtering status methods
|
||||
# --------------------------------------------------
|
||||
|
||||
/filtering/status:
|
||||
/filtering_info:
|
||||
get:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringStatus
|
||||
summary: 'Get status of rules-based filter'
|
||||
operationId: filteringInfo
|
||||
summary: 'Get filtering parameters'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
schema:
|
||||
$ref: "#/definitions/FilteringStatus"
|
||||
$ref: "#/definitions/FilterInfo"
|
||||
|
||||
/filtering/enable:
|
||||
/filtering_config:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringEnable
|
||||
summary: 'Enable filtering'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
/filtering/disable:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringDisable
|
||||
summary: 'Disable filtering'
|
||||
operationId: filteringConfig
|
||||
summary: 'Set filtering parameters'
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/FilterConfig"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
@ -506,42 +502,19 @@ paths:
|
|||
200:
|
||||
description: OK
|
||||
|
||||
/filtering/enable_url:
|
||||
/filtering/set_url:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringEnableURL
|
||||
summary: 'Enable filter URL'
|
||||
operationId: filteringSetURL
|
||||
summary: 'Set URL parameters'
|
||||
consumes:
|
||||
- text/plain
|
||||
- application/json
|
||||
parameters:
|
||||
- in: body
|
||||
name: url
|
||||
description: 'Previously added URL containing filtering rules'
|
||||
required: true
|
||||
- in: "body"
|
||||
name: "body"
|
||||
schema:
|
||||
type: string
|
||||
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
/filtering/disable_url:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringDisableURL
|
||||
summary: 'Disable filter URL'
|
||||
consumes:
|
||||
- text/plain
|
||||
parameters:
|
||||
- in: body
|
||||
name: url
|
||||
description: 'Previously added URL containing filtering rules'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
|
||||
$ref: "#/definitions/FilterSetUrl"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
@ -1051,7 +1024,7 @@ definitions:
|
|||
lastUpdated:
|
||||
type: "string"
|
||||
format: "date-time"
|
||||
example: "2018-10-30T12:18:57.223101822+03:00"
|
||||
example: "2018-10-30T12:18:57+03:00"
|
||||
name:
|
||||
type: "string"
|
||||
example: "AdGuard Simplified Domain Names filter"
|
||||
|
@ -1061,16 +1034,15 @@ definitions:
|
|||
url:
|
||||
type: "string"
|
||||
example: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"
|
||||
FilteringStatus:
|
||||
|
||||
FilterInfo:
|
||||
type: "object"
|
||||
description: "Filtering settings"
|
||||
required:
|
||||
- "enabled"
|
||||
- "filters"
|
||||
- "user_rules"
|
||||
properties:
|
||||
enabled:
|
||||
type: "boolean"
|
||||
interval:
|
||||
type: "integer"
|
||||
filters:
|
||||
type: "array"
|
||||
items:
|
||||
|
@ -1079,9 +1051,25 @@ definitions:
|
|||
type: "array"
|
||||
items:
|
||||
type: "string"
|
||||
example:
|
||||
- '||example.org^'
|
||||
- '||example.com^'
|
||||
|
||||
FilterConfig:
|
||||
type: "object"
|
||||
description: "Filtering settings"
|
||||
properties:
|
||||
enabled:
|
||||
type: "boolean"
|
||||
interval:
|
||||
type: "integer"
|
||||
|
||||
FilterSetUrl:
|
||||
type: "object"
|
||||
description: "Filtering URL settings"
|
||||
properties:
|
||||
url:
|
||||
type: "string"
|
||||
enabled:
|
||||
type: "boolean"
|
||||
|
||||
GetVersionRequest:
|
||||
type: "object"
|
||||
description: "/version.json request data"
|
||||
|
|
|
@ -35,7 +35,7 @@ type queryLog struct {
|
|||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// newQueryLog creates a new instance of the query log
|
||||
// create a new instance of the query log
|
||||
func newQueryLog(conf Config) *queryLog {
|
||||
l := queryLog{}
|
||||
l.logFile = filepath.Join(conf.BaseDir, queryLogFileName)
|
||||
|
@ -53,7 +53,6 @@ func (l *queryLog) Configure(conf Config) {
|
|||
l.conf = conf
|
||||
}
|
||||
|
||||
// Clear memory buffer and remove the file
|
||||
func (l *queryLog) Clear() {
|
||||
l.fileFlushLock.Lock()
|
||||
defer l.fileFlushLock.Unlock()
|
||||
|
@ -164,7 +163,6 @@ func (l *queryLog) Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Res
|
|||
}
|
||||
}
|
||||
|
||||
// getQueryLogJson returns a map with the current query log ready to be converted to a JSON
|
||||
func (l *queryLog) GetData() []map[string]interface{} {
|
||||
l.lock.RLock()
|
||||
values := make([]*logEntry, len(l.cache))
|
||||
|
|
|
@ -10,14 +10,20 @@ import (
|
|||
|
||||
// QueryLog - main interface
|
||||
type QueryLog interface {
|
||||
// Close query log object
|
||||
Close()
|
||||
|
||||
// Set new configuration at runtime
|
||||
// Currently only 'Interval' field is supported.
|
||||
Configure(conf Config)
|
||||
|
||||
// Add a log entry
|
||||
Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Result, elapsed time.Duration, addr net.Addr, upstream string)
|
||||
|
||||
// Get log entries
|
||||
GetData() []map[string]interface{}
|
||||
|
||||
// Clear memory buffer and remove log files
|
||||
Clear()
|
||||
}
|
||||
|
||||
|
@ -27,7 +33,7 @@ type Config struct {
|
|||
Interval uint32 // interval to rotate logs (in hours)
|
||||
}
|
||||
|
||||
// New - create instance
|
||||
// New - create a new instance of the query log
|
||||
func New(conf Config) QueryLog {
|
||||
return newQueryLog(conf)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue