diff --git a/AGHTechDoc.md b/AGHTechDoc.md index cfe0304f..bd526fc8 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -27,6 +27,10 @@ Contents: * DNS access settings * List access settings * Set access settings +* Rewrites + * API: List rewrite entries + * API: Add a rewrite entry + * API: Remove a rewrite entry ## First startup @@ -682,3 +686,60 @@ Request: Response: 200 OK + + +## Rewrites + +This section allows the administrator to easily configure custom DNS response for a specific domain name. +A, AAAA and CNAME records are supported. + + +### API: List rewrite entries + +Request: + + GET /control/rewrite/list + +Response: + + 200 OK + + [ + { + domain: "..." + answer: "..." + } + ... + ] + + +### API: Add a rewrite entry + +Request: + + POST /control/rewrite/add + + { + domain: "..." + answer: "..." // "1.2.3.4" (A) || "::1" (AAAA) || "hostname" (CNAME) + } + +Response: + + 200 OK + + +### API: Remove a rewrite entry + +Request: + + POST /control/rewrite/delete + + { + domain: "..." + answer: "..." + } + +Response: + + 200 OK diff --git a/client/package-lock.json b/client/package-lock.json index 3edaa670..cedee41d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -945,12 +945,27 @@ } }, "axios": { - "version": "0.18.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", - "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", "requires": { - "follow-redirects": "^1.3.0", - "is-buffer": "^1.1.5" + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + }, + "dependencies": { + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } } }, "axobject-query": { @@ -5124,6 +5139,7 @@ "version": "1.5.7", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz", "integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==", + "dev": true, "requires": { "debug": "^3.1.0" } @@ -6933,7 +6949,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-builtin-module": { "version": "1.0.0", @@ -7386,9 +7403,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash-es": { "version": "4.17.10", @@ -7767,9 +7784,9 @@ } }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -10191,6 +10208,14 @@ } } }, + "react-router-hash-link": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-1.2.2.tgz", + "integrity": "sha512-LBthLVHdqPeKDVt3+cFRhy15Z7veikOvdKRZRfyBR2vjqIE7rxn+tKLjb6DOmLm6JpoQVemVDnxQ35RVnEHdQA==", + "requires": { + "prop-types": "^15.6.0" + } + }, "react-table": { "version": "6.8.6", "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz", @@ -10848,9 +10873,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -12478,38 +12503,15 @@ } }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "uniq": { diff --git a/client/package.json b/client/package.json index 05d91b7a..dc64d590 100644 --- a/client/package.json +++ b/client/package.json @@ -10,13 +10,13 @@ }, "dependencies": { "@nivo/line": "^0.49.1", - "axios": "^0.18.0", + "axios": "^0.18.1", "classnames": "^2.2.6", "date-fns": "^1.29.0", "file-saver": "^1.3.8", "i18next": "^12.0.0", "i18next-browser-languagedetector": "^2.2.3", - "lodash": "^4.17.11", + "lodash": "^4.17.15", "nanoid": "^1.2.3", "prop-types": "^15.6.1", "react": "^16.4.0", @@ -27,6 +27,7 @@ "react-redux": "^5.0.7", "react-redux-loading-bar": "^4.0.7", "react-router-dom": "^4.2.2", + "react-router-hash-link": "^1.2.2", "react-table": "^6.8.6", "react-transition-group": "^2.4.0", "redux": "^4.0.0", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index c20ff970..4aaac0e0 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -331,5 +331,18 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy0> supports <1>DNS-over-HTTPS1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox0> supports <1>DNS-over-HTTPS1>.", "setup_dns_privacy_other_5": "You will find more implementations <0>here0> and <1>here1>.", - "setup_dns_notice": "In order to use <1>DNS-over-HTTPS1> or <1>DNS-over-TLS1>, you need to <0>configure Encryption0> in AdGuard Home settings." -} \ No newline at end of file + "setup_dns_notice": "In order to use <1>DNS-over-HTTPS1> or <1>DNS-over-TLS1>, you need to <0>configure Encryption0> in AdGuard Home settings.", + "rewrite_added": "DNS rewrite for \"{{key}}\" successfully added", + "rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted", + "rewrite_add": "Add DNS rewrite", + "rewrite_not_found": "No DNS rewrites found", + "rewrite_confirm_delete": "Are you sure you want to delete DNS rewrite for \"{{key}}\"?", + "rewrite_desc": "Allows to easily configure custom DNS response for a specific domain name.", + "rewrite_applied": "Applied Rewrite rule", + "dns_rewrites": "DNS rewrites", + "form_domain": "Enter domain", + "form_answer": "Enter IP address or domain name", + "form_error_domain_format": "Invalid domain format", + "form_error_answer_format": "Invalid answer format", + "configure": "Configure" +} diff --git a/client/src/actions/rewrites.js b/client/src/actions/rewrites.js new file mode 100644 index 00000000..df846fdd --- /dev/null +++ b/client/src/actions/rewrites.js @@ -0,0 +1,58 @@ +import { createAction } from 'redux-actions'; +import { t } from 'i18next'; +import Api from '../api/Api'; +import { addErrorToast, addSuccessToast } from './index'; + +const apiClient = new Api(); + +export const toggleRewritesModal = createAction('TOGGLE_REWRITES_MODAL'); + +export const getRewritesListRequest = createAction('GET_REWRITES_LIST_REQUEST'); +export const getRewritesListFailure = createAction('GET_REWRITES_LIST_FAILURE'); +export const getRewritesListSuccess = createAction('GET_REWRITES_LIST_SUCCESS'); + +export const getRewritesList = () => async (dispatch) => { + dispatch(getRewritesListRequest()); + try { + const data = await apiClient.getRewritesList(); + dispatch(getRewritesListSuccess(data)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getRewritesListFailure()); + } +}; + +export const addRewriteRequest = createAction('ADD_REWRITE_REQUEST'); +export const addRewriteFailure = createAction('ADD_REWRITE_FAILURE'); +export const addRewriteSuccess = createAction('ADD_REWRITE_SUCCESS'); + +export const addRewrite = config => async (dispatch) => { + dispatch(addRewriteRequest()); + try { + await apiClient.addRewrite(config); + dispatch(addRewriteSuccess(config)); + dispatch(toggleRewritesModal()); + dispatch(getRewritesList()); + dispatch(addSuccessToast(t('rewrite_added', { key: config.domain }))); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(addRewriteFailure()); + } +}; + +export const deleteRewriteRequest = createAction('DELETE_REWRITE_REQUEST'); +export const deleteRewriteFailure = createAction('DELETE_REWRITE_FAILURE'); +export const deleteRewriteSuccess = createAction('DELETE_REWRITE_SUCCESS'); + +export const deleteRewrite = config => async (dispatch) => { + dispatch(deleteRewriteRequest()); + try { + await apiClient.deleteRewrite(config); + dispatch(deleteRewriteSuccess()); + dispatch(getRewritesList()); + dispatch(addSuccessToast(t('rewrite_deleted', { key: config.domain }))); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(deleteRewriteFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 76b17888..766cd499 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -481,4 +481,32 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // DNS rewrites + REWRITES_LIST = { path: 'rewrite/list', method: 'GET' }; + REWRITE_ADD = { path: 'rewrite/add', method: 'POST' }; + REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' }; + + getRewritesList() { + const { path, method } = this.REWRITES_LIST; + return this.makeRequest(path, method); + } + + addRewrite(config) { + const { path, method } = this.REWRITE_ADD; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + + deleteRewrite(config) { + const { path, method } = this.REWRITE_DELETE; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } } diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 1b39f592..3205e424 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -13,6 +13,12 @@ overflow: hidden; } +.logs__row--column { + flex-direction: column; + align-items: flex-start; + overflow: hidden; +} + .logs__row .list-unstyled { margin-bottom: 0; overflow: hidden; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 8a675a05..f8891206 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -5,6 +5,7 @@ import { saveAs } from 'file-saver/FileSaver'; import escapeRegExp from 'lodash/escapeRegExp'; import endsWith from 'lodash/endsWith'; import { Trans, withNamespaces } from 'react-i18next'; +import { HashLink as Link } from 'react-router-hash-link'; import { formatTime, getClientName } from '../../helpers/helpers'; import { getTrackerData } from '../../helpers/trackers/trackers'; @@ -125,6 +126,7 @@ class Logs extends Component { const rule = row && row.original && row.original.rule; const { filterId } = row.original; const { filters } = this.props.filtering; + const isRewrite = reason && reason === 'Rewrite'; let filterName = ''; if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') { @@ -161,14 +163,16 @@ class Logs extends Component { const isRenderTooltip = reason === 'NotFilteredWhiteList'; return ( -