diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 7d223c5b..5d812e6b 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -56,6 +56,7 @@ Contents: * API: Get filtering parameters * API: Set filtering parameters * API: Set URL parameters + * API: Domain Check * Log-in page * API: Log in * API: Log out @@ -1355,6 +1356,30 @@ Response: 200 OK +### API: Domain Check + +Check if host name is filtered. + +Request: + + GET /control/filtering/check_host?name=hostname + +Response: + + 200 OK + + { + "reason":"FilteredBlackList", + "filter_id":1, + "rule":"||doubleclick.net^", + "service_name": "...", // set if reason=FilteredBlockedService + + // if reason=ReasonRewrite: + "cname": "...", + "ip_addrs": ["1.2.3.4", ...], + } + + ## Log-in page After user completes the steps of installation wizard, he must log in into dashboard using his name and password. After user successfully logs in, he gets the Cookie which allows the server to authenticate him next time without password. After the Cookie is expired, user needs to perform log-in operation again. diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index b594c20a..9e11009a 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -446,5 +446,17 @@ "autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuardHome by default.", "tags_title": "Tags", "tags_desc": "You can select the tags that correspond to the client. Tags can be included in the filtering rules and allow you to apply them more accurately. <0>Learn more", - "form_select_tags": "Select client tags" + "form_select_tags": "Select client tags", + "check_title": "Check the filtering", + "check_desc": "Check if the host name is filtered", + "check": "Check", + "form_enter_host": "Enter a host name", + "filtered_custom_rules": "Filtered by Custom filtering rules", + "host_whitelisted": "The host is whitelisted", + "check_ip": "IP addresses: {{ip}}", + "check_cname": "CNAME: {{cname}}", + "check_reason": "Reason: {{reason}}", + "check_rule": "Rule: {{rule}}", + "check_service": "Service name: {{service}}", + "check_not_found": "Doesn't exist in any filter" } diff --git a/client/src/actions/filtering.js b/client/src/actions/filtering.js index 117c22ee..ee8d5c03 100644 --- a/client/src/actions/filtering.js +++ b/client/src/actions/filtering.js @@ -161,3 +161,23 @@ export const setFiltersConfig = config => async (dispatch, getState) => { dispatch(setFiltersConfigFailure()); } }; + +export const checkHostRequest = createAction('CHECK_HOST_REQUEST'); +export const checkHostFailure = createAction('CHECK_HOST_FAILURE'); +export const checkHostSuccess = createAction('CHECK_HOST_SUCCESS'); + +export const checkHost = host => async (dispatch) => { + dispatch(checkHostRequest()); + try { + const data = await apiClient.checkHost(host); + const [hostname] = Object.values(host); + + dispatch(checkHostSuccess({ + hostname, + ...data, + })); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(checkHostFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 3d215144..97d2cc6f 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -82,6 +82,7 @@ class Api { FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' }; FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' }; FILTERING_CONFIG = { path: 'filtering/config', method: 'POST' }; + FILTERING_CHECK_HOST = { path: 'filtering/check_host', method: 'GET' }; getFilteringStatus() { const { path, method } = this.FILTERING_STATUS; @@ -141,6 +142,12 @@ class Api { return this.makeRequest(path, method, parameters); } + checkHost(params) { + const { path, method } = this.FILTERING_CHECK_HOST; + const url = getPathWithQueryString(path, params); + return this.makeRequest(url, method); + } + // Parental PARENTAL_STATUS = { path: 'parental/status', method: 'GET' }; PARENTAL_ENABLE = { path: 'parental/enable', method: 'POST' }; diff --git a/client/src/components/Filters/Check/Info.js b/client/src/components/Filters/Check/Info.js new file mode 100644 index 00000000..d3cd0136 --- /dev/null +++ b/client/src/components/Filters/Check/Info.js @@ -0,0 +1,127 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withNamespaces } from 'react-i18next'; + +import { checkFiltered, checkRewrite, checkBlackList, checkNotFilteredNotFound, checkWhiteList } from '../../../helpers/helpers'; + +const getFilterName = (id, filters, t) => { + if (id === 0) { + return t('filtered_custom_rules'); + } + + const filter = filters.find(filter => filter.id === id); + + if (filter && filter.name) { + return t('query_log_filtered', { filter: filter.name }); + } + + return ''; +}; + +const getTitle = (reason, filterName, t) => { + if (checkNotFilteredNotFound(reason)) { + return t('check_not_found'); + } + + if (checkRewrite(reason)) { + return t('rewrite_applied'); + } + + if (checkBlackList(reason)) { + return filterName; + } + + if (checkWhiteList(reason)) { + return ( + +
+ {t('host_whitelisted')} +
+
+ {filterName} +
+
+ ); + } + + return ( + +
+ {t('check_reason', { reason })} +
+
+ {filterName} +
+
+ ); +}; + +const getColor = (reason) => { + if (checkFiltered(reason)) { + return 'red'; + } else if (checkRewrite(reason)) { + return 'blue'; + } else if (checkWhiteList(reason)) { + return 'green'; + } + + return ''; +}; + +const Info = ({ + filters, + hostname, + reason, + filter_id, + rule, + service_name, + cname, + ip_addrs, + t, +}) => { + const filterName = getFilterName(filter_id, filters, t); + const title = getTitle(reason, filterName, t); + const color = getColor(reason); + + return ( +
+
+ {hostname} +
+ +
{title}
+ + {rule && ( +
{t('check_rule', { rule })}
+ )} + + {service_name && ( +
{t('check_service', { service: service_name })}
+ )} + + {cname && ( +
{t('check_cname', { cname })}
+ )} + + {ip_addrs && ( +
+ {t('check_ip', { ip: ip_addrs.join(', ') })} +
+ )} +
+ ); +}; + +Info.propTypes = { + filters: PropTypes.array.isRequired, + hostname: PropTypes.string.isRequired, + reason: PropTypes.string.isRequired, + filter_id: PropTypes.number, + rule: PropTypes.string, + service_name: PropTypes.string, + cname: PropTypes.string, + ip_addrs: PropTypes.array, + t: PropTypes.func.isRequired, +}; + +export default withNamespaces()(Info); diff --git a/client/src/components/Filters/Check/index.js b/client/src/components/Filters/Check/index.js new file mode 100644 index 00000000..13011157 --- /dev/null +++ b/client/src/components/Filters/Check/index.js @@ -0,0 +1,95 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Trans, withNamespaces } from 'react-i18next'; +import { Field, reduxForm } from 'redux-form'; +import flow from 'lodash/flow'; +import Card from '../../ui/Card'; + +import { renderInputField } from '../../../helpers/form'; +import Info from './Info'; + +const Check = (props) => { + const { + t, + handleSubmit, + pristine, + invalid, + processing, + check, + filters, + } = props; + + const { + hostname, + reason, + filter_id, + rule, + service_name, + cname, + ip_addrs, + } = check; + + return ( + +
+
+
+
+ + + + +
+ {check.hostname && ( + +
+ +
+ )} +
+
+
+
+ ); +}; + +Check.propTypes = { + t: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + invalid: PropTypes.bool.isRequired, + processing: PropTypes.bool.isRequired, + check: PropTypes.object.isRequired, + filters: PropTypes.array.isRequired, +}; + +export default flow([ + withNamespaces(), + reduxForm({ form: 'domainCheckForm' }), +])(Check); diff --git a/client/src/components/Filters/UserRules.js b/client/src/components/Filters/UserRules.js index a2f10507..312d175c 100644 --- a/client/src/components/Filters/UserRules.js +++ b/client/src/components/Filters/UserRules.js @@ -26,7 +26,7 @@ class UserRules extends Component { />
+
+ +
( normalizeOnBlur ? input.onBlur(normalizeOnBlur(event.target.value)) : input.onBlur()); + +export const checkFiltered = reason => reason.indexOf(FILTERED) === 0; +export const checkRewrite = reason => reason === FILTERED_STATUS.REWRITE; +export const checkBlackList = reason => reason === FILTERED_STATUS.FILTERED_BLACK_LIST; +export const checkWhiteList = reason => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST; +export const checkNotFilteredNotFound = reason => reason === FILTERED_STATUS.NOT_FILTERED_NOT_FOUND; diff --git a/client/src/reducers/filtering.js b/client/src/reducers/filtering.js index f3f3d894..da298426 100644 --- a/client/src/reducers/filtering.js +++ b/client/src/reducers/filtering.js @@ -79,6 +79,14 @@ const filtering = handleActions( ...payload, processingSetConfig: false, }), + + [actions.checkHostRequest]: state => ({ ...state, processingCheck: true }), + [actions.checkHostFailure]: state => ({ ...state, processingCheck: false }), + [actions.checkHostSuccess]: (state, { payload }) => ({ + ...state, + check: payload, + processingCheck: false, + }), }, { isModalOpen: false, @@ -89,6 +97,7 @@ const filtering = handleActions( processingConfigFilter: false, processingRemoveFilter: false, processingSetConfig: false, + processingCheck: false, isFilterAdded: false, filters: [], userRules: '', @@ -96,6 +105,7 @@ const filtering = handleActions( enabled: true, modalType: '', modalFilterUrl: '', + check: {}, }, ); diff --git a/home/control_filtering.go b/home/control_filtering.go index 6188c875..77c6cafa 100644 --- a/home/control_filtering.go +++ b/home/control_filtering.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net" "net/http" "net/url" "os" @@ -11,6 +12,7 @@ import ( "time" "github.com/AdguardTeam/golibs/log" + "github.com/miekg/dns" ) // IsValidURL - return TRUE if URL is valid @@ -290,15 +292,58 @@ func handleFilteringConfig(w http.ResponseWriter, r *http.Request) { enableFilters(true) } +type checkHostResp struct { + Reason string `json:"reason"` + FilterID int64 `json:"filter_id"` + Rule string `json:"rule"` + + // for FilteredBlockedService: + SvcName string `json:"service_name"` + + // for ReasonRewrite: + CanonName string `json:"cname"` // CNAME value + IPList []net.IP `json:"ip_addrs"` // list of IP addresses +} + +func handleCheckHost(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + host := q.Get("name") + + setts := Context.dnsFilter.GetConfig() + setts.FilteringEnabled = true + ApplyBlockedServices(&setts, config.DNS.BlockedServices) + result, err := Context.dnsFilter.CheckHost(host, dns.TypeA, &setts) + if err != nil { + httpError(w, http.StatusInternalServerError, "couldn't apply filtering: %s: %s", host, err) + return + } + + resp := checkHostResp{} + resp.Reason = result.Reason.String() + resp.FilterID = result.FilterID + resp.Rule = result.Rule + resp.SvcName = result.ServiceName + resp.CanonName = result.CanonName + resp.IPList = result.IPList + js, err := json.Marshal(resp) + if err != nil { + httpError(w, http.StatusInternalServerError, "json encode: %s", err) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(js) +} + // RegisterFilteringHandlers - register handlers func RegisterFilteringHandlers() { - httpRegister(http.MethodGet, "/control/filtering/status", handleFilteringStatus) - 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) + httpRegister("GET", "/control/filtering/status", handleFilteringStatus) + httpRegister("POST", "/control/filtering/config", handleFilteringConfig) + httpRegister("POST", "/control/filtering/add_url", handleFilteringAddURL) + httpRegister("POST", "/control/filtering/remove_url", handleFilteringRemoveURL) + httpRegister("POST", "/control/filtering/set_url", handleFilteringSetURL) + httpRegister("POST", "/control/filtering/refresh", handleFilteringRefresh) + httpRegister("POST", "/control/filtering/set_rules", handleFilteringSetRules) + httpRegister("GET", "/control/filtering/check_host", handleCheckHost) } func checkFiltersUpdateIntervalHours(i uint32) bool { diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 2d042c62..9ad2c5d1 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -2,7 +2,7 @@ swagger: '2.0' info: title: 'AdGuard Home' description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.' - version: '0.99.3' + version: '0.101' schemes: - http basePath: /control @@ -594,6 +594,22 @@ paths: 200: description: OK + /filtering/check_host: + get: + tags: + - filtering + operationId: filteringCheckHost + summary: 'Check if host name is filtered' + parameters: + - name: name + in: query + type: string + responses: + 200: + description: OK + schema: + $ref: "#/definitions/FilterCheckHostResponse" + # -------------------------------------------------- # Safebrowsing methods # -------------------------------------------------- @@ -1178,6 +1194,42 @@ definitions: enabled: type: "boolean" + FilterCheckHostResponse: + type: "object" + description: "Check Host Result" + properties: + reason: + type: "string" + description: "DNS filter status" + enum: + - "NotFilteredNotFound" + - "NotFilteredWhiteList" + - "NotFilteredError" + - "FilteredBlackList" + - "FilteredSafeBrowsing" + - "FilteredParental" + - "FilteredInvalid" + - "FilteredSafeSearch" + - "FilteredBlockedService" + - "ReasonRewrite" + filter_id: + type: "integer" + rule: + type: "string" + example: "||example.org^" + description: "Filtering rule applied to the request (if any)" + service_name: + type: "string" + description: "Set if reason=FilteredBlockedService" + cname: + type: "string" + description: "Set if reason=ReasonRewrite" + ip_addrs: + type: "array" + items: + type: "string" + description: "Set if reason=ReasonRewrite" + GetVersionRequest: type: "object" description: "/version.json request data" @@ -1471,6 +1523,7 @@ definitions: - "FilteredInvalid" - "FilteredSafeSearch" - "FilteredBlockedService" + - "ReasonRewrite" service_name: type: "string" description: "Set if reason=FilteredBlockedService"