Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home

This commit is contained in:
Andrey Meshkov 2020-07-16 11:12:16 +03:00
commit 4df02714fd
24 changed files with 193 additions and 159 deletions

12
.githooks/pre-commit Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
set -e;
git diff --cached --name-only | grep -q '.js$' && make lint-js;
found=0
git diff --cached --name-only | grep -q '.go$' && found=1
if [ $found == 1 ]; then
make lint-go || exit 1
go test ./... || exit 1
fi
exit 0;

View File

@ -344,10 +344,14 @@ Response:
If `can_autoupdate` is true, then the server can automatically upgrade to a new version. If `can_autoupdate` is true, then the server can automatically upgrade to a new version.
Response with empty body: Response when auto-update is disabled by command-line argument:
200 OK 200 OK
{
"disabled":true
}
It means that update check is disabled by user. UI should do nothing. It means that update check is disabled by user. UI should do nothing.

View File

@ -88,9 +88,12 @@ ifndef DOCKER_IMAGE_NAME
$(error DOCKER_IMAGE_NAME value is not set) $(error DOCKER_IMAGE_NAME value is not set)
endif endif
.PHONY: all build client client-watch docker lint test dependencies clean release docker-multi-arch .PHONY: all build client client-watch docker lint lint-js lint-go test dependencies clean release docker-multi-arch
all: build all: build
init:
git config core.hooksPath .githooks
build: dependencies client build: dependencies client
PATH=$(GOPATH)/bin:$(PATH) go generate ./... PATH=$(GOPATH)/bin:$(PATH) go generate ./...
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)" CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)"
@ -116,11 +119,16 @@ docker:
@echo Now you can run the docker image: @echo Now you can run the docker image:
@echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME) @echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME)
lint: lint: lint-js lint-go
@echo Running linters
golangci-lint run ./... lint-js:
@echo Running js linter
npm --prefix client run lint npm --prefix client run lint
lint-go:
@echo Running go linter
golangci-lint run
test: test:
@echo Running unit-tests @echo Running unit-tests
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./... go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...

View File

@ -150,10 +150,13 @@ Is there a chance to handle this in the future? DNS will never be enough to do t
### Prerequisites ### Prerequisites
Run `make init` to prepare the development environment.
You will need this to build AdGuard Home: You will need this to build AdGuard Home:
* [go](https://golang.org/dl/) v1.14 or later. * [go](https://golang.org/dl/) v1.14 or later.
* [node.js](https://nodejs.org/en/download/) v10 or later. * [node.js](https://nodejs.org/en/download/) v10 or later.
* [golangci-lint](https://github.com/golangci/golangci-lint)
### Building ### Building

View File

@ -2,9 +2,9 @@ import { createAction } from 'redux-actions';
import i18next from 'i18next'; import i18next from 'i18next';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { normalizeTextarea } from '../helpers/helpers';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
import { BLOCK_ACTIONS } from '../helpers/constants'; import { BLOCK_ACTIONS } from '../helpers/constants';
import { splitByNewLine } from '../helpers/helpers';
export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST'); export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');
export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE'); export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE');
@ -31,9 +31,9 @@ export const setAccessList = (config) => async (dispatch) => {
const { allowed_clients, disallowed_clients, blocked_hosts } = config; const { allowed_clients, disallowed_clients, blocked_hosts } = config;
const values = { const values = {
allowed_clients: normalizeTextarea(allowed_clients), allowed_clients: splitByNewLine(allowed_clients),
disallowed_clients: normalizeTextarea(disallowed_clients), disallowed_clients: splitByNewLine(disallowed_clients),
blocked_hosts: normalizeTextarea(blocked_hosts), blocked_hosts: splitByNewLine(blocked_hosts),
}; };
await apiClient.setAccessList(values); await apiClient.setAccessList(values);

View File

@ -1,7 +1,7 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { normalizeTextarea } from '../helpers/helpers'; import { splitByNewLine } from '../helpers/helpers';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST'); export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');
@ -30,11 +30,11 @@ export const setDnsConfig = (config) => async (dispatch) => {
let hasDnsSettings = false; let hasDnsSettings = false;
if (Object.prototype.hasOwnProperty.call(data, 'bootstrap_dns')) { if (Object.prototype.hasOwnProperty.call(data, 'bootstrap_dns')) {
data.bootstrap_dns = normalizeTextarea(config.bootstrap_dns); data.bootstrap_dns = splitByNewLine(config.bootstrap_dns);
hasDnsSettings = true; hasDnsSettings = true;
} }
if (Object.prototype.hasOwnProperty.call(data, 'upstream_dns')) { if (Object.prototype.hasOwnProperty.call(data, 'upstream_dns')) {
data.upstream_dns = normalizeTextarea(config.upstream_dns); data.upstream_dns = splitByNewLine(config.upstream_dns);
hasDnsSettings = true; hasDnsSettings = true;
} }

View File

@ -2,7 +2,7 @@ import { createAction } from 'redux-actions';
import i18next from 'i18next'; import i18next from 'i18next';
import axios from 'axios'; import axios from 'axios';
import { isVersionGreater, normalizeTextarea, sortClients } from '../helpers/helpers'; import { isVersionGreater, splitByNewLine, sortClients } from '../helpers/helpers';
import { CHECK_TIMEOUT, SETTINGS_NAMES } from '../helpers/constants'; import { CHECK_TIMEOUT, SETTINGS_NAMES } from '../helpers/constants';
import { getTlsStatus } from './encryption'; import { getTlsStatus } from './encryption';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
@ -279,8 +279,8 @@ export const testUpstream = (config) => async (dispatch) => {
dispatch(testUpstreamRequest()); dispatch(testUpstreamRequest());
try { try {
const values = { ...config }; const values = { ...config };
values.bootstrap_dns = normalizeTextarea(values.bootstrap_dns); values.bootstrap_dns = splitByNewLine(values.bootstrap_dns);
values.upstream_dns = normalizeTextarea(values.upstream_dns); values.upstream_dns = splitByNewLine(values.upstream_dns);
const upstreamResponse = await apiClient.testUpstream(values); const upstreamResponse = await apiClient.testUpstream(values);
const testMessages = Object.keys(upstreamResponse) const testMessages = Object.keys(upstreamResponse)

View File

@ -62,7 +62,7 @@ const clientCell = (t, toggleClientStatus, processing, disallowedClients) => fun
return ( return (
<> <>
<div className="logs__row logs__row--overflow logs__row--column"> <div className="logs__row logs__row--overflow logs__row--column">
{formatClientCell(row, t)} {formatClientCell(row, true)}
</div> </div>
{ipMatchListStatus !== IP_MATCH_LIST_STATUS.CIDR {ipMatchListStatus !== IP_MATCH_LIST_STATUS.CIDR
&& renderBlockingButton(ipMatchListStatus, value, toggleClientStatus, processing)} && renderBlockingButton(ipMatchListStatus, value, toggleClientStatus, processing)}

View File

@ -9,7 +9,7 @@ const DomainCell = ({ value }) => {
return ( return (
<div className="logs__row"> <div className="logs__row">
<div className="logs__text logs__text--domain" title={value}> <div className="logs__text" title={value}>
{value} {value}
</div> </div>
{trackerData && <Popover data={trackerData} />} {trackerData && <Popover data={trackerData} />}

View File

@ -33,7 +33,7 @@ const getClientCell = ({
const isFiltered = checkFiltered(reason); const isFiltered = checkFiltered(reason);
const nameClass = classNames('w-90 o-hidden d-flex flex-column', { const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
'mt-2': isDetailed && !name, 'mt-2': isDetailed && !name && !whois_info,
'white-space--nowrap': isDetailed, 'white-space--nowrap': isDetailed,
}); });
@ -80,9 +80,9 @@ const getClientCell = ({
})} })}
<div <div
className={nameClass}> className={nameClass}>
<div data-tip={true} data-for={id}>{formatClientCell(row, t, isDetailed)}</div> <div data-tip={true} data-for={id}>{formatClientCell(row, isDetailed)}</div>
{isDetailed && name {isDetailed && name
&& <div className="detailed-info d-none d-sm-block logs__text" && !whois_info && <div className="detailed-info d-none d-sm-block logs__text"
title={name}>{name}</div>} title={name}>{name}</div>}
</div> </div>
{renderBlockingButton(isFiltered, domain)} {renderBlockingButton(isFiltered, domain)}

View File

@ -9,8 +9,7 @@ import getHintElement from './getHintElement';
const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => { const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
const { const {
reason, filterId, rule, status, upstream, elapsedMs, reason, filterId, rule, status, upstream, elapsedMs, response, originalResponse,
domain, response, originalResponse,
} = row.original; } = row.original;
const { filters, whitelistFilters } = filtering; const { filters, whitelistFilters } = filtering;
@ -41,7 +40,6 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
const FILTERED_STATUS_TO_FIELDS_MAP = { const FILTERED_STATUS_TO_FIELDS_MAP = {
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: { [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
domain,
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
@ -49,7 +47,6 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
response_table_header: renderResponses(response), response_table_header: renderResponses(response),
}, },
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: { [FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
domain,
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
@ -59,7 +56,6 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
original_response: renderResponses(originalResponse), original_response: renderResponses(originalResponse),
}, },
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: { [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
domain,
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
@ -68,21 +64,19 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
response_code: status, response_code: status,
}, },
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: { [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
domain,
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
filter, filter,
rule_label: rule, rule_label: rule,
response_code: status, response_code: status,
}, },
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: { [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
domain,
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
response_code: status, response_code: status,
response_table_header: renderResponses(response),
}, },
[FILTERED_STATUS.FILTERED_BLACK_LIST]: { [FILTERED_STATUS.FILTERED_BLACK_LIST]: {
domain,
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
filter, filter,
rule_label: rule, rule_label: rule,
@ -93,7 +87,7 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
}, },
}; };
const fields = FILTERED_STATUS_TO_FIELDS_MAP[reason] const content = FILTERED_STATUS_TO_FIELDS_MAP[reason]
? Object.entries(FILTERED_STATUS_TO_FIELDS_MAP[reason]) ? Object.entries(FILTERED_STATUS_TO_FIELDS_MAP[reason])
: Object.entries(FILTERED_STATUS_TO_FIELDS_MAP.NotFilteredNotFound); : Object.entries(FILTERED_STATUS_TO_FIELDS_MAP.NotFilteredNotFound);
@ -108,7 +102,7 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
contentItemClass: 'text-truncate key-colon o-hidden', contentItemClass: 'text-truncate key-colon o-hidden',
xlinkHref: 'question', xlinkHref: 'question',
title: 'response_details', title: 'response_details',
content: fields, content,
placement: 'bottom', placement: 'bottom',
})} })}
<div className="text-truncate"> <div className="text-truncate">

View File

@ -144,18 +144,20 @@ const Form = (props) => {
e.preventDefault(); e.preventDefault();
}} }}
> >
<Field <div className="field__search">
id={FORM_NAMES.search} <Field
name={FORM_NAMES.search} id={FORM_NAMES.search}
component={renderFilterField} name={FORM_NAMES.search}
type="text" component={renderFilterField}
className={classNames('form-control--search form-control--transparent', className)} type="text"
placeholder={t('domain_or_client')} className={classNames('form-control--search form-control--transparent', className)}
tooltip={t('query_log_strict_search')} placeholder={t('domain_or_client')}
onClearInputClick={onInputClear} tooltip={t('query_log_strict_search')}
onKeyDown={onEnterPress} onClearInputClick={onInputClear}
normalizeOnBlur={normalizeOnBlur} onKeyDown={onEnterPress}
/> normalizeOnBlur={normalizeOnBlur}
/>
</div>
<div className="field__select"> <div className="field__select">
<Field <Field
name={FORM_NAMES.response_status} name={FORM_NAMES.response_status}

View File

@ -13,7 +13,8 @@
} }
.card-table .logs__row { .card-table .logs__row {
overflow: visible; overflow: hidden;
text-overflow: ellipsis;
} }
.logs__row--center { .logs__row--center {
@ -57,10 +58,6 @@
width: 100%; width: 100%;
} }
.logs__text--domain {
max-width: 285px;
}
.logs__text--wrap, .logs__text--wrap,
.logs__text--whois { .logs__text--whois {
line-height: 1.4; line-height: 1.4;
@ -202,6 +199,7 @@
.logs__whois { .logs__whois {
display: inline; display: inline;
font-size: 12px; font-size: 12px;
white-space: nowrap;
} }
.logs__whois::after { .logs__whois::after {
@ -455,15 +453,11 @@
color: var(--danger); color: var(--danger);
} }
.ml-small {
margin-left: 3.3125rem;
}
.form-control--search { .form-control--search {
width: 39.125rem;
box-shadow: 0 1px 0 #ddd; box-shadow: 0 1px 0 #ddd;
padding: 0 2.5rem; padding: 0 2.5rem;
height: 2.25rem; height: 2.25rem;
flex-grow: 1;
} }
.form-control--transparent { .form-control--transparent {
@ -493,31 +487,12 @@
} }
.form-control--container { .form-control--container {
max-width: 100%; flex: auto;
} }
@media (max-width: 1279.98px) { .field__search {
.form-control--search { display: flex;
max-width: 30.125rem; flex-grow: 1;
}
.form-control--container {
max-width: 70%;
}
.form-control--search {
max-width: 50%;
}
}
@media (max-width: 991.98px) {
.form-control--search {
max-width: 40%;
}
.form-control--container {
max-width: 100%;
}
} }
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
@ -528,6 +503,19 @@
.ml-small { .ml-small {
margin-left: 1.5rem; margin-left: 1.5rem;
} }
.form-control--container {
width: 100%;
flex-direction: column;
}
.form-control--search {
width: 100%;
}
.field__select {
margin-top: 1.5rem;
}
} }
@media (max-width: 575px) { @media (max-width: 575px) {
@ -544,16 +532,6 @@
} }
} }
@media (max-width: 500px) {
.form-control--search {
max-width: 85%;
}
.field__select {
margin-top: 1.5rem;
}
}
.loading__container > .-loading-inner { .loading__container > .-loading-inner {
top: 10rem !important; top: 10rem !important;
bottom: initial !important; bottom: initial !important;

View File

@ -25,7 +25,7 @@ import {
formatDateTime, formatDateTime,
formatElapsedMs, formatElapsedMs,
formatTime, formatTime,
processContent,
} from '../../helpers/helpers'; } from '../../helpers/helpers';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import { getSourceData } from '../../helpers/trackers/trackers'; import { getSourceData } from '../../helpers/trackers/trackers';
@ -302,6 +302,7 @@ const Table = (props) => {
filterId, filterId,
rule, rule,
originalResponse, originalResponse,
status,
} = rowInfo.original; } = rowInfo.original;
const hasTracker = !!tracker; const hasTracker = !!tracker;
@ -328,17 +329,20 @@ const Table = (props) => {
}; };
const isBlockedByResponse = originalResponse.length > 0 && isBlocked; const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
const status = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.label || reason); const requestStatus = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.label || reason);
const statusBlocked = <div className="bg--danger">{status}</div>;
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || ''; const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const sourceData = getSourceData(tracker); const sourceData = getSourceData(tracker);
const { filters, whitelistFilters } = filtering;
const filter = getFilterName(filters, whitelistFilters, filterId, t);
const detailedData = { const detailedData = {
time_table_header: formatTime(time, LONG_TIME_FORMAT), time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS), date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
encryption_status: status, encryption_status: isBlocked
? <div className="bg--danger">{requestStatus}</div> : requestStatus,
domain, domain,
type_table_header: type, type_table_header: type,
protocol, protocol,
@ -346,12 +350,19 @@ const Table = (props) => {
table_name: tracker?.name, table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
tracker_source: hasTracker && sourceData tracker_source: hasTracker && sourceData
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer" && <a
className="link--green">{sourceData.name}</a>, href={sourceData.url}
target="_blank"
rel="noopener noreferrer"
className="link--green">{sourceData.name}
</a>,
response_details: 'title', response_details: 'title',
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
filter: isBlocked ? filter : null,
rule_label: rule,
response_table_header: response?.join('\n'), response_table_header: response?.join('\n'),
response_code: status,
client_details: 'title', client_details: 'title',
ip_address: client, ip_address: client,
name: info?.name, name: info?.name,
@ -360,41 +371,14 @@ const Table = (props) => {
network, network,
source_label: source, source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false, validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
[buttonType]: <div onClick={onToggleBlock}
className="title--border bg--danger text-center">{t(buttonType)}</div>,
};
const { filters, whitelistFilters } = filtering;
const filter = getFilterName(filters, whitelistFilters, filterId, t);
const detailedDataBlocked = {
time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
encryption_status: statusBlocked,
domain,
type_table_header: type,
protocol,
known_tracker: 'title',
table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category),
source_label: hasTracker && sourceData
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
className="link--green">{sourceData.name}</a>,
response_details: 'title',
install_settings_dns: upstream,
elapsed: formattedElapsedMs,
filter,
rule_label: rule,
response_table_header: response?.join('\n'),
original_response: originalResponse?.join('\n'), original_response: originalResponse?.join('\n'),
[buttonType]: <div onClick={onToggleBlock} [buttonType]: <div onClick={onToggleBlock}
className="title--border text-center">{t(buttonType)}</div>, className={classNames('title--border text-center', {
'bg--danger': isBlocked,
})}>{t(buttonType)}</div>,
}; };
const detailedDataCurrent = isBlocked ? detailedDataBlocked : detailedData; setDetailedDataCurrent(processContent(detailedData));
setDetailedDataCurrent(detailedDataCurrent);
setButtonType(buttonType); setButtonType(buttonType);
setModalOpened(true); setModalOpened(true);
} }

View File

@ -5,6 +5,7 @@ import Modal from 'react-modal';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import queryString from 'query-string'; import queryString from 'query-string';
import classNames from 'classnames';
import { import {
BLOCK_ACTIONS, BLOCK_ACTIONS,
TABLE_DEFAULT_PAGE_SIZE, TABLE_DEFAULT_PAGE_SIZE,
@ -27,7 +28,7 @@ import {
import { addSuccessToast } from '../../actions/toasts'; import { addSuccessToast } from '../../actions/toasts';
import './Logs.css'; import './Logs.css';
export const processContent = (data, buttonType) => Object.entries(data) const processContent = (data, buttonType) => Object.entries(data)
.map(([key, value]) => { .map(([key, value]) => {
if (!value) { if (!value) {
return null; return null;
@ -49,7 +50,9 @@ export const processContent = (data, buttonType) => Object.entries(data)
return isHidden ? null : <Fragment key={key}> return isHidden ? null : <Fragment key={key}>
<div <div
className={`key__${key} ${keyClass} ${(isBoolean && value === true) ? 'font-weight-bold' : ''}`}> className={classNames(`key__${key}`, keyClass, {
'font-weight-bold': isBoolean && value === true,
})}>
<Trans>{isButton ? value : key}</Trans> <Trans>{isButton ? value : key}</Trans>
</div> </div>
<div className={`value__${key} text-pre text-truncate`}> <div className={`value__${key} text-pre text-truncate`}>
@ -133,7 +136,16 @@ const Logs = (props) => {
}; };
useEffect(() => { useEffect(() => {
mediaQuery.addEventListener('change', mediaQueryHandler); try {
mediaQuery.addEventListener('change', mediaQueryHandler);
} catch (e1) {
try {
// Safari 13.1 do not support mediaQuery.addEventListener('change', handler)
mediaQuery.addListener(mediaQueryHandler);
} catch (e2) {
console.error(e2);
}
}
(async () => { (async () => {
setIsLoading(true); setIsLoading(true);
@ -153,7 +165,16 @@ const Logs = (props) => {
})(); })();
return () => { return () => {
mediaQuery.removeEventListener('change', mediaQueryHandler); try {
mediaQuery.removeEventListener('change', mediaQueryHandler);
} catch (e1) {
try {
mediaQuery.removeListener(mediaQueryHandler);
} catch (e2) {
console.error(e2);
}
}
dispatch(resetFilteredLogs()); dispatch(resetFilteredLogs());
}; };
}, []); }, []);

View File

@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { MODAL_TYPE } from '../../../helpers/constants'; import { MODAL_TYPE } from '../../../helpers/constants';
import { normalizeTextarea } from '../../../helpers/helpers'; import { splitByNewLine } from '../../../helpers/helpers';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Modal from './Modal'; import Modal from './Modal';
import CellWrap from '../../ui/CellWrap'; import CellWrap from '../../ui/CellWrap';
@ -30,7 +30,7 @@ class ClientsTable extends Component {
} }
if (values.upstreams && typeof values.upstreams === 'string') { if (values.upstreams && typeof values.upstreams === 'string') {
config.upstreams = normalizeTextarea(values.upstreams); config.upstreams = splitByNewLine(values.upstreams);
} else { } else {
config.upstreams = []; config.upstreams = [];
} }

View File

@ -4,7 +4,10 @@ import { Field, reduxForm } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { renderTextareaField } from '../../../../helpers/form'; import { renderTextareaField } from '../../../../helpers/form';
import { normalizeMultiline } from '../../../../helpers/helpers'; import {
trimMultilineString,
removeEmptyLines,
} from '../../../../helpers/helpers';
import { FORM_NAME } from '../../../../helpers/constants'; import { FORM_NAME } from '../../../../helpers/constants';
const fields = [ const fields = [
@ -12,16 +15,19 @@ const fields = [
id: 'allowed_clients', id: 'allowed_clients',
title: 'access_allowed_title', title: 'access_allowed_title',
subtitle: 'access_allowed_desc', subtitle: 'access_allowed_desc',
normalizeOnBlur: removeEmptyLines,
}, },
{ {
id: 'disallowed_clients', id: 'disallowed_clients',
title: 'access_disallowed_title', title: 'access_disallowed_title',
subtitle: 'access_disallowed_desc', subtitle: 'access_disallowed_desc',
normalizeOnBlur: trimMultilineString,
}, },
{ {
id: 'blocked_hosts', id: 'blocked_hosts',
title: 'access_blocked_title', title: 'access_blocked_title',
subtitle: 'access_blocked_desc', subtitle: 'access_blocked_desc',
normalizeOnBlur: removeEmptyLines,
}, },
]; ];
@ -31,7 +37,7 @@ const Form = (props) => {
} = props; } = props;
const renderField = ({ const renderField = ({
id, title, subtitle, disabled = processingSet, id, title, subtitle, disabled = processingSet, normalizeOnBlur,
}) => <div key={id} className="form__group mb-5"> }) => <div key={id} className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor={id}> <label className="form__label form__label--with-desc" htmlFor={id}>
<Trans>{title}</Trans> <Trans>{title}</Trans>
@ -46,7 +52,7 @@ const Form = (props) => {
type="text" type="text"
className="form-control form-control--textarea font-monospace" className="form-control form-control--textarea font-monospace"
disabled={disabled} disabled={disabled}
normalizeOnBlur={id === 'disallowed_clients' ? normalizeMultiline : undefined} normalizeOnBlur={normalizeOnBlur}
/> />
</div>; </div>;
@ -55,6 +61,7 @@ const Form = (props) => {
title: PropTypes.string, title: PropTypes.string,
subtitle: PropTypes.string, subtitle: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
normalizeOnBlur: PropTypes.func,
}; };
return ( return (

View File

@ -6,9 +6,10 @@ import { Trans, useTranslation } from 'react-i18next';
import classnames from 'classnames'; import classnames from 'classnames';
import Examples from './Examples'; import Examples from './Examples';
import { renderRadioField } from '../../../../helpers/form'; import { renderRadioField, renderTextareaField } from '../../../../helpers/form';
import { DNS_REQUEST_OPTIONS, FORM_NAME } from '../../../../helpers/constants'; import { DNS_REQUEST_OPTIONS, FORM_NAME } from '../../../../helpers/constants';
import { testUpstream } from '../../../../actions'; import { testUpstream } from '../../../../actions';
import { removeEmptyLines } from '../../../../helpers/helpers';
const getInputFields = () => [{ const getInputFields = () => [{
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
@ -17,9 +18,10 @@ const getInputFields = () => [{
</label>, </label>,
name: 'upstream_dns', name: 'upstream_dns',
type: 'text', type: 'text',
component: 'textarea', component: renderTextareaField,
className: 'form-control form-control--textarea font-monospace', className: 'form-control form-control--textarea font-monospace',
placeholder: 'upstream_dns', placeholder: 'upstream_dns',
normalizeOnBlur: removeEmptyLines,
}, },
{ {
name: 'upstream_mode', name: 'upstream_mode',
@ -69,7 +71,8 @@ const Form = ({
return <form onSubmit={handleSubmit}> return <form onSubmit={handleSubmit}>
<div className="row"> <div className="row">
{INPUT_FIELDS.map(({ {INPUT_FIELDS.map(({
name, component, type, className, placeholder, getTitle, subtitle, disabled, value, name, component, type, className, placeholder,
getTitle, subtitle, disabled, value, normalizeOnBlur,
}) => <div className="col-12 mb-4" key={placeholder}> }) => <div className="col-12 mb-4" key={placeholder}>
{typeof getTitle === 'function' && getTitle()} {typeof getTitle === 'function' && getTitle()}
<Field <Field
@ -82,6 +85,7 @@ const Form = ({
placeholder={t(placeholder)} placeholder={t(placeholder)}
subtitle={t(subtitle)} subtitle={t(subtitle)}
disabled={processingSetConfig || processingTestUpstream || disabled} disabled={processingSetConfig || processingTestUpstream || disabled}
normalizeOnBlur={normalizeOnBlur}
/> />
</div>)} </div>)}
<div className="col-12"> <div className="col-12">
@ -101,11 +105,12 @@ const Form = ({
<Field <Field
id="bootstrap_dns" id="bootstrap_dns"
name="bootstrap_dns" name="bootstrap_dns"
component="textarea" component={renderTextareaField}
type="text" type="text"
className="form-control form-control--textarea form-control--textarea-small font-monospace" className="form-control form-control--textarea form-control--textarea-small font-monospace"
placeholder={t('bootstrap_dns')} placeholder={t('bootstrap_dns')}
disabled={processingSetConfig} disabled={processingSetConfig}
normalizeOnBlur={removeEmptyLines}
/> />
</div> </div>
</div> </div>

View File

@ -45,7 +45,10 @@ const Dns = (props) => {
dnsConfig={dnsConfig} dnsConfig={dnsConfig}
setDnsConfig={setDnsConfig} setDnsConfig={setDnsConfig}
/> />
<Access access={access} setAccessList={setAccessList} /> <Access
access={access}
setAccessList={setAccessList}
/>
</>} </>}
</> </>
); );

View File

@ -2,14 +2,14 @@ import React from 'react';
import { normalizeWhois } from './helpers'; import { normalizeWhois } from './helpers';
import { WHOIS_ICONS } from './constants'; import { WHOIS_ICONS } from './constants';
const getFormattedWhois = (whois, t) => { const getFormattedWhois = (whois) => {
const whoisInfo = normalizeWhois(whois); const whoisInfo = normalizeWhois(whois);
return ( return (
Object.keys(whoisInfo) Object.keys(whoisInfo)
.map((key) => { .map((key) => {
const icon = WHOIS_ICONS[key]; const icon = WHOIS_ICONS[key];
return ( return (
<span className="logs__whois text-muted" key={key} title={t(key)}> <span className="logs__whois text-muted " key={key} title={whoisInfo[key]}>
{icon && ( {icon && (
<> <>
<svg className="logs__whois-icon icons"> <svg className="logs__whois-icon icons">
@ -24,7 +24,7 @@ const getFormattedWhois = (whois, t) => {
); );
}; };
export const formatClientCell = (row, t, isDetailed = false) => { export const formatClientCell = (row, isDetailed = false) => {
const { value, original: { info } } = row; const { value, original: { info } } = row;
let whoisContainer = ''; let whoisContainer = '';
let nameContainer = value; let nameContainer = value;
@ -33,7 +33,7 @@ export const formatClientCell = (row, t, isDetailed = false) => {
const { name, whois_info } = info; const { name, whois_info } = info;
if (name) { if (name) {
nameContainer = isDetailed nameContainer = !whois_info && isDetailed
? <small title={value}>{value}</small> ? <small title={value}>{value}</small>
: <div className="logs__text logs__text--nowrap" title={`${name} (${value})`}> : <div className="logs__text logs__text--nowrap" title={`${name} (${value})`}>
{name} {name}
@ -42,10 +42,10 @@ export const formatClientCell = (row, t, isDetailed = false) => {
</div>; </div>;
} }
if (whois_info) { if (whois_info && isDetailed) {
whoisContainer = ( whoisContainer = (
<div className="logs__text logs__text--wrap logs__text--whois"> <div className="logs__text logs__text--wrap logs__text--whois">
{getFormattedWhois(whois_info, t)} {getFormattedWhois(whois_info)}
</div> </div>
); );
} }

View File

@ -306,15 +306,27 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
} }
}; };
export const normalizeTextarea = (text) => { /**
if (!text) { * @param {string} text
return []; * @returns []string
} */
export const splitByNewLine = (text) => text.split('\n')
.filter((n) => n.trim());
return text.replace(/[;, ]/g, '\n') /**
.split('\n') * @param {string} text
.filter((n) => n); * @returns {string}
}; */
export const trimMultilineString = (text) => splitByNewLine(text)
.map((line) => line.trim())
.join('\n');
/**
* @param {string} text
* @returns {string}
*/
export const removeEmptyLines = (text) => splitByNewLine(text)
.join('\n');
/** /**
* Normalizes the topClients array * Normalizes the topClients array
@ -533,10 +545,6 @@ export const getMap = (arr, key, value) => arr.reduce((acc, curr) => {
return acc; return acc;
}, {}); }, {});
export const normalizeMultiline = (multiline) => `${normalizeTextarea(multiline)
.map((line) => line.trim())
.join('\n')}\n`;
/** /**
* @param parsedIp {object} ipaddr.js IPv4 or IPv6 object * @param parsedIp {object} ipaddr.js IPv4 or IPv6 object
* @param cidr {array} ipaddr.js CIDR array * @param cidr {array} ipaddr.js CIDR array
@ -629,7 +637,6 @@ export const getLogsUrlParams = (search, response_status) => `?${queryString.str
response_status, response_status,
})}`; })}`;
export const processContent = (content) => (Array.isArray(content) export const processContent = (content) => (Array.isArray(content)
? content.filter(([, value]) => value) ? content.filter(([, value]) => value)
.flat() : content); .flat() : content);

View File

@ -82,7 +82,7 @@ const dashboard = handleActions(
[actions.getVersionSuccess]: (state, { payload }) => { [actions.getVersionSuccess]: (state, { payload }) => {
const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion; const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion;
if (payload && isVersionGreater(currentVersion, payload.new_version)) { if (!payload.disabled && isVersionGreater(currentVersion, payload.new_version)) {
const { const {
announcement_url: announcementUrl, announcement_url: announcementUrl,
new_version: newVersion, new_version: newVersion,
@ -96,7 +96,7 @@ const dashboard = handleActions(
canAutoUpdate, canAutoUpdate,
isUpdateAvailable: true, isUpdateAvailable: true,
processingVersion: false, processingVersion: false,
checkUpdateFlag: !!payload, checkUpdateFlag: !payload.disabled,
}; };
return newState; return newState;
} }
@ -104,6 +104,7 @@ const dashboard = handleActions(
return { return {
...state, ...state,
processingVersion: false, processingVersion: false,
checkUpdateFlag: !payload.disabled,
}; };
}, },

View File

@ -395,6 +395,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
if glProcessCookie(r) { if glProcessCookie(r) {
log.Debug("Auth: authentification was handled by GL-Inet submodule") log.Debug("Auth: authentification was handled by GL-Inet submodule")
ok = true
} else if err == nil { } else if err == nil {
r := Context.auth.CheckSession(cookie.Value) r := Context.auth.CheckSession(cookie.Value)

View File

@ -41,6 +41,10 @@ type getVersionJSONRequest struct {
// Get the latest available version from the Internet // Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
if Context.disableUpdate { if Context.disableUpdate {
resp := make(map[string]interface{})
resp["disabled"] = true
d, _ := json.Marshal(resp)
_, _ = w.Write(d)
return return
} }