From a5c0381c46026cdfd5624e38c3fd6bfa045b8895 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 16 Jun 2020 15:38:27 +0300 Subject: [PATCH 1/3] + dns: new settings for cache --- AGHTechDoc.md | 6 ++++++ dnsforward/dnsforward_http.go | 26 ++++++++++++++++++++++++++ openapi/openapi.yaml | 6 ++++++ 3 files changed, 38 insertions(+) diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 61013995..76ac5acc 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -894,6 +894,9 @@ Response: "dnssec_enabled": true | false "disable_ipv6": true | false, "upstream_mode": "" | "parallel" | "fastest_addr" + "cache_size": 1234, // in bytes + "cache_ttl_min": 1234, // in seconds + "cache_ttl_max": 1234, // in seconds } @@ -916,6 +919,9 @@ Request: "dnssec_enabled": true | false "disable_ipv6": true | false, "upstream_mode": "" | "parallel" | "fastest_addr" + "cache_size": 1234, // in bytes + "cache_ttl_min": 1234, // in seconds + "cache_ttl_max": 1234, // in seconds } Response: diff --git a/dnsforward/dnsforward_http.go b/dnsforward/dnsforward_http.go index cc13914d..96f2a14a 100644 --- a/dnsforward/dnsforward_http.go +++ b/dnsforward/dnsforward_http.go @@ -34,6 +34,9 @@ type dnsConfigJSON struct { DNSSECEnabled bool `json:"dnssec_enabled"` DisableIPv6 bool `json:"disable_ipv6"` UpstreamMode string `json:"upstream_mode"` + CacheSize uint32 `json:"cache_size"` + CacheMinTTL uint32 `json:"cache_ttl_min"` + CacheMaxTTL uint32 `json:"cache_ttl_max"` } func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { @@ -50,6 +53,9 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet resp.DNSSECEnabled = s.conf.EnableDNSSEC resp.DisableIPv6 = s.conf.AAAADisabled + resp.CacheSize = s.conf.CacheSize + resp.CacheMinTTL = s.conf.CacheMinTTL + resp.CacheMaxTTL = s.conf.CacheMaxTTL if s.conf.FastestAddr { resp.UpstreamMode = "fastest_addr" } else if s.conf.AllServers { @@ -126,6 +132,11 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) { return } + if req.CacheMinTTL > req.CacheMaxTTL { + httpError(r, w, http.StatusBadRequest, "cache_ttl_min must be less or equal than cache_ttl_max") + return + } + restart := false s.Lock() @@ -177,6 +188,21 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) { s.conf.AAAADisabled = req.DisableIPv6 } + if js.Exists("cache_size") { + s.conf.CacheSize = req.CacheSize + restart = true + } + + if js.Exists("cache_ttl_min") { + s.conf.CacheMinTTL = req.CacheMinTTL + restart = true + } + + if js.Exists("cache_ttl_max") { + s.conf.CacheMaxTTL = req.CacheMaxTTL + restart = true + } + if js.Exists("upstream_mode") { s.conf.FastestAddr = false s.conf.AllServers = false diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index a4066b7e..134138d7 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1019,6 +1019,12 @@ components: type: boolean dnssec_enabled: type: boolean + cache_size: + type: integer + cache_ttl_min: + type: integer + cache_ttl_max: + type: integer upstream_mode: enum: - "" From 2d5287fcf31dfa7b484da9d01cbdc9f18584f56a Mon Sep 17 00:00:00 2001 From: ArtemBaskal Date: Fri, 3 Jul 2020 16:53:53 +0300 Subject: [PATCH 2/3] Add DNS cache setting UI --- client/src/__locales/en.json | 15 ++- .../src/components/Settings/Dns/Cache/Form.js | 100 ++++++++++++++++++ .../components/Settings/Dns/Cache/index.js | 42 ++++++++ client/src/components/Settings/Dns/index.js | 7 +- client/src/helpers/constants.js | 1 + client/src/helpers/form.js | 15 ++- client/src/helpers/helpers.js | 12 +++ 7 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 client/src/components/Settings/Dns/Cache/Form.js create mode 100644 client/src/components/Settings/Dns/Cache/index.js diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index f1178b81..0e82a444 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -206,6 +206,8 @@ "anonymize_client_ip": "Anonymize client IP", "anonymize_client_ip_desc": "Don't save the full IP address of the client in logs and statistics", "dns_config": "DNS server configuration", + "dns_cache_config": "DNS cache configuration", + "dns_cache_config_desc": "Here you can configure DNS cache", "blocking_mode": "Blocking mode", "default": "Default", "nxdomain": "NXDOMAIN", @@ -491,5 +493,16 @@ "list_updated": "{{count}} list updated", "list_updated_plural": "{{count}} lists updated", "dnssec_enable": "Enable DNSSEC", - "dnssec_enable_desc": "Set DNSSEC flag in the outcoming DNS queries and check the result (DNSSEC-enabled resolver is required)" + "dnssec_enable_desc": "Set DNSSEC flag in the outcoming DNS queries and check the result (DNSSEC-enabled resolver is required)", + "cache_size": "Cache size", + "cache_size_desc": "DNS cache size (in bytes)", + "cache_ttl_min_override": "Override minimum TTL", + "cache_ttl_max_override": "Override maximum TTL", + "enter_cache_size": "Enter cache size", + "enter_cache_ttl_min_override": "Enter minimum TTL", + "enter_cache_ttl_max_override": "Enter maximum TTL", + "cache_ttl_min_override_desc": "Override TTL value (minimum) received from upstream server. This value can't larger than 3600 (1 hour)", + "cache_ttl_max_override_desc": "Override TTL value (maximum) received from upstream server", + "min_exceeds_max_value": "Minimum value exceeds maximum value", + "value_not_larger_than": "Value can't be larger than {{maximum}}" } diff --git a/client/src/components/Settings/Dns/Cache/Form.js b/client/src/components/Settings/Dns/Cache/Form.js new file mode 100644 index 00000000..c1314e89 --- /dev/null +++ b/client/src/components/Settings/Dns/Cache/Form.js @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { Trans, useTranslation } from 'react-i18next'; +import { shallowEqual, useSelector } from 'react-redux'; +import { + biggerOrEqualZero, + maxValue, + renderInputField, + required, + toNumber, +} from '../../../../helpers/form'; +import { FORM_NAME } from '../../../../helpers/constants'; + +const maxValue3600 = maxValue(3600); + +const getInputFields = ({ required, maxValue3600 }) => [{ + name: 'cache_size', + title: 'cache_size', + description: 'cache_size_desc', + placeholder: 'enter_cache_size', + validate: required, +}, +{ + name: 'cache_ttl_min', + title: 'cache_ttl_min_override', + description: 'cache_ttl_min_override_desc', + placeholder: 'enter_cache_ttl_min_override', + max: 3600, + validate: maxValue3600, +}, +{ + name: 'cache_ttl_max', + title: 'cache_ttl_max_override', + description: 'cache_ttl_max_override_desc', + placeholder: 'enter_cache_ttl_max_override', +}]; + +const Form = ({ + handleSubmit, submitting, invalid, +}) => { + const { t } = useTranslation(); + + const { processingSetConfig } = useSelector((state) => state.dnsConfig, shallowEqual); + const { + cache_ttl_max, cache_ttl_min, + } = useSelector((state) => state.form[FORM_NAME.CACHE].values, shallowEqual); + + const minExceedsMax = cache_ttl_min > cache_ttl_max; + + const INPUTS_FIELDS = getInputFields({ + required, + maxValue3600, + }); + + return
+
+ {INPUTS_FIELDS.map(({ + name, title, description, placeholder, validate, max, + }) =>
+
+
+ +
{t(description)}
+ +
+
+
)} + {minExceedsMax + && {t('min_exceeds_max_value')}} +
+ +
; +}; + +Form.propTypes = { + handleSubmit: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + invalid: PropTypes.bool.isRequired, +}; + +export default reduxForm({ form: FORM_NAME.CACHE })(Form); diff --git a/client/src/components/Settings/Dns/Cache/index.js b/client/src/components/Settings/Dns/Cache/index.js new file mode 100644 index 00000000..22b8b7b6 --- /dev/null +++ b/client/src/components/Settings/Dns/Cache/index.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import Card from '../../../ui/Card'; +import Form from './Form'; +import { setDnsConfig } from '../../../../actions/dnsConfig'; +import { selectCompletedFields } from '../../../../helpers/helpers'; + +const CacheConfig = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { + cache_size, cache_ttl_max, cache_ttl_min, + } = useSelector((state) => state.dnsConfig, shallowEqual); + + const handleFormSubmit = (values) => { + const completedFields = selectCompletedFields(values); + dispatch(setDnsConfig(completedFields)); + }; + + return ( + +
+
+
+
+ ); +}; + +export default CacheConfig; diff --git a/client/src/components/Settings/Dns/index.js b/client/src/components/Settings/Dns/index.js index 5ed8f9ca..41993f89 100644 --- a/client/src/components/Settings/Dns/index.js +++ b/client/src/components/Settings/Dns/index.js @@ -7,9 +7,10 @@ import Access from './Access'; import Config from './Config'; import PageTitle from '../../ui/PageTitle'; import Loading from '../../ui/Loading'; +import CacheConfig from './Cache'; const Dns = (props) => { - const [t] = useTranslation(); + const { t } = useTranslation(); useEffect(() => { props.getAccessList(); @@ -40,6 +41,10 @@ const Dns = (props) => { dnsConfig={dnsConfig} setDnsConfig={setDnsConfig} /> + } diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 60c22264..9d587d9e 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -406,4 +406,5 @@ export const FORM_NAME = { STATS_CONFIG: 'statsConfig', INSTALL: 'install', LOGIN: 'login', + CACHE: 'cache', }; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index bace77b5..f28ec5f3 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -1,6 +1,7 @@ import React, { Fragment } from 'react'; import { Trans } from 'react-i18next'; import PropTypes from 'prop-types'; +import i18next from 'i18next'; import { R_IPV4, R_MAC, R_HOST, R_IPV6, R_CIDR, R_CIDR_IPV6, UNSAFE_PORTS, R_URL_REQUIRES_PROTOCOL, R_WIN_ABSOLUTE_PATH, R_UNIX_ABSOLUTE_PATH, @@ -10,7 +11,7 @@ import { createOnBlurHandler } from './helpers'; export const renderField = (props, elementType) => { const { input, id, className, placeholder, type, disabled, normalizeOnBlur, - autoComplete, meta: { touched, error }, + autoComplete, meta: { touched, error }, min, max, step, } = props; const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur); @@ -23,14 +24,17 @@ export const renderField = (props, elementType) => { autoComplete, disabled, type, + min, + max, + step, onBlur, }); return ( - + <> {element} {!disabled && touched && error && {error}} - + ); }; @@ -43,6 +47,9 @@ renderField.propTypes = { disabled: PropTypes.bool, autoComplete: PropTypes.bool, normalizeOnBlur: PropTypes.func, + min: PropTypes.number, + max: PropTypes.number, + step: PropTypes.number, meta: PropTypes.shape({ touched: PropTypes.bool, error: PropTypes.object, @@ -238,6 +245,8 @@ export const required = (value) => { return form_error_required; }; +export const maxValue = (maximum) => (value) => (value && value > maximum ? i18next.t('value_not_larger_than', { maximum }) : undefined); + export const ipv4 = (value) => { if (value && !R_IPV4.test(value)) { return form_error_ip4_format; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index be8e884e..3a655ac3 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -562,3 +562,15 @@ export const getIpMatchListStatus = (ip, list) => { return IP_MATCH_LIST_STATUS.NOT_FOUND; } }; + +/** + * @param values {object} + * @returns {object} + */ +export const selectCompletedFields = (values) => Object.entries(values) + .reduce((acc, [key, value]) => { + if (value || value === 0) { + acc[key] = value; + } + return acc; + }, {}); From 0c4905fa2b412189cc7901d810d518337ca466c7 Mon Sep 17 00:00:00 2001 From: ArtemBaskal Date: Fri, 3 Jul 2020 19:10:05 +0300 Subject: [PATCH 3/3] Extract validation functions in the separate file --- client/src/components/Filters/Form.js | 8 +- .../src/components/Filters/Rewrites/Form.js | 10 +- .../src/components/Settings/Clients/Form.js | 7 +- client/src/components/Settings/Dhcp/Form.js | 18 +- .../Settings/Dhcp/StaticLeases/Form.js | 10 +- .../src/components/Settings/Dns/Cache/Form.js | 27 +-- .../components/Settings/Dns/Config/Form.js | 15 +- .../components/Settings/Encryption/Form.js | 11 +- client/src/helpers/constants.js | 2 + client/src/helpers/form.js | 148 +------------ client/src/helpers/validators.js | 209 ++++++++++++++++++ client/src/install/Setup/Settings.js | 9 +- client/src/login/Login/Form.js | 8 +- 13 files changed, 275 insertions(+), 207 deletions(-) create mode 100644 client/src/helpers/validators.js diff --git a/client/src/components/Filters/Form.js b/client/src/components/Filters/Form.js index 46e0579d..0237a5d6 100644 --- a/client/src/components/Filters/Form.js +++ b/client/src/components/Filters/Form.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { Field, reduxForm } from 'redux-form'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; - -import { renderInputField, required, isValidPath } from '../../helpers/form'; +import { renderInputField } from '../../helpers/form'; +import { validatePath, validateRequiredValue } from '../../helpers/validators'; import { FORM_NAME } from '../../helpers/constants'; const Form = (props) => { @@ -28,7 +28,7 @@ const Form = (props) => { component={renderInputField} className="form-control" placeholder={t('enter_name_hint')} - validate={[required]} + validate={[validateRequiredValue]} normalizeOnBlur={(data) => data.trim()} /> @@ -40,7 +40,7 @@ const Form = (props) => { component={renderInputField} className="form-control" placeholder={t('enter_url_or_path_hint')} - validate={[required, isValidPath]} + validate={[validateRequiredValue, validatePath]} normalizeOnBlur={(data) => data.trim()} /> diff --git a/client/src/components/Filters/Rewrites/Form.js b/client/src/components/Filters/Rewrites/Form.js index 5382ee32..2db7be35 100644 --- a/client/src/components/Filters/Rewrites/Form.js +++ b/client/src/components/Filters/Rewrites/Form.js @@ -3,10 +3,8 @@ import PropTypes from 'prop-types'; import { Field, reduxForm } from 'redux-form'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; - -import { - renderInputField, required, domain, answer, -} from '../../../helpers/form'; +import { renderInputField } from '../../../helpers/form'; +import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators'; import { FORM_NAME } from '../../../helpers/constants'; const Form = (props) => { @@ -34,7 +32,7 @@ const Form = (props) => { type="text" className="form-control" placeholder={t('form_domain')} - validate={[required, domain]} + validate={[validateRequiredValue, validateDomain]} /> @@ -61,7 +59,7 @@ const Form = (props) => { type="text" className="form-control" placeholder={t('form_answer')} - validate={[required, answer]} + validate={[validateRequiredValue, validateAnswer]} /> diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 65c16cb3..5c7bf471 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -13,13 +13,12 @@ import Tabs from '../../ui/Tabs'; import Examples from '../Dns/Upstream/Examples'; import { toggleAllServices } from '../../../helpers/helpers'; import { - required, - clientId, renderInputField, renderGroupField, renderSelectField, renderServiceField, } from '../../../helpers/form'; +import { validateClientId, validateRequiredValue } from '../../../helpers/validators'; import { FORM_NAME, SERVICES } from '../../../helpers/constants'; import './Service.css'; @@ -48,12 +47,12 @@ const settingsCheckboxes = [ const validate = (values) => { const errors = {}; const { name, ids } = values; - errors.name = required(name); + errors.name = validateRequiredValue(name); if (ids && ids.length) { const idArrayErrors = []; ids.forEach((id, idx) => { - idArrayErrors[idx] = required(id) || clientId(id); + idArrayErrors[idx] = validateRequiredValue(id) || validateClientId(id); }); if (idArrayErrors.length) { diff --git a/client/src/components/Settings/Dhcp/Form.js b/client/src/components/Settings/Dhcp/Form.js index 6e0a134f..6fd8c40e 100644 --- a/client/src/components/Settings/Dhcp/Form.js +++ b/client/src/components/Settings/Dhcp/Form.js @@ -4,11 +4,9 @@ import PropTypes from 'prop-types'; import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; - -import { - renderInputField, required, ipv4, isPositive, toNumber, -} from '../../../helpers/form'; +import { renderInputField, toNumber } from '../../../helpers/form'; import { FORM_NAME } from '../../../helpers/constants'; +import { validateIpv4, validateIsPositiveValue, validateRequiredValue } from '../../../helpers/validators'; const renderInterfaces = ((interfaces) => ( Object.keys(interfaces).map((item) => { @@ -96,7 +94,7 @@ let Form = (props) => { name="interface_name" component="select" className="form-control custom-select" - validate={[required]} + validate={[validateRequiredValue]} >