Merge: + DNS: Add Rewrites section
Close #818 * commit 'e95aae5744ffd1de0809c95a8df50ea3f5f1af28': + client: handle DNS rewrites + openapi: add /rewrite/* methods + dnsforward: use Rewrites table + control: add /rewrite/* handlers + doc: add Rewrites section * dnsfilter: refactor: a simple approach to convert Reason to string
This commit is contained in:
commit
4ae8c799ee
@ -27,6 +27,10 @@ Contents:
|
|||||||
* DNS access settings
|
* DNS access settings
|
||||||
* List access settings
|
* List access settings
|
||||||
* Set access settings
|
* Set access settings
|
||||||
|
* Rewrites
|
||||||
|
* API: List rewrite entries
|
||||||
|
* API: Add a rewrite entry
|
||||||
|
* API: Remove a rewrite entry
|
||||||
|
|
||||||
|
|
||||||
## First startup
|
## First startup
|
||||||
@ -682,3 +686,60 @@ Request:
|
|||||||
Response:
|
Response:
|
||||||
|
|
||||||
200 OK
|
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
|
||||||
|
86
client/package-lock.json
generated
vendored
86
client/package-lock.json
generated
vendored
@ -945,12 +945,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.1",
|
||||||
"resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
||||||
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
|
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.3.0",
|
"follow-redirects": "1.5.10",
|
||||||
"is-buffer": "^1.1.5"
|
"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": {
|
"axobject-query": {
|
||||||
@ -5124,6 +5139,7 @@
|
|||||||
"version": "1.5.7",
|
"version": "1.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz",
|
||||||
"integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==",
|
"integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "^3.1.0"
|
"debug": "^3.1.0"
|
||||||
}
|
}
|
||||||
@ -6933,7 +6949,8 @@
|
|||||||
"is-buffer": {
|
"is-buffer": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
"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": {
|
"is-builtin-module": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -7386,9 +7403,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.11",
|
"version": "4.17.15",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||||
},
|
},
|
||||||
"lodash-es": {
|
"lodash-es": {
|
||||||
"version": "4.17.10",
|
"version": "4.17.10",
|
||||||
@ -7767,9 +7784,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mixin-deep": {
|
"mixin-deep": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||||
"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
|
"integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"for-in": "^1.0.2",
|
"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": {
|
"react-table": {
|
||||||
"version": "6.8.6",
|
"version": "6.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz",
|
||||||
@ -10848,9 +10873,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"set-value": {
|
"set-value": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||||
"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
|
"integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"extend-shallow": "^2.0.1",
|
"extend-shallow": "^2.0.1",
|
||||||
@ -12478,38 +12503,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"union-value": {
|
"union-value": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||||
"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
|
"integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"arr-union": "^3.1.0",
|
"arr-union": "^3.1.0",
|
||||||
"get-value": "^2.0.6",
|
"get-value": "^2.0.6",
|
||||||
"is-extendable": "^0.1.1",
|
"is-extendable": "^0.1.1",
|
||||||
"set-value": "^0.4.3"
|
"set-value": "^2.0.1"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uniq": {
|
"uniq": {
|
||||||
|
5
client/package.json
vendored
5
client/package.json
vendored
@ -10,13 +10,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nivo/line": "^0.49.1",
|
"@nivo/line": "^0.49.1",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"date-fns": "^1.29.0",
|
"date-fns": "^1.29.0",
|
||||||
"file-saver": "^1.3.8",
|
"file-saver": "^1.3.8",
|
||||||
"i18next": "^12.0.0",
|
"i18next": "^12.0.0",
|
||||||
"i18next-browser-languagedetector": "^2.2.3",
|
"i18next-browser-languagedetector": "^2.2.3",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.15",
|
||||||
"nanoid": "^1.2.3",
|
"nanoid": "^1.2.3",
|
||||||
"prop-types": "^15.6.1",
|
"prop-types": "^15.6.1",
|
||||||
"react": "^16.4.0",
|
"react": "^16.4.0",
|
||||||
@ -27,6 +27,7 @@
|
|||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-redux-loading-bar": "^4.0.7",
|
"react-redux-loading-bar": "^4.0.7",
|
||||||
"react-router-dom": "^4.2.2",
|
"react-router-dom": "^4.2.2",
|
||||||
|
"react-router-hash-link": "^1.2.2",
|
||||||
"react-table": "^6.8.6",
|
"react-table": "^6.8.6",
|
||||||
"react-transition-group": "^2.4.0",
|
"react-transition-group": "^2.4.0",
|
||||||
"redux": "^4.0.0",
|
"redux": "^4.0.0",
|
||||||
|
@ -331,5 +331,18 @@
|
|||||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.",
|
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.",
|
||||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.",
|
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.",
|
||||||
"setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.",
|
"setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.",
|
||||||
"setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings."
|
"setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> 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"
|
||||||
|
}
|
||||||
|
58
client/src/actions/rewrites.js
Normal file
58
client/src/actions/rewrites.js
Normal file
@ -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());
|
||||||
|
}
|
||||||
|
};
|
@ -481,4 +481,32 @@ export default class Api {
|
|||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,12 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs__row--column {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.logs__row .list-unstyled {
|
.logs__row .list-unstyled {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -5,6 +5,7 @@ import { saveAs } from 'file-saver/FileSaver';
|
|||||||
import escapeRegExp from 'lodash/escapeRegExp';
|
import escapeRegExp from 'lodash/escapeRegExp';
|
||||||
import endsWith from 'lodash/endsWith';
|
import endsWith from 'lodash/endsWith';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import { HashLink as Link } from 'react-router-hash-link';
|
||||||
|
|
||||||
import { formatTime, getClientName } from '../../helpers/helpers';
|
import { formatTime, getClientName } from '../../helpers/helpers';
|
||||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||||
@ -125,6 +126,7 @@ class Logs extends Component {
|
|||||||
const rule = row && row.original && row.original.rule;
|
const rule = row && row.original && row.original.rule;
|
||||||
const { filterId } = row.original;
|
const { filterId } = row.original;
|
||||||
const { filters } = this.props.filtering;
|
const { filters } = this.props.filtering;
|
||||||
|
const isRewrite = reason && reason === 'Rewrite';
|
||||||
let filterName = '';
|
let filterName = '';
|
||||||
|
|
||||||
if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') {
|
if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') {
|
||||||
@ -161,14 +163,16 @@ class Logs extends Component {
|
|||||||
const isRenderTooltip = reason === 'NotFilteredWhiteList';
|
const isRenderTooltip = reason === 'NotFilteredWhiteList';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs__row">
|
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
|
||||||
|
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
|
||||||
<ul className="list-unstyled">{liNodes}</ul>
|
<ul className="list-unstyled">{liNodes}</ul>
|
||||||
{this.renderTooltip(isRenderTooltip, rule, filterName)}
|
{this.renderTooltip(isRenderTooltip, rule, filterName)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="logs__row">
|
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
|
||||||
|
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
|
||||||
<span><Trans>empty_response_status</Trans></span>
|
<span><Trans>empty_response_status</Trans></span>
|
||||||
{this.renderTooltip(isFiltered, rule, filterName)}
|
{this.renderTooltip(isFiltered, rule, filterName)}
|
||||||
</div>
|
</div>
|
||||||
@ -197,6 +201,7 @@ class Logs extends Component {
|
|||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const { reason } = row.original;
|
const { reason } = row.original;
|
||||||
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
||||||
|
const isRewrite = reason && reason === 'Rewrite';
|
||||||
const clientName = getClientName(dashboard.clients, row.value)
|
const clientName = getClientName(dashboard.clients, row.value)
|
||||||
|| getClientName(dashboard.autoClients, row.value);
|
|| getClientName(dashboard.autoClients, row.value);
|
||||||
let client;
|
let client;
|
||||||
@ -207,6 +212,21 @@ class Logs extends Component {
|
|||||||
client = row.value;
|
client = row.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRewrite) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="logs__row">
|
||||||
|
{client}
|
||||||
|
</div>
|
||||||
|
<div className="logs__action">
|
||||||
|
<Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary">
|
||||||
|
<Trans>configure</Trans>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="logs__row">
|
<div className="logs__row">
|
||||||
@ -261,6 +281,10 @@ class Logs extends Component {
|
|||||||
return {
|
return {
|
||||||
className: 'green',
|
className: 'green',
|
||||||
};
|
};
|
||||||
|
} else if (rowInfo.original.reason === 'Rewrite') {
|
||||||
|
return {
|
||||||
|
className: 'blue',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
89
client/src/components/Settings/Dns/Rewrites/Form.js
Normal file
89
client/src/components/Settings/Dns/Rewrites/Form.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Field, reduxForm } from 'redux-form';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
|
import { renderField, required, domain, answer } from '../../../../helpers/form';
|
||||||
|
|
||||||
|
const Form = (props) => {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
pristine,
|
||||||
|
submitting,
|
||||||
|
toggleRewritesModal,
|
||||||
|
processingAdd,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="domain"
|
||||||
|
name="domain"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_domain')}
|
||||||
|
validate={[required, domain]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="answer"
|
||||||
|
name="answer"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_answer')}
|
||||||
|
validate={[required, answer]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-standard"
|
||||||
|
disabled={submitting || processingAdd}
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
toggleRewritesModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>cancel_btn</Trans>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={submitting || pristine || processingAdd}
|
||||||
|
>
|
||||||
|
<Trans>save_btn</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Form.propTypes = {
|
||||||
|
pristine: PropTypes.bool.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
reset: PropTypes.func.isRequired,
|
||||||
|
toggleRewritesModal: PropTypes.func.isRequired,
|
||||||
|
submitting: PropTypes.bool.isRequired,
|
||||||
|
processingAdd: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default flow([
|
||||||
|
withNamespaces(),
|
||||||
|
reduxForm({
|
||||||
|
form: 'rewritesForm',
|
||||||
|
enableReinitialize: true,
|
||||||
|
}),
|
||||||
|
])(Form);
|
52
client/src/components/Settings/Dns/Rewrites/Modal.js
Normal file
52
client/src/components/Settings/Dns/Rewrites/Modal.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import ReactModal from 'react-modal';
|
||||||
|
|
||||||
|
import Form from './Form';
|
||||||
|
|
||||||
|
const Modal = (props) => {
|
||||||
|
const {
|
||||||
|
isModalOpen,
|
||||||
|
handleSubmit,
|
||||||
|
toggleRewritesModal,
|
||||||
|
processingAdd,
|
||||||
|
processingDelete,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactModal
|
||||||
|
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
||||||
|
closeTimeoutMS={0}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onRequestClose={() => toggleRewritesModal()}
|
||||||
|
>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h4 className="modal-title">
|
||||||
|
<Trans>Add DNS rewrite</Trans>
|
||||||
|
</h4>
|
||||||
|
<button type="button" className="close" onClick={() => toggleRewritesModal()}>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
toggleRewritesModal={toggleRewritesModal}
|
||||||
|
processingAdd={processingAdd}
|
||||||
|
processingDelete={processingDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ReactModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.propTypes = {
|
||||||
|
isModalOpen: PropTypes.bool.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
toggleRewritesModal: PropTypes.func.isRequired,
|
||||||
|
processingAdd: PropTypes.bool.isRequired,
|
||||||
|
processingDelete: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(Modal);
|
87
client/src/components/Settings/Dns/Rewrites/Table.js
Normal file
87
client/src/components/Settings/Dns/Rewrites/Table.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactTable from 'react-table';
|
||||||
|
import { withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
|
class Table extends Component {
|
||||||
|
cellWrap = ({ value }) => (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text" title={value}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
Header: 'Domain',
|
||||||
|
accessor: 'domain',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Answer',
|
||||||
|
accessor: 'answer',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: this.props.t('actions_table_header'),
|
||||||
|
accessor: 'actions',
|
||||||
|
maxWidth: 100,
|
||||||
|
Cell: value => (
|
||||||
|
<div className="logs__row logs__row--center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||||
|
onClick={() =>
|
||||||
|
this.props.handleDelete({
|
||||||
|
answer: value.row.answer,
|
||||||
|
domain: value.row.domain,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
title={this.props.t('delete_table_action')}
|
||||||
|
>
|
||||||
|
<svg className="icons">
|
||||||
|
<use xlinkHref="#delete" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
t, list, processing, processingAdd, processingDelete,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactTable
|
||||||
|
data={list || []}
|
||||||
|
columns={this.columns}
|
||||||
|
loading={processing || processingAdd || processingDelete}
|
||||||
|
className="-striped -highlight card-table-overflow"
|
||||||
|
showPagination={true}
|
||||||
|
defaultPageSize={10}
|
||||||
|
minRows={5}
|
||||||
|
previousText={t('previous_btn')}
|
||||||
|
nextText={t('next_btn')}
|
||||||
|
loadingText={t('loading_table_status')}
|
||||||
|
pageText={t('page_table_footer_text')}
|
||||||
|
ofText={t('of_table_footer_text')}
|
||||||
|
rowsText={t('rows_table_footer_text')}
|
||||||
|
noDataText={t('rewrite_not_found')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
list: PropTypes.array.isRequired,
|
||||||
|
processing: PropTypes.bool.isRequired,
|
||||||
|
processingAdd: PropTypes.bool.isRequired,
|
||||||
|
processingDelete: PropTypes.bool.isRequired,
|
||||||
|
handleDelete: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(Table);
|
83
client/src/components/Settings/Dns/Rewrites/index.js
Normal file
83
client/src/components/Settings/Dns/Rewrites/index.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
|
import Table from './Table';
|
||||||
|
import Modal from './Modal';
|
||||||
|
import Card from '../../../ui/Card';
|
||||||
|
|
||||||
|
class Rewrites extends Component {
|
||||||
|
handleSubmit = (values) => {
|
||||||
|
this.props.addRewrite(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDelete = (values) => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) {
|
||||||
|
this.props.deleteRewrite(values);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
rewrites,
|
||||||
|
toggleRewritesModal,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
list,
|
||||||
|
isModalOpen,
|
||||||
|
processing,
|
||||||
|
processingAdd,
|
||||||
|
processingDelete,
|
||||||
|
} = rewrites;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
id="rewrites"
|
||||||
|
title={t('dns_rewrites')}
|
||||||
|
subtitle={t('rewrite_desc')}
|
||||||
|
bodyType="card-body box-body--settings"
|
||||||
|
>
|
||||||
|
<Fragment>
|
||||||
|
<Table
|
||||||
|
list={list}
|
||||||
|
processing={processing}
|
||||||
|
processingAdd={processingAdd}
|
||||||
|
processingDelete={processingDelete}
|
||||||
|
handleDelete={this.handleDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-success btn-standard mt-3"
|
||||||
|
onClick={() => toggleRewritesModal()}
|
||||||
|
disabled={processingAdd}
|
||||||
|
>
|
||||||
|
<Trans>rewrite_add</Trans>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
toggleRewritesModal={toggleRewritesModal}
|
||||||
|
handleSubmit={this.handleSubmit}
|
||||||
|
processingAdd={processingAdd}
|
||||||
|
processingDelete={processingDelete}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rewrites.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
getRewritesList: PropTypes.func.isRequired,
|
||||||
|
toggleRewritesModal: PropTypes.func.isRequired,
|
||||||
|
addRewrite: PropTypes.func.isRequired,
|
||||||
|
deleteRewrite: PropTypes.func.isRequired,
|
||||||
|
rewrites: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(Rewrites);
|
@ -4,12 +4,14 @@ import { withNamespaces } from 'react-i18next';
|
|||||||
|
|
||||||
import Upstream from './Upstream';
|
import Upstream from './Upstream';
|
||||||
import Access from './Access';
|
import Access from './Access';
|
||||||
|
import Rewrites from './Rewrites';
|
||||||
import PageTitle from '../../ui/PageTitle';
|
import PageTitle from '../../ui/PageTitle';
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
|
|
||||||
class Dns extends Component {
|
class Dns extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.getAccessList();
|
this.props.getAccessList();
|
||||||
|
this.props.getRewritesList();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -18,9 +20,14 @@ class Dns extends Component {
|
|||||||
dashboard,
|
dashboard,
|
||||||
settings,
|
settings,
|
||||||
access,
|
access,
|
||||||
|
rewrites,
|
||||||
setAccessList,
|
setAccessList,
|
||||||
testUpstream,
|
testUpstream,
|
||||||
setUpstream,
|
setUpstream,
|
||||||
|
getRewritesList,
|
||||||
|
addRewrite,
|
||||||
|
deleteRewrite,
|
||||||
|
toggleRewritesModal,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -39,6 +46,13 @@ class Dns extends Component {
|
|||||||
testUpstream={testUpstream}
|
testUpstream={testUpstream}
|
||||||
/>
|
/>
|
||||||
<Access access={access} setAccessList={setAccessList} />
|
<Access access={access} setAccessList={setAccessList} />
|
||||||
|
<Rewrites
|
||||||
|
rewrites={rewrites}
|
||||||
|
getRewritesList={getRewritesList}
|
||||||
|
addRewrite={addRewrite}
|
||||||
|
deleteRewrite={deleteRewrite}
|
||||||
|
toggleRewritesModal={toggleRewritesModal}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -54,6 +68,11 @@ Dns.propTypes = {
|
|||||||
getAccessList: PropTypes.func.isRequired,
|
getAccessList: PropTypes.func.isRequired,
|
||||||
setAccessList: PropTypes.func.isRequired,
|
setAccessList: PropTypes.func.isRequired,
|
||||||
access: PropTypes.object.isRequired,
|
access: PropTypes.object.isRequired,
|
||||||
|
rewrites: PropTypes.object.isRequired,
|
||||||
|
getRewritesList: PropTypes.func.isRequired,
|
||||||
|
addRewrite: PropTypes.func.isRequired,
|
||||||
|
deleteRewrite: PropTypes.func.isRequired,
|
||||||
|
toggleRewritesModal: PropTypes.func.isRequired,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import './Card.css';
|
import './Card.css';
|
||||||
|
|
||||||
const Card = props => (
|
const Card = props => (
|
||||||
<div className={props.type ? `card ${props.type}` : 'card'}>
|
<div className={props.type ? `card ${props.type}` : 'card'} id={props.id ? props.id : ''}>
|
||||||
{props.title &&
|
{props.title &&
|
||||||
<div className="card-header with-border">
|
<div className="card-header with-border">
|
||||||
<div className="card-inner">
|
<div className="card-inner">
|
||||||
@ -30,6 +30,7 @@ const Card = props => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
Card.propTypes = {
|
Card.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
subtitle: PropTypes.string,
|
subtitle: PropTypes.string,
|
||||||
bodyType: PropTypes.string,
|
bodyType: PropTypes.string,
|
||||||
|
@ -15,3 +15,7 @@
|
|||||||
.rt-tr-group .green {
|
.rt-tr-group .green {
|
||||||
background-color: #f1faf3;
|
background-color: #f1faf3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rt-tr-group .blue {
|
||||||
|
background-color: #ecf7ff;
|
||||||
|
}
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { handleUpstreamChange, setUpstream, testUpstream } from '../actions';
|
import { handleUpstreamChange, setUpstream, testUpstream } from '../actions';
|
||||||
import { getAccessList, setAccessList } from '../actions/access';
|
import { getAccessList, setAccessList } from '../actions/access';
|
||||||
|
import {
|
||||||
|
getRewritesList,
|
||||||
|
addRewrite,
|
||||||
|
deleteRewrite,
|
||||||
|
toggleRewritesModal,
|
||||||
|
} from '../actions/rewrites';
|
||||||
import Dns from '../components/Settings/Dns';
|
import Dns from '../components/Settings/Dns';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
const { dashboard, settings, access } = state;
|
const {
|
||||||
|
dashboard, settings, access, rewrites,
|
||||||
|
} = state;
|
||||||
const props = {
|
const props = {
|
||||||
dashboard,
|
dashboard,
|
||||||
settings,
|
settings,
|
||||||
access,
|
access,
|
||||||
|
rewrites,
|
||||||
};
|
};
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
@ -19,6 +28,10 @@ const mapDispatchToProps = {
|
|||||||
testUpstream,
|
testUpstream,
|
||||||
getAccessList,
|
getAccessList,
|
||||||
setAccessList,
|
setAccessList,
|
||||||
|
getRewritesList,
|
||||||
|
addRewrite,
|
||||||
|
deleteRewrite,
|
||||||
|
toggleRewritesModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/;
|
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/;
|
||||||
|
export const R_HOST = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$/;
|
||||||
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
|
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
|
||||||
|
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]))/g;
|
||||||
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]))$/g;
|
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]))$/g;
|
||||||
|
|
||||||
export const STATS_NAMES = {
|
export const STATS_NAMES = {
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
import { R_IPV4, R_MAC, UNSAFE_PORTS } from '../helpers/constants';
|
import { R_IPV4, R_MAC, R_HOST, R_IPV6, UNSAFE_PORTS } from '../helpers/constants';
|
||||||
|
|
||||||
export const renderField = ({
|
export const renderField = ({
|
||||||
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
input,
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
meta: { touched, error },
|
||||||
}) => (
|
}) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<input
|
<input
|
||||||
@ -15,7 +21,9 @@ export const renderField = ({
|
|||||||
className={className}
|
className={className}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
{!disabled &&
|
||||||
|
touched &&
|
||||||
|
(error && <span className="form__message form__message--error">{error}</span>)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -24,20 +32,17 @@ export const renderSelectField = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<label className="checkbox checkbox--form">
|
<label className="checkbox checkbox--form">
|
||||||
<span className="checkbox__marker"/>
|
<span className="checkbox__marker" />
|
||||||
<input
|
<input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
|
||||||
{...input}
|
|
||||||
type="checkbox"
|
|
||||||
className="checkbox__input"
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<span className="checkbox__label">
|
<span className="checkbox__label">
|
||||||
<span className="checkbox__label-text checkbox__label-text--long">
|
<span className="checkbox__label-text checkbox__label-text--long">
|
||||||
<span className="checkbox__label-title">{placeholder}</span>
|
<span className="checkbox__label-title">{placeholder}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
{!disabled &&
|
||||||
|
touched &&
|
||||||
|
(error && <span className="form__message form__message--error">{error}</span>)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -63,7 +68,7 @@ export const mac = (value) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isPositive = (value) => {
|
export const isPositive = (value) => {
|
||||||
if ((value || value === 0) && (value <= 0)) {
|
if ((value || value === 0) && value <= 0) {
|
||||||
return <Trans>form_error_positive</Trans>;
|
return <Trans>form_error_positive</Trans>;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -92,4 +97,23 @@ export const isSafePort = (value) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const domain = (value) => {
|
||||||
|
if (value && !new RegExp(R_HOST).test(value)) {
|
||||||
|
return <Trans>form_error_domain_format</Trans>;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const answer = (value) => {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
(!new RegExp(R_IPV4).test(value) &&
|
||||||
|
!new RegExp(R_IPV6).test(value) &&
|
||||||
|
!new RegExp(R_HOST).test(value))
|
||||||
|
) {
|
||||||
|
return <Trans>form_error_answer_format</Trans>;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const toNumber = value => value && parseInt(value, 10);
|
export const toNumber = value => value && parseInt(value, 10);
|
||||||
|
@ -9,6 +9,7 @@ import toasts from './toasts';
|
|||||||
import encryption from './encryption';
|
import encryption from './encryption';
|
||||||
import clients from './clients';
|
import clients from './clients';
|
||||||
import access from './access';
|
import access from './access';
|
||||||
|
import rewrites from './rewrites';
|
||||||
|
|
||||||
const settings = handleActions({
|
const settings = handleActions({
|
||||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||||
@ -422,6 +423,7 @@ export default combineReducers({
|
|||||||
encryption,
|
encryption,
|
||||||
clients,
|
clients,
|
||||||
access,
|
access,
|
||||||
|
rewrites,
|
||||||
loadingBar: loadingBarReducer,
|
loadingBar: loadingBarReducer,
|
||||||
form: formReducer,
|
form: formReducer,
|
||||||
});
|
});
|
||||||
|
50
client/src/reducers/rewrites.js
Normal file
50
client/src/reducers/rewrites.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
|
import * as actions from '../actions/rewrites';
|
||||||
|
|
||||||
|
const rewrites = handleActions(
|
||||||
|
{
|
||||||
|
[actions.getRewritesListRequest]: state => ({ ...state, processing: true }),
|
||||||
|
[actions.getRewritesListFailure]: state => ({ ...state, processing: false }),
|
||||||
|
[actions.getRewritesListSuccess]: (state, { payload }) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
list: payload,
|
||||||
|
processing: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.addRewriteRequest]: state => ({ ...state, processingAdd: true }),
|
||||||
|
[actions.addRewriteFailure]: state => ({ ...state, processingAdd: false }),
|
||||||
|
[actions.addRewriteSuccess]: (state, { payload }) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
list: [...state.list, ...payload],
|
||||||
|
processingAdd: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.deleteRewriteRequest]: state => ({ ...state, processingDelete: true }),
|
||||||
|
[actions.deleteRewriteFailure]: state => ({ ...state, processingDelete: false }),
|
||||||
|
[actions.deleteRewriteSuccess]: state => ({ ...state, processingDelete: false }),
|
||||||
|
|
||||||
|
[actions.toggleRewritesModal]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
isModalOpen: !state.isModalOpen,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
processing: true,
|
||||||
|
processingAdd: false,
|
||||||
|
processingDelete: false,
|
||||||
|
isModalOpen: false,
|
||||||
|
list: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default rewrites;
|
19
client/webpack.dev.js
vendored
19
client/webpack.dev.js
vendored
@ -2,15 +2,18 @@ const merge = require('webpack-merge');
|
|||||||
const common = require('./webpack.common.js');
|
const common = require('./webpack.common.js');
|
||||||
|
|
||||||
module.exports = merge(common, {
|
module.exports = merge(common, {
|
||||||
|
devtool: 'eval-source-map',
|
||||||
module: {
|
module: {
|
||||||
rules: [{
|
rules: [
|
||||||
test: /\.js$/,
|
{
|
||||||
exclude: /node_modules/,
|
test: /\.js$/,
|
||||||
loader: 'eslint-loader',
|
exclude: /node_modules/,
|
||||||
options: {
|
loader: 'eslint-loader',
|
||||||
emitWarning: true,
|
options: {
|
||||||
configFile: 'dev.eslintrc',
|
emitWarning: true,
|
||||||
|
configFile: 'dev.eslintrc',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -39,7 +39,7 @@ const defaultParentalURL = "%s://%s/check-parental-control-hash?prefixes=%s&sens
|
|||||||
const defaultParentalSensitivity = 13 // use "TEEN" by default
|
const defaultParentalSensitivity = 13 // use "TEEN" by default
|
||||||
const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control
|
const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control
|
||||||
|
|
||||||
// Custom filtering settings
|
// RequestFilteringSettings is custom filtering settings
|
||||||
type RequestFilteringSettings struct {
|
type RequestFilteringSettings struct {
|
||||||
FilteringEnabled bool
|
FilteringEnabled bool
|
||||||
SafeSearchEnabled bool
|
SafeSearchEnabled bool
|
||||||
@ -47,6 +47,12 @@ type RequestFilteringSettings struct {
|
|||||||
ParentalEnabled bool
|
ParentalEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewriteEntry is a rewrite array element
|
||||||
|
type RewriteEntry struct {
|
||||||
|
Domain string `yaml:"domain"`
|
||||||
|
Answer string `yaml:"answer"` // IP address or canonical name
|
||||||
|
}
|
||||||
|
|
||||||
// Config allows you to configure DNS filtering with New() or just change variables directly.
|
// Config allows you to configure DNS filtering with New() or just change variables directly.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
|
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
|
||||||
@ -60,6 +66,8 @@ type Config struct {
|
|||||||
SafeSearchCacheSize int `yaml:"safesearch_cache_size"`
|
SafeSearchCacheSize int `yaml:"safesearch_cache_size"`
|
||||||
ParentalCacheSize int `yaml:"parental_cache_size"`
|
ParentalCacheSize int `yaml:"parental_cache_size"`
|
||||||
|
|
||||||
|
Rewrites []RewriteEntry `yaml:"rewrites"`
|
||||||
|
|
||||||
// Filtering callback function
|
// Filtering callback function
|
||||||
FilterHandler func(clientAddr string, settings *RequestFilteringSettings) `yaml:"-"`
|
FilterHandler func(clientAddr string, settings *RequestFilteringSettings) `yaml:"-"`
|
||||||
}
|
}
|
||||||
@ -131,8 +139,31 @@ const (
|
|||||||
FilteredInvalid
|
FilteredInvalid
|
||||||
// FilteredSafeSearch - the host was replaced with safesearch variant
|
// FilteredSafeSearch - the host was replaced with safesearch variant
|
||||||
FilteredSafeSearch
|
FilteredSafeSearch
|
||||||
|
|
||||||
|
// ReasonRewrite - rewrite rule was applied
|
||||||
|
ReasonRewrite
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (i Reason) String() string {
|
||||||
|
names := []string{
|
||||||
|
"NotFilteredNotFound",
|
||||||
|
"NotFilteredWhiteList",
|
||||||
|
"NotFilteredError",
|
||||||
|
|
||||||
|
"FilteredBlackList",
|
||||||
|
"FilteredSafeBrowsing",
|
||||||
|
"FilteredParental",
|
||||||
|
"FilteredInvalid",
|
||||||
|
"FilteredSafeSearch",
|
||||||
|
|
||||||
|
"Rewrite",
|
||||||
|
}
|
||||||
|
if uint(i) >= uint(len(names)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return names[i]
|
||||||
|
}
|
||||||
|
|
||||||
type dnsFilterContext struct {
|
type dnsFilterContext struct {
|
||||||
stats Stats
|
stats Stats
|
||||||
dialCache gcache.Cache // "host" -> "IP" cache for safebrowsing and parental control servers
|
dialCache gcache.Cache // "host" -> "IP" cache for safebrowsing and parental control servers
|
||||||
@ -150,6 +181,10 @@ type Result struct {
|
|||||||
Rule string `json:",omitempty"` // Original rule text
|
Rule string `json:",omitempty"` // Original rule text
|
||||||
IP net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax
|
IP net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax
|
||||||
FilterID int64 `json:",omitempty"` // Filter ID the rule belongs to
|
FilterID int64 `json:",omitempty"` // Filter ID the rule belongs to
|
||||||
|
|
||||||
|
// for ReasonRewrite:
|
||||||
|
CanonName string `json:",omitempty"` // CNAME value
|
||||||
|
IPList []net.IP `json:",omitempty"` // list of IP addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matched can be used to see if any match at all was found, no matter filtered or not
|
// Matched can be used to see if any match at all was found, no matter filtered or not
|
||||||
@ -180,6 +215,12 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res
|
|||||||
|
|
||||||
var result Result
|
var result Result
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
result = d.processRewrites(host, qtype)
|
||||||
|
if result.Reason == ReasonRewrite {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// try filter lists first
|
// try filter lists first
|
||||||
if setts.FilteringEnabled {
|
if setts.FilteringEnabled {
|
||||||
result, err = d.matchHost(host, qtype)
|
result, err = d.matchHost(host, qtype)
|
||||||
@ -234,6 +275,57 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res
|
|||||||
return Result{}, nil
|
return Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process rewrites table
|
||||||
|
// . Find CNAME for a domain name
|
||||||
|
// . if found, set domain name to canonical name
|
||||||
|
// . Find A or AAAA record for a domain name
|
||||||
|
// . if found, return IP addresses
|
||||||
|
func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result {
|
||||||
|
var res Result
|
||||||
|
|
||||||
|
for _, r := range d.Rewrites {
|
||||||
|
if r.Domain != host {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(r.Answer)
|
||||||
|
if ip == nil {
|
||||||
|
log.Debug("Rewrite: CNAME for %s is %s", host, r.Answer)
|
||||||
|
host = r.Answer
|
||||||
|
res.CanonName = r.Answer
|
||||||
|
res.Reason = ReasonRewrite
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range d.Rewrites {
|
||||||
|
if r.Domain != host {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(r.Answer)
|
||||||
|
if ip == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip4 := ip.To4()
|
||||||
|
|
||||||
|
if qtype == dns.TypeA && ip4 != nil {
|
||||||
|
res.IPList = append(res.IPList, ip4)
|
||||||
|
log.Debug("Rewrite: A for %s is %s", host, ip4)
|
||||||
|
|
||||||
|
} else if qtype == dns.TypeAAAA && ip4 == nil {
|
||||||
|
res.IPList = append(res.IPList, ip)
|
||||||
|
log.Debug("Rewrite: AAAA for %s is %s", host, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.IPList) != 0 {
|
||||||
|
res.Reason = ReasonRewrite
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
func setCacheResult(cache *fastcache.Cache, host string, res Result) {
|
func setCacheResult(cache *fastcache.Cache, host string, res Result) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
enc := gob.NewEncoder(&buf)
|
enc := gob.NewEncoder(&buf)
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
// Code generated by "stringer -type=Reason"; DO NOT EDIT.
|
|
||||||
|
|
||||||
package dnsfilter
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
const _Reason_name = "NotFilteredNotFoundNotFilteredWhiteListNotFilteredErrorFilteredBlackListFilteredSafeBrowsingFilteredParentalFilteredInvalidFilteredSafeSearch"
|
|
||||||
|
|
||||||
var _Reason_index = [...]uint8{0, 19, 39, 55, 72, 92, 108, 123, 141}
|
|
||||||
|
|
||||||
func (i Reason) String() string {
|
|
||||||
if i < 0 || i >= Reason(len(_Reason_index)-1) {
|
|
||||||
return "Reason(" + strconv.FormatInt(int64(i), 10) + ")"
|
|
||||||
}
|
|
||||||
return _Reason_name[_Reason_index[i]:_Reason_index[i+1]]
|
|
||||||
}
|
|
@ -453,11 +453,31 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if d.Res == nil {
|
if d.Res == nil {
|
||||||
|
answer := []dns.RR{}
|
||||||
|
originalQuestion := d.Req.Question[0]
|
||||||
|
|
||||||
|
if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
|
||||||
|
answer = append(answer, s.genCNAMEAnswer(d.Req, res.CanonName))
|
||||||
|
// resolve canonical name, not the original host name
|
||||||
|
d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
|
||||||
|
}
|
||||||
|
|
||||||
// request was not filtered so let it be processed further
|
// request was not filtered so let it be processed further
|
||||||
err = p.Resolve(d)
|
err = p.Resolve(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
|
||||||
|
|
||||||
|
d.Req.Question[0] = originalQuestion
|
||||||
|
d.Res.Question[0] = originalQuestion
|
||||||
|
|
||||||
|
if len(d.Res.Answer) != 0 {
|
||||||
|
answer = append(answer, d.Res.Answer...) // host -> IP
|
||||||
|
d.Res.Answer = answer
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldLog := true
|
shouldLog := true
|
||||||
@ -485,8 +505,10 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
|
|||||||
|
|
||||||
// filterDNSRequest applies the dnsFilter and sets d.Res if the request was filtered
|
// filterDNSRequest applies the dnsFilter and sets d.Res if the request was filtered
|
||||||
func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error) {
|
func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error) {
|
||||||
msg := d.Req
|
var res dnsfilter.Result
|
||||||
host := strings.TrimSuffix(msg.Question[0].Name, ".")
|
req := d.Req
|
||||||
|
host := strings.TrimSuffix(req.Question[0].Name, ".")
|
||||||
|
origHost := host
|
||||||
|
|
||||||
s.RLock()
|
s.RLock()
|
||||||
protectionEnabled := s.conf.ProtectionEnabled
|
protectionEnabled := s.conf.ProtectionEnabled
|
||||||
@ -497,7 +519,10 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var res dnsfilter.Result
|
if host != origHost {
|
||||||
|
log.Debug("Rewrite: not supported: CNAME for %s is %s", origHost, host)
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
clientAddr := ""
|
clientAddr := ""
|
||||||
@ -508,9 +533,35 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Return immediately if there's an error
|
// Return immediately if there's an error
|
||||||
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
||||||
|
|
||||||
} else if res.IsFiltered {
|
} else if res.IsFiltered {
|
||||||
// log.Tracef("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule)
|
// log.Tracef("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule)
|
||||||
d.Res = s.genDNSFilterMessage(d, &res)
|
d.Res = s.genDNSFilterMessage(d, &res)
|
||||||
|
|
||||||
|
} else if res.Reason == dnsfilter.ReasonRewrite && len(res.IPList) != 0 {
|
||||||
|
resp := dns.Msg{}
|
||||||
|
resp.SetReply(req)
|
||||||
|
|
||||||
|
name := host
|
||||||
|
if len(res.CanonName) != 0 {
|
||||||
|
resp.Answer = append(resp.Answer, s.genCNAMEAnswer(req, res.CanonName))
|
||||||
|
name = res.CanonName
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range res.IPList {
|
||||||
|
if req.Question[0].Qtype == dns.TypeA {
|
||||||
|
a := s.genAAnswer(req, ip)
|
||||||
|
a.Hdr.Name = dns.Fqdn(name)
|
||||||
|
resp.Answer = append(resp.Answer, a)
|
||||||
|
|
||||||
|
} else if req.Question[0].Qtype == dns.TypeAAAA {
|
||||||
|
a := s.genAAAAAnswer(req, res.IP)
|
||||||
|
a.Hdr.Name = dns.Fqdn(name)
|
||||||
|
resp.Answer = append(resp.Answer, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Res = &resp
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, err
|
return &res, err
|
||||||
@ -644,6 +695,19 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
|
|||||||
return &resp
|
return &resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make a CNAME response
|
||||||
|
func (s *Server) genCNAMEAnswer(req *dns.Msg, cname string) *dns.CNAME {
|
||||||
|
answer := new(dns.CNAME)
|
||||||
|
answer.Hdr = dns.RR_Header{
|
||||||
|
Name: req.Question[0].Name,
|
||||||
|
Rrtype: dns.TypeCNAME,
|
||||||
|
Ttl: s.conf.BlockedResponseTTL,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
}
|
||||||
|
answer.Target = dns.Fqdn(cname)
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) genNXDomain(request *dns.Msg) *dns.Msg {
|
func (s *Server) genNXDomain(request *dns.Msg) *dns.Msg {
|
||||||
resp := dns.Msg{}
|
resp := dns.Msg{}
|
||||||
resp.SetRcode(request, dns.RcodeNameError)
|
resp.SetRcode(request, dns.RcodeNameError)
|
||||||
|
@ -1021,6 +1021,7 @@ func registerControlHandlers() {
|
|||||||
|
|
||||||
RegisterTLSHandlers()
|
RegisterTLSHandlers()
|
||||||
RegisterClientsHandlers()
|
RegisterClientsHandlers()
|
||||||
|
registerRewritesHandlers()
|
||||||
|
|
||||||
http.HandleFunc("/dns-query", postInstall(handleDOH))
|
http.HandleFunc("/dns-query", postInstall(handleDOH))
|
||||||
}
|
}
|
||||||
|
107
home/dns_rewrites.go
Normal file
107
home/dns_rewrites.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rewriteEntryJSON struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRewriteList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
arr := []*rewriteEntryJSON{}
|
||||||
|
|
||||||
|
config.RLock()
|
||||||
|
for _, ent := range config.DNS.Rewrites {
|
||||||
|
jsent := rewriteEntryJSON{
|
||||||
|
Domain: ent.Domain,
|
||||||
|
Answer: ent.Answer,
|
||||||
|
}
|
||||||
|
arr = append(arr, &jsent)
|
||||||
|
}
|
||||||
|
config.RUnlock()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(arr)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "json.Encode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
jsent := rewriteEntryJSON{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&jsent)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ent := dnsfilter.RewriteEntry{
|
||||||
|
Domain: jsent.Domain,
|
||||||
|
Answer: jsent.Answer,
|
||||||
|
}
|
||||||
|
config.Lock()
|
||||||
|
config.DNS.Rewrites = append(config.DNS.Rewrites, ent)
|
||||||
|
config.Unlock()
|
||||||
|
log.Debug("Rewrites: added element: %s -> %s [%d]",
|
||||||
|
ent.Domain, ent.Answer, len(config.DNS.Rewrites))
|
||||||
|
|
||||||
|
err = writeAllConfigsAndReloadDNS()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
jsent := rewriteEntryJSON{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&jsent)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entDel := dnsfilter.RewriteEntry{
|
||||||
|
Domain: jsent.Domain,
|
||||||
|
Answer: jsent.Answer,
|
||||||
|
}
|
||||||
|
arr := []dnsfilter.RewriteEntry{}
|
||||||
|
config.Lock()
|
||||||
|
for _, ent := range config.DNS.Rewrites {
|
||||||
|
if ent == entDel {
|
||||||
|
log.Debug("Rewrites: removed element: %s -> %s", ent.Domain, ent.Answer)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
arr = append(arr, ent)
|
||||||
|
}
|
||||||
|
config.DNS.Rewrites = arr
|
||||||
|
config.Unlock()
|
||||||
|
|
||||||
|
err = writeAllConfigsAndReloadDNS()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRewritesHandlers() {
|
||||||
|
http.HandleFunc("/control/rewrite/list", postInstall(optionalAuth(ensureGET(handleRewriteList))))
|
||||||
|
http.HandleFunc("/control/rewrite/add", postInstall(optionalAuth(ensurePOST(handleRewriteAdd))))
|
||||||
|
http.HandleFunc("/control/rewrite/delete", postInstall(optionalAuth(ensurePOST(handleRewriteDelete))))
|
||||||
|
}
|
@ -786,6 +786,54 @@ paths:
|
|||||||
200:
|
200:
|
||||||
description: OK
|
description: OK
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Rewrite methods
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
/rewrite/list:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- rewrite
|
||||||
|
operationId: rewriteList
|
||||||
|
summary: 'Get list of Rewrite rules'
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/RewriteList"
|
||||||
|
|
||||||
|
/rewrite/add:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- rewrite
|
||||||
|
operationId: rewriteAdd
|
||||||
|
summary: 'Add a new Rewrite rule'
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/RewriteEntry"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
|
/rewrite/delete:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- rewrite
|
||||||
|
operationId: rewriteDelete
|
||||||
|
summary: 'Remove a Rewrite rule'
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/RewriteEntry"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# I18N methods
|
# I18N methods
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@ -1571,6 +1619,25 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
$ref: "#/definitions/ClientAuto"
|
$ref: "#/definitions/ClientAuto"
|
||||||
description: "Auto-Clients array"
|
description: "Auto-Clients array"
|
||||||
|
|
||||||
|
RewriteList:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/RewriteEntry"
|
||||||
|
description: "Rewrite rules array"
|
||||||
|
RewriteEntry:
|
||||||
|
type: "object"
|
||||||
|
description: "Rewrite rule"
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
type: "string"
|
||||||
|
description: "Domain name"
|
||||||
|
example: "example.org"
|
||||||
|
answer:
|
||||||
|
type: "string"
|
||||||
|
description: "value of A, AAAA or CNAME DNS record"
|
||||||
|
example: "127.0.0.1"
|
||||||
|
|
||||||
CheckConfigRequest:
|
CheckConfigRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Configuration to be checked"
|
description: "Configuration to be checked"
|
||||||
|
Loading…
Reference in New Issue
Block a user