diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d933e462..4fd4e4f5 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,6 +1,10 @@ #!/bin/bash set -e; -git diff --cached --name-only | grep -q '.js$' && make lint-js; +git diff --cached --name-only | grep -q '.js$' && found=1 +if [ $found == 1 ]; then + make lint-js || exit 1 + npm run test --prefix client || exit 1 +fi found=0 git diff --cached --name-only | grep -q '.go$' && found=1 diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 50ad9d90..1a90fd91 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -743,6 +743,7 @@ Response: "server_name":"...", "port_https":443, "port_dns_over_tls":853, + "port_dns_over_quic":784, "certificate_chain":"...", "private_key":"...", "certificate_path":"...", @@ -774,6 +775,7 @@ Request: "force_https":false, "port_https":443, "port_dns_over_tls":853, + "port_dns_over_quic":784, "certificate_chain":"...", "private_key":"...", "certificate_path":"...", // if set, certificate_chain must be empty diff --git a/client/package-lock.json b/client/package-lock.json index 84c8e186..42cac10f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12377,9 +12377,9 @@ } }, "react-i18next": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz", - "integrity": "sha512-lyOZSSQkif4H9HnHN3iEKVkryLI+WkdZSEw3VAZzinZLopfYRMHVY5YxCopdkXPLEHs6S5GjKYPh3+j0j336Fg==", + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.2.tgz", + "integrity": "sha512-Djj3K3hh5Tecla2CI9rLO3TZBYGMFrGilm0JY4cLofAQONCi5TK6nVmUPKoB59n1ZffgjfgJt6zlbE9aGF6Q0Q==", "requires": { "@babel/runtime": "^7.3.1", "html-parse-stringify2": "2.0.1" diff --git a/client/package.json b/client/package.json index fdc19c9d..4ad8d5eb 100644 --- a/client/package.json +++ b/client/package.json @@ -28,7 +28,7 @@ "react": "^16.13.1", "react-click-outside": "^3.0.1", "react-dom": "^16.13.1", - "react-i18next": "^11.4.0", + "react-i18next": "^11.7.2", "react-modal": "^3.11.2", "react-popper-tooltip": "^2.11.1", "react-redux": "^7.2.0", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 674332d7..2fe1e760 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -133,6 +133,7 @@ "dhcp_settings": "DHCP settings", "upstream_dns": "Upstream DNS servers", "upstream_dns_hint": "If you keep this field empty, AdGuard Home will use Quad9 as an upstream.", + "upstream_dns_configured_in_file": "Configured in {{path}}", "test_upstream_btn": "Test upstreams", "upstreams": "Upstreams", "apply_btn": "Apply", @@ -186,6 +187,7 @@ "example_upstream_regular": "regular DNS (over UDP)", "example_upstream_dot": "encrypted <0>DNS-over-TLS", "example_upstream_doh": "encrypted <0>DNS-over-HTTPS", + "example_upstream_doq": "encrypted <0>DNS-over-QUIC", "example_upstream_sdns": "you can use <0>DNS Stamps for <1>DNSCrypt or <2>DNS-over-HTTPS resolvers", "example_upstream_tcp": "regular DNS (over TCP)", "all_lists_up_to_date_toast": "All lists are already up-to-date", @@ -330,6 +332,8 @@ "encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '/dns-query' location.", "encryption_dot": "DNS-over-TLS port", "encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.", + "encryption_doq": "DNS-over-QUIC port", + "encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port. It's experimental and may not be reliable. Also, there are not too many clients that support it at the moment.", "encryption_certificates": "Certificates", "encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}} or you can buy it from one of the trusted Certificate Authorities.", "encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.", @@ -363,7 +367,7 @@ "fix": "Fix", "dns_providers": "Here is a <0>list of known DNS providers to choose from.", "update_now": "Update now", - "update_failed": "Auto-update failed. Please follow the steps to update manually.", + "update_failed": "Auto-update failed. Please follow these steps to update manually.", "processing_update": "Please wait, AdGuard Home is being updated", "clients_title": "Clients", "clients_desc": "Configure devices connected to AdGuard Home", @@ -575,6 +579,6 @@ "click_to_view_queries": "Click to view queries", "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction on how to resolve this.", "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.", - "configured_in": "Configured in {{path}}", - "please_read_wiki": "Please read the wiki" + "please_read_wiki": "Please read the wiki", + "experimental": "Experimental" } diff --git a/client/src/__tests__/helpers.test.js b/client/src/__tests__/helpers.test.js index f974cca6..bb371be4 100644 --- a/client/src/__tests__/helpers.test.js +++ b/client/src/__tests__/helpers.test.js @@ -1,5 +1,7 @@ -import { getIpMatchListStatus, sortIp } from '../helpers/helpers'; -import { IP_MATCH_LIST_STATUS } from '../helpers/constants'; +import { + countClientsStatistics, findAddressType, getIpMatchListStatus, sortIp, +} from '../helpers/helpers'; +import { ADDRESS_TYPES, IP_MATCH_LIST_STATUS } from '../helpers/constants'; describe('getIpMatchListStatus', () => { describe('IPv4', () => { @@ -482,3 +484,56 @@ describe('sortIp', () => { }); }); }); + +describe('findAddressType', () => { + describe('ip', () => { + expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP); + }); + describe('cidr', () => { + expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR); + }); + describe('mac', () => { + expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN); + }); +}); + +describe('countClientsStatistics', () => { + test('single ip', () => { + expect(countClientsStatistics(['127.0.0.1'], { + '127.0.0.1': 1, + })).toStrictEqual(1); + }); + test('multiple ip', () => { + expect(countClientsStatistics(['127.0.0.1', '127.0.0.2'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr', () => { + expect(countClientsStatistics(['127.0.0.0/8'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr and multiple ip', () => { + expect(countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(1 + 2 + 3); + }); + test('mac', () => { + expect(countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(2 + 3); + }); + test('not found', () => { + expect(countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(0); + }); +}); diff --git a/client/src/actions/encryption.js b/client/src/actions/encryption.js index 0e743323..36faf2ec 100644 --- a/client/src/actions/encryption.js +++ b/client/src/actions/encryption.js @@ -34,6 +34,7 @@ export const setTlsConfig = (config) => async (dispatch, getState) => { values.private_key = btoa(values.private_key); values.port_https = values.port_https || 0; values.port_dns_over_tls = values.port_dns_over_tls || 0; + values.port_dns_over_quic = values.port_dns_over_quic || 0; const response = await apiClient.setTlsConfig(values); response.certificate_chain = atob(response.certificate_chain); @@ -59,6 +60,7 @@ export const validateTlsConfig = (config) => async (dispatch) => { values.private_key = btoa(values.private_key); values.port_https = values.port_https || 0; values.port_dns_over_tls = values.port_dns_over_tls || 0; + values.port_dns_over_quic = values.port_dns_over_quic || 0; const response = await apiClient.validateTlsConfig(values); response.certificate_chain = atob(response.certificate_chain); diff --git a/client/src/actions/index.js b/client/src/actions/index.js index d4018bb0..ff039af1 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -4,9 +4,10 @@ import axios from 'axios'; import endsWith from 'lodash/endsWith'; import escapeRegExp from 'lodash/escapeRegExp'; +import React from 'react'; import { splitByNewLine, sortClients } from '../helpers/helpers'; import { - BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, + BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, GETTING_STARTED_LINK, } from '../helpers/constants'; import { areEqualVersions } from '../helpers/version'; import { getTlsStatus } from './encryption'; @@ -184,7 +185,14 @@ export const getUpdate = () => async (dispatch, getState) => { dispatch(getUpdateRequest()); const handleRequestError = () => { - dispatch(addNoticeToast({ error: 'update_failed' })); + const options = { + components: { + a: , + }, + }; + + dispatch(addNoticeToast({ error: 'update_failed', options })); dispatch(getUpdateFailure()); }; diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index fd088b79..b230ab8b 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -388,3 +388,28 @@ .logs__table .loading:before { min-height: 100%; } + +.logs__whois { + display: inline; + font-size: 12px; + white-space: nowrap; +} + +.logs__whois::after { + content: "|"; + padding: 0 5px; + opacity: 0.3; +} + +.logs__whois:last-child::after { + content: ""; +} + +.logs__whois-icon.icons { + position: relative; + top: -2px; + width: 12px; + height: 12px; + margin-right: 1px; + opacity: 0.5; +} diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index cb7f2914..ac613164 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next'; import ReactTable from 'react-table'; import { MODAL_TYPE } from '../../../helpers/constants'; -import { splitByNewLine } from '../../../helpers/helpers'; +import { splitByNewLine, countClientsStatistics } from '../../../helpers/helpers'; import Card from '../../ui/Card'; import Modal from './Modal'; import CellWrap from '../../ui/CellWrap'; @@ -204,7 +204,10 @@ class ClientsTable extends Component { { Header: this.props.t('requests_count'), id: 'statistics', - accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0, + accessor: (row) => countClientsStatistics( + row.ids, + this.props.normalizedTopClients.auto, + ), sortMethod: (a, b) => b - a, minWidth: 120, Cell: (row) => { diff --git a/client/src/components/Settings/Clients/whoisCell.js b/client/src/components/Settings/Clients/whoisCell.js index 94afd6cc..1a8b0484 100644 --- a/client/src/components/Settings/Clients/whoisCell.js +++ b/client/src/components/Settings/Clients/whoisCell.js @@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
{icon && ( - +   diff --git a/client/src/components/Settings/Dns/Upstream/Examples.js b/client/src/components/Settings/Dns/Upstream/Examples.js index de779b18..70797909 100644 --- a/client/src/components/Settings/Dns/Upstream/Examples.js +++ b/client/src/components/Settings/Dns/Upstream/Examples.js @@ -63,6 +63,27 @@ const Examples = (props) => ( +
  • + quic://dns-unfiltered.adguard.com:784 –  + + + DNS-over-QUIC + , + ]} + > + example_upstream_doq + +   + (experimental) + +
  • tcp://9.9.9.9example_upstream_tcp
  • diff --git a/client/src/components/Settings/Dns/Upstream/index.js b/client/src/components/Settings/Dns/Upstream/index.js index 4092638c..315a8aea 100644 --- a/client/src/components/Settings/Dns/Upstream/index.js +++ b/client/src/components/Settings/Dns/Upstream/index.js @@ -32,7 +32,7 @@ const Upstream = () => { dispatch(setDnsConfig(dnsConfig)); }; - const upstreamDns = upstream_dns_file ? t('configured_in', { path: upstream_dns_file }) : upstream_dns; + const upstreamDns = upstream_dns_file ? t('upstream_dns_configured_in_file', { path: upstream_dns_file }) : upstream_dns; return { const errors = {}; @@ -38,6 +42,7 @@ const clearFields = (change, setTlsConfig, t) => { certificate_path: '', port_https: STANDARD_HTTPS_PORT, port_dns_over_tls: DNS_OVER_TLS_PORT, + port_dns_over_quic: DNS_OVER_QUIC_PORT, server_name: '', force_https: false, enabled: false, @@ -189,6 +194,30 @@ let Form = (props) => {
    +
    +
    + + +
    + encryption_doq_desc +
    +
    +
    diff --git a/client/src/components/Settings/Encryption/index.js b/client/src/components/Settings/Encryption/index.js index 7c2cccc8..f7ca52e0 100644 --- a/client/src/components/Settings/Encryption/index.js +++ b/client/src/components/Settings/Encryption/index.js @@ -66,6 +66,7 @@ class Encryption extends Component { force_https, port_https, port_dns_over_tls, + port_dns_over_quic, certificate_chain, private_key, certificate_path, @@ -78,6 +79,7 @@ class Encryption extends Component { force_https, port_https, port_dns_over_tls, + port_dns_over_quic, certificate_chain, private_key, certificate_path, diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 3bf1a121..4efb0868 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -54,7 +54,7 @@ } .form__message--error { - color: var(--red); + color: #cd201f; } .form__message--left-pad { diff --git a/client/src/components/Toasts/Toast.js b/client/src/components/Toasts/Toast.js index a4c58aad..4c46078a 100644 --- a/client/src/components/Toasts/Toast.js +++ b/client/src/components/Toasts/Toast.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { useTranslation } from 'react-i18next'; +import { Trans } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { TOAST_TIMEOUTS } from '../../helpers/constants'; import { removeToast } from '../../actions'; @@ -9,8 +9,8 @@ const Toast = ({ id, message, type, + options, }) => { - const { t } = useTranslation(); const dispatch = useDispatch(); const [timerId, setTimerId] = useState(null); @@ -30,7 +30,12 @@ const Toast = ({ return
    -

    {t(message)}

    +

    + +