Merge: + DNS: Ability to check from UI if a host name is filtered

Close #856

* commit '8ec7c37715e410c5564c512162be03383b577e39':
  + client: handle check host
  + GET /filtering/check_host: Check if host name is filtered
This commit is contained in:
Simon Zolin 2020-01-30 12:16:17 +03:00
commit e27cbdf81b
15 changed files with 449 additions and 12 deletions

View file

@ -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.

View file

@ -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</0>",
"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"
}

View file

@ -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());
}
};

View file

@ -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' };

View file

@ -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 (
<Fragment>
<div>
{t('host_whitelisted')}
</div>
<div>
{filterName}
</div>
</Fragment>
);
}
return (
<Fragment>
<div>
{t('check_reason', { reason })}
</div>
<div>
{filterName}
</div>
</Fragment>
);
};
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 (
<div className={`card mb-0 p-3 ${color}`}>
<div>
<strong>{hostname}</strong>
</div>
<div>{title}</div>
{rule && (
<div>{t('check_rule', { rule })}</div>
)}
{service_name && (
<div>{t('check_service', { service: service_name })}</div>
)}
{cname && (
<div>{t('check_cname', { cname })}</div>
)}
{ip_addrs && (
<div>
{t('check_ip', { ip: ip_addrs.join(', ') })}
</div>
)}
</div>
);
};
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);

View file

@ -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 (
<Card
title={t('check_title')}
subtitle={t('check_desc')}
>
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-group">
<Field
id="name"
name="name"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_enter_host')}
/>
<span className="input-group-append">
<button
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={this.handleSubmit}
disabled={pristine || invalid || processing}
>
<Trans>check</Trans>
</button>
</span>
</div>
{check.hostname && (
<Fragment>
<hr/>
<Info
filters={filters}
hostname={hostname}
reason={reason}
filter_id={filter_id}
rule={rule}
service_name={service_name}
cname={cname}
ip_addrs={ip_addrs}
/>
</Fragment>
)}
</div>
</div>
</form>
</Card>
);
};
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);

View file

@ -26,7 +26,7 @@ class UserRules extends Component {
/>
<div className="card-actions">
<button
className="btn btn-success btn-standard"
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={this.handleSubmit}
>

View file

@ -8,8 +8,9 @@ import Card from '../ui/Card';
import CellWrap from '../ui/CellWrap';
import UserRules from './UserRules';
import Modal from './Modal';
import { formatDetailedDateTime } from '../../helpers/helpers';
import Check from './Check';
import { formatDetailedDateTime } from '../../helpers/helpers';
import { MODAL_TYPE } from '../../helpers/constants';
class Filters extends Component {
@ -76,6 +77,10 @@ class Filters extends Component {
return { name: '', url: '' };
};
handleCheck = (values) => {
this.props.checkHost(values);
}
columns = [
{
Header: <Trans>enabled_table_header</Trans>,
@ -180,6 +185,8 @@ class Filters extends Component {
processingFilters,
modalType,
modalFilterUrl,
processingCheck,
check,
} = filtering;
const currentFilterData = this.getFilter(modalFilterUrl, filters);
@ -216,7 +223,7 @@ class Filters extends Component {
/>
<div className="card-actions">
<button
className="btn btn-success btn-standard mr-2"
className="btn btn-success btn-standard mr-2 btn-large"
type="submit"
onClick={() =>
toggleFilteringModal({ type: MODAL_TYPE.ADD })
@ -242,6 +249,14 @@ class Filters extends Component {
handleRulesSubmit={this.handleRulesSubmit}
/>
</div>
<div className="col-md-12">
<Check
filters={filters}
check={check}
onSubmit={this.handleCheck}
processing={processingCheck}
/>
</div>
</div>
</div>
<Modal
@ -274,6 +289,7 @@ Filters.propTypes = {
processingConfigFilter: PropTypes.bool.isRequired,
processingRemoveFilter: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
processingCheck: PropTypes.bool.isRequired,
}),
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,
@ -282,6 +298,7 @@ Filters.propTypes = {
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
editFilter: PropTypes.func.isRequired,
checkHost: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};

View file

@ -112,3 +112,15 @@
font-size: 14px;
}
}
.card .red {
background-color: #fff4f2;
}
.card .green {
background-color: #f1faf3;
}
.card .blue {
background-color: #ecf7ff;
}

View file

@ -9,6 +9,7 @@ import {
refreshFilters,
handleRulesChange,
editFilter,
checkHost,
} from '../actions/filtering';
import Filters from '../components/Filters';
@ -28,6 +29,7 @@ const mapDispatchToProps = {
refreshFilters,
handleRulesChange,
editFilter,
checkHost,
};
export default connect(

View file

@ -349,10 +349,14 @@ export const ENCRYPTION_SOURCE = {
export const FILTERED_STATUS = {
FILTERED_BLACK_LIST: 'FilteredBlackList',
NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList',
NOT_FILTERED_NOT_FOUND: 'NotFilteredNotFound',
FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',
REWRITE: 'Rewrite',
};
export const FILTERED = 'Filtered';
export const NOT_FILTERED = 'NotFiltered';
export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];

View file

@ -22,6 +22,8 @@ import {
DEFAULT_DATE_FORMAT_OPTIONS,
DETAILED_DATE_FORMAT_OPTIONS,
DEFAULT_LANGUAGE,
FILTERED_STATUS,
FILTERED,
} from './constants';
/**
@ -418,3 +420,9 @@ export const createOnBlurHandler = (event, input, normalizeOnBlur) => (
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;

View file

@ -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: {},
},
);

View file

@ -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 {

View file

@ -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"