Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb9a2e872 | ||
|
|
cfdfd250a0 | ||
|
|
cb01d05ef4 | ||
|
|
c7e7b76cec | ||
|
|
f5128d27f1 | ||
|
|
005f8fb279 | ||
|
|
244fe093cd | ||
|
|
ff9d1c234c | ||
|
|
1c9d3acaa8 | ||
|
|
0dab36a108 | ||
|
|
611ed94884 | ||
|
|
22935c5fed | ||
|
|
4a8dcbeeed | ||
|
|
1ab650bb86 | ||
|
|
4743743b1f | ||
|
|
dd2c9d96e7 | ||
|
|
946bda37a3 | ||
|
|
ad4e85d8f5 | ||
|
|
4b9ab97271 | ||
|
|
d2bf1e176e | ||
|
|
ffeb88ac0c | ||
|
|
c71b8d3ad2 |
4
Makefile
4
Makefile
@@ -134,7 +134,9 @@ lint-go:
|
||||
golangci-lint run
|
||||
|
||||
test:
|
||||
@echo Running unit-tests
|
||||
@echo Running JS unit-tests
|
||||
npm run test --prefix client
|
||||
@echo Running Go unit-tests
|
||||
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
ci: dependencies client test
|
||||
|
||||
@@ -230,7 +230,7 @@ There are three options how you can install an unstable version:
|
||||
|
||||
There are three options how you can install an unstable version.
|
||||
|
||||
1. You can either install a beta version of AdGuard Home which we update periodically.
|
||||
1. You can either install a beta or edge version of AdGuard Home which we update periodically. If you're already using stable version of AdGuard Home, just replace the executable with a new one.
|
||||
2. You can use the Docker image from the `edge` tag, which is synced with the repo master branch.
|
||||
3. You can install AdGuard Home from `beta` or `edge` channels on the Snap Store.
|
||||
|
||||
|
||||
9
client/package-lock.json
generated
vendored
9
client/package-lock.json
generated
vendored
@@ -10168,9 +10168,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.15",
|
||||
@@ -13491,8 +13491,7 @@
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
||||
"resolved": "",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
||||
2
client/package.json
vendored
2
client/package.json
vendored
@@ -19,7 +19,7 @@
|
||||
"i18next": "^19.4.4",
|
||||
"i18next-browser-languagedetector": "^4.2.0",
|
||||
"ipaddr.js": "^1.9.1",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.19",
|
||||
"nanoid": "^3.1.9",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^6.13.1",
|
||||
|
||||
@@ -28,10 +28,6 @@ body {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.container--wrap {
|
||||
min-height: calc(100vh);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const Row = ({
|
||||
<Trans components={translationComponents}>{label}</Trans>
|
||||
<Tooltip content={tooltipTitle} placement="top"
|
||||
className="tooltip-container tooltip-custom--narrow text-center">
|
||||
<svg className="icons icon--20 icon--lightgray ml-1">
|
||||
<svg className="icons icon--20 icon--lightgray ml-2">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
|
||||
@@ -62,23 +62,13 @@ class Table extends Component {
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={t('rewrite_not_found')}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,24 +128,15 @@ class Table extends Component {
|
||||
columns={this.columns}
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
loading={loading}
|
||||
minRows={6}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={whitelist ? t('no_whitelist_added') : t('no_blocklist_added')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const getClientCell = ({
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
@@ -33,7 +34,7 @@ const getClientCell = ({
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||
'mt-2': isDetailed && !name && !whois_info,
|
||||
'mt-2': isDetailed && !name && !whoisAvailable,
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
@@ -78,12 +79,19 @@ const getClientCell = ({
|
||||
content: processedData,
|
||||
placement: 'bottom',
|
||||
})}
|
||||
<div
|
||||
className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>{formatClientCell(row, isDetailed)}</div>
|
||||
{isDetailed && name
|
||||
&& !whois_info && <div className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}>{name}</div>}
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{formatClientCell(row, isDetailed)}
|
||||
</div>
|
||||
|
||||
{isDetailed && name && !whoisAvailable && (
|
||||
<div
|
||||
className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>
|
||||
|
||||
@@ -38,59 +38,20 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
|
||||
})}</div>;
|
||||
};
|
||||
|
||||
const FILTERED_STATUS_TO_FIELDS_MAP = {
|
||||
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
response_table_header: renderResponses(response),
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_code: status,
|
||||
original_response: renderResponses(originalResponse),
|
||||
},
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_code: status,
|
||||
},
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_code: status,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
response_table_header: renderResponses(response),
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
original_response: renderResponses(originalResponse),
|
||||
},
|
||||
const COMMON_CONTENT = {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_table_header: renderResponses(response),
|
||||
original_response: renderResponses(originalResponse),
|
||||
};
|
||||
|
||||
const content = FILTERED_STATUS_TO_FIELDS_MAP[reason]
|
||||
? Object.entries(FILTERED_STATUS_TO_FIELDS_MAP[reason])
|
||||
: Object.entries(FILTERED_STATUS_TO_FIELDS_MAP.NotFilteredNotFound);
|
||||
|
||||
const content = rule
|
||||
? Object.entries(COMMON_CONTENT)
|
||||
: Object.entries({ ...COMMON_CONTENT, filter: '' });
|
||||
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
||||
|
||||
return (
|
||||
|
||||
@@ -143,10 +143,11 @@ const Form = (props) => {
|
||||
const normalizeOnBlur = (data) => data.trim();
|
||||
|
||||
return (
|
||||
<form className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
<form
|
||||
className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="field__search">
|
||||
<Field
|
||||
@@ -166,13 +167,21 @@ const Form = (props) => {
|
||||
<Field
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent', responseStatusClass)}
|
||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent', responseStatusClass)}
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
query, label, disabled,
|
||||
}) => <option key={label} value={query}
|
||||
disabled={disabled}>{t(label)}</option>)}
|
||||
}) => (
|
||||
<option
|
||||
key={label}
|
||||
value={query}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t(label)}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</Field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logs__text--wrap,
|
||||
.logs__text--whois {
|
||||
.logs__text--wrap {
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
@@ -71,6 +70,7 @@
|
||||
|
||||
.logs__text--whois {
|
||||
line-height: 1.2;
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.logs__row .tooltip-custom {
|
||||
@@ -362,7 +362,7 @@
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.-pageInfo {
|
||||
.logs__table .-pageInfo {
|
||||
--side-size: 2rem;
|
||||
font-variant-numeric: tabular-nums !important;
|
||||
background-color: transparent !important;
|
||||
@@ -376,18 +376,18 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-bottom {
|
||||
.logs__table .pagination-bottom {
|
||||
justify-content: center !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.-center:before {
|
||||
.logs__table .-center:before {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.-center:after {
|
||||
.logs__table .-center:after {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
@@ -437,12 +437,13 @@
|
||||
}
|
||||
|
||||
.custom-select__arrow--left {
|
||||
background: #fff url('./chevron-down.svg') no-repeat left 0.2rem center;
|
||||
background-size: 1.5rem;
|
||||
background: #fff url('./chevron-down.svg') no-repeat;
|
||||
background-position: 5px 9px;
|
||||
background-size: 22px;
|
||||
}
|
||||
|
||||
.custom-select--logs {
|
||||
padding: 0.5rem 0.75rem 0.5rem 1.75rem !important;
|
||||
padding: 0.5rem 0.75rem 0.5rem 2rem !important;
|
||||
}
|
||||
|
||||
.bg--danger {
|
||||
@@ -511,6 +512,8 @@
|
||||
|
||||
.field__select {
|
||||
margin-top: 1.5rem;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ const Table = (props) => {
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter: isBlocked ? filter : null,
|
||||
filter: rule ? filter : null,
|
||||
rule_label: rule,
|
||||
response_table_header: response?.join('\n'),
|
||||
response_code: status,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
BLOCK_ACTIONS,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
smallScreenSize,
|
||||
SMALL_SCREEN_SIZE,
|
||||
} from '../../helpers/constants';
|
||||
import Loading from '../ui/Loading';
|
||||
import Filters from './Filters';
|
||||
@@ -76,7 +76,7 @@ const Logs = (props) => {
|
||||
const search = filter?.search || search_url_param;
|
||||
const response_status = filter?.response_status || response_status_url_param;
|
||||
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < smallScreenSize);
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < SMALL_SCREEN_SIZE);
|
||||
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
||||
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
||||
const [isModalOpened, setModalOpened] = useState(false);
|
||||
@@ -114,7 +114,7 @@ const Logs = (props) => {
|
||||
},
|
||||
} = props;
|
||||
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${smallScreenSize}px)`);
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`);
|
||||
const mediaQueryHandler = (e) => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
|
||||
@@ -85,23 +85,13 @@ class AutoClients extends Component {
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={t('clients_not_found')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -297,23 +297,13 @@ class ClientsTable extends Component {
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={t('clients_not_found')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
.tooltip-container {
|
||||
border: 0;
|
||||
padding: 0.7rem;
|
||||
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tooltip-custom--narrow {
|
||||
max-width: 13.75rem;
|
||||
max-width: 14rem;
|
||||
}
|
||||
|
||||
.tooltip-custom--wide {
|
||||
|
||||
@@ -2,7 +2,12 @@ import React from 'react';
|
||||
import TooltipTrigger from 'react-popper-tooltip';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HIDE_TOOLTIP_DELAY } from '../../helpers/constants';
|
||||
|
||||
import {
|
||||
HIDE_TOOLTIP_DELAY,
|
||||
HIDE_TOOLTIP_CLICK_DELAY,
|
||||
MEDIUM_SCREEN_SIZE,
|
||||
} from '../../helpers/constants';
|
||||
import 'react-popper-tooltip/dist/styles.css';
|
||||
import './Tooltip.css';
|
||||
|
||||
@@ -16,27 +21,42 @@ const Tooltip = ({
|
||||
delayHide = HIDE_TOOLTIP_DELAY,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
let triggerValue = trigger;
|
||||
let delayValue = delayHide;
|
||||
|
||||
return <TooltipTrigger
|
||||
placement={placement}
|
||||
trigger={trigger}
|
||||
delayHide={delayHide}
|
||||
tooltip={({
|
||||
tooltipRef,
|
||||
getTooltipProps,
|
||||
}) => <div {...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className,
|
||||
})}>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>
|
||||
}>{({ getTriggerProps, triggerRef }) => <span
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
className: triggerClass,
|
||||
})}
|
||||
>{children}</span>}
|
||||
</TooltipTrigger>;
|
||||
if (window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`).matches) {
|
||||
triggerValue = 'click';
|
||||
delayValue = HIDE_TOOLTIP_CLICK_DELAY;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
placement={placement}
|
||||
trigger={triggerValue}
|
||||
delayHide={delayValue}
|
||||
tooltip={({ tooltipRef, getTooltipProps }) => (
|
||||
<div
|
||||
{...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className,
|
||||
})}
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{({ getTriggerProps, triggerRef }) => (
|
||||
<span
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
className: triggerClass,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
Tooltip.propTypes = {
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/;
|
||||
export const R_HOST = /^(\*\.)?([\w-]+\.)+[\w-]+$/;
|
||||
|
||||
// matches hostname or *.wildcard
|
||||
export const R_HOST = /^(\*\.)?[\w.-]+$/;
|
||||
|
||||
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/;
|
||||
|
||||
export const R_IPV6 = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
||||
|
||||
export const R_CIDR = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
|
||||
|
||||
export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]))$/;
|
||||
|
||||
export const R_CIDR_IPV6 = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))$/;
|
||||
|
||||
export const R_PATH_LAST_PART = /\/[^/]*$/;
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
export const R_UNIX_ABSOLUTE_PATH = /^(\/[^/\x00]+)+$/;
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
export const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\|\/)(?:[^\\/:*?"<>|\x00]+\\)*[^\\/:*?"<>|\x00]*$/;
|
||||
|
||||
@@ -62,6 +72,7 @@ export const CHECK_TIMEOUT = 1000;
|
||||
export const SUCCESS_TOAST_TIMEOUT = 5000;
|
||||
export const FAILURE_TOAST_TIMEOUT = 30000;
|
||||
export const HIDE_TOOLTIP_DELAY = 300;
|
||||
export const HIDE_TOOLTIP_CLICK_DELAY = 100;
|
||||
export const MODAL_OPEN_TIMEOUT = 150;
|
||||
|
||||
export const UNSAFE_PORTS = [
|
||||
@@ -288,50 +299,6 @@ export const WHOIS_ICONS = {
|
||||
descr: '',
|
||||
};
|
||||
|
||||
export const DNS_RECORD_TYPES = [
|
||||
'A',
|
||||
'AAAA',
|
||||
'AFSDB',
|
||||
'APL',
|
||||
'CAA',
|
||||
'CDNSKEY',
|
||||
'CDS',
|
||||
'CERT',
|
||||
'CNAME',
|
||||
'CSYNC',
|
||||
'DHCID',
|
||||
'DLV',
|
||||
'DNAME',
|
||||
'DNSKEY',
|
||||
'DS',
|
||||
'HIP',
|
||||
'IPSECKEY',
|
||||
'KEY',
|
||||
'KX',
|
||||
'LOC',
|
||||
'MX',
|
||||
'NAPTR',
|
||||
'NS',
|
||||
'NSEC',
|
||||
'NSEC3',
|
||||
'NSEC3PARAM',
|
||||
'OPENPGPKEY',
|
||||
'PTR',
|
||||
'RRSIG',
|
||||
'RP',
|
||||
'SIG',
|
||||
'SMIMEA',
|
||||
'SOA',
|
||||
'SRV',
|
||||
'SSHFP',
|
||||
'TA',
|
||||
'TKEY',
|
||||
'TLSA',
|
||||
'TSIG',
|
||||
'TXT',
|
||||
'URI',
|
||||
];
|
||||
|
||||
export const DEFAULT_LOGS_FILTER = {
|
||||
search: '',
|
||||
response_status: '',
|
||||
@@ -339,7 +306,7 @@ export const DEFAULT_LOGS_FILTER = {
|
||||
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
export const TABLE_DEFAULT_PAGE_SIZE = 50;
|
||||
export const TABLE_DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
export const TABLE_FIRST_PAGE = 0;
|
||||
|
||||
@@ -516,6 +483,7 @@ export const FORM_NAME = {
|
||||
CACHE: 'cache',
|
||||
};
|
||||
|
||||
export const smallScreenSize = 767;
|
||||
export const SMALL_SCREEN_SIZE = 767;
|
||||
export const MEDIUM_SCREEN_SIZE = 1023;
|
||||
|
||||
export const SECONDS_IN_HOUR = 60 * 60;
|
||||
|
||||
@@ -31,10 +31,11 @@ export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
|
||||
|
||||
if (info) {
|
||||
const { name, whois_info } = info;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
if (name) {
|
||||
if (isLogs) {
|
||||
nameContainer = !whois_info && isDetailed
|
||||
nameContainer = !whoisAvailable && isDetailed
|
||||
? (
|
||||
<small title={value}>{value}</small>
|
||||
) : (
|
||||
@@ -54,7 +55,7 @@ export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (whois_info && isDetailed) {
|
||||
if (whoisAvailable && isDetailed) {
|
||||
whoisContainer = (
|
||||
<div className="logs__text logs__text--wrap logs__text--whois">
|
||||
{getFormattedWhois(whois_info)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import subHours from 'date-fns/sub_hours';
|
||||
import addHours from 'date-fns/add_hours';
|
||||
import addDays from 'date-fns/add_days';
|
||||
import subDays from 'date-fns/sub_days';
|
||||
import isSameDay from 'date-fns/is_same_day';
|
||||
import round from 'lodash/round';
|
||||
import axios from 'axios';
|
||||
import i18n from 'i18next';
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
DETAILED_DATE_FORMAT_OPTIONS,
|
||||
DNS_RECORD_TYPES,
|
||||
FILTERED,
|
||||
FILTERED_STATUS,
|
||||
IP_MATCH_LIST_STATUS,
|
||||
@@ -31,6 +29,7 @@ import {
|
||||
|
||||
/**
|
||||
* @param time {string} The time to format
|
||||
* @param options {string}
|
||||
* @returns {string} Returns the time in the format HH:mm:ss
|
||||
*/
|
||||
export const formatTime = (time, options = DEFAULT_TIME_FORMAT) => {
|
||||
@@ -60,12 +59,6 @@ export const formatDetailedDateTime = (dateTime) => formatDateTime(
|
||||
dateTime, DETAILED_DATE_FORMAT_OPTIONS,
|
||||
);
|
||||
|
||||
/**
|
||||
* @param date {string}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isToday = (date) => isSameDay(new Date(date), new Date());
|
||||
|
||||
export const normalizeLogs = (logs) => logs.map((log) => {
|
||||
const {
|
||||
answer,
|
||||
@@ -351,39 +344,6 @@ export const normalizeTopClients = (topClients) => topClients.reduce(
|
||||
},
|
||||
);
|
||||
|
||||
export const getClientInfo = (clients, ip) => {
|
||||
const client = clients
|
||||
.find((item) => item.ip_addrs?.find((clientIp) => clientIp === ip));
|
||||
|
||||
if (!client) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { name, whois_info } = client;
|
||||
const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
|
||||
|
||||
return {
|
||||
name,
|
||||
whois,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAutoClientInfo = (clients, ip) => {
|
||||
const client = clients.find((item) => ip === item.ip);
|
||||
|
||||
if (!client) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { name, whois_info } = client;
|
||||
const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
|
||||
|
||||
return {
|
||||
name,
|
||||
whois,
|
||||
};
|
||||
};
|
||||
|
||||
export const sortClients = (clients) => {
|
||||
const compare = (a, b) => {
|
||||
const nameA = a.name.toUpperCase();
|
||||
@@ -443,8 +403,6 @@ export const normalizeWhois = (whois) => {
|
||||
return whois;
|
||||
};
|
||||
|
||||
export const isValidQuestionType = (type) => type && DNS_RECORD_TYPES.includes(type.toUpperCase());
|
||||
|
||||
export const getPathWithQueryString = (path, params) => {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
@@ -542,10 +500,10 @@ export const getMap = (arr, key, value) => arr.reduce((acc, curr) => {
|
||||
|
||||
/**
|
||||
* @param parsedIp {object} ipaddr.js IPv4 or IPv6 object
|
||||
* @param cidr {array} ipaddr.js CIDR array
|
||||
* @param parsedCidr {array} ipaddr.js CIDR array
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isIpMatchCidr = (parsedIp, parsedCidr) => {
|
||||
const isIpMatchCidr = (parsedIp, parsedCidr) => {
|
||||
try {
|
||||
const cidrIpVersion = parsedCidr[0].kind();
|
||||
const ipVersion = parsedIp.kind();
|
||||
@@ -556,6 +514,75 @@ export const isIpMatchCidr = (parsedIp, parsedCidr) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The purpose of this method is to quickly check
|
||||
* if this IP can possibly be in the specified CIDR range.
|
||||
*
|
||||
* @param ip {string}
|
||||
* @param listItem {string}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isIpQuickMatchCIDR = (ip, listItem) => {
|
||||
const ipv6 = ip.indexOf(':') !== -1;
|
||||
const cidrIpv6 = listItem.indexOf(':') !== -1;
|
||||
if (ipv6 !== cidrIpv6) {
|
||||
// CIDR is for a different IP type
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cidrIpv6) {
|
||||
// We don't do quick check for IPv6 addresses
|
||||
return true;
|
||||
}
|
||||
|
||||
const idx = listItem.indexOf('/');
|
||||
if (idx === -1) {
|
||||
// Not a CIDR, return false immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
const cidrIp = listItem.substring(0, idx);
|
||||
const cidrRange = parseInt(listItem.substring(idx + 1), 10);
|
||||
if (Number.isNaN(cidrRange)) {
|
||||
// Not a valid CIDR
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = cidrIp.split('.');
|
||||
if (parts.length !== 4) {
|
||||
// Invalid IP, return immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now depending on the range we check if the IP can possibly be in that range
|
||||
if (cidrRange < 8) {
|
||||
// Use the slow approach
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cidrRange < 16) {
|
||||
// Check the first part
|
||||
// Example: 0.0.0.0/8 matches 0.*.*.*
|
||||
return ip.indexOf(`${parts[0]}.`) === 0;
|
||||
}
|
||||
|
||||
if (cidrRange < 24) {
|
||||
// Check the first two parts
|
||||
// Example: 0.0.0.0/16 matches 0.0.*.*
|
||||
return ip.indexOf(`${parts[0]}.${parts[1]}.`) === 0;
|
||||
}
|
||||
|
||||
if (cidrRange <= 32) {
|
||||
// Check the first two parts
|
||||
// Example: 0.0.0.0/16 matches 0.0.*.*
|
||||
return ip.indexOf(`${parts[0]}.${parts[1]}.${parts[2]}.`) === 0;
|
||||
}
|
||||
|
||||
// range for IPv4 CIDR cannot be more than 32
|
||||
// no need to check further, this CIDR is invalid
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param ip {string}
|
||||
* @param list {string}
|
||||
@@ -573,20 +600,29 @@ export const getIpMatchListStatus = (ip, list) => {
|
||||
for (let i = 0; i < listArr.length; i += 1) {
|
||||
const listItem = listArr[i];
|
||||
|
||||
const parsedIp = ipaddr.parse(ip);
|
||||
const isItemAnIp = ipaddr.isValid(listItem);
|
||||
const parsedItem = isItemAnIp ? ipaddr.parse(listItem) : ipaddr.parseCIDR(listItem);
|
||||
|
||||
if (isItemAnIp && parsedIp.toString() === parsedItem.toString()) {
|
||||
if (ip === listItem.trim()) {
|
||||
return IP_MATCH_LIST_STATUS.EXACT;
|
||||
}
|
||||
|
||||
if (!isItemAnIp && isIpMatchCidr(parsedIp, parsedItem)) {
|
||||
return IP_MATCH_LIST_STATUS.CIDR;
|
||||
// Using ipaddr.js is quite slow so we first do a quick check
|
||||
// to see if it's possible that this IP may be in the specified CIDR range
|
||||
if (isIpQuickMatchCIDR(ip, listItem)) {
|
||||
const parsedIp = ipaddr.parse(ip);
|
||||
const isItemAnIp = ipaddr.isValid(listItem);
|
||||
const parsedItem = isItemAnIp ? ipaddr.parse(listItem) : ipaddr.parseCIDR(listItem);
|
||||
|
||||
if (isItemAnIp && parsedIp.toString() === parsedItem.toString()) {
|
||||
return IP_MATCH_LIST_STATUS.EXACT;
|
||||
}
|
||||
|
||||
if (!isItemAnIp && isIpMatchCidr(parsedIp, parsedItem)) {
|
||||
return IP_MATCH_LIST_STATUS.CIDR;
|
||||
}
|
||||
}
|
||||
}
|
||||
return IP_MATCH_LIST_STATUS.NOT_FOUND;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return IP_MATCH_LIST_STATUS.NOT_FOUND;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -670,6 +670,11 @@ func TestRewrite(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "1.2.3.4", a.A.String())
|
||||
|
||||
req = createTestMessageWithType("test.com.", dns.TypeAAAA)
|
||||
reply, err = dns.Exchange(req, addr.String())
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(reply.Answer))
|
||||
|
||||
req = createTestMessageWithType("alias.test.com.", dns.TypeA)
|
||||
reply, err = dns.Exchange(req, addr.String())
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -54,8 +54,12 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
|
||||
// log.Tracef("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule)
|
||||
d.Res = s.genDNSFilterMessage(d, &res)
|
||||
|
||||
} else if (res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteEtcHosts) &&
|
||||
len(res.IPList) != 0 {
|
||||
} else if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 && len(res.IPList) == 0 {
|
||||
ctx.origQuestion = d.Req.Question[0]
|
||||
// resolve canonical name, not the original host name
|
||||
d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
|
||||
|
||||
} else if res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteEtcHosts {
|
||||
resp := s.makeResponse(req)
|
||||
|
||||
name := host
|
||||
@@ -78,11 +82,6 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
|
||||
|
||||
d.Res = resp
|
||||
|
||||
} else if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
|
||||
ctx.origQuestion = d.Req.Question[0]
|
||||
// resolve canonical name, not the original host name
|
||||
d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
|
||||
|
||||
} else if res.Reason == dnsfilter.RewriteEtcHosts && len(res.ReverseHost) != 0 {
|
||||
|
||||
resp := s.makeResponse(req)
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.14
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.29.1
|
||||
github.com/AdguardTeam/golibs v0.4.2
|
||||
github.com/AdguardTeam/urlfilter v0.11.0
|
||||
github.com/AdguardTeam/urlfilter v0.11.2
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gobuffalo/packr v1.30.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -4,8 +4,8 @@ github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
|
||||
github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=
|
||||
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||
github.com/AdguardTeam/urlfilter v0.11.0 h1:tgZss6uZs1UZAaxpovD/QuX+VVIQLDOlKc7rdF8dwNw=
|
||||
github.com/AdguardTeam/urlfilter v0.11.0/go.mod h1:aMuejlNxpWppOVjiEV87X6z0eMf7wsXHTAIWQuylfZY=
|
||||
github.com/AdguardTeam/urlfilter v0.11.2 h1:gCrWGh63Yqw3z4yi9pgikfsbshIEyvAu/KYV3MvTBlc=
|
||||
github.com/AdguardTeam/urlfilter v0.11.2/go.mod h1:aMuejlNxpWppOVjiEV87X6z0eMf7wsXHTAIWQuylfZY=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
|
||||
@@ -242,7 +242,7 @@ func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteri
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Using settings for client with IP %s", clientAddr)
|
||||
log.Debug("Using settings for client %s with IP %s", c.Name, clientAddr)
|
||||
|
||||
if c.UseOwnBlockedServices {
|
||||
Context.dnsFilter.ApplyBlockedServices(setts, c.BlockedServices, false)
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
## v0.103: API changes
|
||||
|
||||
### API: replace settings in GET /control/dns_info & POST /control/dns_config
|
||||
|
||||
* added "upstream_mode"
|
||||
|
||||
"upstream_mode": "" | "parallel" | "fastest_addr"
|
||||
|
||||
* removed "fastest_addr", "parallel_requests"
|
||||
|
||||
|
||||
### API: Get querylog: GET /control/querylog
|
||||
|
||||
* Added optional "offset" and "limit" parameters
|
||||
|
||||
We are still using "older_than" approach in AdGuard Home UI, but we realize that it's easier to use offset/limit so here is this option now.
|
||||
|
||||
|
||||
## v0.102: API changes
|
||||
|
||||
### API: Get general status: GET /control/status
|
||||
|
||||
Reference in New Issue
Block a user