Compare commits
23 Commits
2044-gc
...
fix-contex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc1060d428 | ||
|
|
06594bde8f | ||
|
|
84938c5603 | ||
|
|
15a82233f3 | ||
|
|
8dc0108868 | ||
|
|
fc43e2ac6f | ||
|
|
1a6bd29462 | ||
|
|
340052090c | ||
|
|
b54ce85d3d | ||
|
|
9e33bd5259 | ||
|
|
050e996a35 | ||
|
|
07db05dd80 | ||
|
|
4efc464e98 | ||
|
|
e56c746b60 | ||
|
|
7b9cef3a08 | ||
|
|
f363c95ef5 | ||
|
|
729f4b1766 | ||
|
|
67bf027616 | ||
|
|
7931e50673 | ||
|
|
6b61429572 | ||
|
|
0a4781be97 | ||
|
|
268d90b5bc | ||
|
|
7d3a72e626 |
@@ -69,6 +69,7 @@ Contents:
|
|||||||
* API: Log out
|
* API: Log out
|
||||||
* API: Get current user info
|
* API: Get current user info
|
||||||
* Safe services
|
* Safe services
|
||||||
|
* ipset
|
||||||
|
|
||||||
|
|
||||||
## Relations between subsystems
|
## Relations between subsystems
|
||||||
@@ -1438,6 +1439,11 @@ When UI asks for data from query log (see "API: Get query log"), server reads th
|
|||||||
|
|
||||||
We store data for a limited amount of time - the log file is automatically rotated.
|
We store data for a limited amount of time - the log file is automatically rotated.
|
||||||
|
|
||||||
|
* On AGH startup read the first line from query logs and store its time value
|
||||||
|
* If there's no log file yet, set the time value of the first log event when the file is created
|
||||||
|
* If this time value is older than our time limit, perform file rotate procedure
|
||||||
|
* While AGH is running, check the previous condition every 24 hours
|
||||||
|
|
||||||
|
|
||||||
### API: Get query log
|
### API: Get query log
|
||||||
|
|
||||||
@@ -1882,3 +1888,25 @@ Check if host name is blocked by SB/PC service:
|
|||||||
sha256(host.com)[0..1] -> hashes[0],hashes[1],...
|
sha256(host.com)[0..1] -> hashes[0],hashes[1],...
|
||||||
sha256(sub.host.com)[0..1] -> hashes[2],...
|
sha256(sub.host.com)[0..1] -> hashes[2],...
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
## ipset
|
||||||
|
|
||||||
|
AGH can add IP addresses of the specified in configuration domain names to an ipset list.
|
||||||
|
|
||||||
|
Prepare: user creates an ipset list and configures AGH for using it.
|
||||||
|
|
||||||
|
1. User --( ipset create my_ipset hash:ip ) -> OS
|
||||||
|
2. User --( ipset: host.com,host2.com/my_ipset )-> AGH
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
|
||||||
|
ipset: "DOMAIN[,DOMAIN].../IPSET1_NAME[,IPSET2_NAME]..."
|
||||||
|
|
||||||
|
IPv4 addresses are added to an ipset list with `ipv4` family, IPv6 addresses - to `ipv6` ipset list.
|
||||||
|
|
||||||
|
Run-time: AGH adds IP addresses of a domain name to a corresponding ipset list.
|
||||||
|
|
||||||
|
1. AGH --( resolve host.com )-> upstream
|
||||||
|
2. AGH <-( host.com:[1.1.1.1,2.2.2.2] )-- upstream
|
||||||
|
3. AGH --( ipset.add(my_ipset, [1.1.1.1,2.2.2.2] ))-> OS
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -108,6 +108,12 @@ ifndef DOCKER_IMAGE_NAME
|
|||||||
$(error DOCKER_IMAGE_NAME value is not set)
|
$(error DOCKER_IMAGE_NAME value is not set)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# OS-specific flags
|
||||||
|
TEST_FLAGS := -race
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
TEST_FLAGS :=
|
||||||
|
endif
|
||||||
|
|
||||||
.PHONY: all build client client-watch docker lint lint-js lint-go test dependencies clean release docker-multi-arch
|
.PHONY: all build client client-watch docker lint lint-js lint-go test dependencies clean release docker-multi-arch
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -158,7 +164,7 @@ test:
|
|||||||
@echo Running JS unit-tests
|
@echo Running JS unit-tests
|
||||||
npm run test --prefix client
|
npm run test --prefix client
|
||||||
@echo Running Go unit-tests
|
@echo Running Go unit-tests
|
||||||
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
go test $(TEST_FLAGS) -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
|
||||||
ci: client_with_deps
|
ci: client_with_deps
|
||||||
go mod download
|
go mod download
|
||||||
|
|||||||
122
client/package-lock.json
generated
vendored
122
client/package-lock.json
generated
vendored
@@ -1356,17 +1356,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
|
||||||
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
||||||
},
|
},
|
||||||
"@hot-loader/react-dom": {
|
|
||||||
"version": "16.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-16.13.0.tgz",
|
|
||||||
"integrity": "sha512-lJZrmkucz2MrQJTQtJobx5MICXcfQvKihszqv655p557HPi0hMOWxrNpiHv3DWD8ugNWjtWcVWqRnFvwsHq1mQ==",
|
|
||||||
"requires": {
|
|
||||||
"loose-envify": "^1.1.0",
|
|
||||||
"object-assign": "^4.1.1",
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"scheduler": "^0.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@istanbuljs/load-nyc-config": {
|
"@istanbuljs/load-nyc-config": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
@@ -10784,6 +10773,24 @@
|
|||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mississippi": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"concat-stream": "^1.5.0",
|
||||||
|
"duplexify": "^3.4.2",
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"flush-write-stream": "^1.0.0",
|
||||||
|
"from2": "^2.1.0",
|
||||||
|
"parallel-transform": "^1.1.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"pumpify": "^1.3.3",
|
||||||
|
"stream-each": "^1.1.0",
|
||||||
|
"through2": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"mixin-deep": {
|
"mixin-deep": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||||
@@ -12169,9 +12176,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pump": {
|
"pump": {
|
||||||
"version": "2.0.1",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
|
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
@@ -12187,6 +12194,18 @@
|
|||||||
"duplexify": "^3.6.0",
|
"duplexify": "^3.6.0",
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"pump": "^2.0.0"
|
"pump": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pump": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"punycode": {
|
"punycode": {
|
||||||
@@ -12358,9 +12377,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-i18next": {
|
"react-i18next": {
|
||||||
"version": "11.4.0",
|
"version": "11.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.2.tgz",
|
||||||
"integrity": "sha512-lyOZSSQkif4H9HnHN3iEKVkryLI+WkdZSEw3VAZzinZLopfYRMHVY5YxCopdkXPLEHs6S5GjKYPh3+j0j336Fg==",
|
"integrity": "sha512-Djj3K3hh5Tecla2CI9rLO3TZBYGMFrGilm0JY4cLofAQONCi5TK6nVmUPKoB59n1ZffgjfgJt6zlbE9aGF6Q0Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.3.1",
|
"@babel/runtime": "^7.3.1",
|
||||||
"html-parse-stringify2": "2.0.1"
|
"html-parse-stringify2": "2.0.1"
|
||||||
@@ -13281,10 +13300,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serialize-javascript": {
|
"serialize-javascript": {
|
||||||
"version": "3.0.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
|
||||||
"integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==",
|
"integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"randombytes": "^2.1.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"serve-index": {
|
"serve-index": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
@@ -14619,16 +14641,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"terser-webpack-plugin": {
|
"terser-webpack-plugin": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
|
||||||
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
|
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"cacache": "^12.0.2",
|
"cacache": "^12.0.2",
|
||||||
"find-cache-dir": "^2.1.0",
|
"find-cache-dir": "^2.1.0",
|
||||||
"is-wsl": "^1.1.0",
|
"is-wsl": "^1.1.0",
|
||||||
"schema-utils": "^1.0.0",
|
"schema-utils": "^1.0.0",
|
||||||
"serialize-javascript": "^2.1.2",
|
"serialize-javascript": "^4.0.0",
|
||||||
"source-map": "^0.6.1",
|
"source-map": "^0.6.1",
|
||||||
"terser": "^4.1.2",
|
"terser": "^4.1.2",
|
||||||
"webpack-sources": "^1.4.0",
|
"webpack-sources": "^1.4.0",
|
||||||
@@ -14688,15 +14710,6 @@
|
|||||||
"path-exists": "^3.0.0"
|
"path-exists": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lru-cache": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"yallist": "^3.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"make-dir": {
|
"make-dir": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||||
@@ -14707,24 +14720,6 @@
|
|||||||
"semver": "^5.6.0"
|
"semver": "^5.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mississippi": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"concat-stream": "^1.5.0",
|
|
||||||
"duplexify": "^3.4.2",
|
|
||||||
"end-of-stream": "^1.1.0",
|
|
||||||
"flush-write-stream": "^1.0.0",
|
|
||||||
"from2": "^2.1.0",
|
|
||||||
"parallel-transform": "^1.1.0",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"pumpify": "^1.3.3",
|
|
||||||
"stream-each": "^1.1.0",
|
|
||||||
"through2": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"p-limit": {
|
"p-limit": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
@@ -14764,22 +14759,15 @@
|
|||||||
"find-up": "^3.0.0"
|
"find-up": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pump": {
|
"serialize-javascript": {
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"end-of-stream": "^1.1.0",
|
"randombytes": "^2.1.0"
|
||||||
"once": "^1.3.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serialize-javascript": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -14794,12 +14782,6 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"figgy-pudding": "^3.5.1"
|
"figgy-pudding": "^3.5.1"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"yallist": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
3
client/package.json
vendored
3
client/package.json
vendored
@@ -13,7 +13,6 @@
|
|||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hot-loader/react-dom": "^16.13.0",
|
|
||||||
"@nivo/line": "^0.49.1",
|
"@nivo/line": "^0.49.1",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-click-outside": "^3.0.1",
|
"react-click-outside": "^3.0.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-i18next": "^11.4.0",
|
"react-i18next": "^11.7.2",
|
||||||
"react-modal": "^3.11.2",
|
"react-modal": "^3.11.2",
|
||||||
"react-popper-tooltip": "^2.11.1",
|
"react-popper-tooltip": "^2.11.1",
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
|
|||||||
@@ -139,8 +139,8 @@
|
|||||||
"page_table_footer_text": "Страница",
|
"page_table_footer_text": "Страница",
|
||||||
"rows_table_footer_text": "редове",
|
"rows_table_footer_text": "редове",
|
||||||
"updated_custom_filtering_toast": "Обновени местни правила за филтриране",
|
"updated_custom_filtering_toast": "Обновени местни правила за филтриране",
|
||||||
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране",
|
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране",
|
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
|
||||||
"plain_dns": "Обикновен DNS",
|
"plain_dns": "Обикновен DNS",
|
||||||
"source_label": "Източник",
|
"source_label": "Източник",
|
||||||
"found_in_known_domain_db": "Намерен в списъците с домейни.",
|
"found_in_known_domain_db": "Намерен в списъците с домейни.",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Stránka",
|
"page_table_footer_text": "Stránka",
|
||||||
"rows_table_footer_text": "řádky",
|
"rows_table_footer_text": "řádky",
|
||||||
"updated_custom_filtering_toast": "Aktualizovaná vlastní pravidla filtrování",
|
"updated_custom_filtering_toast": "Aktualizovaná vlastní pravidla filtrování",
|
||||||
"rule_removed_from_custom_filtering_toast": "Pravidlo odstraněno z vlastních pravidel filtrování",
|
"rule_removed_from_custom_filtering_toast": "Pravidlo odstraněno z vlastních pravidel filtrování: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Pravidlo přidáno do vlastních pravidel filtrování",
|
"rule_added_to_custom_filtering_toast": "Pravidlo přidáno do vlastních pravidel filtrování: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrováno pomocí {{filter}}",
|
"query_log_filtered": "Filtrováno pomocí {{filter}}",
|
||||||
"query_log_confirm_clear": "Opravdu chcete vymazat celý protokol dotazů?",
|
"query_log_confirm_clear": "Opravdu chcete vymazat celý protokol dotazů?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Side",
|
"page_table_footer_text": "Side",
|
||||||
"rows_table_footer_text": "rækker",
|
"rows_table_footer_text": "rækker",
|
||||||
"updated_custom_filtering_toast": "De brugerdefinerede filtreringsregler er blevet opdateret",
|
"updated_custom_filtering_toast": "De brugerdefinerede filtreringsregler er blevet opdateret",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regel fjernet fra de brugerdefinerede filtreringsregler",
|
"rule_removed_from_custom_filtering_toast": "Regel fjernet fra de brugerdefinerede filtreringsregler: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regel tilføjet til de brugerdefinerede filtreringsregler",
|
"rule_added_to_custom_filtering_toast": "Regel tilføjet til de brugerdefinerede filtreringsregler: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtreret af {{filter}}",
|
"query_log_filtered": "Filtreret af {{filter}}",
|
||||||
"query_log_confirm_clear": "Er du sikker på, at du vil rydde hele forespørgselsloggen?",
|
"query_log_confirm_clear": "Er du sikker på, at du vil rydde hele forespørgselsloggen?",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"dhcp_warning": "Wenn Sie den DHCP-Server trotzdem aktivieren möchten, stellen Sie sicher, dass sich in Ihrem Netzwerk kein anderer aktiver DHCP-Server befindet. Andernfalls kann es bei angeschlossenen Geräten zu einem Ausfall des Internets kommen!",
|
"dhcp_warning": "Wenn Sie den DHCP-Server trotzdem aktivieren möchten, stellen Sie sicher, dass sich in Ihrem Netzwerk kein anderer aktiver DHCP-Server befindet. Andernfalls kann es bei angeschlossenen Geräten zu einem Ausfall des Internets kommen!",
|
||||||
"dhcp_error": "Es konnte nicht ermittelt werden, ob es einen anderen DHCP-Server im Netzwerk gibt.",
|
"dhcp_error": "Es konnte nicht ermittelt werden, ob es einen anderen DHCP-Server im Netzwerk gibt.",
|
||||||
"dhcp_static_ip_error": "Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Es konnte nicht ermittelt werden, ob diese Netzwerkschnittstelle mit statischer IP-Adresse konfiguriert ist. Bitte legen Sie eine statische IP-Adresse manuell fest.",
|
"dhcp_static_ip_error": "Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Es konnte nicht ermittelt werden, ob diese Netzwerkschnittstelle mit statischer IP-Adresse konfiguriert ist. Bitte legen Sie eine statische IP-Adresse manuell fest.",
|
||||||
"dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}</0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}}</0>. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.",
|
"dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}</0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}</0>. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.",
|
||||||
"dhcp_lease_added": "Statischer Lease „{{key}}” erfolgreich hinzugefügt",
|
"dhcp_lease_added": "Statischer Lease „{{key}}” erfolgreich hinzugefügt",
|
||||||
"dhcp_lease_deleted": "Statischer Lease „{{key}}” erfolgreich entfernt",
|
"dhcp_lease_deleted": "Statischer Lease „{{key}}” erfolgreich entfernt",
|
||||||
"dhcp_new_static_lease": "Neuer statischer Lease",
|
"dhcp_new_static_lease": "Neuer statischer Lease",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"no_clients_found": "Keine Clients gefunden",
|
"no_clients_found": "Keine Clients gefunden",
|
||||||
"general_statistics": "Allgemeine Statistiken",
|
"general_statistics": "Allgemeine Statistiken",
|
||||||
"number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen",
|
"number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen",
|
||||||
"number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}}} Tagen verarbeitet wurden",
|
"number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}} Tagen verarbeitet wurden",
|
||||||
"number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen",
|
"number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen",
|
||||||
"number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Blocklisten geblockten DNS-Anfragen",
|
"number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Blocklisten geblockten DNS-Anfragen",
|
||||||
"number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul für Internet-Sicherheit blockierten DNS-Anfragen",
|
"number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul für Internet-Sicherheit blockierten DNS-Anfragen",
|
||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Seite",
|
"page_table_footer_text": "Seite",
|
||||||
"rows_table_footer_text": "Reihen",
|
"rows_table_footer_text": "Reihen",
|
||||||
"updated_custom_filtering_toast": "Die benutzerdefinierten Filterregeln wurden aktualisiert",
|
"updated_custom_filtering_toast": "Die benutzerdefinierten Filterregeln wurden aktualisiert",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt",
|
"rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt",
|
"rule_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Gefiltert nach {{filter}}",
|
"query_log_filtered": "Gefiltert nach {{filter}}",
|
||||||
"query_log_confirm_clear": "Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?",
|
"query_log_confirm_clear": "Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?",
|
||||||
|
|||||||
@@ -194,6 +194,10 @@
|
|||||||
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
||||||
"unblock": "Unblock",
|
"unblock": "Unblock",
|
||||||
"block": "Block",
|
"block": "Block",
|
||||||
|
"disallow_this_client": "Disallow this client",
|
||||||
|
"allow_this_client": "Allow this client",
|
||||||
|
"block_for_this_client_only": "Block for this client only",
|
||||||
|
"unblock_for_this_client_only": "Unblock for this client only",
|
||||||
"time_table_header": "Time",
|
"time_table_header": "Time",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"domain_name_table_header": "Domain name",
|
"domain_name_table_header": "Domain name",
|
||||||
@@ -213,8 +217,8 @@
|
|||||||
"page_table_footer_text": "Page",
|
"page_table_footer_text": "Page",
|
||||||
"rows_table_footer_text": "rows",
|
"rows_table_footer_text": "rows",
|
||||||
"updated_custom_filtering_toast": "Updated the custom filtering rules",
|
"updated_custom_filtering_toast": "Updated the custom filtering rules",
|
||||||
"rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules",
|
"rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules",
|
"rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtered by {{filter}}",
|
"query_log_filtered": "Filtered by {{filter}}",
|
||||||
"query_log_confirm_clear": "Are you sure you want to clear the entire query log?",
|
"query_log_confirm_clear": "Are you sure you want to clear the entire query log?",
|
||||||
@@ -359,7 +363,7 @@
|
|||||||
"fix": "Fix",
|
"fix": "Fix",
|
||||||
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
||||||
"update_now": "Update now",
|
"update_now": "Update now",
|
||||||
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps</a> to update manually.",
|
"update_failed": "Auto-update failed. Please <a>follow these steps</a> to update manually.",
|
||||||
"processing_update": "Please wait, AdGuard Home is being updated",
|
"processing_update": "Please wait, AdGuard Home is being updated",
|
||||||
"clients_title": "Clients",
|
"clients_title": "Clients",
|
||||||
"clients_desc": "Configure devices connected to AdGuard Home",
|
"clients_desc": "Configure devices connected to AdGuard Home",
|
||||||
@@ -569,5 +573,6 @@
|
|||||||
"setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server",
|
"setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server",
|
||||||
"original_response": "Original response",
|
"original_response": "Original response",
|
||||||
"click_to_view_queries": "Click to view queries",
|
"click_to_view_queries": "Click to view queries",
|
||||||
"port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction</0> on how to resolve this."
|
"port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction</0> on how to resolve this.",
|
||||||
|
"adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Página",
|
"page_table_footer_text": "Página",
|
||||||
"rows_table_footer_text": "filas",
|
"rows_table_footer_text": "filas",
|
||||||
"updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas",
|
"updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado",
|
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regla añadida a las reglas de filtrado personalizado",
|
"rule_added_to_custom_filtering_toast": "Regla añadida a las reglas de filtrado personalizado: {{rule}}",
|
||||||
"query_log_response_status": "Estado: {{value}}",
|
"query_log_response_status": "Estado: {{value}}",
|
||||||
"query_log_filtered": "Filtrado por {{filter}}",
|
"query_log_filtered": "Filtrado por {{filter}}",
|
||||||
"query_log_confirm_clear": "¿Está seguro de que desea borrar todo el registro de consultas?",
|
"query_log_confirm_clear": "¿Está seguro de que desea borrar todo el registro de consultas?",
|
||||||
|
|||||||
@@ -202,8 +202,8 @@
|
|||||||
"page_table_footer_text": "صفحه",
|
"page_table_footer_text": "صفحه",
|
||||||
"rows_table_footer_text": "سطر",
|
"rows_table_footer_text": "سطر",
|
||||||
"updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است",
|
"updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است",
|
||||||
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد",
|
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد",
|
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد {{rule}}",
|
||||||
"query_log_response_status": "وضعیت: {{value}}",
|
"query_log_response_status": "وضعیت: {{value}}",
|
||||||
"query_log_filtered": "فیلتر شده با {{filter}}",
|
"query_log_filtered": "فیلتر شده با {{filter}}",
|
||||||
"query_log_confirm_clear": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟",
|
"query_log_confirm_clear": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Page",
|
"page_table_footer_text": "Page",
|
||||||
"rows_table_footer_text": "lignes",
|
"rows_table_footer_text": "lignes",
|
||||||
"updated_custom_filtering_toast": "Règles de filtrage d'utilisateur mises à jour",
|
"updated_custom_filtering_toast": "Règles de filtrage d'utilisateur mises à jour",
|
||||||
"rule_removed_from_custom_filtering_toast": "Règle retirée des règles d'utilisateur",
|
"rule_removed_from_custom_filtering_toast": "Règle retirée des règles d'utilisateur: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Règle ajoutée aux règles d'utilisateur",
|
"rule_added_to_custom_filtering_toast": "Règle ajoutée aux règles d'utilisateur: {{rule}}",
|
||||||
"query_log_response_status": "Statut : {{value}}",
|
"query_log_response_status": "Statut : {{value}}",
|
||||||
"query_log_filtered": "Filtré par {{filter}}",
|
"query_log_filtered": "Filtré par {{filter}}",
|
||||||
"query_log_confirm_clear": "Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?",
|
"query_log_confirm_clear": "Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Stranica",
|
"page_table_footer_text": "Stranica",
|
||||||
"rows_table_footer_text": "redova",
|
"rows_table_footer_text": "redova",
|
||||||
"updated_custom_filtering_toast": "Ažurirana su prilagođena pravila filtriranja",
|
"updated_custom_filtering_toast": "Ažurirana su prilagođena pravila filtriranja",
|
||||||
"rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja",
|
"rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja",
|
"rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrirao {{filter}}",
|
"query_log_filtered": "Filtrirao {{filter}}",
|
||||||
"query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?",
|
"query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Halaman",
|
"page_table_footer_text": "Halaman",
|
||||||
"rows_table_footer_text": "baris",
|
"rows_table_footer_text": "baris",
|
||||||
"updated_custom_filtering_toast": "Perbarui aturan penyaringan khusus",
|
"updated_custom_filtering_toast": "Perbarui aturan penyaringan khusus",
|
||||||
"rule_removed_from_custom_filtering_toast": "Aturan dihapus dari aturan penyaringan khusus",
|
"rule_removed_from_custom_filtering_toast": "Aturan dihapus dari aturan penyaringan khusus: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Aturan ditambah ke aturan penyaringan khusus",
|
"rule_added_to_custom_filtering_toast": "Aturan ditambah ke aturan penyaringan khusus: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Difilter oleh {{filter}}",
|
"query_log_filtered": "Difilter oleh {{filter}}",
|
||||||
"query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?",
|
"query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Pagina",
|
"page_table_footer_text": "Pagina",
|
||||||
"rows_table_footer_text": "righe",
|
"rows_table_footer_text": "righe",
|
||||||
"updated_custom_filtering_toast": "Le regole dei filtri personalizzate sono state aggiornate",
|
"updated_custom_filtering_toast": "Le regole dei filtri personalizzate sono state aggiornate",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regola rimossa dalle regole dei filtri personalizzate",
|
"rule_removed_from_custom_filtering_toast": "Regola rimossa dalle regole dei filtri personalizzate: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate",
|
"rule_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrato da {{filter}}",
|
"query_log_filtered": "Filtrato da {{filter}}",
|
||||||
"query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?",
|
"query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "ページ",
|
"page_table_footer_text": "ページ",
|
||||||
"rows_table_footer_text": "行",
|
"rows_table_footer_text": "行",
|
||||||
"updated_custom_filtering_toast": "カスタム・フィルタリングルールを更新しました",
|
"updated_custom_filtering_toast": "カスタム・フィルタリングルールを更新しました",
|
||||||
"rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました",
|
"rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました",
|
"rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました {{rule}}",
|
||||||
"query_log_response_status": "ステータス: {{value}}",
|
"query_log_response_status": "ステータス: {{value}}",
|
||||||
"query_log_filtered": "{{filter}}によるフィルタ",
|
"query_log_filtered": "{{filter}}によるフィルタ",
|
||||||
"query_log_confirm_clear": "クエリ・ログ全体を消去してもよろしいですか?",
|
"query_log_confirm_clear": "クエリ・ログ全体を消去してもよろしいですか?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "페이지",
|
"page_table_footer_text": "페이지",
|
||||||
"rows_table_footer_text": "행",
|
"rows_table_footer_text": "행",
|
||||||
"updated_custom_filtering_toast": "사용자 정의 필터링 규칙 업데이트",
|
"updated_custom_filtering_toast": "사용자 정의 필터링 규칙 업데이트",
|
||||||
"rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거",
|
"rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거 {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙",
|
"rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙 {{rule}}",
|
||||||
"query_log_response_status": "상태: {{value}}",
|
"query_log_response_status": "상태: {{value}}",
|
||||||
"query_log_filtered": "필터: {{filter}}",
|
"query_log_filtered": "필터: {{filter}}",
|
||||||
"query_log_confirm_clear": "정말로 모든 쿼리 로그를 비우시겠습니까?",
|
"query_log_confirm_clear": "정말로 모든 쿼리 로그를 비우시겠습니까?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Pagina",
|
"page_table_footer_text": "Pagina",
|
||||||
"rows_table_footer_text": "rijen",
|
"rows_table_footer_text": "rijen",
|
||||||
"updated_custom_filtering_toast": "Aangepaste filter regels zijn bijgewerkt",
|
"updated_custom_filtering_toast": "Aangepaste filter regels zijn bijgewerkt",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regel verwijderd uit de aangepaste filterregels",
|
"rule_removed_from_custom_filtering_toast": "Regel verwijderd uit de aangepaste filterregels: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regel toegevoegd aan de aangepaste filterregels",
|
"rule_added_to_custom_filtering_toast": "Regel toegevoegd aan de aangepaste filterregels: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Gefilterd door {{filter}}",
|
"query_log_filtered": "Gefilterd door {{filter}}",
|
||||||
"query_log_confirm_clear": "Weet u zeker dat u het hele query logboek wilt legen?",
|
"query_log_confirm_clear": "Weet u zeker dat u het hele query logboek wilt legen?",
|
||||||
|
|||||||
@@ -207,8 +207,8 @@
|
|||||||
"page_table_footer_text": "Side",
|
"page_table_footer_text": "Side",
|
||||||
"rows_table_footer_text": "rekker",
|
"rows_table_footer_text": "rekker",
|
||||||
"updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene",
|
"updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene",
|
||||||
"rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene",
|
"rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene",
|
"rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrert av {{filter}}",
|
"query_log_filtered": "Filtrert av {{filter}}",
|
||||||
"query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?",
|
"query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Strona",
|
"page_table_footer_text": "Strona",
|
||||||
"rows_table_footer_text": "wierszy",
|
"rows_table_footer_text": "wierszy",
|
||||||
"updated_custom_filtering_toast": "Zaktualizowano niestandardowe reguły filtrowania",
|
"updated_custom_filtering_toast": "Zaktualizowano niestandardowe reguły filtrowania",
|
||||||
"rule_removed_from_custom_filtering_toast": "Reguła usunięta z niestandardowych reguł filtrowania",
|
"rule_removed_from_custom_filtering_toast": "Reguła usunięta z niestandardowych reguł filtrowania: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Reguła dodana do niestandardowych reguł filtrowania",
|
"rule_added_to_custom_filtering_toast": "Reguła dodana do niestandardowych reguł filtrowania: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrowane przez {{filter}}",
|
"query_log_filtered": "Filtrowane przez {{filter}}",
|
||||||
"query_log_confirm_clear": "Czy na pewno chcesz wyczyścić cały dziennik zapytań?",
|
"query_log_confirm_clear": "Czy na pewno chcesz wyczyścić cały dziennik zapytań?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Página",
|
"page_table_footer_text": "Página",
|
||||||
"rows_table_footer_text": "linhas",
|
"rows_table_footer_text": "linhas",
|
||||||
"updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas",
|
"updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas",
|
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas",
|
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrado por {{filter}}",
|
"query_log_filtered": "Filtrado por {{filter}}",
|
||||||
"query_log_confirm_clear": "Você tem certeza que deseja limpar o registro de consulta?",
|
"query_log_confirm_clear": "Você tem certeza que deseja limpar o registro de consulta?",
|
||||||
|
|||||||
@@ -168,8 +168,8 @@
|
|||||||
"page_table_footer_text": "Página",
|
"page_table_footer_text": "Página",
|
||||||
"rows_table_footer_text": "linhas",
|
"rows_table_footer_text": "linhas",
|
||||||
"updated_custom_filtering_toast": "Regras de filtragem personalizadas actualizadas",
|
"updated_custom_filtering_toast": "Regras de filtragem personalizadas actualizadas",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas",
|
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas",
|
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrado por {{filter}}",
|
"query_log_filtered": "Filtrado por {{filter}}",
|
||||||
"query_log_confirm_clear": "Tem a certeza de que deseja limpar todo o registo de consulta?",
|
"query_log_confirm_clear": "Tem a certeza de que deseja limpar todo o registo de consulta?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Pagina",
|
"page_table_footer_text": "Pagina",
|
||||||
"rows_table_footer_text": "linii",
|
"rows_table_footer_text": "linii",
|
||||||
"updated_custom_filtering_toast": "Reguli personalizate de filtrare aduse la zi",
|
"updated_custom_filtering_toast": "Reguli personalizate de filtrare aduse la zi",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regulă scoasă din regullei personalizate de filtrare",
|
"rule_removed_from_custom_filtering_toast": "Regulă scoasă din regullei personalizate de filtrare: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate",
|
"rule_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate: {{rule}}",
|
||||||
"query_log_response_status": "Statut: {{value}}",
|
"query_log_response_status": "Statut: {{value}}",
|
||||||
"query_log_filtered": "Filtrat de {{filter}}",
|
"query_log_filtered": "Filtrat de {{filter}}",
|
||||||
"query_log_confirm_clear": "Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?",
|
"query_log_confirm_clear": "Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Страница",
|
"page_table_footer_text": "Страница",
|
||||||
"rows_table_footer_text": "строк",
|
"rows_table_footer_text": "строк",
|
||||||
"updated_custom_filtering_toast": "Внесены изменения в пользовательские правила",
|
"updated_custom_filtering_toast": "Внесены изменения в пользовательские правила",
|
||||||
"rule_removed_from_custom_filtering_toast": "Правило удалено из авторского списка правил фильтрации",
|
"rule_removed_from_custom_filtering_toast": "Пользовательское правило удалено: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено",
|
"rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено: {{rule}}",
|
||||||
"query_log_response_status": "Статус: {{value}}",
|
"query_log_response_status": "Статус: {{value}}",
|
||||||
"query_log_filtered": "Отфильтровано с помощью {{filter}}",
|
"query_log_filtered": "Отфильтровано с помощью {{filter}}",
|
||||||
"query_log_confirm_clear": "Вы уверены, что хотите очистить весь журнал запросов?",
|
"query_log_confirm_clear": "Вы уверены, что хотите очистить весь журнал запросов?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Stránka",
|
"page_table_footer_text": "Stránka",
|
||||||
"rows_table_footer_text": "riadky",
|
"rows_table_footer_text": "riadky",
|
||||||
"updated_custom_filtering_toast": "Aktualizované vlastné filtračné pravidlá",
|
"updated_custom_filtering_toast": "Aktualizované vlastné filtračné pravidlá",
|
||||||
"rule_removed_from_custom_filtering_toast": "Pravidlo odstránené z vlastných filtračných pravidiel",
|
"rule_removed_from_custom_filtering_toast": "Pravidlo odstránené z vlastných filtračných pravidiel: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel",
|
"rule_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel: {{rule}}",
|
||||||
"query_log_response_status": "Stav: {{value}}",
|
"query_log_response_status": "Stav: {{value}}",
|
||||||
"query_log_filtered": "Vyfiltrované pomocou {{filter}}",
|
"query_log_filtered": "Vyfiltrované pomocou {{filter}}",
|
||||||
"query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?",
|
"query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Stran",
|
"page_table_footer_text": "Stran",
|
||||||
"rows_table_footer_text": "vrstic",
|
"rows_table_footer_text": "vrstic",
|
||||||
"updated_custom_filtering_toast": "Posodobljena pravila filtriranja po meri",
|
"updated_custom_filtering_toast": "Posodobljena pravila filtriranja po meri",
|
||||||
"rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil filtriranja po meri",
|
"rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil filtriranja po meri: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano pravilom filtriranja po meri",
|
"rule_added_to_custom_filtering_toast": "Pravilo je dodano pravilom filtriranja po meri: {{rule}}",
|
||||||
"query_log_response_status": "Stanje: {{value}}",
|
"query_log_response_status": "Stanje: {{value}}",
|
||||||
"query_log_filtered": "Filtriran z {{filter}}",
|
"query_log_filtered": "Filtriran z {{filter}}",
|
||||||
"query_log_confirm_clear": "Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?",
|
"query_log_confirm_clear": "Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "Stranica",
|
"page_table_footer_text": "Stranica",
|
||||||
"rows_table_footer_text": "redovi",
|
"rows_table_footer_text": "redovi",
|
||||||
"updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja",
|
"updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja",
|
||||||
"rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja",
|
"rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja",
|
"rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja: {{rule}}",
|
||||||
"query_log_response_status": "Stanje: {{value}}",
|
"query_log_response_status": "Stanje: {{value}}",
|
||||||
"query_log_filtered": "Filtrirano od {{filter}}",
|
"query_log_filtered": "Filtrirano od {{filter}}",
|
||||||
"query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?",
|
"query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?",
|
||||||
|
|||||||
@@ -162,8 +162,8 @@
|
|||||||
"page_table_footer_text": "Sida",
|
"page_table_footer_text": "Sida",
|
||||||
"rows_table_footer_text": "rader",
|
"rows_table_footer_text": "rader",
|
||||||
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
|
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
|
||||||
"rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna",
|
"rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna",
|
"rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna: {{rule}}",
|
||||||
"query_log_response_status": "Status: {{value}}",
|
"query_log_response_status": "Status: {{value}}",
|
||||||
"query_log_filtered": "Filtrerat av {{filter}}",
|
"query_log_filtered": "Filtrerat av {{filter}}",
|
||||||
"query_log_confirm_clear": "Är du säker på att du vill rensa hela förfrågningsloggen?",
|
"query_log_confirm_clear": "Är du säker på att du vill rensa hela förfrågningsloggen?",
|
||||||
|
|||||||
@@ -167,8 +167,8 @@
|
|||||||
"page_table_footer_text": "หน้า",
|
"page_table_footer_text": "หน้า",
|
||||||
"rows_table_footer_text": "ตาราง",
|
"rows_table_footer_text": "ตาราง",
|
||||||
"updated_custom_filtering_toast": "อัปเดตกฎการกรองที่กำหนดเอง",
|
"updated_custom_filtering_toast": "อัปเดตกฎการกรองที่กำหนดเอง",
|
||||||
"rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว",
|
"rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว",
|
"rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว {{rule}}",
|
||||||
"query_log_response_status": "สถานะ: {{value}}",
|
"query_log_response_status": "สถานะ: {{value}}",
|
||||||
"query_log_filtered": "กรองโดย {{filter}}",
|
"query_log_filtered": "กรองโดย {{filter}}",
|
||||||
"query_log_confirm_clear": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?",
|
"query_log_confirm_clear": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?",
|
||||||
|
|||||||
@@ -197,8 +197,8 @@
|
|||||||
"page_table_footer_text": "Sayfa",
|
"page_table_footer_text": "Sayfa",
|
||||||
"rows_table_footer_text": "satır",
|
"rows_table_footer_text": "satır",
|
||||||
"updated_custom_filtering_toast": "İsteğe bağlı filtreleme kuralları güncellendi",
|
"updated_custom_filtering_toast": "İsteğe bağlı filtreleme kuralları güncellendi",
|
||||||
"rule_removed_from_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarından kaldırıldı",
|
"rule_removed_from_custom_filtering_toast": "Özel filtreleme kurallarından kural kaldırıldı: {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarına eklendi",
|
"rule_added_to_custom_filtering_toast": "Özel filtreleme kurallarına kural eklendi: {{rule}}",
|
||||||
"query_log_response_status": "Durum: {{value}}",
|
"query_log_response_status": "Durum: {{value}}",
|
||||||
"query_log_filtered": "{{filter}} tarafından filtrelendi",
|
"query_log_filtered": "{{filter}} tarafından filtrelendi",
|
||||||
"query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?",
|
"query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?",
|
||||||
|
|||||||
@@ -172,8 +172,8 @@
|
|||||||
"page_table_footer_text": "Trang",
|
"page_table_footer_text": "Trang",
|
||||||
"rows_table_footer_text": "hàng",
|
"rows_table_footer_text": "hàng",
|
||||||
"updated_custom_filtering_toast": "Đã cập nhật quy tắc lọc tuỳ chỉnh",
|
"updated_custom_filtering_toast": "Đã cập nhật quy tắc lọc tuỳ chỉnh",
|
||||||
"rule_removed_from_custom_filtering_toast": "Quy tắc đã được xoá khỏi quy tắc lọc tuỳ chỉnh",
|
"rule_removed_from_custom_filtering_toast": "Quy tắc đã được xoá khỏi quy tắc lọc tuỳ chỉnh {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "Quy tắc đã được thêm vào quy tắc lọc tuỳ chỉnh",
|
"rule_added_to_custom_filtering_toast": "Quy tắc đã được thêm vào quy tắc lọc tuỳ chỉnh: {{rule}}",
|
||||||
"query_log_response_status": "Trạng thái: {{value}}",
|
"query_log_response_status": "Trạng thái: {{value}}",
|
||||||
"query_log_filtered": "Được lọc bởi {{filter}}",
|
"query_log_filtered": "Được lọc bởi {{filter}}",
|
||||||
"query_log_confirm_clear": "Bạn có chắc chắn muốn xóa toàn bộ nhật ký truy vấn không?",
|
"query_log_confirm_clear": "Bạn có chắc chắn muốn xóa toàn bộ nhật ký truy vấn không?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "页",
|
"page_table_footer_text": "页",
|
||||||
"rows_table_footer_text": "行",
|
"rows_table_footer_text": "行",
|
||||||
"updated_custom_filtering_toast": "自定义过滤规则已更新",
|
"updated_custom_filtering_toast": "自定义过滤规则已更新",
|
||||||
"rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除",
|
"rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除 {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中",
|
"rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中 {{rule}}",
|
||||||
"query_log_response_status": "状态: {{value}}",
|
"query_log_response_status": "状态: {{value}}",
|
||||||
"query_log_filtered": "被 {{filter}} 过滤",
|
"query_log_filtered": "被 {{filter}} 过滤",
|
||||||
"query_log_confirm_clear": "你确定想要清除全部查询日志吗?",
|
"query_log_confirm_clear": "你确定想要清除全部查询日志吗?",
|
||||||
|
|||||||
@@ -208,8 +208,8 @@
|
|||||||
"page_table_footer_text": "頁面",
|
"page_table_footer_text": "頁面",
|
||||||
"rows_table_footer_text": "列",
|
"rows_table_footer_text": "列",
|
||||||
"updated_custom_filtering_toast": "已更新自訂的過濾規則",
|
"updated_custom_filtering_toast": "已更新自訂的過濾規則",
|
||||||
"rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除",
|
"rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除 {{rule}}",
|
||||||
"rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中",
|
"rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中 {{rule}}",
|
||||||
"query_log_response_status": "狀態:{{value}}",
|
"query_log_response_status": "狀態:{{value}}",
|
||||||
"query_log_filtered": "被 {{filter}} 過濾",
|
"query_log_filtered": "被 {{filter}} 過濾",
|
||||||
"query_log_confirm_clear": "您確定您想要清除整個查詢記錄嗎?",
|
"query_log_confirm_clear": "您確定您想要清除整個查詢記錄嗎?",
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ import { createAction } from 'redux-actions';
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import endsWith from 'lodash/endsWith';
|
||||||
|
import escapeRegExp from 'lodash/escapeRegExp';
|
||||||
|
import React from 'react';
|
||||||
import { splitByNewLine, sortClients } from '../helpers/helpers';
|
import { splitByNewLine, sortClients } from '../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME,
|
BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, GETTING_STARTED_LINK,
|
||||||
} from '../helpers/constants';
|
} from '../helpers/constants';
|
||||||
import { areEqualVersions } from '../helpers/version';
|
import { areEqualVersions } from '../helpers/version';
|
||||||
import { getTlsStatus } from './encryption';
|
import { getTlsStatus } from './encryption';
|
||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts';
|
import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts';
|
||||||
|
import { getFilteringStatus, setRules } from './filtering';
|
||||||
|
|
||||||
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
||||||
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||||
@@ -181,7 +185,14 @@ export const getUpdate = () => async (dispatch, getState) => {
|
|||||||
|
|
||||||
dispatch(getUpdateRequest());
|
dispatch(getUpdateRequest());
|
||||||
const handleRequestError = () => {
|
const handleRequestError = () => {
|
||||||
dispatch(addNoticeToast({ error: 'update_failed' }));
|
const options = {
|
||||||
|
components: {
|
||||||
|
a: <a href={GETTING_STARTED_LINK} target="_blank"
|
||||||
|
rel="noopener noreferrer" />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(addNoticeToast({ error: 'update_failed', options }));
|
||||||
dispatch(getUpdateFailure());
|
dispatch(getUpdateFailure());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -541,3 +552,44 @@ export const removeStaticLease = (config) => async (dispatch) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const removeToast = createAction('REMOVE_TOAST');
|
export const removeToast = createAction('REMOVE_TOAST');
|
||||||
|
|
||||||
|
export const toggleBlocking = (
|
||||||
|
type, domain, baseRule, baseUnblocking,
|
||||||
|
) => async (dispatch, getState) => {
|
||||||
|
const baseBlockingRule = baseRule || `||${domain}^$important`;
|
||||||
|
const baseUnblockingRule = baseUnblocking || `@@${baseBlockingRule}`;
|
||||||
|
const { userRules } = getState().filtering;
|
||||||
|
|
||||||
|
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
||||||
|
|
||||||
|
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule;
|
||||||
|
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule;
|
||||||
|
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||||
|
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||||
|
|
||||||
|
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
|
||||||
|
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
|
||||||
|
|
||||||
|
if (matchPreparedBlockingRule) {
|
||||||
|
dispatch(setRules(userRules.replace(`${blockingRule}`, '')));
|
||||||
|
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||||
|
} else if (!matchPreparedUnblockingRule) {
|
||||||
|
dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`));
|
||||||
|
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||||
|
} else if (matchPreparedUnblockingRule) {
|
||||||
|
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||||
|
return;
|
||||||
|
} else if (!matchPreparedBlockingRule) {
|
||||||
|
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(getFilteringStatus());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleBlockingForClient = (type, domain, client) => {
|
||||||
|
const baseRule = `||${domain}^$client='${client.replace(/'/g, '/\'')}'`;
|
||||||
|
const baseUnblocking = `@@${baseRule}`;
|
||||||
|
|
||||||
|
return toggleBlocking(type, domain, baseRule, baseUnblocking);
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createAction } from 'redux-actions';
|
|||||||
|
|
||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
import { addErrorToast } from './toasts';
|
import { addErrorToast } from './toasts';
|
||||||
|
import { HTML_PAGES } from '../helpers/constants';
|
||||||
|
|
||||||
export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST');
|
export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST');
|
||||||
export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE');
|
export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE');
|
||||||
@@ -11,7 +12,8 @@ export const processLogin = (values) => async (dispatch) => {
|
|||||||
dispatch(processLoginRequest());
|
dispatch(processLoginRequest());
|
||||||
try {
|
try {
|
||||||
await apiClient.login(values);
|
await apiClient.login(values);
|
||||||
const dashboardUrl = window.location.origin + window.location.pathname.replace('/login.html', '/');
|
const dashboardUrl = window.location.origin
|
||||||
|
+ window.location.pathname.replace(HTML_PAGES.LOGIN, HTML_PAGES.MAIN);
|
||||||
window.location.replace(dashboardUrl);
|
window.location.replace(dashboardUrl);
|
||||||
dispatch(processLoginSuccess());
|
dispatch(processLoginSuccess());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { createAction } from 'redux-actions';
|
|||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOGS_FILTER,
|
DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT,
|
||||||
TABLE_DEFAULT_PAGE_SIZE,
|
|
||||||
TABLE_FIRST_PAGE,
|
|
||||||
} from '../helpers/constants';
|
} from '../helpers/constants';
|
||||||
import { addErrorToast, addSuccessToast } from './toasts';
|
import { addErrorToast, addSuccessToast } from './toasts';
|
||||||
|
|
||||||
@@ -37,15 +35,22 @@ export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUES
|
|||||||
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
|
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
|
||||||
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
|
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
|
||||||
|
|
||||||
const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
const shortPollQueryLogs = async (data, filter, dispatch, getState, total) => {
|
||||||
const { logs, oldest } = data;
|
const { logs, oldest } = data;
|
||||||
const totalData = total || { logs };
|
const totalData = total || { logs };
|
||||||
|
|
||||||
const needToGetAdditionalLogs = (logs.length < TABLE_DEFAULT_PAGE_SIZE
|
const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
|
||||||
|| totalData.logs.length < TABLE_DEFAULT_PAGE_SIZE)
|
const currentQuery = queryForm && queryForm.values.search;
|
||||||
&& oldest !== '';
|
const previousQuery = filter?.search;
|
||||||
|
const isQueryTheSame = typeof previousQuery === 'string'
|
||||||
|
&& typeof currentQuery === 'string'
|
||||||
|
&& previousQuery === currentQuery;
|
||||||
|
|
||||||
if (needToGetAdditionalLogs) {
|
const isShortPollingNeeded = (logs.length < QUERY_LOGS_PAGE_LIMIT
|
||||||
|
|| totalData.logs.length < QUERY_LOGS_PAGE_LIMIT)
|
||||||
|
&& oldest !== '' && isQueryTheSame;
|
||||||
|
|
||||||
|
if (isShortPollingNeeded) {
|
||||||
dispatch(getAdditionalLogsRequest());
|
dispatch(getAdditionalLogsRequest());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -54,7 +59,7 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
|||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
if (additionalLogs.oldest.length > 0) {
|
if (additionalLogs.oldest.length > 0) {
|
||||||
return await checkFilteredLogs(additionalLogs, filter, dispatch, {
|
return await shortPollQueryLogs(additionalLogs, filter, dispatch, getState, {
|
||||||
logs: [...totalData.logs, ...additionalLogs.logs],
|
logs: [...totalData.logs, ...additionalLogs.logs],
|
||||||
oldest: additionalLogs.oldest,
|
oldest: additionalLogs.oldest,
|
||||||
});
|
});
|
||||||
@@ -71,31 +76,25 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
|||||||
return totalData;
|
return totalData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setLogsPagination = createAction('LOGS_PAGINATION');
|
|
||||||
export const setLogsPage = createAction('SET_LOG_PAGE');
|
|
||||||
export const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS');
|
export const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS');
|
||||||
|
|
||||||
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||||
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||||
|
|
||||||
export const getLogs = (config) => async (dispatch, getState) => {
|
export const getLogs = () => async (dispatch, getState) => {
|
||||||
dispatch(getLogsRequest());
|
dispatch(getLogsRequest());
|
||||||
try {
|
try {
|
||||||
const { isFiltered, filter, page } = getState().queryLogs;
|
const { isFiltered, filter, oldest } = getState().queryLogs;
|
||||||
const data = await getLogsWithParams({
|
const data = await getLogsWithParams({
|
||||||
...config,
|
older_than: oldest,
|
||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFiltered) {
|
if (isFiltered) {
|
||||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
||||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||||
dispatch(getLogsSuccess(updatedData));
|
dispatch(getLogsSuccess(updatedData));
|
||||||
dispatch(setLogsPagination({
|
|
||||||
page,
|
|
||||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(getLogsSuccess(data));
|
dispatch(getLogsSuccess(data));
|
||||||
}
|
}
|
||||||
@@ -111,7 +110,7 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
|
|||||||
*
|
*
|
||||||
* @param filter
|
* @param filter
|
||||||
* @param {string} filter.search
|
* @param {string} filter.search
|
||||||
* @param {string} filter.response_status query field of RESPONSE_FILTER object
|
* @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object
|
||||||
* @returns function
|
* @returns function
|
||||||
*/
|
*/
|
||||||
export const setLogsFilter = (filter) => setLogsFilterRequest(filter);
|
export const setLogsFilter = (filter) => setLogsFilterRequest(filter);
|
||||||
@@ -120,21 +119,20 @@ export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST');
|
|||||||
export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
|
export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
|
||||||
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
|
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
|
||||||
|
|
||||||
export const setFilteredLogs = (filter) => async (dispatch) => {
|
export const setFilteredLogs = (filter) => async (dispatch, getState) => {
|
||||||
dispatch(setFilteredLogsRequest());
|
dispatch(setFilteredLogsRequest());
|
||||||
try {
|
try {
|
||||||
const data = await getLogsWithParams({
|
const data = await getLogsWithParams({
|
||||||
older_than: '',
|
older_than: '',
|
||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
||||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||||
|
|
||||||
dispatch(setFilteredLogsSuccess({
|
dispatch(setFilteredLogsSuccess({
|
||||||
...updatedData,
|
...updatedData,
|
||||||
filter,
|
filter,
|
||||||
}));
|
}));
|
||||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(setFilteredLogsFailure(error));
|
dispatch(setFilteredLogsFailure(error));
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { getPathWithQueryString } from '../helpers/helpers';
|
import { getPathWithQueryString } from '../helpers/helpers';
|
||||||
import { R_PATH_LAST_PART } from '../helpers/constants';
|
import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART } from '../helpers/constants';
|
||||||
import { BASE_URL } from '../../constants';
|
import { BASE_URL } from '../../constants';
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
baseUrl = BASE_URL;
|
baseUrl = BASE_URL;
|
||||||
|
|
||||||
async makeRequest(path, method = 'POST', config) {
|
async makeRequest(path, method = 'POST', config) {
|
||||||
|
const url = `${this.baseUrl}/${path}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
url: `${this.baseUrl}/${path}`,
|
url,
|
||||||
method,
|
method,
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
const errorPath = url;
|
||||||
const errorPath = `${this.baseUrl}/${path}`;
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
if (error.response.status === 403) {
|
const { pathname } = document.location;
|
||||||
const loginPageUrl = window.location.href.replace(R_PATH_LAST_PART, '/login.html');
|
const shouldRedirect = pathname !== HTML_PAGES.LOGIN
|
||||||
|
&& pathname !== HTML_PAGES.INSTALL;
|
||||||
|
|
||||||
|
if (error.response.status === 403 && shouldRedirect) {
|
||||||
|
const loginPageUrl = window.location.href
|
||||||
|
.replace(R_PATH_LAST_PART, HTML_PAGES.LOGIN);
|
||||||
window.location.replace(loginPageUrl);
|
window.location.replace(loginPageUrl);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -530,6 +536,8 @@ class Api {
|
|||||||
|
|
||||||
getQueryLog(params) {
|
getQueryLog(params) {
|
||||||
const { path, method } = this.GET_QUERY_LOG;
|
const { path, method } = this.GET_QUERY_LOG;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
params.limit = QUERY_LOGS_PAGE_LIMIT;
|
||||||
const url = getPathWithQueryString(path, params);
|
const url = getPathWithQueryString(path, params);
|
||||||
return this.makeRequest(url, method);
|
return this.makeRequest(url, method);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,3 +66,12 @@ body {
|
|||||||
.select--no-warning {
|
.select--no-warning {
|
||||||
margin-bottom: 1.375rem;
|
margin-bottom: 1.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-action {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__row:hover .button-action,
|
||||||
|
.button-action--active {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import Header from '../Header';
|
|||||||
import { changeLanguage, getDnsStatus } from '../../actions';
|
import { changeLanguage, getDnsStatus } from '../../actions';
|
||||||
|
|
||||||
import Dashboard from '../../containers/Dashboard';
|
import Dashboard from '../../containers/Dashboard';
|
||||||
import Logs from '../../containers/Logs';
|
|
||||||
import SetupGuide from '../../containers/SetupGuide';
|
import SetupGuide from '../../containers/SetupGuide';
|
||||||
import Settings from '../../containers/Settings';
|
import Settings from '../../containers/Settings';
|
||||||
import Dns from '../../containers/Dns';
|
import Dns from '../../containers/Dns';
|
||||||
@@ -38,6 +37,7 @@ import DnsAllowlist from '../../containers/DnsAllowlist';
|
|||||||
import DnsRewrites from '../../containers/DnsRewrites';
|
import DnsRewrites from '../../containers/DnsRewrites';
|
||||||
import CustomRules from '../../containers/CustomRules';
|
import CustomRules from '../../containers/CustomRules';
|
||||||
import Services from '../Filters/Services';
|
import Services from '../Filters/Services';
|
||||||
|
import Logs from '../Logs';
|
||||||
|
|
||||||
|
|
||||||
const ROUTES = [
|
const ROUTES = [
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
import Cell from '../ui/Cell';
|
import Cell from '../ui/Cell';
|
||||||
|
|
||||||
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
|
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
|
||||||
import { IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
|
import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
|
||||||
import { formatClientCell } from '../../helpers/formatClientCell';
|
import { toggleClientBlock } from '../../actions/access';
|
||||||
|
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
|
||||||
|
|
||||||
const getClientsPercentColor = (percent) => {
|
const getClientsPercentColor = (percent) => {
|
||||||
if (percent > 50) {
|
if (percent > 50) {
|
||||||
@@ -20,126 +23,132 @@ const getClientsPercentColor = (percent) => {
|
|||||||
return STATUS_COLORS.red;
|
return STATUS_COLORS.red;
|
||||||
};
|
};
|
||||||
|
|
||||||
const countCell = (dnsQueries) => function cell(row) {
|
const CountCell = (row) => {
|
||||||
const { value } = row;
|
const { value, original: { ip } } = row;
|
||||||
const percent = getPercent(dnsQueries, value);
|
const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual);
|
||||||
|
|
||||||
|
const percent = getPercent(numDnsQueries, value);
|
||||||
const percentColor = getClientsPercentColor(percent);
|
const percentColor = getClientsPercentColor(percent);
|
||||||
|
|
||||||
return <Cell value={value} percent={percent} color={percentColor} search={row.original.ip} />;
|
return <Cell value={value} percent={percent} color={percentColor} search={ip} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) => {
|
const renderBlockingButton = (ip) => {
|
||||||
const buttonProps = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND
|
const dispatch = useDispatch();
|
||||||
? {
|
const { t } = useTranslation();
|
||||||
className: 'btn-outline-danger',
|
const processingSet = useSelector((state) => state.access.processingSet);
|
||||||
text: 'block',
|
const disallowed_clients = useSelector(
|
||||||
type: 'block',
|
(state) => state.access.disallowed_clients, shallowEqual,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ipMatchListStatus = getIpMatchListStatus(ip, disallowed_clients);
|
||||||
|
|
||||||
|
if (ipMatchListStatus === IP_MATCH_LIST_STATUS.CIDR) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND;
|
||||||
|
const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
|
||||||
|
const text = type;
|
||||||
|
|
||||||
|
const buttonClass = classNames('button-action button-action--main', {
|
||||||
|
'button-action--unblock': !isNotFound,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleClientStatus = (type, ip) => {
|
||||||
|
const confirmMessage = type === BLOCK_ACTIONS.BLOCK
|
||||||
|
? `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`
|
||||||
|
: t('client_confirm_unblock', { ip });
|
||||||
|
|
||||||
|
if (window.confirm(confirmMessage)) {
|
||||||
|
dispatch(toggleClientBlock(type, ip));
|
||||||
}
|
}
|
||||||
: {
|
};
|
||||||
className: 'btn-outline-secondary',
|
|
||||||
text: 'unblock',
|
|
||||||
type: 'unblock',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const onClick = () => toggleClientStatus(type, ip);
|
||||||
<div className="table__action button__action">
|
|
||||||
<button
|
return <div className="table__action pl-4">
|
||||||
type="button"
|
<button
|
||||||
className={`btn btn-sm ${buttonProps.className}`}
|
type="button"
|
||||||
onClick={() => handleClick(buttonProps.type, ip)}
|
className={buttonClass}
|
||||||
disabled={processing}
|
onClick={onClick}
|
||||||
>
|
disabled={processingSet}
|
||||||
<Trans>{buttonProps.text}</Trans>
|
>
|
||||||
</button>
|
<Trans>{text}</Trans>
|
||||||
</div>
|
</button>
|
||||||
);
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clientCell = (t, toggleClientStatus, processing, disallowedClients) => function cell(row) {
|
const ClientCell = (row) => {
|
||||||
const { value } = row;
|
const { value, original: { info } } = row;
|
||||||
const ipMatchListStatus = getIpMatchListStatus(value, disallowedClients);
|
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<>
|
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
|
||||||
<div className="logs__row logs__row--overflow logs__row--column">
|
{renderFormattedClientCell(value, info, true)}
|
||||||
{formatClientCell(row, true, false)}
|
{renderBlockingButton(value)}
|
||||||
</div>
|
</div>
|
||||||
{ipMatchListStatus !== IP_MATCH_LIST_STATUS.CIDR
|
</>;
|
||||||
&& renderBlockingButton(ipMatchListStatus, value, toggleClientStatus, processing)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Clients = ({
|
const Clients = ({
|
||||||
t,
|
|
||||||
refreshButton,
|
refreshButton,
|
||||||
topClients,
|
|
||||||
subtitle,
|
subtitle,
|
||||||
dnsQueries,
|
}) => {
|
||||||
toggleClientStatus,
|
const { t } = useTranslation();
|
||||||
processingAccessSet,
|
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
|
||||||
disallowedClients,
|
const disallowedClients = useSelector((state) => state.access.disallowed_clients, shallowEqual);
|
||||||
}) => (
|
|
||||||
<Card
|
return <Card
|
||||||
title={t('top_clients')}
|
title={t('top_clients')}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
bodyType="card-table"
|
bodyType="card-table"
|
||||||
refresh={refreshButton}
|
refresh={refreshButton}
|
||||||
>
|
>
|
||||||
<ReactTable
|
<ReactTable
|
||||||
data={topClients.map(({
|
data={topClients.map(({
|
||||||
name: ip, count, info, blocked,
|
name: ip, count, info, blocked,
|
||||||
}) => ({
|
}) => ({
|
||||||
ip,
|
ip,
|
||||||
count,
|
count,
|
||||||
info,
|
info,
|
||||||
blocked,
|
blocked,
|
||||||
}))}
|
}))}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
Header: 'IP',
|
Header: 'IP',
|
||||||
accessor: 'ip',
|
accessor: 'ip',
|
||||||
sortMethod: sortIp,
|
sortMethod: sortIp,
|
||||||
Cell: clientCell(t, toggleClientStatus, processingAccessSet, disallowedClients),
|
Cell: ClientCell,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: <Trans>requests_count</Trans>,
|
Header: <Trans>requests_count</Trans>,
|
||||||
accessor: 'count',
|
accessor: 'count',
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
maxWidth: 200,
|
maxWidth: 200,
|
||||||
Cell: countCell(dnsQueries),
|
Cell: CountCell,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
showPagination={false}
|
showPagination={false}
|
||||||
noDataText={t('no_clients_found')}
|
noDataText={t('no_clients_found')}
|
||||||
minRows={6}
|
minRows={6}
|
||||||
defaultPageSize={100}
|
defaultPageSize={100}
|
||||||
className="-highlight card-table-overflow--limited clients__table"
|
className="-highlight card-table-overflow--limited clients__table"
|
||||||
getTrProps={(_state, rowInfo) => {
|
getTrProps={(_state, rowInfo) => {
|
||||||
if (!rowInfo) {
|
if (!rowInfo) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ip } = rowInfo.original;
|
const { ip } = rowInfo.original;
|
||||||
|
|
||||||
return getIpMatchListStatus(ip, disallowedClients)
|
return getIpMatchListStatus(ip, disallowedClients) === IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' };
|
||||||
=== IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' };
|
}}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>;
|
||||||
);
|
|
||||||
|
|
||||||
Clients.propTypes = {
|
|
||||||
topClients: PropTypes.array.isRequired,
|
|
||||||
dnsQueries: PropTypes.number.isRequired,
|
|
||||||
refreshButton: PropTypes.node.isRequired,
|
|
||||||
clients: PropTypes.array.isRequired,
|
|
||||||
autoClients: PropTypes.array.isRequired,
|
|
||||||
subtitle: PropTypes.string.isRequired,
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
toggleClientStatus: PropTypes.func.isRequired,
|
|
||||||
processingAccessSet: PropTypes.bool.isRequired,
|
|
||||||
disallowedClients: PropTypes.string.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTranslation()(Clients);
|
Clients.propTypes = {
|
||||||
|
refreshButton: PropTypes.node.isRequired,
|
||||||
|
subtitle: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Clients;
|
||||||
|
|||||||
@@ -47,32 +47,32 @@ const Counters = ({ refreshButton, subtitle }) => {
|
|||||||
label: 'dns_query',
|
label: 'dns_query',
|
||||||
count: numDnsQueries,
|
count: numDnsQueries,
|
||||||
tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }),
|
tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }),
|
||||||
response_status: RESPONSE_FILTER.ALL.query,
|
response_status: RESPONSE_FILTER.ALL.QUERY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'blocked_by',
|
label: 'blocked_by',
|
||||||
count: numBlockedFiltering,
|
count: numBlockedFiltering,
|
||||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours',
|
tooltipTitle: 'number_of_dns_query_blocked_24_hours',
|
||||||
response_status: RESPONSE_FILTER.BLOCKED.query,
|
response_status: RESPONSE_FILTER.BLOCKED.QUERY,
|
||||||
translationComponents: [<a href="#filters" key="0">link</a>],
|
translationComponents: [<a href="#filters" key="0">link</a>],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'stats_malware_phishing',
|
label: 'stats_malware_phishing',
|
||||||
count: numReplacedSafebrowsing,
|
count: numReplacedSafebrowsing,
|
||||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',
|
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',
|
||||||
response_status: RESPONSE_FILTER.BLOCKED_THREATS.query,
|
response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'stats_adult',
|
label: 'stats_adult',
|
||||||
count: numReplacedParental,
|
count: numReplacedParental,
|
||||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',
|
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',
|
||||||
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.query,
|
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'enforced_save_search',
|
label: 'enforced_save_search',
|
||||||
count: numReplacedSafesearch,
|
count: numReplacedSafesearch,
|
||||||
tooltipTitle: 'number_of_dns_query_to_safe_search',
|
tooltipTitle: 'number_of_dns_query_to_safe_search',
|
||||||
response_status: RESPONSE_FILTER.SAFE_SEARCH.query,
|
response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'average_processing_time',
|
label: 'average_processing_time',
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import BlockedDomains from './BlockedDomains';
|
|||||||
|
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import { BLOCK_ACTIONS } from '../../helpers/constants';
|
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
|
|
||||||
const Dashboard = ({
|
const Dashboard = ({
|
||||||
@@ -19,7 +18,6 @@ const Dashboard = ({
|
|||||||
getStatsConfig,
|
getStatsConfig,
|
||||||
dashboard,
|
dashboard,
|
||||||
toggleProtection,
|
toggleProtection,
|
||||||
toggleClientBlock,
|
|
||||||
stats,
|
stats,
|
||||||
access,
|
access,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -50,14 +48,6 @@ const Dashboard = ({
|
|||||||
</button>;
|
</button>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleClientStatus = (type, ip) => {
|
|
||||||
const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock';
|
|
||||||
|
|
||||||
if (window.confirm(t(confirmMessage, { ip }))) {
|
|
||||||
toggleClientBlock(type, ip);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshButton = <button
|
const refreshButton = <button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-icon btn-outline-primary btn-sm"
|
className="btn btn-icon btn-outline-primary btn-sm"
|
||||||
@@ -122,7 +112,6 @@ const Dashboard = ({
|
|||||||
clients={dashboard.clients}
|
clients={dashboard.clients}
|
||||||
autoClients={dashboard.autoClients}
|
autoClients={dashboard.autoClients}
|
||||||
refreshButton={refreshButton}
|
refreshButton={refreshButton}
|
||||||
toggleClientStatus={toggleClientStatus}
|
|
||||||
processingAccessSet={access.processingSet}
|
processingAccessSet={access.processingSet}
|
||||||
disallowedClients={access.disallowed_clients}
|
disallowedClients={access.disallowed_clients}
|
||||||
/>
|
/>
|
||||||
@@ -157,7 +146,6 @@ Dashboard.propTypes = {
|
|||||||
getStatsConfig: PropTypes.func.isRequired,
|
getStatsConfig: PropTypes.func.isRequired,
|
||||||
toggleProtection: PropTypes.func.isRequired,
|
toggleProtection: PropTypes.func.isRequired,
|
||||||
getClients: PropTypes.func.isRequired,
|
getClients: PropTypes.func.isRequired,
|
||||||
toggleClientBlock: PropTypes.func.isRequired,
|
|
||||||
getAccessList: PropTypes.func.isRequired,
|
getAccessList: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,25 +11,10 @@ import {
|
|||||||
checkWhiteList,
|
checkWhiteList,
|
||||||
checkSafeSearch,
|
checkSafeSearch,
|
||||||
checkSafeBrowsing,
|
checkSafeBrowsing,
|
||||||
checkParental,
|
checkParental, getFilterName,
|
||||||
} from '../../../helpers/helpers';
|
} from '../../../helpers/helpers';
|
||||||
import { FILTERED } from '../../../helpers/constants';
|
import { FILTERED } from '../../../helpers/constants';
|
||||||
|
|
||||||
const getFilterName = (id, filters, whitelistFilters, t) => {
|
|
||||||
if (id === 0) {
|
|
||||||
return t('filtered_custom_rules');
|
|
||||||
}
|
|
||||||
|
|
||||||
const filter = filters.find((filter) => filter.id === id)
|
|
||||||
|| whitelistFilters.find((filter) => filter.id === id);
|
|
||||||
|
|
||||||
if (filter && filter.name) {
|
|
||||||
return t('query_log_filtered', { filter: filter.name });
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTitle = (reason, filterName, t, onlyFiltered) => {
|
const getTitle = (reason, filterName, t, onlyFiltered) => {
|
||||||
if (checkNotFilteredNotFound(reason)) {
|
if (checkNotFilteredNotFound(reason)) {
|
||||||
return t('check_not_found');
|
return t('check_not_found');
|
||||||
@@ -101,7 +86,12 @@ const Info = ({
|
|||||||
ip_addrs,
|
ip_addrs,
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const filterName = getFilterName(filter_id, filters, whitelistFilters, t);
|
const filterName = getFilterName(filters,
|
||||||
|
whitelistFilters,
|
||||||
|
filter_id,
|
||||||
|
'filtered_custom_rules',
|
||||||
|
(filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''));
|
||||||
|
|
||||||
const onlyFiltered = checkSafeSearch(reason)
|
const onlyFiltered = checkSafeSearch(reason)
|
||||||
|| checkSafeBrowsing(reason)
|
|| checkSafeBrowsing(reason)
|
||||||
|| checkParental(reason);
|
|| checkParental(reason);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Table extends Component {
|
|||||||
accessor: 'url',
|
accessor: 'url',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
Cell: ({ value }) => (
|
Cell: ({ value }) => (
|
||||||
<div className="logs__row o-hidden">
|
<div className="logs__row">
|
||||||
{isValidAbsolutePath(value) ? value
|
{isValidAbsolutePath(value) ? value
|
||||||
: <a
|
: <a
|
||||||
href={value}
|
href={value}
|
||||||
|
|||||||
@@ -164,6 +164,10 @@
|
|||||||
color: #9aa0ac;
|
color: #9aa0ac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-icon--white {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.header-brand-img {
|
.header-brand-img {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
174
client/src/components/Logs/Cells/ClientCell.js
Normal file
174
client/src/components/Logs/Cells/ClientCell.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
|
||||||
|
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||||
|
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
|
||||||
|
import IconTooltip from './IconTooltip';
|
||||||
|
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
|
||||||
|
import { toggleClientBlock } from '../../../actions/access';
|
||||||
|
import { getBlockClientInfo } from './helpers';
|
||||||
|
|
||||||
|
const ClientCell = ({
|
||||||
|
client,
|
||||||
|
domain,
|
||||||
|
info,
|
||||||
|
info: { name, whois_info },
|
||||||
|
reason,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||||
|
const processingRules = useSelector((state) => state.filtering.processingRules);
|
||||||
|
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||||
|
const [isOptionsOpened, setOptionsOpened] = useState(false);
|
||||||
|
|
||||||
|
const disallowed_clients = useSelector(
|
||||||
|
(state) => state.access.disallowed_clients,
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||||
|
const source = autoClient?.source;
|
||||||
|
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||||
|
|
||||||
|
const id = nanoid();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
address: client,
|
||||||
|
name,
|
||||||
|
country: whois_info?.country,
|
||||||
|
city: whois_info?.city,
|
||||||
|
network: whois_info?.orgname,
|
||||||
|
source_label: source,
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedData = Object.entries(data);
|
||||||
|
|
||||||
|
const isFiltered = checkFiltered(reason);
|
||||||
|
|
||||||
|
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||||
|
'mt-2': isDetailed && !name && !whoisAvailable,
|
||||||
|
'white-space--nowrap': isDetailed,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
|
||||||
|
'my-3': isDetailed,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderBlockingButton = (isFiltered, domain) => {
|
||||||
|
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||||
|
const clients = useSelector((state) => state.dashboard.clients);
|
||||||
|
|
||||||
|
const {
|
||||||
|
confirmMessage,
|
||||||
|
buttonKey: blockingClientKey,
|
||||||
|
type,
|
||||||
|
} = getBlockClientInfo(client, disallowed_clients);
|
||||||
|
|
||||||
|
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
|
||||||
|
const clientNameBlockingFor = getBlockingClientName(clients, client);
|
||||||
|
|
||||||
|
const BUTTON_OPTIONS_TO_ACTION_MAP = {
|
||||||
|
[blockingForClientKey]: () => {
|
||||||
|
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
|
||||||
|
},
|
||||||
|
[blockingClientKey]: () => {
|
||||||
|
const message = `${type === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`;
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
dispatch(toggleClientBlock(type, client));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = () => dispatch(toggleBlocking(buttonType, domain));
|
||||||
|
|
||||||
|
const getOptions = (optionToActionMap) => {
|
||||||
|
const options = Object.entries(optionToActionMap);
|
||||||
|
if (options.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <>{options
|
||||||
|
.map(([name, onClick]) => <div
|
||||||
|
key={name}
|
||||||
|
className="button-action--arrow-option px-4 py-2"
|
||||||
|
onClick={onClick}
|
||||||
|
>{t(name)}
|
||||||
|
</div>)}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getOptions(BUTTON_OPTIONS_TO_ACTION_MAP);
|
||||||
|
|
||||||
|
const buttonClass = classNames('button-action button-action--main', {
|
||||||
|
'button-action--unblock': isFiltered,
|
||||||
|
'button-action--with-options': content,
|
||||||
|
'button-action--active': isOptionsOpened,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonArrowClass = classNames('button-action button-action--arrow', {
|
||||||
|
'button-action--unblock': isFiltered,
|
||||||
|
'button-action--active': isOptionsOpened,
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerClass = classNames('button-action__container', {
|
||||||
|
'button-action__container--detailed': isDetailed,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={containerClass}>
|
||||||
|
<button type="button"
|
||||||
|
className={buttonClass}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={processingRules}
|
||||||
|
>
|
||||||
|
{t(buttonType)}
|
||||||
|
</button>
|
||||||
|
{content && <button className={buttonArrowClass} disabled={processingRules}>
|
||||||
|
<IconTooltip
|
||||||
|
className='h-100'
|
||||||
|
tooltipClass='button-action--arrow-option-container'
|
||||||
|
xlinkHref='chevron-down'
|
||||||
|
triggerClass='button-action--icon'
|
||||||
|
content={content} placement="bottom-end" trigger="click"
|
||||||
|
onVisibilityChange={setOptionsOpened}
|
||||||
|
/>
|
||||||
|
</button>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell">
|
||||||
|
<IconTooltip className={hintClass} columnClass='grid grid--limited' tooltipClass='px-5 pb-5 pt-4 mw-75'
|
||||||
|
xlinkHref='question' contentItemClass="contentItemClass" title="client_details"
|
||||||
|
content={processedData} placement="bottom" />
|
||||||
|
<div className={nameClass}>
|
||||||
|
<div data-tip={true} data-for={id}>
|
||||||
|
{renderFormattedClientCell(client, info, isDetailed, true)}
|
||||||
|
</div>
|
||||||
|
{isDetailed && name && !whoisAvailable
|
||||||
|
&& <div className="detailed-info d-none d-sm-block logs__text"
|
||||||
|
title={name}>{name}</div>}
|
||||||
|
</div>
|
||||||
|
{renderBlockingButton(isFiltered, domain)}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
ClientCell.propTypes = {
|
||||||
|
client: propTypes.string.isRequired,
|
||||||
|
domain: propTypes.string.isRequired,
|
||||||
|
info: propTypes.oneOfType([
|
||||||
|
propTypes.string,
|
||||||
|
propTypes.shape({
|
||||||
|
name: propTypes.string.isRequired,
|
||||||
|
whois_info: propTypes.shape({
|
||||||
|
country: propTypes.string,
|
||||||
|
city: propTypes.string,
|
||||||
|
orgname: propTypes.string,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
reason: propTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientCell;
|
||||||
29
client/src/components/Logs/Cells/DateCell.js
Normal file
29
client/src/components/Logs/Cells/DateCell.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
import { formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||||
|
import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants';
|
||||||
|
|
||||||
|
const DateCell = ({ time }) => {
|
||||||
|
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||||
|
|
||||||
|
if (!time) {
|
||||||
|
return '–';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
|
||||||
|
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
|
||||||
|
|
||||||
|
return <div className="logs__cell logs__cell logs__cell--date text-truncate" role="gridcell">
|
||||||
|
<div className="logs__time" title={formattedTime}>{formattedTime}</div>
|
||||||
|
{isDetailed
|
||||||
|
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||||
|
title={formattedDate}>{formattedDate}</div>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
DateCell.propTypes = {
|
||||||
|
time: propTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateCell;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import propTypes from 'prop-types';
|
||||||
import getIconTooltip from './getIconTooltip';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||||
LONG_TIME_FORMAT,
|
LONG_TIME_FORMAT,
|
||||||
@@ -9,15 +10,20 @@ import {
|
|||||||
} from '../../../helpers/constants';
|
} from '../../../helpers/constants';
|
||||||
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
|
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||||
import { getSourceData } from '../../../helpers/trackers/trackers';
|
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||||
|
import IconTooltip from './IconTooltip';
|
||||||
|
|
||||||
const getDomainCell = (props) => {
|
const DomainCell = ({
|
||||||
const {
|
answer_dnssec,
|
||||||
row, t, isDetailed, dnssec_enabled,
|
service_name,
|
||||||
} = props;
|
client_proto,
|
||||||
|
domain,
|
||||||
const {
|
time,
|
||||||
tracker, type, answer_dnssec, client_proto, domain, time,
|
tracker,
|
||||||
} = row.original;
|
type,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||||
|
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||||
|
|
||||||
const hasTracker = !!tracker;
|
const hasTracker = !!tracker;
|
||||||
|
|
||||||
@@ -44,14 +50,18 @@ const getDomainCell = (props) => {
|
|||||||
protocol,
|
protocol,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (service_name) {
|
||||||
|
requestDetailsObj.check_service = service_name;
|
||||||
|
}
|
||||||
|
|
||||||
const sourceData = getSourceData(tracker);
|
const sourceData = getSourceData(tracker);
|
||||||
|
|
||||||
const knownTrackerDataObj = {
|
const knownTrackerDataObj = {
|
||||||
name_table_header: tracker?.name,
|
name_table_header: tracker?.name,
|
||||||
category_label: hasTracker && captitalizeWords(tracker.category),
|
category_label: hasTracker && captitalizeWords(tracker.category),
|
||||||
source_label: sourceData
|
source_label: sourceData
|
||||||
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
|
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
|
||||||
className="link--green">{sourceData.name}</a>,
|
className="link--green">{sourceData.name}</a>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGrid = (content, idx) => {
|
const renderGrid = (content, idx) => {
|
||||||
@@ -72,51 +82,43 @@ const getDomainCell = (props) => {
|
|||||||
|
|
||||||
const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
|
const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
|
||||||
|
|
||||||
const trackerHint = getIconTooltip({
|
const valueClass = classNames('w-100 text-truncate', {
|
||||||
className: privacyIconClass,
|
|
||||||
tooltipClass: 'pt-4 pb-5 px-5 mw-75',
|
|
||||||
xlinkHref: 'privacy',
|
|
||||||
contentItemClass: 'key-colon',
|
|
||||||
renderContent,
|
|
||||||
place: 'bottom',
|
|
||||||
});
|
|
||||||
|
|
||||||
const valueClass = classNames('w-100', {
|
|
||||||
'px-2 d-flex justify-content-center flex-column': isDetailed,
|
'px-2 d-flex justify-content-center flex-column': isDetailed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const details = [ip, protocol].filter(Boolean)
|
const details = [ip, protocol].filter(Boolean)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
return (
|
return <div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell">
|
||||||
<div className="logs__row o-hidden">
|
{dnssec_enabled && <IconTooltip
|
||||||
{dnssec_enabled && getIconTooltip({
|
className={lockIconClass}
|
||||||
className: lockIconClass,
|
tooltipClass='py-4 px-5 pb-45'
|
||||||
tooltipClass: 'py-4 px-5 pb-45',
|
canShowTooltip={!!answer_dnssec}
|
||||||
canShowTooltip: answer_dnssec,
|
xlinkHref='lock'
|
||||||
xlinkHref: 'lock',
|
columnClass='w-100'
|
||||||
columnClass: 'w-100',
|
content='validated_with_dnssec'
|
||||||
content: 'validated_with_dnssec',
|
placement='bottom'
|
||||||
placement: 'bottom',
|
/>}
|
||||||
})}
|
<IconTooltip className={privacyIconClass} tooltipClass='pt-4 pb-5 px-5 mw-75'
|
||||||
{trackerHint}
|
xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent}
|
||||||
<div className={valueClass}>
|
place='bottom' />
|
||||||
<div className="text-truncate" title={domain}>{domain}</div>
|
<div className={valueClass}>
|
||||||
{details && isDetailed
|
<div className="text-truncate" title={domain}>{service_name || domain}</div>
|
||||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
{details && isDetailed
|
||||||
title={details}>{details}</div>}
|
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||||
</div>
|
title={details}>{details}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
getDomainCell.propTypes = {
|
DomainCell.propTypes = {
|
||||||
row: PropTypes.object.isRequired,
|
answer_dnssec: propTypes.bool.isRequired,
|
||||||
t: PropTypes.func.isRequired,
|
client_proto: propTypes.string.isRequired,
|
||||||
isDetailed: PropTypes.bool.isRequired,
|
domain: propTypes.string.isRequired,
|
||||||
toggleBlocking: PropTypes.func.isRequired,
|
time: propTypes.string.isRequired,
|
||||||
autoClients: PropTypes.array.isRequired,
|
type: propTypes.string.isRequired,
|
||||||
dnssec_enabled: PropTypes.bool.isRequired,
|
service_name: propTypes.string,
|
||||||
|
tracker: propTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getDomainCell;
|
export default DomainCell;
|
||||||
54
client/src/components/Logs/Cells/Header.js
Normal file
54
client/src/components/Logs/Cells/Header.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { toggleDetailedLogs } from '../../../actions/queryLogs';
|
||||||
|
import HeaderCell from './HeaderCell';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||||
|
const disableDetailedMode = () => dispatch(toggleDetailedLogs(false));
|
||||||
|
const enableDetailedMode = () => dispatch(toggleDetailedLogs(true));
|
||||||
|
|
||||||
|
const HEADERS = [
|
||||||
|
{
|
||||||
|
className: 'logs__cell--date',
|
||||||
|
content: 'time_table_header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'logs__cell--domain',
|
||||||
|
content: 'request_table_header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'logs__cell--response',
|
||||||
|
content: 'response_table_header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'logs__cell--client',
|
||||||
|
content: <>
|
||||||
|
{t('client_table_header')}
|
||||||
|
{<span>
|
||||||
|
<svg className={classNames('icons icon--24 icon--green cursor--pointer mr-2', { 'icon--selected': !isDetailed })}
|
||||||
|
onClick={disableDetailedMode}
|
||||||
|
>
|
||||||
|
<title>{t('compact')}</title>
|
||||||
|
<use xlinkHref='#list' /></svg>
|
||||||
|
<svg className={classNames('icons icon--24 icon--green cursor--pointer', { 'icon--selected': isDetailed })}
|
||||||
|
onClick={enableDetailedMode}
|
||||||
|
>
|
||||||
|
<title>{t('default')}</title>
|
||||||
|
<use xlinkHref='#detailed_list' />
|
||||||
|
</svg>
|
||||||
|
</span>}
|
||||||
|
</>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <div className="logs__cell--header__container px-5" role="row">
|
||||||
|
{HEADERS.map(HeaderCell)}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
22
client/src/components/Logs/Cells/HeaderCell.js
Normal file
22
client/src/components/Logs/Cells/HeaderCell.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const HeaderCell = ({ content, className }, idx) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return <div
|
||||||
|
key={idx}
|
||||||
|
className={classNames('logs__cell--header__item logs__cell logs__text--bold', className)}
|
||||||
|
role="columnheader"
|
||||||
|
>
|
||||||
|
{typeof content === 'string' ? t(content) : content}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
HeaderCell.propTypes = {
|
||||||
|
content: propTypes.oneOfType([propTypes.string, propTypes.element]).isRequired,
|
||||||
|
className: propTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderCell;
|
||||||
@@ -6,17 +6,21 @@ import { processContent } from '../../../helpers/helpers';
|
|||||||
import Tooltip from '../../ui/Tooltip';
|
import Tooltip from '../../ui/Tooltip';
|
||||||
import 'react-popper-tooltip/dist/styles.css';
|
import 'react-popper-tooltip/dist/styles.css';
|
||||||
import './IconTooltip.css';
|
import './IconTooltip.css';
|
||||||
|
import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants';
|
||||||
|
|
||||||
const getIconTooltip = ({
|
const IconTooltip = ({
|
||||||
className,
|
className,
|
||||||
contentItemClass,
|
contentItemClass,
|
||||||
columnClass,
|
columnClass,
|
||||||
|
triggerClass,
|
||||||
canShowTooltip = true,
|
canShowTooltip = true,
|
||||||
xlinkHref,
|
xlinkHref,
|
||||||
title,
|
title,
|
||||||
placement,
|
placement,
|
||||||
tooltipClass,
|
tooltipClass,
|
||||||
content,
|
content,
|
||||||
|
trigger,
|
||||||
|
onVisibilityChange,
|
||||||
renderContent = content ? React.Children.map(
|
renderContent = content ? React.Children.map(
|
||||||
processContent(content),
|
processContent(content),
|
||||||
(item, idx) => <div key={idx} className={contentItemClass}>
|
(item, idx) => <div key={idx} className={contentItemClass}>
|
||||||
@@ -36,6 +40,10 @@ const getIconTooltip = ({
|
|||||||
className={tooltipClassName}
|
className={tooltipClassName}
|
||||||
content={tooltipContent}
|
content={tooltipContent}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
|
triggerClass={triggerClass}
|
||||||
|
trigger={trigger}
|
||||||
|
onVisibilityChange={onVisibilityChange}
|
||||||
|
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
|
||||||
>
|
>
|
||||||
{xlinkHref && <svg className={className}>
|
{xlinkHref && <svg className={className}>
|
||||||
<use xlinkHref={`#${xlinkHref}`} />
|
<use xlinkHref={`#${xlinkHref}`} />
|
||||||
@@ -43,20 +51,20 @@ const getIconTooltip = ({
|
|||||||
</Tooltip>;
|
</Tooltip>;
|
||||||
};
|
};
|
||||||
|
|
||||||
getIconTooltip.propTypes = {
|
IconTooltip.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
trigger: PropTypes.string,
|
||||||
|
triggerClass: PropTypes.string,
|
||||||
contentItemClass: PropTypes.string,
|
contentItemClass: PropTypes.string,
|
||||||
columnClass: PropTypes.string,
|
columnClass: PropTypes.string,
|
||||||
tooltipClass: PropTypes.string,
|
tooltipClass: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
placement: PropTypes.string,
|
placement: PropTypes.string,
|
||||||
canShowTooltip: PropTypes.string,
|
canShowTooltip: PropTypes.bool,
|
||||||
xlinkHref: PropTypes.string,
|
xlinkHref: PropTypes.string,
|
||||||
content: PropTypes.oneOfType([
|
content: PropTypes.node,
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.array,
|
|
||||||
]),
|
|
||||||
renderContent: PropTypes.arrayOf(PropTypes.element),
|
renderContent: PropTypes.arrayOf(PropTypes.element),
|
||||||
|
onVisibilityChange: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getIconTooltip;
|
export default IconTooltip;
|
||||||
101
client/src/components/Logs/Cells/ResponseCell.js
Normal file
101
client/src/components/Logs/Cells/ResponseCell.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
import { formatElapsedMs, getFilterName } from '../../../helpers/helpers';
|
||||||
|
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
|
||||||
|
import IconTooltip from './IconTooltip';
|
||||||
|
|
||||||
|
const ResponseCell = ({
|
||||||
|
elapsedMs,
|
||||||
|
originalResponse,
|
||||||
|
reason,
|
||||||
|
response,
|
||||||
|
status,
|
||||||
|
upstream,
|
||||||
|
rule,
|
||||||
|
filterId,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
|
||||||
|
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||||
|
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||||
|
|
||||||
|
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||||
|
|
||||||
|
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
||||||
|
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
||||||
|
|
||||||
|
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
||||||
|
|
||||||
|
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
|
||||||
|
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
|
||||||
|
const filter = getFilterName(filters, whitelistFilters, filterId);
|
||||||
|
|
||||||
|
const renderResponses = (responseArr) => {
|
||||||
|
if (!responseArr || responseArr.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{responseArr.map((response) => {
|
||||||
|
const className = classNames('white-space--nowrap', {
|
||||||
|
'overflow-break': response.length > 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div key={response} className={className}>{`${response}\n`}</div>;
|
||||||
|
})}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMMON_CONTENT = {
|
||||||
|
encryption_status: boldStatusLabel,
|
||||||
|
install_settings_dns: upstream,
|
||||||
|
elapsed: formattedElapsedMs,
|
||||||
|
response_code: status,
|
||||||
|
filter,
|
||||||
|
rule_label: rule,
|
||||||
|
response_table_header: renderResponses(response),
|
||||||
|
original_response: renderResponses(originalResponse),
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = rule
|
||||||
|
? Object.entries(COMMON_CONTENT)
|
||||||
|
: Object.entries({
|
||||||
|
...COMMON_CONTENT,
|
||||||
|
filter: '',
|
||||||
|
});
|
||||||
|
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
||||||
|
|
||||||
|
|
||||||
|
return <div className="logs__cell logs__cell--response" role="gridcell">
|
||||||
|
<IconTooltip
|
||||||
|
className={classNames('icons mr-4 icon--24 icon--lightgray', { 'my-3': isDetailed })}
|
||||||
|
columnClass='grid grid--limited'
|
||||||
|
tooltipClass='px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details'
|
||||||
|
contentItemClass='text-truncate key-colon o-hidden'
|
||||||
|
xlinkHref='question'
|
||||||
|
title='response_details'
|
||||||
|
content={content}
|
||||||
|
placement='bottom'
|
||||||
|
/>
|
||||||
|
<div className="text-truncate">
|
||||||
|
<div className="text-truncate" title={statusLabel}>{statusLabel}</div>
|
||||||
|
{isDetailed && <div
|
||||||
|
className="detailed-info d-none d-sm-block pt-1 text-truncate"
|
||||||
|
title={detailedInfo}>{detailedInfo}</div>}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
ResponseCell.propTypes = {
|
||||||
|
elapsedMs: propTypes.string.isRequired,
|
||||||
|
originalResponse: propTypes.array.isRequired,
|
||||||
|
reason: propTypes.string.isRequired,
|
||||||
|
response: propTypes.array.isRequired,
|
||||||
|
status: propTypes.string.isRequired,
|
||||||
|
upstream: propTypes.string.isRequired,
|
||||||
|
rule: propTypes.string,
|
||||||
|
filterId: propTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResponseCell;
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { formatClientCell } from '../../../helpers/formatClientCell';
|
|
||||||
import getIconTooltip from './getIconTooltip';
|
|
||||||
import { checkFiltered } from '../../../helpers/helpers';
|
|
||||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
|
||||||
|
|
||||||
const getClientCell = ({
|
|
||||||
row, t, isDetailed, toggleBlocking, autoClients, processingRules,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
reason, client, domain, info: { name, whois_info },
|
|
||||||
} = row.original;
|
|
||||||
|
|
||||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
|
||||||
const source = autoClient?.source;
|
|
||||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
|
||||||
|
|
||||||
const id = nanoid();
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
address: client,
|
|
||||||
name,
|
|
||||||
country: whois_info?.country,
|
|
||||||
city: whois_info?.city,
|
|
||||||
network: whois_info?.orgname,
|
|
||||||
source_label: source,
|
|
||||||
};
|
|
||||||
|
|
||||||
const processedData = Object.entries(data);
|
|
||||||
|
|
||||||
const isFiltered = checkFiltered(reason);
|
|
||||||
|
|
||||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
|
||||||
'mt-2': isDetailed && !name && !whoisAvailable,
|
|
||||||
'white-space--nowrap': isDetailed,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
|
|
||||||
'my-3': isDetailed,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderBlockingButton = (isFiltered, domain) => {
|
|
||||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
|
||||||
|
|
||||||
const buttonClass = classNames('logs__action button__action', {
|
|
||||||
'btn-outline-secondary': isFiltered,
|
|
||||||
'btn-outline-danger': !isFiltered,
|
|
||||||
'logs__action--detailed': isDetailed,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onClick = () => toggleBlocking(buttonType, domain);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={buttonClass}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-sm ${buttonClass}`}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={processingRules}
|
|
||||||
>
|
|
||||||
{t(buttonType)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="logs__row o-hidden h-100">
|
|
||||||
{getIconTooltip({
|
|
||||||
className: hintClass,
|
|
||||||
columnClass: 'grid grid--limited',
|
|
||||||
tooltipClass: 'px-5 pb-5 pt-4 mw-75',
|
|
||||||
xlinkHref: 'question',
|
|
||||||
contentItemClass: 'text-truncate key-colon',
|
|
||||||
title: 'client_details',
|
|
||||||
content: processedData,
|
|
||||||
placement: 'bottom',
|
|
||||||
})}
|
|
||||||
<div className={nameClass}>
|
|
||||||
<div data-tip={true} data-for={id}>
|
|
||||||
{formatClientCell(row, isDetailed)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isDetailed && name && !whoisAvailable && (
|
|
||||||
<div
|
|
||||||
className="detailed-info d-none d-sm-block logs__text"
|
|
||||||
title={name}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{renderBlockingButton(isFiltered, domain)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getClientCell.propTypes = {
|
|
||||||
row: PropTypes.object.isRequired,
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
isDetailed: PropTypes.bool.isRequired,
|
|
||||||
toggleBlocking: PropTypes.func.isRequired,
|
|
||||||
autoClients: PropTypes.array.isRequired,
|
|
||||||
processingRules: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getClientCell;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { formatTime, formatDateTime } from '../../../helpers/helpers';
|
|
||||||
import {
|
|
||||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
|
||||||
DEFAULT_TIME_FORMAT,
|
|
||||||
} from '../../../helpers/constants';
|
|
||||||
|
|
||||||
const getDateCell = (row, isDetailed) => {
|
|
||||||
const { time } = row.original;
|
|
||||||
|
|
||||||
if (!time) {
|
|
||||||
return '–';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
|
|
||||||
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="logs__cell">
|
|
||||||
<div className="logs__time" title={formattedTime}>{formattedTime}</div>
|
|
||||||
{isDetailed && <div className="detailed-info d-none d-sm-block text-truncate"
|
|
||||||
title={formattedDate}>{formattedDate}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getDateCell;
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { formatElapsedMs } from '../../../helpers/helpers';
|
|
||||||
import {
|
|
||||||
FILTERED_STATUS,
|
|
||||||
FILTERED_STATUS_TO_META_MAP,
|
|
||||||
} from '../../../helpers/constants';
|
|
||||||
import getIconTooltip from './getIconTooltip';
|
|
||||||
|
|
||||||
const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
|
|
||||||
const {
|
|
||||||
reason, filterId, rule, status, upstream, elapsedMs, response, originalResponse,
|
|
||||||
} = row.original;
|
|
||||||
|
|
||||||
const { filters, whitelistFilters } = filtering;
|
|
||||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
|
||||||
|
|
||||||
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
|
||||||
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
|
||||||
|
|
||||||
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
|
||||||
|
|
||||||
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.label || reason);
|
|
||||||
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
|
|
||||||
const filter = getFilterName(filters, whitelistFilters, filterId, t);
|
|
||||||
|
|
||||||
const renderResponses = (responseArr) => {
|
|
||||||
if (responseArr?.length === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>{responseArr.map((response) => {
|
|
||||||
const className = classNames('white-space--nowrap', {
|
|
||||||
'overflow-break': response.length > 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div key={response} className={className}>{`${response}\n`}</div>;
|
|
||||||
})}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const COMMON_CONTENT = {
|
|
||||||
encryption_status: boldStatusLabel,
|
|
||||||
install_settings_dns: upstream,
|
|
||||||
elapsed: formattedElapsedMs,
|
|
||||||
response_code: status,
|
|
||||||
filter,
|
|
||||||
rule_label: rule,
|
|
||||||
response_table_header: renderResponses(response),
|
|
||||||
original_response: renderResponses(originalResponse),
|
|
||||||
};
|
|
||||||
|
|
||||||
const content = rule
|
|
||||||
? Object.entries(COMMON_CONTENT)
|
|
||||||
: Object.entries({ ...COMMON_CONTENT, filter: '' });
|
|
||||||
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="logs__row">
|
|
||||||
{getIconTooltip({
|
|
||||||
className: classNames('icons mr-4 icon--24 icon--lightgray', { 'my-3': isDetailed }),
|
|
||||||
columnClass: 'grid grid--limited',
|
|
||||||
tooltipClass: 'px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details',
|
|
||||||
contentItemClass: 'text-truncate key-colon o-hidden',
|
|
||||||
xlinkHref: 'question',
|
|
||||||
title: 'response_details',
|
|
||||||
content,
|
|
||||||
placement: 'bottom',
|
|
||||||
})}
|
|
||||||
<div className="text-truncate">
|
|
||||||
<div className="text-truncate" title={statusLabel}>{statusLabel}</div>
|
|
||||||
{isDetailed && <div
|
|
||||||
className="detailed-info d-none d-sm-block pt-1 text-truncate"
|
|
||||||
title={detailedInfo}>{detailedInfo}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getResponseCell;
|
|
||||||
19
client/src/components/Logs/Cells/helpers/index.js
Normal file
19
client/src/components/Logs/Cells/helpers/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { getIpMatchListStatus } from '../../../../helpers/helpers';
|
||||||
|
import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS } from '../../../../helpers/constants';
|
||||||
|
|
||||||
|
export const BUTTON_PREFIX = 'btn_';
|
||||||
|
|
||||||
|
export const getBlockClientInfo = (client, disallowed_clients) => {
|
||||||
|
const ipMatchListStatus = getIpMatchListStatus(client, disallowed_clients);
|
||||||
|
|
||||||
|
const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND;
|
||||||
|
const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
|
||||||
|
|
||||||
|
const confirmMessage = isNotFound ? 'client_confirm_block' : 'client_confirm_unblock';
|
||||||
|
const buttonKey = isNotFound ? 'disallow_this_client' : 'allow_this_client';
|
||||||
|
return {
|
||||||
|
confirmMessage,
|
||||||
|
buttonKey,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
};
|
||||||
229
client/src/components/Logs/Cells/index.js
Normal file
229
client/src/components/Logs/Cells/index.js
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
captitalizeWords,
|
||||||
|
checkFiltered,
|
||||||
|
formatDateTime,
|
||||||
|
formatElapsedMs,
|
||||||
|
formatTime,
|
||||||
|
getBlockingClientName,
|
||||||
|
getFilterName,
|
||||||
|
processContent,
|
||||||
|
} from '../../../helpers/helpers';
|
||||||
|
import {
|
||||||
|
BLOCK_ACTIONS,
|
||||||
|
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||||
|
FILTERED_STATUS,
|
||||||
|
FILTERED_STATUS_TO_META_MAP,
|
||||||
|
LONG_TIME_FORMAT,
|
||||||
|
QUERY_STATUS_COLORS,
|
||||||
|
SCHEME_TO_PROTOCOL_MAP,
|
||||||
|
} from '../../../helpers/constants';
|
||||||
|
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||||
|
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
|
||||||
|
import DateCell from './DateCell';
|
||||||
|
import DomainCell from './DomainCell';
|
||||||
|
import ResponseCell from './ResponseCell';
|
||||||
|
import ClientCell from './ClientCell';
|
||||||
|
import '../Logs.css';
|
||||||
|
import { toggleClientBlock } from '../../../actions/access';
|
||||||
|
import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
|
||||||
|
|
||||||
|
const Row = memo(({
|
||||||
|
style,
|
||||||
|
rowProps,
|
||||||
|
rowProps: { reason },
|
||||||
|
isSmallScreen,
|
||||||
|
setDetailedDataCurrent,
|
||||||
|
setButtonType,
|
||||||
|
setModalOpened,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||||
|
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
|
||||||
|
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||||
|
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||||
|
|
||||||
|
const disallowed_clients = useSelector(
|
||||||
|
(state) => state.access.disallowed_clients,
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
|
|
||||||
|
const clients = useSelector((state) => state.dashboard.clients);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (!isSmallScreen) { return; }
|
||||||
|
const {
|
||||||
|
answer_dnssec,
|
||||||
|
client,
|
||||||
|
domain,
|
||||||
|
elapsedMs,
|
||||||
|
info,
|
||||||
|
reason,
|
||||||
|
response,
|
||||||
|
time,
|
||||||
|
tracker,
|
||||||
|
upstream,
|
||||||
|
type,
|
||||||
|
client_proto,
|
||||||
|
filterId,
|
||||||
|
rule,
|
||||||
|
originalResponse,
|
||||||
|
status,
|
||||||
|
} = rowProps;
|
||||||
|
|
||||||
|
const hasTracker = !!tracker;
|
||||||
|
|
||||||
|
const autoClient = autoClients
|
||||||
|
.find((autoClient) => autoClient.name === client);
|
||||||
|
|
||||||
|
const { whois_info } = info;
|
||||||
|
const country = whois_info?.country;
|
||||||
|
const city = whois_info?.city;
|
||||||
|
const network = whois_info?.orgname;
|
||||||
|
|
||||||
|
const source = autoClient?.source;
|
||||||
|
|
||||||
|
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||||
|
const isFiltered = checkFiltered(reason);
|
||||||
|
|
||||||
|
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
||||||
|
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
||||||
|
|
||||||
|
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||||
|
const onToggleBlock = () => {
|
||||||
|
dispatch(toggleBlocking(buttonType, domain));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
||||||
|
const requestStatus = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
|
||||||
|
|
||||||
|
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
|
||||||
|
|
||||||
|
const sourceData = getSourceData(tracker);
|
||||||
|
|
||||||
|
const filter = getFilterName(filters, whitelistFilters, filterId);
|
||||||
|
|
||||||
|
const {
|
||||||
|
confirmMessage,
|
||||||
|
buttonKey: blockingClientKey,
|
||||||
|
type: blockType,
|
||||||
|
} = getBlockClientInfo(client, disallowed_clients);
|
||||||
|
|
||||||
|
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
|
||||||
|
const clientNameBlockingFor = getBlockingClientName(clients, client);
|
||||||
|
|
||||||
|
const onBlockingForClientClick = () => {
|
||||||
|
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlockingClientClick = () => {
|
||||||
|
const message = `${blockType === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`;
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
dispatch(toggleClientBlock(blockType, client));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const detailedData = {
|
||||||
|
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
||||||
|
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||||
|
encryption_status: isBlocked
|
||||||
|
? <div className="bg--danger">{requestStatus}</div> : requestStatus,
|
||||||
|
domain,
|
||||||
|
type_table_header: type,
|
||||||
|
protocol,
|
||||||
|
known_tracker: hasTracker && 'title',
|
||||||
|
table_name: tracker?.name,
|
||||||
|
category_label: hasTracker && captitalizeWords(tracker.category),
|
||||||
|
tracker_source: hasTracker && sourceData
|
||||||
|
&& <a
|
||||||
|
href={sourceData.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="link--green">{sourceData.name}
|
||||||
|
</a>,
|
||||||
|
response_details: 'title',
|
||||||
|
install_settings_dns: upstream,
|
||||||
|
elapsed: formattedElapsedMs,
|
||||||
|
filter: rule ? filter : null,
|
||||||
|
rule_label: rule,
|
||||||
|
response_table_header: response?.join('\n'),
|
||||||
|
response_code: status,
|
||||||
|
client_details: 'title',
|
||||||
|
ip_address: client,
|
||||||
|
name: info?.name,
|
||||||
|
country,
|
||||||
|
city,
|
||||||
|
network,
|
||||||
|
source_label: source,
|
||||||
|
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
||||||
|
original_response: originalResponse?.join('\n'),
|
||||||
|
[BUTTON_PREFIX + buttonType]: <div onClick={onToggleBlock}
|
||||||
|
className={classNames('title--border text-center', {
|
||||||
|
'bg--danger': isBlocked,
|
||||||
|
})}>{t(buttonType)}</div>,
|
||||||
|
[BUTTON_PREFIX + blockingForClientKey]: <div onClick={onBlockingForClientClick} className='text-center font-weight-bold py-2'>{t(blockingForClientKey)}</div>,
|
||||||
|
[BUTTON_PREFIX + blockingClientKey]: <div onClick={onBlockingClientClick} className='text-center font-weight-bold py-2'>{t(blockingClientKey)}</div>,
|
||||||
|
};
|
||||||
|
|
||||||
|
setDetailedDataCurrent(processContent(detailedData));
|
||||||
|
setButtonType(buttonType);
|
||||||
|
setModalOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||||
|
|
||||||
|
const className = classNames('d-flex px-5 logs__row',
|
||||||
|
`logs__row--${FILTERED_STATUS_TO_META_MAP?.[reason]?.COLOR ?? QUERY_STATUS_COLORS.WHITE}`, {
|
||||||
|
'logs__cell--detailed': isDetailed,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div style={style} className={className} onClick={onClick} role="row">
|
||||||
|
<DateCell {...rowProps} />
|
||||||
|
<DomainCell {...rowProps} />
|
||||||
|
<ResponseCell {...rowProps} />
|
||||||
|
<ClientCell {...rowProps} />
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
Row.displayName = 'Row';
|
||||||
|
|
||||||
|
Row.propTypes = {
|
||||||
|
style: propTypes.object,
|
||||||
|
rowProps: propTypes.shape({
|
||||||
|
reason: propTypes.string.isRequired,
|
||||||
|
answer_dnssec: propTypes.bool.isRequired,
|
||||||
|
client: propTypes.string.isRequired,
|
||||||
|
domain: propTypes.string.isRequired,
|
||||||
|
elapsedMs: propTypes.string.isRequired,
|
||||||
|
info: propTypes.oneOfType([
|
||||||
|
propTypes.string,
|
||||||
|
propTypes.shape({
|
||||||
|
whois_info: propTypes.shape({
|
||||||
|
country: propTypes.string,
|
||||||
|
city: propTypes.string,
|
||||||
|
orgname: propTypes.string,
|
||||||
|
}),
|
||||||
|
})]),
|
||||||
|
response: propTypes.array.isRequired,
|
||||||
|
time: propTypes.string.isRequired,
|
||||||
|
tracker: propTypes.object,
|
||||||
|
upstream: propTypes.string.isRequired,
|
||||||
|
type: propTypes.string.isRequired,
|
||||||
|
client_proto: propTypes.string.isRequired,
|
||||||
|
filterId: propTypes.number,
|
||||||
|
rule: propTypes.string,
|
||||||
|
originalResponse: propTypes.array,
|
||||||
|
status: propTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
isSmallScreen: propTypes.bool.isRequired,
|
||||||
|
setDetailedDataCurrent: propTypes.func.isRequired,
|
||||||
|
setButtonType: propTypes.func.isRequired,
|
||||||
|
setModalOpened: propTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Row;
|
||||||
@@ -107,7 +107,7 @@ const Form = (props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
response_status, search,
|
response_status, search,
|
||||||
} = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
} = useSelector((state) => state?.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
debouncedSearch,
|
debouncedSearch,
|
||||||
@@ -171,14 +171,14 @@ const Form = (props) => {
|
|||||||
>
|
>
|
||||||
{Object.values(RESPONSE_FILTER)
|
{Object.values(RESPONSE_FILTER)
|
||||||
.map(({
|
.map(({
|
||||||
query, label, disabled,
|
QUERY, LABEL, disabled,
|
||||||
}) => (
|
}) => (
|
||||||
<option
|
<option
|
||||||
key={label}
|
key={LABEL}
|
||||||
value={query}
|
value={QUERY}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{t(label)}
|
{t(LABEL)}
|
||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -197,5 +197,4 @@ Form.propTypes = {
|
|||||||
|
|
||||||
export default reduxForm({
|
export default reduxForm({
|
||||||
form: FORM_NAME.LOGS_FILTER,
|
form: FORM_NAME.LOGS_FILTER,
|
||||||
enableReinitialize: true,
|
|
||||||
})(Form);
|
})(Form);
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
|
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||||
|
import { addSuccessToast } from '../../../actions/toasts';
|
||||||
|
|
||||||
const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
const Filters = ({ filter, setIsLoading }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const refreshLogs = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await dispatch(refreshFilteredLogs());
|
||||||
|
dispatch(addSuccessToast('query_log_updated'));
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
return <div className="page-header page-header--logs">
|
return <div className="page-header page-header--logs">
|
||||||
<h1 className="page-title page-title--large">
|
<h1 className="page-title page-title--large">
|
||||||
@@ -29,7 +40,6 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
|||||||
|
|
||||||
Filters.propTypes = {
|
Filters.propTypes = {
|
||||||
filter: PropTypes.object.isRequired,
|
filter: PropTypes.object.isRequired,
|
||||||
refreshLogs: PropTypes.func.isRequired,
|
|
||||||
processingGetLogs: PropTypes.bool.isRequired,
|
processingGetLogs: PropTypes.bool.isRequired,
|
||||||
setIsLoading: PropTypes.func.isRequired,
|
setIsLoading: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
87
client/src/components/Logs/InfiniteTable.js
Normal file
87
client/src/components/Logs/InfiniteTable.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import Loading from '../ui/Loading';
|
||||||
|
import Header from './Cells/Header';
|
||||||
|
import { getLogs } from '../../actions/queryLogs';
|
||||||
|
import Row from './Cells';
|
||||||
|
import { isScrolledIntoView } from '../../helpers/helpers';
|
||||||
|
import { QUERY_LOGS_PAGE_LIMIT } from '../../helpers/constants';
|
||||||
|
|
||||||
|
const InfiniteTable = ({
|
||||||
|
isLoading,
|
||||||
|
items,
|
||||||
|
isSmallScreen,
|
||||||
|
setDetailedDataCurrent,
|
||||||
|
setButtonType,
|
||||||
|
setModalOpened,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const loader = useRef(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEntireLog,
|
||||||
|
processingGetLogs,
|
||||||
|
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||||
|
|
||||||
|
const loading = isLoading || processingGetLogs;
|
||||||
|
|
||||||
|
const listener = useCallback(() => {
|
||||||
|
if (loader.current && isScrolledIntoView(loader.current)) {
|
||||||
|
dispatch(getLogs());
|
||||||
|
}
|
||||||
|
}, [loader.current, isScrolledIntoView, getLogs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listener();
|
||||||
|
}, [items.length < QUERY_LOGS_PAGE_LIMIT]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const THROTTLE_TIME = 100;
|
||||||
|
const throttledListener = throttle(listener, THROTTLE_TIME);
|
||||||
|
|
||||||
|
window.addEventListener('scroll', throttledListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', throttledListener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderRow = (row, idx) => <Row
|
||||||
|
key={idx}
|
||||||
|
rowProps={row}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||||
|
setButtonType={setButtonType}
|
||||||
|
setModalOpened={setModalOpened}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const isNothingFound = items.length === 0 && !processingGetLogs;
|
||||||
|
|
||||||
|
return <div className='logs__table' role='grid'>
|
||||||
|
{loading && <Loading />}
|
||||||
|
<Header />
|
||||||
|
{isNothingFound
|
||||||
|
? <label className="logs__no-data">{t('nothing_found')}</label>
|
||||||
|
: <>{items.map(renderRow)}
|
||||||
|
{!isEntireLog && <div ref={loader} className="logs__loading text-center">{t('loading_table_status')}</div>}
|
||||||
|
</>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
InfiniteTable.propTypes = {
|
||||||
|
isLoading: propTypes.bool.isRequired,
|
||||||
|
items: propTypes.array.isRequired,
|
||||||
|
isSmallScreen: propTypes.bool.isRequired,
|
||||||
|
setDetailedDataCurrent: propTypes.func.isRequired,
|
||||||
|
setButtonType: propTypes.func.isRequired,
|
||||||
|
setModalOpened: propTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfiniteTable;
|
||||||
@@ -1,44 +1,32 @@
|
|||||||
:root {
|
:root {
|
||||||
|
--blue: #e5effd;
|
||||||
|
--green-pale: rgba(103, 178, 121, 0.1);
|
||||||
|
--red: rgba(223, 56, 18, 0.05);
|
||||||
|
--white: #fff;
|
||||||
|
--yellow: rgba(247, 181, 0, 0.1);
|
||||||
|
--size-date: 70;
|
||||||
|
--size-domain: 180;
|
||||||
|
--size-response: 150;
|
||||||
|
--size-client: 123;
|
||||||
|
--gray-216: rgba(216, 216, 216, 0.23);
|
||||||
--gray-4d: #4D4D4D;
|
--gray-4d: #4D4D4D;
|
||||||
|
--gray-f3: #F3F3F3;
|
||||||
--gray-8: #888;
|
--gray-8: #888;
|
||||||
--danger: #DF3812;
|
--danger: #DF3812;
|
||||||
|
--white80: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
--btn-block: #C23814;
|
||||||
|
--btn-block-disabled: #E3B3A6;
|
||||||
|
--btn-block-active: #A62200;
|
||||||
|
|
||||||
|
--btn-unblock: #888888;
|
||||||
|
--btn-unblock-disabled: #D8D8D8;
|
||||||
|
--btn-unblock-active: #4D4D4D;
|
||||||
|
|
||||||
|
--option-border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__row {
|
.logs__text {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
min-height: 26px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-table .logs__row {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__row--center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__row--column {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__row--icons {
|
|
||||||
max-width: 180px;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__row .list-unstyled {
|
|
||||||
margin-bottom: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__text,
|
|
||||||
.logs__row .list-unstyled li {
|
|
||||||
padding: 0 1px;
|
padding: 0 1px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -54,237 +42,6 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__text--full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__text--wrap {
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__text--nowrap {
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__text--whois {
|
|
||||||
line-height: 1.2;
|
|
||||||
color: #9aa0ac;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__row .tooltip-custom {
|
|
||||||
top: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip__option {
|
|
||||||
height: 2.5rem !important;
|
|
||||||
width: 10.5rem;
|
|
||||||
padding: 0.3125rem 1.5rem 0.6875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip__option:hover {
|
|
||||||
background-color: var(--gray-f3);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button__action {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table__action {
|
|
||||||
position: absolute;
|
|
||||||
top: 11px;
|
|
||||||
right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__action {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__action--detailed {
|
|
||||||
top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-td,
|
|
||||||
.clients__table .rt-td {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-thead, .logs__table .rt-tbody {
|
|
||||||
min-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr:hover .logs__action,
|
|
||||||
.clients__table .rt-tr:hover .table__action {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr-group:first-child .tooltip-custom:before {
|
|
||||||
top: calc(100% + 12px);
|
|
||||||
bottom: initial;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr-group:first-child .tooltip-custom:after {
|
|
||||||
top: initial;
|
|
||||||
bottom: -4px;
|
|
||||||
border-top: 6px solid transparent;
|
|
||||||
border-bottom: 6px solid #585965;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr-group:first-child .popover__body {
|
|
||||||
top: calc(100% + 5px);
|
|
||||||
bottom: initial;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr-group:first-child .popover__body:after {
|
|
||||||
top: -11px;
|
|
||||||
border-top: 6px solid transparent;
|
|
||||||
border-bottom: 6px solid #585965;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-thead.-filters input,
|
|
||||||
.logs__table .rt-thead.-filters select {
|
|
||||||
padding: 6px 7px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #495057;
|
|
||||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-thead.-filters select {
|
|
||||||
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
|
||||||
background-size: 8px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-thead.-filters input:focus,
|
|
||||||
.logs__table .rt-thead.-filters select:focus {
|
|
||||||
border-color: #1991eb;
|
|
||||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__text-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__list-wrap {
|
|
||||||
display: flex;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__list-item {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__input-wrap {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__whois {
|
|
||||||
display: inline;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__whois::after {
|
|
||||||
content: "|";
|
|
||||||
padding: 0 5px;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__whois:last-child::after {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__whois-icon.icons {
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
margin-right: 1px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* New logs */
|
|
||||||
.logs__table {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 42rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table--detailed {
|
|
||||||
min-height: 50rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-thead.-header {
|
|
||||||
box-shadow: none;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-thead .rt-th {
|
|
||||||
padding: 0.9375rem 0.9375rem 0.875rem 0;
|
|
||||||
text-align: left;
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tbody .rt-td {
|
|
||||||
padding: 1rem 1rem 0.5rem 0;
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-thead .rt-th:last-child,
|
|
||||||
.logs__table .rt-tbody .rt-td:last-child {
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tbody .rt-tr-group {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr {
|
|
||||||
position: relative;
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr {
|
|
||||||
position: relative;
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr-group:not(:first-child) .rt-tr:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 1.5rem;
|
|
||||||
right: 1.5rem;
|
|
||||||
top: 0;
|
|
||||||
width: calc(100% - 3rem);
|
|
||||||
height: 2px;
|
|
||||||
background-color: rgba(216, 216, 216, 0.23);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .rt-tr-group:last-child .rt-tr:after,
|
|
||||||
.logs__table .rt-thead .rt-tr:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__time {
|
.logs__time {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -302,132 +59,24 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide 3 and 4 column on mobile */
|
|
||||||
.logs__table .rt-thead .rt-th:nth-child(3),
|
|
||||||
.logs__table .rt-thead .rt-th:nth-child(4),
|
|
||||||
.logs__table .rt-tbody .rt-td:nth-child(3),
|
|
||||||
.logs__table .rt-tbody .rt-td:nth-child(4) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.logs__table .rt-thead .rt-th:nth-child(3),
|
|
||||||
.logs__table .rt-thead .rt-th:nth-child(4),
|
|
||||||
.logs__table .rt-tbody .rt-td:nth-child(3),
|
|
||||||
.logs__table .rt-tbody .rt-td:nth-child(4) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-pre {
|
.text-pre {
|
||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-pagination {
|
|
||||||
width: 11.875rem !important;
|
|
||||||
background-color: transparent;
|
|
||||||
box-shadow: none !important;
|
|
||||||
border: none !important;
|
|
||||||
align-items: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination--padding {
|
|
||||||
padding: 2.5rem 0 2.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination .-btn {
|
|
||||||
--side-size: 2rem;
|
|
||||||
background-color: transparent !important;
|
|
||||||
border: 1px solid var(--gray-d8) !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
width: var(--side-size) !important;
|
|
||||||
height: var(--side-size) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination .-btn:enabled:hover {
|
|
||||||
background-color: var(--gray-f3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination .-previous {
|
|
||||||
flex: 0 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination .-next {
|
|
||||||
flex: 0 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination .-btn {
|
|
||||||
display: flex !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .-pageInfo {
|
|
||||||
--side-size: 2rem;
|
|
||||||
font-variant-numeric: tabular-nums !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
border: 1px solid var(--gray-d8) !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
width: var(--side-size) !important;
|
|
||||||
height: var(--side-size) !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
display: flex !important;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .pagination-bottom {
|
|
||||||
justify-content: center !important;
|
|
||||||
display: flex !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .-center:before {
|
|
||||||
content: '...';
|
|
||||||
transform: translateY(-0.25rem);
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table .-center:after {
|
|
||||||
content: '...';
|
|
||||||
transform: translateY(-0.25rem);
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon--detailed-info {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link--green {
|
.link--green {
|
||||||
color: var(--green79);
|
color: var(--green79);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row--detailed {
|
|
||||||
height: 4.9rem
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-90 {
|
.w-90 {
|
||||||
max-width: 90% !important;
|
max-width: 90% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-85 {
|
|
||||||
height: 85% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pt-45 {
|
|
||||||
padding-top: 1.25rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pb-45 {
|
.pb-45 {
|
||||||
padding-bottom: 1.25rem !important;
|
padding-bottom: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.py-45 {
|
|
||||||
padding-top: 1.25rem !important;
|
|
||||||
padding-bottom: 1.25rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mh-100 {
|
.mh-100 {
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
}
|
}
|
||||||
@@ -493,14 +142,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.rt-tr .logs__row .logs__text {
|
|
||||||
max-width: calc(100% - 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-small {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control--container {
|
.form-control--container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -517,38 +158,258 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 575px) {
|
@media screen and (max-width: 767.98px) {
|
||||||
.logs__table .rt-tr {
|
.logs__table .logs__cell--response,
|
||||||
height: 3.125rem;
|
.logs__table .logs__cell--client {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__table .rt-tbody .rt-td {
|
|
||||||
padding: 0.625rem 1rem 0.875rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs__table {
|
|
||||||
min-height: 42rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading__container > .-loading-inner {
|
|
||||||
top: 10rem !important;
|
|
||||||
bottom: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading__text {
|
|
||||||
transform: translateY(3rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__refresh {
|
.logs__refresh {
|
||||||
|
--size: 2.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 40px;
|
width: var(--size);
|
||||||
height: 40px;
|
height: var(--size);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 15px;
|
margin-left: 0.9375rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs__cell {
|
||||||
|
padding: 1rem 1rem 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__cell--date {
|
||||||
|
width: 4.375rem;
|
||||||
|
flex: var(--size-date) 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__cell--domain {
|
||||||
|
width: 11.25rem;
|
||||||
|
flex: var(--size-domain) 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__cell--response {
|
||||||
|
width: 9.375rem;
|
||||||
|
flex: var(--size-response) 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__cell--client {
|
||||||
|
width: 7.6875rem;
|
||||||
|
flex: var(--size-client) 0 auto;
|
||||||
|
padding-right: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__cell--header__container > .logs__cell--header__item {
|
||||||
|
border-right: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__cell--header__container > .logs__cell--header__item:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action__container {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action__container--detailed {
|
||||||
|
bottom: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action {
|
||||||
|
outline: 0 !important;
|
||||||
|
background: var(--btn-block);
|
||||||
|
border-radius: var(--option-border-radius);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--white);
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 28px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--unblock {
|
||||||
|
background: var(--btn-unblock);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--main {
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--with-options {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--arrow {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-left: 1px solid var(--white);
|
||||||
|
width: 1.5625rem;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--arrow .button-action--icon {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action:active {
|
||||||
|
background: var(--btn-block-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--unblock:active {
|
||||||
|
background: var(--btn-unblock-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action:disabled {
|
||||||
|
background: var(--btn-block-disabled);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--unblock:disabled {
|
||||||
|
background: var(--btn-unblock-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--arrow-option:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--gray-f3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-action--arrow-option-container {
|
||||||
|
overflow: visible;
|
||||||
|
transform-origin: left;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__row {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-height: 26px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__table .logs__row {
|
||||||
|
border-bottom: 2px solid var(--gray-216);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QUERY_STATUS_COLORS */
|
||||||
|
.logs__row--blue {
|
||||||
|
background-color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__row--green {
|
||||||
|
background-color: var(--green-pale);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__row--red {
|
||||||
|
background-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__row--white {
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__row--yellow {
|
||||||
|
background-color: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__no-data {
|
||||||
|
color: var(--gray-4d);
|
||||||
|
background-color: var(--white80);
|
||||||
|
pointer-events: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 21rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__loading {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__table {
|
||||||
|
background-color: var(--white);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 43rem;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
contain: layout;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
will-change: scroll-position;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__table .logs__cell--response,
|
||||||
|
.logs__table .logs__cell--client {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__cell--header__container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__table > .logs__cell--header__container > .logs__cell--client {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__table .loading:after {
|
||||||
|
top: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__table .loading:before {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__whois {
|
||||||
|
display: inline;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__whois::after {
|
||||||
|
content: "|";
|
||||||
|
padding: 0 5px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__whois:last-child::after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs__whois-icon.icons {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 1px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
|
||||||
import ReactTable from 'react-table';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import endsWith from 'lodash/endsWith';
|
|
||||||
import escapeRegExp from 'lodash/escapeRegExp';
|
|
||||||
import {
|
|
||||||
BLOCK_ACTIONS,
|
|
||||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
|
||||||
LONG_TIME_FORMAT,
|
|
||||||
FILTERED_STATUS_TO_META_MAP,
|
|
||||||
TABLE_DEFAULT_PAGE_SIZE,
|
|
||||||
SCHEME_TO_PROTOCOL_MAP,
|
|
||||||
CUSTOM_FILTERING_RULES_ID, FILTERED_STATUS,
|
|
||||||
} from '../../helpers/constants';
|
|
||||||
import getDateCell from './Cells/getDateCell';
|
|
||||||
import getDomainCell from './Cells/getDomainCell';
|
|
||||||
import getClientCell from './Cells/getClientCell';
|
|
||||||
import getResponseCell from './Cells/getResponseCell';
|
|
||||||
|
|
||||||
import {
|
|
||||||
captitalizeWords,
|
|
||||||
checkFiltered,
|
|
||||||
formatDateTime,
|
|
||||||
formatElapsedMs,
|
|
||||||
formatTime,
|
|
||||||
processContent,
|
|
||||||
} from '../../helpers/helpers';
|
|
||||||
import Loading from '../ui/Loading';
|
|
||||||
import { getSourceData } from '../../helpers/trackers/trackers';
|
|
||||||
|
|
||||||
const Table = (props) => {
|
|
||||||
const {
|
|
||||||
setDetailedDataCurrent,
|
|
||||||
setButtonType,
|
|
||||||
setModalOpened,
|
|
||||||
isSmallScreen,
|
|
||||||
setIsLoading,
|
|
||||||
filtering,
|
|
||||||
isDetailed,
|
|
||||||
toggleDetailedLogs,
|
|
||||||
setLogsPage,
|
|
||||||
setLogsPagination,
|
|
||||||
processingGetLogs,
|
|
||||||
logs,
|
|
||||||
pages,
|
|
||||||
page,
|
|
||||||
isLoading,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const toggleBlocking = (type, domain) => {
|
|
||||||
const {
|
|
||||||
setRules, getFilteringStatus, addSuccessToast,
|
|
||||||
} = props;
|
|
||||||
const { userRules } = filtering;
|
|
||||||
|
|
||||||
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
|
||||||
const baseRule = `||${domain}^$important`;
|
|
||||||
const baseUnblocking = `@@${baseRule}`;
|
|
||||||
|
|
||||||
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule;
|
|
||||||
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking;
|
|
||||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
|
||||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
|
||||||
|
|
||||||
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
|
|
||||||
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
|
|
||||||
|
|
||||||
if (matchPreparedBlockingRule) {
|
|
||||||
setRules(userRules.replace(`${blockingRule}`, ''));
|
|
||||||
addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
|
||||||
} else if (!matchPreparedUnblockingRule) {
|
|
||||||
setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
|
|
||||||
addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
|
||||||
} else if (matchPreparedUnblockingRule) {
|
|
||||||
addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
|
||||||
return;
|
|
||||||
} else if (!matchPreparedBlockingRule) {
|
|
||||||
addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilteringStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilterName = (filters, whitelistFilters, filterId, t) => {
|
|
||||||
if (filterId === CUSTOM_FILTERING_RULES_ID) {
|
|
||||||
return t('custom_filter_rules');
|
|
||||||
}
|
|
||||||
|
|
||||||
const filter = filters.find((filter) => filter.id === filterId)
|
|
||||||
|| whitelistFilters.find((filter) => filter.id === filterId);
|
|
||||||
let filterName = '';
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
filterName = filter.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filterName) {
|
|
||||||
filterName = t('unknown_filter', { filterId });
|
|
||||||
}
|
|
||||||
|
|
||||||
return filterName;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
Header: t('time_table_header'),
|
|
||||||
accessor: 'time',
|
|
||||||
Cell: (row) => getDateCell(row, isDetailed),
|
|
||||||
minWidth: 70,
|
|
||||||
maxHeight: 60,
|
|
||||||
headerClassName: 'logs__text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: t('request_table_header'),
|
|
||||||
accessor: 'domain',
|
|
||||||
Cell: (row) => {
|
|
||||||
const {
|
|
||||||
isDetailed,
|
|
||||||
autoClients,
|
|
||||||
dnssec_enabled,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return getDomainCell({
|
|
||||||
row,
|
|
||||||
t,
|
|
||||||
isDetailed,
|
|
||||||
toggleBlocking,
|
|
||||||
autoClients,
|
|
||||||
dnssec_enabled,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
minWidth: 180,
|
|
||||||
maxHeight: 60,
|
|
||||||
headerClassName: 'logs__text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: t('response_table_header'),
|
|
||||||
accessor: 'response',
|
|
||||||
Cell: (row) => getResponseCell(
|
|
||||||
row,
|
|
||||||
filtering,
|
|
||||||
t,
|
|
||||||
isDetailed,
|
|
||||||
getFilterName,
|
|
||||||
),
|
|
||||||
minWidth: 150,
|
|
||||||
maxHeight: 60,
|
|
||||||
headerClassName: 'logs__text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: function Header() {
|
|
||||||
return <div className="d-flex justify-content-between">
|
|
||||||
{t('client_table_header')}
|
|
||||||
{<span>
|
|
||||||
<svg
|
|
||||||
className={classNames('icons icon--24 icon--green mr-2 cursor--pointer', {
|
|
||||||
'icon--selected': !isDetailed,
|
|
||||||
})}
|
|
||||||
onClick={() => toggleDetailedLogs(false)}
|
|
||||||
>
|
|
||||||
<title><Trans>compact</Trans></title>
|
|
||||||
<use xlinkHref='#list' />
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
className={classNames('icons icon--24 icon--green cursor--pointer', {
|
|
||||||
'icon--selected': isDetailed,
|
|
||||||
})}
|
|
||||||
onClick={() => toggleDetailedLogs(true)}
|
|
||||||
>
|
|
||||||
<title><Trans>default</Trans></title>
|
|
||||||
<use xlinkHref='#detailed_list' />
|
|
||||||
</svg>
|
|
||||||
</span>}
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
accessor: 'client',
|
|
||||||
Cell: (row) => {
|
|
||||||
const {
|
|
||||||
isDetailed,
|
|
||||||
autoClients,
|
|
||||||
filtering: { processingRules },
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return getClientCell({
|
|
||||||
row,
|
|
||||||
t,
|
|
||||||
isDetailed,
|
|
||||||
toggleBlocking,
|
|
||||||
autoClients,
|
|
||||||
processingRules,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
minWidth: 123,
|
|
||||||
maxHeight: 60,
|
|
||||||
headerClassName: 'logs__text',
|
|
||||||
className: 'pb-0',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const changePage = async (page) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const { oldest, getLogs, pages } = props;
|
|
||||||
const isLastPage = pages && (page + 1 === pages);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
setLogsPage(page),
|
|
||||||
setLogsPagination({
|
|
||||||
page,
|
|
||||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
|
||||||
}),
|
|
||||||
].concat(isLastPage ? getLogs(oldest, page) : []));
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tableClass = classNames('logs__table', {
|
|
||||||
'logs__table--detailed': isDetailed,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactTable
|
|
||||||
manual
|
|
||||||
minRows={0}
|
|
||||||
page={page}
|
|
||||||
pages={pages}
|
|
||||||
columns={columns}
|
|
||||||
filterable={false}
|
|
||||||
sortable={false}
|
|
||||||
resizable={false}
|
|
||||||
data={logs || []}
|
|
||||||
loading={isLoading || processingGetLogs}
|
|
||||||
showPageJump={false}
|
|
||||||
showPageSizeOptions={false}
|
|
||||||
onPageChange={changePage}
|
|
||||||
className={tableClass}
|
|
||||||
defaultPageSize={TABLE_DEFAULT_PAGE_SIZE}
|
|
||||||
loadingText={
|
|
||||||
<>
|
|
||||||
<Loading />
|
|
||||||
<h6 className="loading__text">{t('loading_table_status')}</h6>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
getLoadingProps={() => ({ className: 'loading__container' })}
|
|
||||||
rowsText={t('rows_table_footer_text')}
|
|
||||||
noDataText={!processingGetLogs
|
|
||||||
&& <label className="logs__text logs__text--bold">{t('nothing_found')}</label>}
|
|
||||||
pageText=''
|
|
||||||
ofText=''
|
|
||||||
showPagination={logs.length > 0}
|
|
||||||
getPaginationProps={() => ({ className: 'custom-pagination custom-pagination--padding' })}
|
|
||||||
getTbodyProps={() => ({ className: 'd-block' })}
|
|
||||||
previousText={
|
|
||||||
<svg className="icons icon--24 icon--gray w-100 h-100 cursor--pointer">
|
|
||||||
<title><Trans>previous_btn</Trans></title>
|
|
||||||
<use xlinkHref="#arrow-left" />
|
|
||||||
</svg>}
|
|
||||||
nextText={
|
|
||||||
<svg className="icons icon--24 icon--gray w-100 h-100 cursor--pointer">
|
|
||||||
<title><Trans>next_btn</Trans></title>
|
|
||||||
<use xlinkHref="#arrow-right" />
|
|
||||||
</svg>}
|
|
||||||
renderTotalPagesCount={() => false}
|
|
||||||
getTrGroupProps={(_state, rowInfo) => {
|
|
||||||
if (!rowInfo) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { reason } = rowInfo.original;
|
|
||||||
const colorClass = FILTERED_STATUS_TO_META_MAP[reason] ? FILTERED_STATUS_TO_META_MAP[reason].color : 'white';
|
|
||||||
|
|
||||||
return { className: colorClass };
|
|
||||||
}}
|
|
||||||
getTrProps={(state, rowInfo) => ({
|
|
||||||
className: isDetailed ? 'row--detailed' : '',
|
|
||||||
onClick: () => {
|
|
||||||
if (isSmallScreen) {
|
|
||||||
const { dnssec_enabled, autoClients } = props;
|
|
||||||
const {
|
|
||||||
answer_dnssec,
|
|
||||||
client,
|
|
||||||
domain,
|
|
||||||
elapsedMs,
|
|
||||||
info,
|
|
||||||
reason,
|
|
||||||
response,
|
|
||||||
time,
|
|
||||||
tracker,
|
|
||||||
upstream,
|
|
||||||
type,
|
|
||||||
client_proto,
|
|
||||||
filterId,
|
|
||||||
rule,
|
|
||||||
originalResponse,
|
|
||||||
status,
|
|
||||||
} = rowInfo.original;
|
|
||||||
|
|
||||||
const hasTracker = !!tracker;
|
|
||||||
|
|
||||||
const autoClient = autoClients
|
|
||||||
.find((autoClient) => autoClient.name === client);
|
|
||||||
|
|
||||||
const { whois_info } = info;
|
|
||||||
const country = whois_info?.country;
|
|
||||||
const city = whois_info?.city;
|
|
||||||
const network = whois_info?.orgname;
|
|
||||||
|
|
||||||
const source = autoClient?.source;
|
|
||||||
|
|
||||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
|
||||||
const isFiltered = checkFiltered(reason);
|
|
||||||
|
|
||||||
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
|
||||||
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
|
||||||
|
|
||||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
|
||||||
const onToggleBlock = () => {
|
|
||||||
toggleBlocking(buttonType, domain);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
|
||||||
const requestStatus = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.label || reason);
|
|
||||||
|
|
||||||
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
|
|
||||||
|
|
||||||
const sourceData = getSourceData(tracker);
|
|
||||||
|
|
||||||
const { filters, whitelistFilters } = filtering;
|
|
||||||
const filter = getFilterName(filters, whitelistFilters, filterId, t);
|
|
||||||
|
|
||||||
const detailedData = {
|
|
||||||
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
|
||||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
|
||||||
encryption_status: isBlocked
|
|
||||||
? <div className="bg--danger">{requestStatus}</div> : requestStatus,
|
|
||||||
domain,
|
|
||||||
type_table_header: type,
|
|
||||||
protocol,
|
|
||||||
known_tracker: hasTracker && 'title',
|
|
||||||
table_name: tracker?.name,
|
|
||||||
category_label: hasTracker && captitalizeWords(tracker.category),
|
|
||||||
tracker_source: hasTracker && sourceData
|
|
||||||
&& <a
|
|
||||||
href={sourceData.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="link--green">{sourceData.name}
|
|
||||||
</a>,
|
|
||||||
response_details: 'title',
|
|
||||||
install_settings_dns: upstream,
|
|
||||||
elapsed: formattedElapsedMs,
|
|
||||||
filter: rule ? filter : null,
|
|
||||||
rule_label: rule,
|
|
||||||
response_table_header: response?.join('\n'),
|
|
||||||
response_code: status,
|
|
||||||
client_details: 'title',
|
|
||||||
ip_address: client,
|
|
||||||
name: info?.name,
|
|
||||||
country,
|
|
||||||
city,
|
|
||||||
network,
|
|
||||||
source_label: source,
|
|
||||||
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
|
||||||
original_response: originalResponse?.join('\n'),
|
|
||||||
[buttonType]: <div onClick={onToggleBlock}
|
|
||||||
className={classNames('title--border text-center', {
|
|
||||||
'bg--danger': isBlocked,
|
|
||||||
})}>{t(buttonType)}</div>,
|
|
||||||
};
|
|
||||||
|
|
||||||
setDetailedDataCurrent(processContent(detailedData));
|
|
||||||
setButtonType(buttonType);
|
|
||||||
setModalOpened(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Table.propTypes = {
|
|
||||||
logs: PropTypes.array.isRequired,
|
|
||||||
pages: PropTypes.number.isRequired,
|
|
||||||
page: PropTypes.number.isRequired,
|
|
||||||
autoClients: PropTypes.array.isRequired,
|
|
||||||
defaultPageSize: PropTypes.number,
|
|
||||||
oldest: PropTypes.string.isRequired,
|
|
||||||
filtering: PropTypes.object.isRequired,
|
|
||||||
processingGetLogs: PropTypes.bool.isRequired,
|
|
||||||
processingGetConfig: PropTypes.bool.isRequired,
|
|
||||||
isDetailed: PropTypes.bool.isRequired,
|
|
||||||
setLogsPage: PropTypes.func.isRequired,
|
|
||||||
setLogsPagination: PropTypes.func.isRequired,
|
|
||||||
getLogs: PropTypes.func.isRequired,
|
|
||||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
|
||||||
setRules: PropTypes.func.isRequired,
|
|
||||||
addSuccessToast: PropTypes.func.isRequired,
|
|
||||||
getFilteringStatus: PropTypes.func.isRequired,
|
|
||||||
isLoading: PropTypes.bool.isRequired,
|
|
||||||
setIsLoading: PropTypes.func.isRequired,
|
|
||||||
dnssec_enabled: PropTypes.bool.isRequired,
|
|
||||||
setDetailedDataCurrent: PropTypes.func.isRequired,
|
|
||||||
setButtonType: PropTypes.func.isRequired,
|
|
||||||
setModalOpened: PropTypes.func.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Table;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { Fragment, useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import Modal from 'react-modal';
|
import Modal from 'react-modal';
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -8,34 +7,32 @@ import queryString from 'query-string';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
BLOCK_ACTIONS,
|
BLOCK_ACTIONS,
|
||||||
TABLE_DEFAULT_PAGE_SIZE,
|
|
||||||
TABLE_FIRST_PAGE,
|
|
||||||
SMALL_SCREEN_SIZE,
|
SMALL_SCREEN_SIZE,
|
||||||
} from '../../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import Filters from './Filters';
|
import Filters from './Filters';
|
||||||
import Table from './Table';
|
|
||||||
import Disabled from './Disabled';
|
import Disabled from './Disabled';
|
||||||
import { getFilteringStatus } from '../../actions/filtering';
|
import { getFilteringStatus } from '../../actions/filtering';
|
||||||
import { getClients } from '../../actions';
|
import { getClients } from '../../actions';
|
||||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||||
import {
|
import {
|
||||||
getLogsConfig,
|
getLogsConfig,
|
||||||
refreshFilteredLogs,
|
|
||||||
resetFilteredLogs,
|
resetFilteredLogs,
|
||||||
setFilteredLogs,
|
setFilteredLogs,
|
||||||
|
toggleDetailedLogs,
|
||||||
} from '../../actions/queryLogs';
|
} from '../../actions/queryLogs';
|
||||||
import { addSuccessToast } from '../../actions/toasts';
|
import InfiniteTable from './InfiniteTable';
|
||||||
import './Logs.css';
|
import './Logs.css';
|
||||||
|
import { BUTTON_PREFIX } from './Cells/helpers';
|
||||||
|
|
||||||
const processContent = (data, buttonType) => Object.entries(data)
|
const processContent = (data) => Object.entries(data)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTitle = value === 'title';
|
const isTitle = value === 'title';
|
||||||
const isButton = key === buttonType;
|
const isButton = key.startsWith(BUTTON_PREFIX);
|
||||||
const isBoolean = typeof value === 'boolean';
|
const isBoolean = typeof value === 'boolean';
|
||||||
const isHidden = isBoolean && value === false;
|
const isHidden = isBoolean && value === false;
|
||||||
|
|
||||||
@@ -48,21 +45,20 @@ const processContent = (data, buttonType) => Object.entries(data)
|
|||||||
keyClass = '';
|
keyClass = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return isHidden ? null : <Fragment key={key}>
|
return isHidden ? null : <div key={key}>
|
||||||
<div
|
<div
|
||||||
className={classNames(`key__${key}`, keyClass, {
|
className={classNames(`key__${key}`, keyClass, {
|
||||||
'font-weight-bold': isBoolean && value === true,
|
'font-weight-bold': isBoolean && value === true,
|
||||||
})}>
|
})}>
|
||||||
<Trans>{isButton ? value : key}</Trans>
|
<Trans>{isButton ? value : key}</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className={`value__${key} text-pre text-truncate`}>
|
<div className={`value__${key} text-pre text-truncate`}>
|
||||||
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Logs = () => {
|
||||||
const Logs = (props) => {
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -71,7 +67,14 @@ const Logs = (props) => {
|
|||||||
search: search_url_param = '',
|
search: search_url_param = '',
|
||||||
} = queryString.parse(history.location.search);
|
} = queryString.parse(history.location.search);
|
||||||
|
|
||||||
const { filter } = useSelector((state) => state.queryLogs, shallowEqual);
|
const {
|
||||||
|
enabled,
|
||||||
|
processingGetConfig,
|
||||||
|
processingAdditionalLogs,
|
||||||
|
processingGetLogs,
|
||||||
|
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||||
|
const filter = useSelector((state) => state.queryLogs.filter, shallowEqual);
|
||||||
|
const logs = useSelector((state) => state.queryLogs.logs, shallowEqual);
|
||||||
|
|
||||||
const search = filter?.search || search_url_param;
|
const search = filter?.search || search_url_param;
|
||||||
const response_status = filter?.response_status || response_status_url_param;
|
const response_status = filter?.response_status || response_status_url_param;
|
||||||
@@ -82,6 +85,7 @@ const Logs = (props) => {
|
|||||||
const [isModalOpened, setModalOpened] = useState(false);
|
const [isModalOpened, setModalOpened] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const closeModal = () => setModalOpened(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -94,44 +98,11 @@ const Logs = (props) => {
|
|||||||
})();
|
})();
|
||||||
}, [response_status, search]);
|
}, [response_status, search]);
|
||||||
|
|
||||||
const {
|
|
||||||
filtering,
|
|
||||||
setLogsPage,
|
|
||||||
setLogsPagination,
|
|
||||||
toggleDetailedLogs,
|
|
||||||
dashboard,
|
|
||||||
dnsConfig,
|
|
||||||
queryLogs: {
|
|
||||||
enabled,
|
|
||||||
processingGetConfig,
|
|
||||||
processingAdditionalLogs,
|
|
||||||
processingGetLogs,
|
|
||||||
oldest,
|
|
||||||
logs,
|
|
||||||
pages,
|
|
||||||
page,
|
|
||||||
isDetailed,
|
|
||||||
},
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`);
|
const mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`);
|
||||||
const mediaQueryHandler = (e) => {
|
const mediaQueryHandler = (e) => {
|
||||||
setIsSmallScreen(e.matches);
|
setIsSmallScreen(e.matches);
|
||||||
if (e.matches) {
|
if (e.matches) {
|
||||||
toggleDetailedLogs(false);
|
dispatch(toggleDetailedLogs(false));
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeModal = () => setModalOpened(false);
|
|
||||||
|
|
||||||
const getLogs = (older_than, page, initial) => {
|
|
||||||
if (enabled) {
|
|
||||||
props.getLogs({
|
|
||||||
older_than,
|
|
||||||
page,
|
|
||||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
|
||||||
initial,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,7 +120,6 @@ const Logs = (props) => {
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
|
||||||
dispatch(getFilteringStatus());
|
dispatch(getFilteringStatus());
|
||||||
dispatch(getClients());
|
dispatch(getClients());
|
||||||
try {
|
try {
|
||||||
@@ -169,6 +139,7 @@ const Logs = (props) => {
|
|||||||
mediaQuery.removeEventListener('change', mediaQueryHandler);
|
mediaQuery.removeEventListener('change', mediaQueryHandler);
|
||||||
} catch (e1) {
|
} catch (e1) {
|
||||||
try {
|
try {
|
||||||
|
// Safari 13.1 do not support mediaQuery.addEventListener('change', handler)
|
||||||
mediaQuery.removeListener(mediaQueryHandler);
|
mediaQuery.removeListener(mediaQueryHandler);
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
console.error(e2);
|
console.error(e2);
|
||||||
@@ -179,99 +150,53 @@ const Logs = (props) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshLogs = async () => {
|
const renderPage = () => <>
|
||||||
setIsLoading(true);
|
<Filters
|
||||||
await Promise.all([
|
filter={{
|
||||||
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
response_status,
|
||||||
dispatch(refreshFilteredLogs()),
|
search,
|
||||||
]);
|
}}
|
||||||
dispatch(addSuccessToast('query_log_updated'));
|
setIsLoading={setIsLoading}
|
||||||
setIsLoading(false);
|
processingGetLogs={processingGetLogs}
|
||||||
};
|
processingAdditionalLogs={processingAdditionalLogs}
|
||||||
|
/>
|
||||||
|
<InfiniteTable
|
||||||
|
isLoading={isLoading}
|
||||||
|
items={logs}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||||
|
setButtonType={setButtonType}
|
||||||
|
setModalOpened={setModalOpened}
|
||||||
|
/>
|
||||||
|
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
|
||||||
|
onRequestClose={closeModal}
|
||||||
|
style={{
|
||||||
|
content: {
|
||||||
|
width: '100%',
|
||||||
|
height: 'fit-content',
|
||||||
|
left: 0,
|
||||||
|
top: 47,
|
||||||
|
padding: '1rem 1.5rem 1rem',
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
|
||||||
|
onClick={closeModal}>
|
||||||
|
<use xlinkHref="#cross" />
|
||||||
|
</svg>
|
||||||
|
{processContent(detailedDataCurrent, buttonType)}
|
||||||
|
</Modal>
|
||||||
|
</>;
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<>
|
{enabled && processingGetConfig && <Loading />}
|
||||||
{enabled && processingGetConfig && <Loading />}
|
{enabled && !processingGetConfig && renderPage()}
|
||||||
{enabled && !processingGetConfig && (
|
{!enabled && !processingGetConfig && <Disabled />}
|
||||||
<>
|
</>;
|
||||||
<Filters
|
|
||||||
filter={{
|
|
||||||
response_status,
|
|
||||||
search,
|
|
||||||
}}
|
|
||||||
setIsLoading={setIsLoading}
|
|
||||||
processingGetLogs={processingGetLogs}
|
|
||||||
processingAdditionalLogs={processingAdditionalLogs}
|
|
||||||
refreshLogs={refreshLogs}
|
|
||||||
/>
|
|
||||||
<Table
|
|
||||||
isLoading={isLoading}
|
|
||||||
setIsLoading={setIsLoading}
|
|
||||||
logs={logs}
|
|
||||||
pages={pages}
|
|
||||||
page={page}
|
|
||||||
autoClients={dashboard.autoClients}
|
|
||||||
oldest={oldest}
|
|
||||||
filtering={filtering}
|
|
||||||
processingGetLogs={processingGetLogs}
|
|
||||||
processingGetConfig={processingGetConfig}
|
|
||||||
isDetailed={isDetailed}
|
|
||||||
setLogsPagination={setLogsPagination}
|
|
||||||
setLogsPage={setLogsPage}
|
|
||||||
toggleDetailedLogs={toggleDetailedLogs}
|
|
||||||
getLogs={getLogs}
|
|
||||||
setRules={props.setRules}
|
|
||||||
addSuccessToast={props.addSuccessToast}
|
|
||||||
getFilteringStatus={props.getFilteringStatus}
|
|
||||||
dnssec_enabled={dnsConfig.dnssec_enabled}
|
|
||||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
|
||||||
setButtonType={setButtonType}
|
|
||||||
setModalOpened={setModalOpened}
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
/>
|
|
||||||
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
|
|
||||||
onRequestClose={closeModal}
|
|
||||||
style={{
|
|
||||||
content: {
|
|
||||||
width: '100%',
|
|
||||||
height: 'fit-content',
|
|
||||||
left: 0,
|
|
||||||
top: 47,
|
|
||||||
padding: '1rem 1.5rem 1rem',
|
|
||||||
},
|
|
||||||
overlay: {
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
|
|
||||||
onClick={closeModal}>
|
|
||||||
<use xlinkHref="#cross" />
|
|
||||||
</svg>
|
|
||||||
{processContent(detailedDataCurrent, buttonType)}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!enabled && !processingGetConfig && (
|
|
||||||
<Disabled />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Logs.propTypes = {
|
|
||||||
getLogs: PropTypes.func.isRequired,
|
|
||||||
queryLogs: PropTypes.object.isRequired,
|
|
||||||
dashboard: PropTypes.object.isRequired,
|
|
||||||
getFilteringStatus: PropTypes.func.isRequired,
|
|
||||||
filtering: PropTypes.object.isRequired,
|
|
||||||
setRules: PropTypes.func.isRequired,
|
|
||||||
addSuccessToast: PropTypes.func.isRequired,
|
|
||||||
setLogsPagination: PropTypes.func.isRequired,
|
|
||||||
setLogsPage: PropTypes.func.isRequired,
|
|
||||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
|
||||||
dnsConfig: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Logs;
|
export default Logs;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
|
|||||||
<div key={key} title={t(key)}>
|
<div key={key} title={t(key)}>
|
||||||
{icon && (
|
{icon && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<svg className="logs__whois-icon text-muted-dark icons">
|
<svg className="logs__whois-icon text-muted-dark icons icon--24">
|
||||||
<use xlinkHref={`#${icon}`} />
|
<use xlinkHref={`#${icon}`} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|||||||
@@ -77,12 +77,12 @@ const StaticLeases = ({
|
|||||||
title={t('delete_table_action')}
|
title={t('delete_table_action')}
|
||||||
disabled={processingDeleting}
|
disabled={processingDeleting}
|
||||||
onClick={() => handleDelete(ip, mac, hostname)}
|
onClick={() => handleDelete(ip, mac, hostname)}
|
||||||
>
|
>
|
||||||
<svg className="icons">
|
<svg className="icons">
|
||||||
<use xlinkHref="#delete" />
|
<use xlinkHref="#delete"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1,71 +1,56 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import { FAILURE_TOAST_TIMEOUT, SUCCESS_TOAST_TIMEOUT } from '../../helpers/constants';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { TOAST_TIMEOUTS } from '../../helpers/constants';
|
||||||
|
import { removeToast } from '../../actions';
|
||||||
|
|
||||||
class Toast extends Component {
|
const Toast = ({
|
||||||
state = {
|
id,
|
||||||
timerId: null,
|
message,
|
||||||
|
type,
|
||||||
|
options,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [timerId, setTimerId] = useState(null);
|
||||||
|
|
||||||
|
const clearRemoveToastTimeout = () => clearTimeout(timerId);
|
||||||
|
const removeCurrentToast = () => dispatch(removeToast(id));
|
||||||
|
const setRemoveToastTimeout = () => {
|
||||||
|
const timeout = TOAST_TIMEOUTS[type];
|
||||||
|
const timerId = setTimeout(removeCurrentToast, timeout);
|
||||||
|
|
||||||
|
setTimerId(timerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
this.setRemoveToastTimeout();
|
setRemoveToastTimeout();
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
return <div className={`toast toast--${type}`}
|
||||||
return false;
|
onMouseOver={clearRemoveToastTimeout}
|
||||||
}
|
onMouseOut={setRemoveToastTimeout}>
|
||||||
|
<p className="toast__content">
|
||||||
clearRemoveToastTimeout = () => clearTimeout(this.state.timerId);
|
<Trans
|
||||||
|
i18nKey={message}
|
||||||
setRemoveToastTimeout = () => {
|
{...options}
|
||||||
const timeout = this.props.type === 'success' ? SUCCESS_TOAST_TIMEOUT : FAILURE_TOAST_TIMEOUT;
|
/>
|
||||||
|
</p>
|
||||||
const timerId = setTimeout(() => {
|
<button className="toast__dismiss" onClick={removeCurrentToast}>
|
||||||
this.props.removeToast(this.props.id);
|
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
|
||||||
}, timeout);
|
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m18 6-12 12" />
|
||||||
this.setState({ timerId });
|
<path d="m6 6 12 12" />
|
||||||
};
|
</svg>
|
||||||
|
</button>
|
||||||
showMessage(t, type, message) {
|
</div>;
|
||||||
if (type === 'notice') {
|
};
|
||||||
return <span dangerouslySetInnerHTML={{ __html: t(message) }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Trans>{message}</Trans>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
type, id, t, message,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`toast toast--${type}`}
|
|
||||||
onMouseOver={this.clearRemoveToastTimeout}
|
|
||||||
onMouseOut={this.setRemoveToastTimeout}>
|
|
||||||
<p className="toast__content">
|
|
||||||
{this.showMessage(t, type, message)}
|
|
||||||
</p>
|
|
||||||
<button className="toast__dismiss" onClick={() => this.props.removeToast(id)}>
|
|
||||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="m18 6-12 12" />
|
|
||||||
<path d="m6 6 12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.propTypes = {
|
Toast.propTypes = {
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
message: PropTypes.string.isRequired,
|
message: PropTypes.string.isRequired,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
removeToast: PropTypes.func.isRequired,
|
options: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTranslation()(Toast);
|
export default Toast;
|
||||||
|
|||||||
@@ -1,41 +1,25 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { useSelector, shallowEqual } from 'react-redux';
|
||||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||||
import * as actionCreators from '../../actions';
|
import { TOAST_TRANSITION_TIMEOUT } from '../../helpers/constants';
|
||||||
import Toast from './Toast';
|
import Toast from './Toast';
|
||||||
|
|
||||||
import './Toast.css';
|
import './Toast.css';
|
||||||
|
|
||||||
const Toasts = (props) => (
|
const Toasts = () => {
|
||||||
<TransitionGroup className="toasts">
|
const toasts = useSelector((state) => state.toasts, shallowEqual);
|
||||||
{props.toasts.notices?.map((toast) => {
|
|
||||||
const { id } = toast;
|
|
||||||
return (
|
|
||||||
<CSSTransition
|
|
||||||
key={id}
|
|
||||||
timeout={500}
|
|
||||||
classNames="toast"
|
|
||||||
>
|
|
||||||
<Toast removeToast={props.removeToast} {...toast} />
|
|
||||||
</CSSTransition>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TransitionGroup>
|
|
||||||
);
|
|
||||||
|
|
||||||
Toasts.propTypes = {
|
return <TransitionGroup className="toasts">
|
||||||
toasts: PropTypes.object,
|
{toasts.notices?.map((toast) => {
|
||||||
removeToast: PropTypes.func,
|
const { id } = toast;
|
||||||
|
return <CSSTransition
|
||||||
|
key={id}
|
||||||
|
timeout={TOAST_TRANSITION_TIMEOUT}
|
||||||
|
classNames="toast"
|
||||||
|
>
|
||||||
|
<Toast {...toast} />
|
||||||
|
</CSSTransition>;
|
||||||
|
})}
|
||||||
|
</TransitionGroup>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
export default Toasts;
|
||||||
const { toasts } = state;
|
|
||||||
const props = { toasts };
|
|
||||||
return props;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
actionCreators,
|
|
||||||
)(Toasts);
|
|
||||||
|
|||||||
@@ -16,7 +16,11 @@
|
|||||||
|
|
||||||
.card-table-overflow--limited {
|
.card-table-overflow--limited {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 280px;
|
max-height: 17.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-table-overflow--limited.clients__table {
|
||||||
|
max-height: 18rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
@@ -118,14 +122,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .red {
|
.card .logs__cell--red {
|
||||||
background-color: #fff4f2;
|
background-color: #fff4f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .green {
|
.card .logs__cell--green {
|
||||||
background-color: #f1faf3;
|
background-color: #f1faf3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .blue {
|
.card .logs__row--blue {
|
||||||
background-color: #ecf7ff;
|
background-color: #ecf7ff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,6 +344,14 @@ const Icons = () => (
|
|||||||
<path d="M60 54.5h8v40h-8zM60 35.5h8v8h-8z" />
|
<path d="M60 54.5h8v40h-8zM60 35.5h8v8h-8z" />
|
||||||
</svg>
|
</svg>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="chevron-down" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" fillRule="evenodd">
|
||||||
|
<path d="M0 0h24v24H0z" fill="#878787" fillOpacity=".01" />
|
||||||
|
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
|
||||||
|
d="M8.036 10.93l3.93 4.07 4.068-3.93" />
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: rgba(255, 255, 255, 0.6);
|
background-color: rgba(255, 255, 255, 0.48);
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading:after {
|
.loading:after {
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import './Loading.css';
|
import './Loading.css';
|
||||||
|
|
||||||
const Loading = ({ className }) => (
|
const Loading = ({ className, text }) => {
|
||||||
<div className={classNames('loading', className)} />
|
const { t } = useTranslation();
|
||||||
);
|
return <div className={classNames('loading', className)}>{t(text)}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
Loading.propTypes = {
|
Loading.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
text: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Loading;
|
export default Loading;
|
||||||
|
|||||||
@@ -13,18 +13,18 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rt-tr-group.red {
|
.rt-tr-group.logs__row--red {
|
||||||
background-color: rgba(223, 56, 18, 0.05);
|
background-color: rgba(223, 56, 18, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rt-tr-group.green {
|
.rt-tr-group.logs__row--green {
|
||||||
background-color: rgba(103, 178, 121, 0.1);
|
background-color: rgba(103, 178, 121, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rt-tr-group.blue {
|
.rt-tr-group.logs__row--blue {
|
||||||
background-color: #e5effd;
|
background-color: #e5effd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rt-tr-group.yellow {
|
.rt-tr-group.logs__row--yellow {
|
||||||
background-color: var(--yellow-pale);
|
background-color: var(--yellow-pale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const Tooltip = ({
|
|||||||
trigger = 'hover',
|
trigger = 'hover',
|
||||||
delayShow = SHOW_TOOLTIP_DELAY,
|
delayShow = SHOW_TOOLTIP_DELAY,
|
||||||
delayHide = HIDE_TOOLTIP_DELAY,
|
delayHide = HIDE_TOOLTIP_DELAY,
|
||||||
|
onVisibilityChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const touchEventsAvailable = 'ontouchstart' in window;
|
const touchEventsAvailable = 'ontouchstart' in window;
|
||||||
@@ -34,33 +35,48 @@ const Tooltip = ({
|
|||||||
delayShowValue = 0;
|
delayShowValue = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderTooltip = ({ tooltipRef, getTooltipProps }) => (
|
||||||
|
<div
|
||||||
|
{...getTooltipProps({
|
||||||
|
ref: tooltipRef,
|
||||||
|
className,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{typeof content === 'string' ? t(content) : content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTrigger = ({ getTriggerProps, triggerRef }) => (
|
||||||
|
<span
|
||||||
|
{...getTriggerProps({
|
||||||
|
ref: triggerRef,
|
||||||
|
className: triggerClass,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
renderTooltip.propTypes = {
|
||||||
|
tooltipRef: propTypes.object,
|
||||||
|
getTooltipProps: propTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTrigger.propTypes = {
|
||||||
|
triggerRef: propTypes.object,
|
||||||
|
getTriggerProps: propTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
placement={placement}
|
placement={placement}
|
||||||
trigger={triggerValue}
|
trigger={triggerValue}
|
||||||
delayHide={delayHideValue}
|
delayHide={delayHideValue}
|
||||||
delayShow={delayShowValue}
|
delayShow={delayShowValue}
|
||||||
tooltip={({ tooltipRef, getTooltipProps }) => (
|
tooltip={renderTooltip}
|
||||||
<div
|
onVisibilityChange={onVisibilityChange}
|
||||||
{...getTooltipProps({
|
|
||||||
ref: tooltipRef,
|
|
||||||
className,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{typeof content === 'string' ? t(content) : content}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{({ getTriggerProps, triggerRef }) => (
|
{renderTrigger}
|
||||||
<span
|
|
||||||
{...getTriggerProps({
|
|
||||||
ref: triggerRef,
|
|
||||||
className: triggerClass,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -76,10 +92,11 @@ Tooltip.propTypes = {
|
|||||||
).isRequired,
|
).isRequired,
|
||||||
placement: propTypes.string,
|
placement: propTypes.string,
|
||||||
trigger: propTypes.string,
|
trigger: propTypes.string,
|
||||||
delayHide: propTypes.string,
|
delayHide: propTypes.number,
|
||||||
delayShow: propTypes.string,
|
delayShow: propTypes.number,
|
||||||
className: propTypes.string,
|
className: propTypes.string,
|
||||||
triggerClass: propTypes.string,
|
triggerClass: propTypes.string,
|
||||||
|
onVisibilityChange: propTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tooltip;
|
export default Tooltip;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toggleProtection, getClients } from '../actions';
|
import { toggleProtection, getClients } from '../actions';
|
||||||
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
|
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
|
||||||
import { toggleClientBlock, getAccessList } from '../actions/access';
|
import { getAccessList } from '../actions/access';
|
||||||
import Dashboard from '../components/Dashboard';
|
import Dashboard from '../components/Dashboard';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
@@ -16,7 +16,6 @@ const mapDispatchToProps = {
|
|||||||
getStats,
|
getStats,
|
||||||
getStatsConfig,
|
getStatsConfig,
|
||||||
setStatsConfig,
|
setStatsConfig,
|
||||||
toggleClientBlock,
|
|
||||||
getAccessList,
|
getAccessList,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { getFilteringStatus, setRules } from '../actions/filtering';
|
|
||||||
import {
|
|
||||||
getLogs, setLogsPagination, setLogsPage, toggleDetailedLogs,
|
|
||||||
} from '../actions/queryLogs';
|
|
||||||
import Logs from '../components/Logs';
|
|
||||||
import { addSuccessToast } from '../actions/toasts';
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
|
||||||
const {
|
|
||||||
queryLogs, dashboard, filtering, dnsConfig,
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
queryLogs,
|
|
||||||
dashboard,
|
|
||||||
filtering,
|
|
||||||
dnsConfig,
|
|
||||||
};
|
|
||||||
return props;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
getLogs,
|
|
||||||
getFilteringStatus,
|
|
||||||
setRules,
|
|
||||||
addSuccessToast,
|
|
||||||
setLogsPagination,
|
|
||||||
setLogsPage,
|
|
||||||
toggleDetailedLogs,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
)(Logs);
|
|
||||||
@@ -21,6 +21,12 @@ export const R_UNIX_ABSOLUTE_PATH = /^(\/[^/\x00]+)+$/;
|
|||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
export const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\|\/)(?:[^\\/:*?"<>|\x00]+\\)*[^\\/:*?"<>|\x00]*$/;
|
export const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\|\/)(?:[^\\/:*?"<>|\x00]+\\)*[^\\/:*?"<>|\x00]*$/;
|
||||||
|
|
||||||
|
export const HTML_PAGES = {
|
||||||
|
INSTALL: '/install.html',
|
||||||
|
LOGIN: '/login.html',
|
||||||
|
MAIN: '/',
|
||||||
|
};
|
||||||
|
|
||||||
export const STATS_NAMES = {
|
export const STATS_NAMES = {
|
||||||
avg_processing_time: 'average_processing_time',
|
avg_processing_time: 'average_processing_time',
|
||||||
blocked_filtering: 'Blocked by filters',
|
blocked_filtering: 'Blocked by filters',
|
||||||
@@ -47,6 +53,8 @@ export const REPOSITORY = {
|
|||||||
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
||||||
export const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse';
|
export const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse';
|
||||||
|
|
||||||
|
export const GETTING_STARTED_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update';
|
||||||
|
|
||||||
export const ADDRESS_IN_USE_TEXT = 'address already in use';
|
export const ADDRESS_IN_USE_TEXT = 'address already in use';
|
||||||
|
|
||||||
export const INSTALL_FIRST_STEP = 1;
|
export const INSTALL_FIRST_STEP = 1;
|
||||||
@@ -70,8 +78,6 @@ export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
|||||||
export const DEBOUNCE_TIMEOUT = 300;
|
export const DEBOUNCE_TIMEOUT = 300;
|
||||||
export const DEBOUNCE_FILTER_TIMEOUT = 500;
|
export const DEBOUNCE_FILTER_TIMEOUT = 500;
|
||||||
export const CHECK_TIMEOUT = 1000;
|
export const CHECK_TIMEOUT = 1000;
|
||||||
export const SUCCESS_TOAST_TIMEOUT = 5000;
|
|
||||||
export const FAILURE_TOAST_TIMEOUT = 30000;
|
|
||||||
export const HIDE_TOOLTIP_DELAY = 300;
|
export const HIDE_TOOLTIP_DELAY = 300;
|
||||||
export const SHOW_TOOLTIP_DELAY = 200;
|
export const SHOW_TOOLTIP_DELAY = 200;
|
||||||
export const MODAL_OPEN_TIMEOUT = 150;
|
export const MODAL_OPEN_TIMEOUT = 150;
|
||||||
@@ -307,9 +313,7 @@ export const DEFAULT_LOGS_FILTER = {
|
|||||||
|
|
||||||
export const DEFAULT_LANGUAGE = 'en';
|
export const DEFAULT_LANGUAGE = 'en';
|
||||||
|
|
||||||
export const TABLE_DEFAULT_PAGE_SIZE = 25;
|
export const QUERY_LOGS_PAGE_LIMIT = 20;
|
||||||
|
|
||||||
export const TABLE_FIRST_PAGE = 0;
|
|
||||||
|
|
||||||
export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20;
|
export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20;
|
||||||
|
|
||||||
@@ -327,85 +331,93 @@ export const FILTERED_STATUS = {
|
|||||||
|
|
||||||
export const RESPONSE_FILTER = {
|
export const RESPONSE_FILTER = {
|
||||||
ALL: {
|
ALL: {
|
||||||
query: 'all',
|
QUERY: 'all',
|
||||||
label: 'all_queries',
|
LABEL: 'all_queries',
|
||||||
},
|
},
|
||||||
FILTERED: {
|
FILTERED: {
|
||||||
query: 'filtered',
|
QUERY: 'filtered',
|
||||||
label: 'filtered',
|
LABEL: 'filtered',
|
||||||
},
|
},
|
||||||
PROCESSED: {
|
PROCESSED: {
|
||||||
query: 'processed',
|
QUERY: 'processed',
|
||||||
label: 'show_processed_responses',
|
LABEL: 'show_processed_responses',
|
||||||
},
|
},
|
||||||
BLOCKED: {
|
BLOCKED: {
|
||||||
query: 'blocked',
|
QUERY: 'blocked',
|
||||||
label: 'show_blocked_responses',
|
LABEL: 'show_blocked_responses',
|
||||||
},
|
},
|
||||||
BLOCKED_THREATS: {
|
BLOCKED_THREATS: {
|
||||||
query: 'blocked_safebrowsing',
|
QUERY: 'blocked_safebrowsing',
|
||||||
label: 'blocked_threats',
|
LABEL: 'blocked_threats',
|
||||||
},
|
},
|
||||||
BLOCKED_ADULT_WEBSITES: {
|
BLOCKED_ADULT_WEBSITES: {
|
||||||
query: 'blocked_parental',
|
QUERY: 'blocked_parental',
|
||||||
label: 'blocked_adult_websites',
|
LABEL: 'blocked_adult_websites',
|
||||||
},
|
},
|
||||||
ALLOWED: {
|
ALLOWED: {
|
||||||
query: 'whitelisted',
|
QUERY: 'whitelisted',
|
||||||
label: 'allowed',
|
LABEL: 'allowed',
|
||||||
},
|
},
|
||||||
REWRITTEN: {
|
REWRITTEN: {
|
||||||
query: 'rewritten',
|
QUERY: 'rewritten',
|
||||||
label: 'rewritten',
|
LABEL: 'rewritten',
|
||||||
},
|
},
|
||||||
SAFE_SEARCH: {
|
SAFE_SEARCH: {
|
||||||
query: 'safe_search',
|
QUERY: 'safe_search',
|
||||||
label: 'safe_search',
|
LABEL: 'safe_search',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER)
|
export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER)
|
||||||
.reduce((acc, { query }) => {
|
.reduce((acc, { QUERY }) => {
|
||||||
acc[query] = query;
|
acc[QUERY] = QUERY;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
export const QUERY_STATUS_COLORS = {
|
||||||
|
BLUE: 'blue',
|
||||||
|
GREEN: 'green',
|
||||||
|
RED: 'red',
|
||||||
|
WHITE: 'white',
|
||||||
|
YELLOW: 'yellow',
|
||||||
|
};
|
||||||
|
|
||||||
export const FILTERED_STATUS_TO_META_MAP = {
|
export const FILTERED_STATUS_TO_META_MAP = {
|
||||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||||
label: RESPONSE_FILTER.ALLOWED.label,
|
LABEL: RESPONSE_FILTER.ALLOWED.LABEL,
|
||||||
color: 'green',
|
COLOR: QUERY_STATUS_COLORS.GREEN,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
|
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
|
||||||
label: RESPONSE_FILTER.PROCESSED.label,
|
LABEL: RESPONSE_FILTER.PROCESSED.LABEL,
|
||||||
color: 'white',
|
COLOR: QUERY_STATUS_COLORS.WHITE,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
|
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
|
||||||
label: RESPONSE_FILTER.BLOCKED.label,
|
LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
|
||||||
color: 'red',
|
COLOR: QUERY_STATUS_COLORS.RED,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
|
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
|
||||||
label: RESPONSE_FILTER.SAFE_SEARCH.label,
|
LABEL: RESPONSE_FILTER.SAFE_SEARCH.LABEL,
|
||||||
color: 'yellow',
|
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: {
|
[FILTERED_STATUS.FILTERED_BLACK_LIST]: {
|
||||||
label: RESPONSE_FILTER.BLOCKED.label,
|
LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
|
||||||
color: 'red',
|
COLOR: QUERY_STATUS_COLORS.RED,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.REWRITE]: {
|
[FILTERED_STATUS.REWRITE]: {
|
||||||
label: RESPONSE_FILTER.REWRITTEN.label,
|
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||||
color: 'blue',
|
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.REWRITE_HOSTS]: {
|
[FILTERED_STATUS.REWRITE_HOSTS]: {
|
||||||
label: RESPONSE_FILTER.REWRITTEN.label,
|
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||||
color: 'blue',
|
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: {
|
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: {
|
||||||
label: RESPONSE_FILTER.BLOCKED_THREATS.label,
|
LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL,
|
||||||
color: 'yellow',
|
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||||
},
|
},
|
||||||
[FILTERED_STATUS.FILTERED_PARENTAL]: {
|
[FILTERED_STATUS.FILTERED_PARENTAL]: {
|
||||||
label: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.label,
|
LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL,
|
||||||
color: 'yellow',
|
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -519,3 +531,20 @@ export const DHCP_DESCRIPTION_PLACEHOLDERS = {
|
|||||||
lease_duration: 'dhcp_form_lease_input',
|
lease_duration: 'dhcp_form_lease_input',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TOAST_TRANSITION_TIMEOUT = 500;
|
||||||
|
|
||||||
|
export const TOAST_TYPES = {
|
||||||
|
SUCCESS: 'success',
|
||||||
|
ERROR: 'error',
|
||||||
|
NOTICE: 'notice',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SUCCESS_TOAST_TIMEOUT = 5000;
|
||||||
|
export const FAILURE_TOAST_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
export const TOAST_TIMEOUTS = {
|
||||||
|
[TOAST_TYPES.SUCCESS]: SUCCESS_TOAST_TIMEOUT,
|
||||||
|
[TOAST_TYPES.ERROR]: FAILURE_TOAST_TIMEOUT,
|
||||||
|
[TOAST_TYPES.NOTICE]: FAILURE_TOAST_TIMEOUT,
|
||||||
|
};
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"name": "NoCoin Filter List",
|
"name": "NoCoin Filter List",
|
||||||
"categoryId": "security",
|
"categoryId": "security",
|
||||||
"homepage": "https://github.com/hoshsadiq/adblock-nocoin-list/",
|
"homepage": "https://github.com/hoshsadiq/adblock-nocoin-list/",
|
||||||
"source": "https://raw.githubusercontent.com/hoshsadiq/adblock-nocoin-list/master/nocoin.txt"
|
"source": "https://raw.githubusercontent.com/hoshsadiq/adblock-nocoin-list/master/hosts.txt"
|
||||||
},
|
},
|
||||||
"the-big-list-of-hacked-malware-web-sites": {
|
"the-big-list-of-hacked-malware-web-sites": {
|
||||||
"name": "The Big List of Hacked Malware Web Sites",
|
"name": "The Big List of Hacked Malware Web Sites",
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { normalizeWhois } from './helpers';
|
|
||||||
import { WHOIS_ICONS } from './constants';
|
|
||||||
|
|
||||||
const getFormattedWhois = (whois) => {
|
|
||||||
const whoisInfo = normalizeWhois(whois);
|
|
||||||
return (
|
|
||||||
Object.keys(whoisInfo)
|
|
||||||
.map((key) => {
|
|
||||||
const icon = WHOIS_ICONS[key];
|
|
||||||
return (
|
|
||||||
<span className="logs__whois text-muted " key={key} title={whoisInfo[key]}>
|
|
||||||
{icon && (
|
|
||||||
<>
|
|
||||||
<svg className="logs__whois-icon icons">
|
|
||||||
<use xlinkHref={`#${icon}`} />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
</>
|
|
||||||
)}{whoisInfo[key]}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
|
|
||||||
const { value, original: { info } } = row;
|
|
||||||
let whoisContainer = '';
|
|
||||||
let nameContainer = value;
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
const { name, whois_info } = info;
|
|
||||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
if (isLogs) {
|
|
||||||
nameContainer = !whoisAvailable && isDetailed
|
|
||||||
? (
|
|
||||||
<small title={value}>{value}</small>
|
|
||||||
) : (
|
|
||||||
<div className="logs__text logs__text--nowrap" title={`${name} (${value})`}>
|
|
||||||
{name} <small>{`(${value})`}</small>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
nameContainer = (
|
|
||||||
<div
|
|
||||||
className="logs__text logs__text--nowrap"
|
|
||||||
title={`${name} (${value})`}
|
|
||||||
>
|
|
||||||
{name} <small>{`(${value})`}</small>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (whoisAvailable && isDetailed) {
|
|
||||||
whoisContainer = (
|
|
||||||
<div className="logs__text logs__text--wrap logs__text--whois">
|
|
||||||
{getFormattedWhois(whois_info)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="logs__text mw-100" title={value}>
|
|
||||||
<>
|
|
||||||
{nameContainer}
|
|
||||||
{whoisContainer}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -15,6 +15,7 @@ import { getTrackerData } from './trackers/trackers';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CHECK_TIMEOUT,
|
CHECK_TIMEOUT,
|
||||||
|
CUSTOM_FILTERING_RULES_ID,
|
||||||
DEFAULT_DATE_FORMAT_OPTIONS,
|
DEFAULT_DATE_FORMAT_OPTIONS,
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
DEFAULT_TIME_FORMAT,
|
DEFAULT_TIME_FORMAT,
|
||||||
@@ -96,7 +97,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
|
|||||||
filterId,
|
filterId,
|
||||||
rule,
|
rule,
|
||||||
status,
|
status,
|
||||||
serviceName: service_name,
|
service_name,
|
||||||
originalAnswer: original_answer,
|
originalAnswer: original_answer,
|
||||||
originalResponse: processResponse(original_answer),
|
originalResponse: processResponse(original_answer),
|
||||||
tracker: getTrackerData(domain),
|
tracker: getTrackerData(domain),
|
||||||
@@ -742,6 +743,30 @@ export const sortIp = (a, b) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {array} filters
|
||||||
|
* @param {array} whitelistFilters
|
||||||
|
* @param {number} filterId
|
||||||
|
* @param {function} t - translate
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const getFilterName = (
|
||||||
|
filters,
|
||||||
|
whitelistFilters,
|
||||||
|
filterId,
|
||||||
|
customFilterTranslationKey = 'custom_filter_rules',
|
||||||
|
resolveFilterName = (filter) => (filter ? filter.name : i18n.t('unknown_filter', { filterId })),
|
||||||
|
) => {
|
||||||
|
if (filterId === CUSTOM_FILTERING_RULES_ID) {
|
||||||
|
return i18n.t(customFilterTranslationKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchIdPredicate = (filter) => filter.id === filterId;
|
||||||
|
const filter = filters.find(matchIdPredicate) || whitelistFilters.find(matchIdPredicate);
|
||||||
|
|
||||||
|
return resolveFilterName(filter);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ip {string}
|
* @param ip {string}
|
||||||
* @param gateway_ip {string}
|
* @param gateway_ip {string}
|
||||||
@@ -803,3 +828,29 @@ export const enrichWithConcatenatedIpAddresses = (interfaces) => Object.entries(
|
|||||||
acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses);
|
acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses);
|
||||||
return acc;
|
return acc;
|
||||||
}, interfaces);
|
}, interfaces);
|
||||||
|
|
||||||
|
export const isScrolledIntoView = (el) => {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const elemTop = rect.top;
|
||||||
|
const elemBottom = rect.bottom;
|
||||||
|
|
||||||
|
return elemTop < window.innerHeight && elemBottom >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is a manually created client, return its name.
|
||||||
|
* If this is a "runtime" client, return it's IP address.
|
||||||
|
* @param clients {Array.<object>}
|
||||||
|
* @param ip {string}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const getBlockingClientName = (clients, ip) => {
|
||||||
|
for (let i = 0; i < clients.length; i += 1) {
|
||||||
|
const client = clients[i];
|
||||||
|
|
||||||
|
if (client.ids.includes(ip)) {
|
||||||
|
return client.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
};
|
||||||
|
|||||||
69
client/src/helpers/renderFormattedClientCell.js
Normal file
69
client/src/helpers/renderFormattedClientCell.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { normalizeWhois } from './helpers';
|
||||||
|
import { WHOIS_ICONS } from './constants';
|
||||||
|
|
||||||
|
const getFormattedWhois = (whois) => {
|
||||||
|
const whoisInfo = normalizeWhois(whois);
|
||||||
|
return (
|
||||||
|
Object.keys(whoisInfo)
|
||||||
|
.map((key) => {
|
||||||
|
const icon = WHOIS_ICONS[key];
|
||||||
|
return (
|
||||||
|
<span className="logs__whois text-muted" key={key} title={whoisInfo[key]}>
|
||||||
|
{icon && (
|
||||||
|
<>
|
||||||
|
<svg className="logs__whois-icon icons icon--18">
|
||||||
|
<use xlinkHref={`#${icon}`} />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}{whoisInfo[key]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
* @param {object} info
|
||||||
|
* @param {string} info.name
|
||||||
|
* @param {object} info.whois_info
|
||||||
|
* @param {boolean} [isDetailed]
|
||||||
|
* @param {boolean} [isLogs]
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => {
|
||||||
|
let whoisContainer = null;
|
||||||
|
let nameContainer = value;
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
const { name, whois_info } = info;
|
||||||
|
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
const nameValue = <div className="logs__text logs__text--nowrap" title={`${name} (${value})`}>
|
||||||
|
{name} <small>{`(${value})`}</small>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
if (!isLogs) {
|
||||||
|
nameContainer = nameValue;
|
||||||
|
} else {
|
||||||
|
nameContainer = !whoisAvailable && isDetailed
|
||||||
|
? <small title={value}>{value}</small>
|
||||||
|
: nameValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (whoisAvailable && isDetailed) {
|
||||||
|
whoisContainer = <div className="logs__text logs__text--wrap logs__text--whois">
|
||||||
|
{getFormattedWhois(whois_info)}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="logs__text mw-100" title={value}>
|
||||||
|
{nameContainer}
|
||||||
|
{whoisContainer}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
@@ -1,30 +1,10 @@
|
|||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
import * as actions from '../actions/queryLogs';
|
import * as actions from '../actions/queryLogs';
|
||||||
import { DEFAULT_LOGS_FILTER, TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants';
|
import { DEFAULT_LOGS_FILTER } from '../helpers/constants';
|
||||||
|
|
||||||
const queryLogs = handleActions(
|
const queryLogs = handleActions(
|
||||||
{
|
{
|
||||||
[actions.setLogsPagination]: (state, { payload }) => {
|
|
||||||
const { page, pageSize } = payload;
|
|
||||||
const { allLogs } = state;
|
|
||||||
const rowsStart = pageSize * page;
|
|
||||||
const rowsEnd = (pageSize * page) + pageSize;
|
|
||||||
const logsSlice = allLogs.slice(rowsStart, rowsEnd);
|
|
||||||
const pages = Math.ceil(allLogs.length / pageSize);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
pages,
|
|
||||||
logs: logsSlice,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.setLogsPage]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
page: payload,
|
|
||||||
}),
|
|
||||||
|
|
||||||
[actions.setFilteredLogsRequest]: (state) => ({ ...state, processingGetLogs: true }),
|
[actions.setFilteredLogsRequest]: (state) => ({ ...state, processingGetLogs: true }),
|
||||||
[actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
[actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||||
[actions.toggleDetailedLogs]: (state, { payload }) => ({
|
[actions.toggleDetailedLogs]: (state, { payload }) => ({
|
||||||
@@ -34,14 +14,7 @@ const queryLogs = handleActions(
|
|||||||
|
|
||||||
[actions.setFilteredLogsSuccess]: (state, { payload }) => {
|
[actions.setFilteredLogsSuccess]: (state, { payload }) => {
|
||||||
const { logs, oldest, filter } = payload;
|
const { logs, oldest, filter } = payload;
|
||||||
const pageSize = TABLE_DEFAULT_PAGE_SIZE;
|
|
||||||
const page = 0;
|
|
||||||
|
|
||||||
const pages = Math.ceil(logs.length / pageSize);
|
|
||||||
const total = logs.length;
|
|
||||||
const rowsStart = pageSize * page;
|
|
||||||
const rowsEnd = rowsStart + pageSize;
|
|
||||||
const logsSlice = logs.slice(rowsStart, rowsEnd);
|
|
||||||
const isFiltered = filter && Object.keys(filter).some((key) => filter[key]);
|
const isFiltered = filter && Object.keys(filter).some((key) => filter[key]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -49,10 +22,8 @@ const queryLogs = handleActions(
|
|||||||
oldest,
|
oldest,
|
||||||
filter,
|
filter,
|
||||||
isFiltered,
|
isFiltered,
|
||||||
pages,
|
logs,
|
||||||
total,
|
isEntireLog: logs.length < 1,
|
||||||
logs: logsSlice,
|
|
||||||
allLogs: logs,
|
|
||||||
processingGetLogs: false,
|
processingGetLogs: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -67,29 +38,13 @@ const queryLogs = handleActions(
|
|||||||
[actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
[actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||||
[actions.getLogsSuccess]: (state, { payload }) => {
|
[actions.getLogsSuccess]: (state, { payload }) => {
|
||||||
const {
|
const {
|
||||||
logs, oldest, older_than, page, pageSize, initial,
|
logs, oldest, older_than,
|
||||||
} = payload;
|
} = payload;
|
||||||
let logsWithOffset = state.allLogs.length > 0 && !initial ? state.allLogs : logs;
|
|
||||||
let allLogs = logs;
|
|
||||||
|
|
||||||
if (older_than) {
|
|
||||||
logsWithOffset = [...state.allLogs, ...logs];
|
|
||||||
allLogs = [...state.allLogs, ...logs];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pages = Math.ceil(logsWithOffset.length / pageSize);
|
|
||||||
const total = logsWithOffset.length;
|
|
||||||
const rowsStart = pageSize * page;
|
|
||||||
const rowsEnd = (pageSize * page) + pageSize;
|
|
||||||
const logsSlice = logsWithOffset.slice(rowsStart, rowsEnd);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
oldest,
|
oldest,
|
||||||
pages,
|
logs: older_than ? [...state.logs, ...logs] : logs,
|
||||||
total,
|
|
||||||
allLogs,
|
|
||||||
logs: logsSlice,
|
|
||||||
isEntireLog: logs.length < 1,
|
isEntireLog: logs.length < 1,
|
||||||
processingGetLogs: false,
|
processingGetLogs: false,
|
||||||
};
|
};
|
||||||
@@ -126,7 +81,7 @@ const queryLogs = handleActions(
|
|||||||
...state, processingAdditionalLogs: false, processingGetLogs: false,
|
...state, processingAdditionalLogs: false, processingGetLogs: false,
|
||||||
}),
|
}),
|
||||||
[actions.getAdditionalLogsSuccess]: (state) => ({
|
[actions.getAdditionalLogsSuccess]: (state) => ({
|
||||||
...state, processingAdditionalLogs: false, processingGetLogs: false,
|
...state, processingAdditionalLogs: false, processingGetLogs: false, isEntireLog: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -135,18 +90,15 @@ const queryLogs = handleActions(
|
|||||||
processingGetConfig: false,
|
processingGetConfig: false,
|
||||||
processingSetConfig: false,
|
processingSetConfig: false,
|
||||||
processingAdditionalLogs: false,
|
processingAdditionalLogs: false,
|
||||||
logs: [],
|
|
||||||
interval: 1,
|
interval: 1,
|
||||||
allLogs: [],
|
logs: [],
|
||||||
page: 0,
|
|
||||||
pages: 0,
|
|
||||||
total: 0,
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
oldest: '',
|
oldest: '',
|
||||||
filter: DEFAULT_LOGS_FILTER,
|
filter: DEFAULT_LOGS_FILTER,
|
||||||
isFiltered: false,
|
isFiltered: false,
|
||||||
anonymize_client_ip: false,
|
anonymize_client_ip: false,
|
||||||
isDetailed: true,
|
isDetailed: true,
|
||||||
|
isEntireLog: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import {
|
|||||||
addErrorToast, addNoticeToast, addSuccessToast,
|
addErrorToast, addNoticeToast, addSuccessToast,
|
||||||
} from '../actions/toasts';
|
} from '../actions/toasts';
|
||||||
import { removeToast } from '../actions';
|
import { removeToast } from '../actions';
|
||||||
|
import { TOAST_TYPES } from '../helpers/constants';
|
||||||
|
|
||||||
const toasts = handleActions({
|
const toasts = handleActions({
|
||||||
[addErrorToast]: (state, { payload }) => {
|
[addErrorToast]: (state, { payload }) => {
|
||||||
const message = payload.error.toString();
|
const message = payload.error.toString();
|
||||||
console.error(message);
|
console.error(payload.error);
|
||||||
|
|
||||||
const errorToast = {
|
const errorToast = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
message,
|
message,
|
||||||
type: 'error',
|
options: payload.options,
|
||||||
|
type: TOAST_TYPES.ERROR,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||||
@@ -24,7 +26,7 @@ const toasts = handleActions({
|
|||||||
const successToast = {
|
const successToast = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
message: payload,
|
message: payload,
|
||||||
type: 'success',
|
type: TOAST_TYPES.SUCCESS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||||
@@ -34,7 +36,8 @@ const toasts = handleActions({
|
|||||||
const noticeToast = {
|
const noticeToast = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
message: payload.error.toString(),
|
message: payload.error.toString(),
|
||||||
type: 'notice',
|
options: payload.options,
|
||||||
|
type: TOAST_TYPES.NOTICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = { ...state, notices: [...state.notices, noticeToast] };
|
const newState = { ...state, notices: [...state.notices, noticeToast] };
|
||||||
|
|||||||
5
client/webpack.common.js
vendored
5
client/webpack.common.js
vendored
@@ -41,9 +41,8 @@ const config = {
|
|||||||
alias: {
|
alias: {
|
||||||
MainRoot: path.resolve(__dirname, '../'),
|
MainRoot: path.resolve(__dirname, '../'),
|
||||||
ClientRoot: path.resolve(__dirname, './src'),
|
ClientRoot: path.resolve(__dirname, './src'),
|
||||||
// TODO: change to '@hot-loader/react-dom' when v16.13.1 is released
|
// TODO: uncomment when v16.13.1 is released https://stackoverflow.com/a/62671689/12942752
|
||||||
// https://stackoverflow.com/a/62671689/12942752
|
// 'react-dom': '@hot-loader/react-dom',
|
||||||
'react-dom': 'react-dom',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||||
// +build go1.12
|
// +build go1.12
|
||||||
|
|
||||||
// Package nclient4 is a small, minimum-functionality client for DHCPv4.
|
// Package nclient4 is a small, minimum-functionality client for DHCPv4.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
//
|
//
|
||||||
// This file contains code taken from gVisor.
|
// This file contains code taken from gVisor.
|
||||||
|
|
||||||
|
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||||
// +build go1.12
|
// +build go1.12
|
||||||
|
|
||||||
package nclient4
|
package nclient4
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ type FilteringConfig struct {
|
|||||||
EnableDNSSEC bool `yaml:"enable_dnssec"` // Set DNSSEC flag in outcoming DNS request
|
EnableDNSSEC bool `yaml:"enable_dnssec"` // Set DNSSEC flag in outcoming DNS request
|
||||||
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
|
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
|
||||||
MaxGoroutines uint32 `yaml:"max_goroutines"` // Max. number of parallel goroutines for processing incoming requests
|
MaxGoroutines uint32 `yaml:"max_goroutines"` // Max. number of parallel goroutines for processing incoming requests
|
||||||
|
|
||||||
|
// IPSET configuration - add IP addresses of the specified domain names to an ipset list
|
||||||
|
// Syntax:
|
||||||
|
// "DOMAIN[,DOMAIN].../IPSET_NAME"
|
||||||
|
IPSETList []string `yaml:"ipset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
|
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ type Server struct {
|
|||||||
stats stats.Stats
|
stats stats.Stats
|
||||||
access *accessCtx
|
access *accessCtx
|
||||||
|
|
||||||
|
ipset ipsetCtx
|
||||||
|
|
||||||
tableHostToIP map[string]net.IP // "hostname -> IP" table for internal addresses (DHCP)
|
tableHostToIP map[string]net.IP // "hostname -> IP" table for internal addresses (DHCP)
|
||||||
tableHostToIPLock sync.Mutex
|
tableHostToIPLock sync.Mutex
|
||||||
|
|
||||||
@@ -168,7 +170,7 @@ func (s *Server) startInternal() error {
|
|||||||
|
|
||||||
// Prepare the object
|
// Prepare the object
|
||||||
func (s *Server) Prepare(config *ServerConfig) error {
|
func (s *Server) Prepare(config *ServerConfig) error {
|
||||||
// 1. Initialize the server configuration
|
// Initialize the server configuration
|
||||||
// --
|
// --
|
||||||
if config != nil {
|
if config != nil {
|
||||||
s.conf = *config
|
s.conf = *config
|
||||||
@@ -184,18 +186,22 @@ func (s *Server) Prepare(config *ServerConfig) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Set default values in the case if nothing is configured
|
// Set default values in the case if nothing is configured
|
||||||
// --
|
// --
|
||||||
s.initDefaultSettings()
|
s.initDefaultSettings()
|
||||||
|
|
||||||
// 3. Prepare DNS servers settings
|
// Initialize IPSET configuration
|
||||||
|
// --
|
||||||
|
s.ipset.init(s.conf.IPSETList)
|
||||||
|
|
||||||
|
// Prepare DNS servers settings
|
||||||
// --
|
// --
|
||||||
err := s.prepareUpstreamSettings()
|
err := s.prepareUpstreamSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create DNS proxy configuration
|
// Create DNS proxy configuration
|
||||||
// --
|
// --
|
||||||
var proxyConfig proxy.Config
|
var proxyConfig proxy.Config
|
||||||
proxyConfig, err = s.createProxyConfig()
|
proxyConfig, err = s.createProxyConfig()
|
||||||
@@ -203,11 +209,11 @@ func (s *Server) Prepare(config *ServerConfig) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Prepare a DNS proxy instance that we use for internal DNS queries
|
// Prepare a DNS proxy instance that we use for internal DNS queries
|
||||||
// --
|
// --
|
||||||
s.prepareIntlProxy()
|
s.prepareIntlProxy()
|
||||||
|
|
||||||
// 5. Initialize DNS access module
|
// Initialize DNS access module
|
||||||
// --
|
// --
|
||||||
s.access = &accessCtx{}
|
s.access = &accessCtx{}
|
||||||
err = s.access.Init(s.conf.AllowedClients, s.conf.DisallowedClients, s.conf.BlockedHosts)
|
err = s.access.Init(s.conf.AllowedClients, s.conf.DisallowedClients, s.conf.BlockedHosts)
|
||||||
@@ -215,14 +221,14 @@ func (s *Server) Prepare(config *ServerConfig) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Register web handlers if necessary
|
// Register web handlers if necessary
|
||||||
// --
|
// --
|
||||||
if !webRegistered && s.conf.HTTPRegister != nil {
|
if !webRegistered && s.conf.HTTPRegister != nil {
|
||||||
webRegistered = true
|
webRegistered = true
|
||||||
s.registerHandlers()
|
s.registerHandlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Create the main DNS proxy instance
|
// Create the main DNS proxy instance
|
||||||
// --
|
// --
|
||||||
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
|
|||||||
processUpstream,
|
processUpstream,
|
||||||
processDNSSECAfterResponse,
|
processDNSSECAfterResponse,
|
||||||
processFilteringAfterResponse,
|
processFilteringAfterResponse,
|
||||||
|
s.ipset.process,
|
||||||
processQueryLogsAndStats,
|
processQueryLogsAndStats,
|
||||||
}
|
}
|
||||||
for _, process := range mods {
|
for _, process := range mods {
|
||||||
|
|||||||
133
dnsforward/ipset.go
Normal file
133
dnsforward/ipset.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package dnsforward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/util"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ipsetCtx struct {
|
||||||
|
ipsetList map[string][]string // domain -> []ipset_name
|
||||||
|
ipsetCache map[[4]byte]bool // cache for IP[] to prevent duplicate calls to ipset program
|
||||||
|
ipset6Cache map[[16]byte]bool // cache for IP[] to prevent duplicate calls to ipset program
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert configuration settings to an internal map
|
||||||
|
// DOMAIN[,DOMAIN].../IPSET1_NAME[,IPSET2_NAME]...
|
||||||
|
func (c *ipsetCtx) init(ipsetConfig []string) {
|
||||||
|
c.ipsetList = make(map[string][]string)
|
||||||
|
c.ipsetCache = make(map[[4]byte]bool)
|
||||||
|
c.ipset6Cache = make(map[[16]byte]bool)
|
||||||
|
|
||||||
|
for _, it := range ipsetConfig {
|
||||||
|
it = strings.TrimSpace(it)
|
||||||
|
hostsAndNames := strings.Split(it, "/")
|
||||||
|
if len(hostsAndNames) != 2 {
|
||||||
|
log.Debug("IPSET: invalid value '%s'", it)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipsetNames := strings.Split(hostsAndNames[1], ",")
|
||||||
|
if len(ipsetNames) == 0 {
|
||||||
|
log.Debug("IPSET: invalid value '%s'", it)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bad := false
|
||||||
|
for i := range ipsetNames {
|
||||||
|
ipsetNames[i] = strings.TrimSpace(ipsetNames[i])
|
||||||
|
if len(ipsetNames[i]) == 0 {
|
||||||
|
bad = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bad {
|
||||||
|
log.Debug("IPSET: invalid value '%s'", it)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := strings.Split(hostsAndNames[0], ",")
|
||||||
|
for _, host := range hosts {
|
||||||
|
host = strings.TrimSpace(host)
|
||||||
|
host = strings.ToLower(host)
|
||||||
|
if len(host) == 0 {
|
||||||
|
log.Debug("IPSET: invalid value '%s'", it)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.ipsetList[host] = ipsetNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debug("IPSET: added %d hosts", len(c.ipsetList))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ipsetCtx) getIP(rr dns.RR) net.IP {
|
||||||
|
switch a := rr.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
var ip4 [4]byte
|
||||||
|
copy(ip4[:], a.A.To4())
|
||||||
|
_, found := c.ipsetCache[ip4]
|
||||||
|
if found {
|
||||||
|
return nil // this IP was added before
|
||||||
|
}
|
||||||
|
c.ipsetCache[ip4] = false
|
||||||
|
return a.A
|
||||||
|
|
||||||
|
case *dns.AAAA:
|
||||||
|
var ip6 [16]byte
|
||||||
|
copy(ip6[:], a.AAAA)
|
||||||
|
_, found := c.ipset6Cache[ip6]
|
||||||
|
if found {
|
||||||
|
return nil // this IP was added before
|
||||||
|
}
|
||||||
|
c.ipset6Cache[ip6] = false
|
||||||
|
return a.AAAA
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add IP addresses of the specified in configuration domain names to an ipset list
|
||||||
|
func (c *ipsetCtx) process(ctx *dnsContext) int {
|
||||||
|
req := ctx.proxyCtx.Req
|
||||||
|
if !(req.Question[0].Qtype == dns.TypeA ||
|
||||||
|
req.Question[0].Qtype == dns.TypeAAAA) ||
|
||||||
|
!ctx.responseFromUpstream {
|
||||||
|
return resultDone
|
||||||
|
}
|
||||||
|
|
||||||
|
host := req.Question[0].Name
|
||||||
|
host = strings.TrimSuffix(host, ".")
|
||||||
|
host = strings.ToLower(host)
|
||||||
|
ipsetNames, found := c.ipsetList[host]
|
||||||
|
if !found {
|
||||||
|
return resultDone
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("IPSET: found ipsets %v for host %s", ipsetNames, host)
|
||||||
|
|
||||||
|
for _, it := range ctx.proxyCtx.Res.Answer {
|
||||||
|
ip := c.getIP(it)
|
||||||
|
if ip == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipStr := ip.String()
|
||||||
|
for _, name := range ipsetNames {
|
||||||
|
code, out, err := util.RunCommand("ipset", "add", name, ipStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("IPSET: %s(%s) -> %s: %s", host, ipStr, name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if code != 0 {
|
||||||
|
log.Info("IPSET: ipset add: code:%d output:'%s'", code, out)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Debug("IPSET: added %s(%s) -> %s", host, ipStr, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultDone
|
||||||
|
}
|
||||||
41
dnsforward/ipset_test.go
Normal file
41
dnsforward/ipset_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package dnsforward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIPSET(t *testing.T) {
|
||||||
|
s := Server{}
|
||||||
|
s.conf.IPSETList = append(s.conf.IPSETList, "HOST.com/name")
|
||||||
|
s.conf.IPSETList = append(s.conf.IPSETList, "host2.com,host3.com/name23")
|
||||||
|
s.conf.IPSETList = append(s.conf.IPSETList, "host4.com/name4,name41")
|
||||||
|
c := ipsetCtx{}
|
||||||
|
c.init(s.conf.IPSETList)
|
||||||
|
|
||||||
|
assert.Equal(t, "name", c.ipsetList["host.com"][0])
|
||||||
|
assert.Equal(t, "name23", c.ipsetList["host2.com"][0])
|
||||||
|
assert.Equal(t, "name23", c.ipsetList["host3.com"][0])
|
||||||
|
assert.Equal(t, "name4", c.ipsetList["host4.com"][0])
|
||||||
|
assert.Equal(t, "name41", c.ipsetList["host4.com"][1])
|
||||||
|
|
||||||
|
_, ok := c.ipsetList["host0.com"]
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
ctx := &dnsContext{
|
||||||
|
srv: &s,
|
||||||
|
}
|
||||||
|
ctx.proxyCtx = &proxy.DNSContext{}
|
||||||
|
ctx.proxyCtx.Req = &dns.Msg{
|
||||||
|
Question: []dns.Question{
|
||||||
|
{
|
||||||
|
Name: "host.com.",
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, resultDone, c.process(ctx))
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.14
|
|||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.31.1
|
github.com/AdguardTeam/dnsproxy v0.31.1
|
||||||
github.com/AdguardTeam/golibs v0.4.2
|
github.com/AdguardTeam/golibs v0.4.2
|
||||||
github.com/AdguardTeam/urlfilter v0.11.2
|
github.com/AdguardTeam/urlfilter v0.12.2
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
github.com/fsnotify/fsnotify v1.4.9
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
github.com/gobuffalo/packr v1.30.1
|
github.com/gobuffalo/packr v1.30.1
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -4,8 +4,8 @@ github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
|
|||||||
github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=
|
github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=
|
||||||
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||||
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||||
github.com/AdguardTeam/urlfilter v0.11.2 h1:gCrWGh63Yqw3z4yi9pgikfsbshIEyvAu/KYV3MvTBlc=
|
github.com/AdguardTeam/urlfilter v0.12.2 h1:5ZkH/+AWNBK8cCfbcgOL1MIrpPJ2NJXDs00KjYJr2WE=
|
||||||
github.com/AdguardTeam/urlfilter v0.11.2/go.mod h1:aMuejlNxpWppOVjiEV87X6z0eMf7wsXHTAIWQuylfZY=
|
github.com/AdguardTeam/urlfilter v0.12.2/go.mod h1:1fcCQx5TGJANrQN6sHNNM9KPBl7qx7BJml45ko6vru0=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||||
@@ -21,8 +21,6 @@ github.com/ameshkov/dnscrypt v1.1.0/go.mod h1:ikduAxNLCTEfd1AaCgpIA5TgroIVQ8JY3V
|
|||||||
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
|
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
|
||||||
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
|
||||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
|
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
|
||||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ func (a *Auth) GetCurrentUser(r *http.Request) User {
|
|||||||
// there's no Cookie, check Basic authentication
|
// there's no Cookie, check Basic authentication
|
||||||
user, pass, ok := r.BasicAuth()
|
user, pass, ok := r.BasicAuth()
|
||||||
if ok {
|
if ok {
|
||||||
u := Context.auth.UserFind(user, pass)
|
u := a.UserFind(user, pass)
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
return User{}
|
return User{}
|
||||||
|
|||||||
102
home/context.go
Normal file
102
home/context.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/querylog"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/stats"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/update"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/util"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global context
|
||||||
|
type homeContext struct {
|
||||||
|
// Modules
|
||||||
|
// --
|
||||||
|
|
||||||
|
clients clientsContainer // per-client-settings module
|
||||||
|
stats stats.Stats // statistics module
|
||||||
|
queryLog querylog.QueryLog // query log module
|
||||||
|
dnsServer *dnsforward.Server // DNS module
|
||||||
|
rdns *RDNS // rDNS module
|
||||||
|
whois *Whois // WHOIS module
|
||||||
|
dnsFilter *dnsfilter.Dnsfilter // DNS filtering module
|
||||||
|
dhcpServer *dhcpd.Server // DHCP module
|
||||||
|
auth *Auth // HTTP authentication module
|
||||||
|
filters Filtering // DNS filtering module
|
||||||
|
web *Web // Web (HTTP, HTTPS) module
|
||||||
|
tls *TLSMod // TLS module
|
||||||
|
autoHosts util.AutoHosts // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files
|
||||||
|
updater *update.Updater
|
||||||
|
|
||||||
|
// Runtime properties
|
||||||
|
// --
|
||||||
|
|
||||||
|
controlLock sync.Mutex
|
||||||
|
configFilename string // Config filename (can be overridden via the command line arguments)
|
||||||
|
workDir string // Location of our directory, used to protect against CWD being somewhere else
|
||||||
|
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
|
||||||
|
pidFileName string // PID file name. Empty if no PID file was created.
|
||||||
|
disableUpdate bool // If set, don't check for updates
|
||||||
|
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
|
||||||
|
tlsCiphers []uint16 // list of TLS ciphers to use
|
||||||
|
transport *http.Transport
|
||||||
|
client *http.Client
|
||||||
|
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
|
||||||
|
// runningAsService flag is set to true when options are passed from the service runner
|
||||||
|
runningAsService bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDataDir returns path to the directory where we store databases and filters
|
||||||
|
func (c *homeContext) getDataDir() string {
|
||||||
|
return filepath.Join(c.workDir, dataDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context - a global context object
|
||||||
|
var Context homeContext
|
||||||
|
|
||||||
|
func (c *homeContext) cleanup() {
|
||||||
|
log.Info("Stopping AdGuard Home")
|
||||||
|
|
||||||
|
if c.web != nil {
|
||||||
|
c.web.Close()
|
||||||
|
c.web = nil
|
||||||
|
}
|
||||||
|
if c.auth != nil {
|
||||||
|
c.auth.Close()
|
||||||
|
c.auth = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.stopDNSServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Couldn't stop DNS server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.dhcpServer != nil {
|
||||||
|
c.dhcpServer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.autoHosts.Close()
|
||||||
|
|
||||||
|
if c.tls != nil {
|
||||||
|
c.tls.Close()
|
||||||
|
c.tls = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is called before application exits
|
||||||
|
func (c *homeContext) cleanupAlways() {
|
||||||
|
if len(c.pidFileName) != 0 {
|
||||||
|
_ = os.Remove(c.pidFileName)
|
||||||
|
}
|
||||||
|
log.Info("Stopped")
|
||||||
|
}
|
||||||
@@ -47,10 +47,10 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
Context.dnsServer.WriteDiskConfig(&c)
|
Context.dnsServer.WriteDiskConfig(&c)
|
||||||
}
|
}
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"dns_addresses": getDNSAddresses(),
|
"dns_addresses": Context.getDNSAddresses(),
|
||||||
"http_port": config.BindPort,
|
"http_port": config.BindPort,
|
||||||
"dns_port": config.DNS.Port,
|
"dns_port": config.DNS.Port,
|
||||||
"running": isRunning(),
|
"running": Context.isRunning(),
|
||||||
"version": versionString,
|
"version": versionString,
|
||||||
"language": config.Language,
|
"language": config.Language,
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respData.DNS.Status = fmt.Sprintf("%v", err)
|
respData.DNS.Status = fmt.Sprintf("%v", err)
|
||||||
} else {
|
} else if reqData.DNS.IP != "0.0.0.0" {
|
||||||
respData.StaticIP = handleStaticIP(reqData.DNS.IP, reqData.SetStaticIP)
|
respData.StaticIP = handleStaticIP(reqData.DNS.IP, reqData.SetStaticIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ func getVersionResp(info update.VersionInfo) []byte {
|
|||||||
// Complete an update procedure
|
// Complete an update procedure
|
||||||
func finishUpdate() {
|
func finishUpdate() {
|
||||||
log.Info("Stopping all tasks")
|
log.Info("Stopping all tasks")
|
||||||
cleanup()
|
Context.cleanup()
|
||||||
cleanupAlways()
|
Context.cleanupAlways()
|
||||||
|
|
||||||
exeName := "AdGuardHome"
|
exeName := "AdGuardHome"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
|
|||||||
213
home/dns.go
213
home/dns.go
@@ -23,9 +23,9 @@ func onConfigModified() {
|
|||||||
// initDNSServer creates an instance of the dnsforward.Server
|
// initDNSServer creates an instance of the dnsforward.Server
|
||||||
// Please note that we must do it even if we don't start it
|
// Please note that we must do it even if we don't start it
|
||||||
// so that we had access to the query log and the stats
|
// so that we had access to the query log and the stats
|
||||||
func initDNSServer() error {
|
func (c *homeContext) initDNSServer() error {
|
||||||
var err error
|
var err error
|
||||||
baseDir := Context.getDataDir()
|
baseDir := c.getDataDir()
|
||||||
|
|
||||||
statsConf := stats.Config{
|
statsConf := stats.Config{
|
||||||
Filename: filepath.Join(baseDir, "stats.db"),
|
Filename: filepath.Join(baseDir, "stats.db"),
|
||||||
@@ -34,7 +34,7 @@ func initDNSServer() error {
|
|||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
HTTPRegister: httpRegister,
|
HTTPRegister: httpRegister,
|
||||||
}
|
}
|
||||||
Context.stats, err = stats.New(statsConf)
|
c.stats, err = stats.New(statsConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("couldn't initialize statistics module")
|
return fmt.Errorf("couldn't initialize statistics module")
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ func initDNSServer() error {
|
|||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
HTTPRegister: httpRegister,
|
HTTPRegister: httpRegister,
|
||||||
}
|
}
|
||||||
Context.queryLog = querylog.New(conf)
|
c.queryLog = querylog.New(conf)
|
||||||
|
|
||||||
filterConf := config.DNS.DnsfilterConf
|
filterConf := config.DNS.DnsfilterConf
|
||||||
bindhost := config.DNS.BindHost
|
bindhost := config.DNS.BindHost
|
||||||
@@ -56,93 +56,39 @@ func initDNSServer() error {
|
|||||||
bindhost = "127.0.0.1"
|
bindhost = "127.0.0.1"
|
||||||
}
|
}
|
||||||
filterConf.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
|
filterConf.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
|
||||||
filterConf.AutoHosts = &Context.autoHosts
|
filterConf.AutoHosts = &c.autoHosts
|
||||||
filterConf.ConfigModified = onConfigModified
|
filterConf.ConfigModified = onConfigModified
|
||||||
filterConf.HTTPRegister = httpRegister
|
filterConf.HTTPRegister = httpRegister
|
||||||
Context.dnsFilter = dnsfilter.New(&filterConf, nil)
|
c.dnsFilter = dnsfilter.New(&filterConf, nil)
|
||||||
|
|
||||||
p := dnsforward.DNSCreateParams{
|
p := dnsforward.DNSCreateParams{
|
||||||
DNSFilter: Context.dnsFilter,
|
DNSFilter: c.dnsFilter,
|
||||||
Stats: Context.stats,
|
Stats: c.stats,
|
||||||
QueryLog: Context.queryLog,
|
QueryLog: c.queryLog,
|
||||||
}
|
}
|
||||||
if Context.dhcpServer != nil {
|
if c.dhcpServer != nil {
|
||||||
p.DHCPServer = Context.dhcpServer
|
p.DHCPServer = c.dhcpServer
|
||||||
}
|
}
|
||||||
Context.dnsServer = dnsforward.NewServer(p)
|
c.dnsServer = dnsforward.NewServer(p)
|
||||||
dnsConfig := generateServerConfig()
|
dnsConfig := c.generateServerConfig()
|
||||||
err = Context.dnsServer.Prepare(&dnsConfig)
|
err = c.dnsServer.Prepare(&dnsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeDNSServer()
|
c.closeDNSServer()
|
||||||
return fmt.Errorf("dnsServer.Prepare: %s", err)
|
return fmt.Errorf("dnsServer.Prepare: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.rdns = InitRDNS(Context.dnsServer, &Context.clients)
|
c.rdns = InitRDNS(c.dnsServer, &c.clients)
|
||||||
Context.whois = initWhois(&Context.clients)
|
c.whois = initWhois(&c.clients)
|
||||||
|
|
||||||
Context.filters.Init()
|
c.filters.Init()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isRunning() bool {
|
func (c *homeContext) isRunning() bool {
|
||||||
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
|
return c.dnsServer != nil && c.dnsServer.IsRunning()
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint (gocyclo)
|
func (c *homeContext) onDNSRequest(d *proxy.DNSContext) {
|
||||||
// Return TRUE if IP is within public Internet IP range
|
|
||||||
func isPublicIP(ip net.IP) bool {
|
|
||||||
ip4 := ip.To4()
|
|
||||||
if ip4 != nil {
|
|
||||||
switch ip4[0] {
|
|
||||||
case 0:
|
|
||||||
return false //software
|
|
||||||
case 10:
|
|
||||||
return false //private network
|
|
||||||
case 127:
|
|
||||||
return false //loopback
|
|
||||||
case 169:
|
|
||||||
if ip4[1] == 254 {
|
|
||||||
return false //link-local
|
|
||||||
}
|
|
||||||
case 172:
|
|
||||||
if ip4[1] >= 16 && ip4[1] <= 31 {
|
|
||||||
return false //private network
|
|
||||||
}
|
|
||||||
case 192:
|
|
||||||
if (ip4[1] == 0 && ip4[2] == 0) || //private network
|
|
||||||
(ip4[1] == 0 && ip4[2] == 2) || //documentation
|
|
||||||
(ip4[1] == 88 && ip4[2] == 99) || //reserved
|
|
||||||
(ip4[1] == 168) { //private network
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case 198:
|
|
||||||
if (ip4[1] == 18 || ip4[2] == 19) || //private network
|
|
||||||
(ip4[1] == 51 || ip4[2] == 100) { //documentation
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case 203:
|
|
||||||
if ip4[1] == 0 && ip4[2] == 113 { //documentation
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case 224:
|
|
||||||
if ip4[1] == 0 && ip4[2] == 0 { //multicast
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case 255:
|
|
||||||
if ip4[1] == 255 && ip4[2] == 255 && ip4[3] == 255 { //subnet
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func onDNSRequest(d *proxy.DNSContext) {
|
|
||||||
ip := dnsforward.GetIPString(d.Addr)
|
ip := dnsforward.GetIPString(d.Addr)
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
// This would be quite weird if we get here
|
// This would be quite weird if we get here
|
||||||
@@ -151,25 +97,25 @@ func onDNSRequest(d *proxy.DNSContext) {
|
|||||||
|
|
||||||
ipAddr := net.ParseIP(ip)
|
ipAddr := net.ParseIP(ip)
|
||||||
if !ipAddr.IsLoopback() {
|
if !ipAddr.IsLoopback() {
|
||||||
Context.rdns.Begin(ip)
|
c.rdns.Begin(ip)
|
||||||
}
|
}
|
||||||
if isPublicIP(ipAddr) {
|
if util.IsPublicIP(ipAddr) {
|
||||||
Context.whois.Begin(ip)
|
c.whois.Begin(ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateServerConfig() dnsforward.ServerConfig {
|
func (c *homeContext) generateServerConfig() dnsforward.ServerConfig {
|
||||||
newconfig := dnsforward.ServerConfig{
|
newconfig := dnsforward.ServerConfig{
|
||||||
UDPListenAddr: &net.UDPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
|
UDPListenAddr: &net.UDPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
|
||||||
TCPListenAddr: &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
|
TCPListenAddr: &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
|
||||||
FilteringConfig: config.DNS.FilteringConfig,
|
FilteringConfig: config.DNS.FilteringConfig,
|
||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
HTTPRegister: httpRegister,
|
HTTPRegister: httpRegister,
|
||||||
OnDNSRequest: onDNSRequest,
|
OnDNSRequest: c.onDNSRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConf := tlsConfigSettings{}
|
tlsConf := tlsConfigSettings{}
|
||||||
Context.tls.WriteDiskConfig(&tlsConf)
|
c.tls.WriteDiskConfig(&tlsConf)
|
||||||
if tlsConf.Enabled {
|
if tlsConf.Enabled {
|
||||||
newconfig.TLSConfig = tlsConf.TLSConfig
|
newconfig.TLSConfig = tlsConf.TLSConfig
|
||||||
if tlsConf.PortDNSOverTLS != 0 {
|
if tlsConf.PortDNSOverTLS != 0 {
|
||||||
@@ -179,17 +125,17 @@ func generateServerConfig() dnsforward.ServerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newconfig.TLSv12Roots = Context.tlsRoots
|
newconfig.TLSv12Roots = c.tlsRoots
|
||||||
newconfig.TLSCiphers = Context.tlsCiphers
|
newconfig.TLSCiphers = c.tlsCiphers
|
||||||
newconfig.TLSAllowUnencryptedDOH = tlsConf.AllowUnencryptedDOH
|
newconfig.TLSAllowUnencryptedDOH = tlsConf.AllowUnencryptedDOH
|
||||||
|
|
||||||
newconfig.FilterHandler = applyAdditionalFiltering
|
newconfig.FilterHandler = c.applyAdditionalFiltering
|
||||||
newconfig.GetCustomUpstreamByClient = Context.clients.FindUpstreams
|
newconfig.GetCustomUpstreamByClient = c.clients.FindUpstreams
|
||||||
return newconfig
|
return newconfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the list of DNS addresses the server is listening on
|
// Get the list of DNS addresses the server is listening on
|
||||||
func getDNSAddresses() []string {
|
func (c *homeContext) getDNSAddresses() []string {
|
||||||
dnsAddresses := []string{}
|
dnsAddresses := []string{}
|
||||||
|
|
||||||
if config.DNS.BindHost == "0.0.0.0" {
|
if config.DNS.BindHost == "0.0.0.0" {
|
||||||
@@ -209,7 +155,7 @@ func getDNSAddresses() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tlsConf := tlsConfigSettings{}
|
tlsConf := tlsConfigSettings{}
|
||||||
Context.tls.WriteDiskConfig(&tlsConf)
|
c.tls.WriteDiskConfig(&tlsConf)
|
||||||
if tlsConf.Enabled && len(tlsConf.ServerName) != 0 {
|
if tlsConf.Enabled && len(tlsConf.ServerName) != 0 {
|
||||||
|
|
||||||
if tlsConf.PortHTTPS != 0 {
|
if tlsConf.PortHTTPS != 0 {
|
||||||
@@ -231,75 +177,75 @@ func getDNSAddresses() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If a client has his own settings, apply them
|
// If a client has his own settings, apply them
|
||||||
func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
func (c *homeContext) applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
||||||
Context.dnsFilter.ApplyBlockedServices(setts, nil, true)
|
c.dnsFilter.ApplyBlockedServices(setts, nil, true)
|
||||||
|
|
||||||
if len(clientAddr) == 0 {
|
if len(clientAddr) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setts.ClientIP = clientAddr
|
setts.ClientIP = clientAddr
|
||||||
|
|
||||||
c, ok := Context.clients.Find(clientAddr)
|
cl, ok := c.clients.Find(clientAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Using settings for client %s with IP %s", c.Name, clientAddr)
|
log.Debug("Using settings for client %s with IP %s", cl.Name, clientAddr)
|
||||||
|
|
||||||
if c.UseOwnBlockedServices {
|
if cl.UseOwnBlockedServices {
|
||||||
Context.dnsFilter.ApplyBlockedServices(setts, c.BlockedServices, false)
|
c.dnsFilter.ApplyBlockedServices(setts, cl.BlockedServices, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
setts.ClientName = c.Name
|
setts.ClientName = cl.Name
|
||||||
setts.ClientTags = c.Tags
|
setts.ClientTags = cl.Tags
|
||||||
|
|
||||||
if !c.UseOwnSettings {
|
if !cl.UseOwnSettings {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setts.FilteringEnabled = c.FilteringEnabled
|
setts.FilteringEnabled = cl.FilteringEnabled
|
||||||
setts.SafeSearchEnabled = c.SafeSearchEnabled
|
setts.SafeSearchEnabled = cl.SafeSearchEnabled
|
||||||
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
|
setts.SafeBrowsingEnabled = cl.SafeBrowsingEnabled
|
||||||
setts.ParentalEnabled = c.ParentalEnabled
|
setts.ParentalEnabled = cl.ParentalEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDNSServer() error {
|
func (c *homeContext) startDNSServer() error {
|
||||||
if isRunning() {
|
if c.isRunning() {
|
||||||
return fmt.Errorf("unable to start forwarding DNS server: Already running")
|
return fmt.Errorf("unable to start forwarding DNS server: Already running")
|
||||||
}
|
}
|
||||||
|
|
||||||
enableFilters(false)
|
enableFilters(false)
|
||||||
|
|
||||||
Context.clients.Start()
|
c.clients.Start()
|
||||||
|
|
||||||
err := Context.dnsServer.Start()
|
err := c.dnsServer.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.dnsFilter.Start()
|
c.dnsFilter.Start()
|
||||||
Context.filters.Start()
|
c.filters.Start()
|
||||||
Context.stats.Start()
|
c.stats.Start()
|
||||||
Context.queryLog.Start()
|
c.queryLog.Start()
|
||||||
|
|
||||||
const topClientsNumber = 100 // the number of clients to get
|
const topClientsNumber = 100 // the number of clients to get
|
||||||
topClients := Context.stats.GetTopClientsIP(topClientsNumber)
|
topClients := c.stats.GetTopClientsIP(topClientsNumber)
|
||||||
for _, ip := range topClients {
|
for _, ip := range topClients {
|
||||||
ipAddr := net.ParseIP(ip)
|
ipAddr := net.ParseIP(ip)
|
||||||
if !ipAddr.IsLoopback() {
|
if !ipAddr.IsLoopback() {
|
||||||
Context.rdns.Begin(ip)
|
c.rdns.Begin(ip)
|
||||||
}
|
}
|
||||||
if isPublicIP(ipAddr) {
|
if util.IsPublicIP(ipAddr) {
|
||||||
Context.whois.Begin(ip)
|
c.whois.Begin(ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func reconfigureDNSServer() error {
|
func (c *homeContext) reconfigureDNSServer() error {
|
||||||
newconfig := generateServerConfig()
|
newconfig := c.generateServerConfig()
|
||||||
err := Context.dnsServer.Reconfigure(&newconfig)
|
err := c.dnsServer.Reconfigure(&newconfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
||||||
}
|
}
|
||||||
@@ -307,43 +253,42 @@ func reconfigureDNSServer() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopDNSServer() error {
|
func (c *homeContext) stopDNSServer() error {
|
||||||
if !isRunning() {
|
if !c.isRunning() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := Context.dnsServer.Stop()
|
err := c.dnsServer.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "Couldn't stop forwarding DNS server")
|
return errorx.Decorate(err, "Couldn't stop forwarding DNS server")
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDNSServer()
|
c.closeDNSServer()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeDNSServer() {
|
func (c *homeContext) closeDNSServer() {
|
||||||
// DNS forward module must be closed BEFORE stats or queryLog because it depends on them
|
// DNS forward module must be closed BEFORE stats or queryLog because it depends on them
|
||||||
if Context.dnsServer != nil {
|
if c.dnsServer != nil {
|
||||||
Context.dnsServer.Close()
|
c.dnsServer.Close()
|
||||||
Context.dnsServer = nil
|
c.dnsServer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if Context.dnsFilter != nil {
|
if c.dnsFilter != nil {
|
||||||
Context.dnsFilter.Close()
|
c.dnsFilter.Close()
|
||||||
Context.dnsFilter = nil
|
c.dnsFilter = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if Context.stats != nil {
|
if c.stats != nil {
|
||||||
Context.stats.Close()
|
c.stats.Close()
|
||||||
Context.stats = nil
|
c.stats = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if Context.queryLog != nil {
|
if c.queryLog != nil {
|
||||||
Context.queryLog.Close()
|
c.queryLog.Close()
|
||||||
Context.queryLog = nil
|
c.queryLog = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.filters.Close()
|
c.filters.Close()
|
||||||
|
|
||||||
log.Debug("Closed all DNS modules")
|
log.Debug("Closed all DNS modules")
|
||||||
}
|
}
|
||||||
|
|||||||
104
home/home.go
104
home/home.go
@@ -3,7 +3,6 @@ package home
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -29,9 +27,6 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/querylog"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/stats"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,52 +43,6 @@ var (
|
|||||||
ARMVersion = ""
|
ARMVersion = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global context
|
|
||||||
type homeContext struct {
|
|
||||||
// Modules
|
|
||||||
// --
|
|
||||||
|
|
||||||
clients clientsContainer // per-client-settings module
|
|
||||||
stats stats.Stats // statistics module
|
|
||||||
queryLog querylog.QueryLog // query log module
|
|
||||||
dnsServer *dnsforward.Server // DNS module
|
|
||||||
rdns *RDNS // rDNS module
|
|
||||||
whois *Whois // WHOIS module
|
|
||||||
dnsFilter *dnsfilter.Dnsfilter // DNS filtering module
|
|
||||||
dhcpServer *dhcpd.Server // DHCP module
|
|
||||||
auth *Auth // HTTP authentication module
|
|
||||||
filters Filtering // DNS filtering module
|
|
||||||
web *Web // Web (HTTP, HTTPS) module
|
|
||||||
tls *TLSMod // TLS module
|
|
||||||
autoHosts util.AutoHosts // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files
|
|
||||||
updater *update.Updater
|
|
||||||
|
|
||||||
// Runtime properties
|
|
||||||
// --
|
|
||||||
|
|
||||||
configFilename string // Config filename (can be overridden via the command line arguments)
|
|
||||||
workDir string // Location of our directory, used to protect against CWD being somewhere else
|
|
||||||
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
|
|
||||||
pidFileName string // PID file name. Empty if no PID file was created.
|
|
||||||
disableUpdate bool // If set, don't check for updates
|
|
||||||
controlLock sync.Mutex
|
|
||||||
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
|
|
||||||
tlsCiphers []uint16 // list of TLS ciphers to use
|
|
||||||
transport *http.Transport
|
|
||||||
client *http.Client
|
|
||||||
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
|
|
||||||
// runningAsService flag is set to true when options are passed from the service runner
|
|
||||||
runningAsService bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDataDir returns path to the directory where we store databases and filters
|
|
||||||
func (c *homeContext) getDataDir() string {
|
|
||||||
return filepath.Join(c.workDir, dataDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context - a global context object
|
|
||||||
var Context homeContext
|
|
||||||
|
|
||||||
// Main is the entry point
|
// Main is the entry point
|
||||||
func Main(version string, channel string, armVer string) {
|
func Main(version string, channel string, armVer string) {
|
||||||
// Init update-related global variables
|
// Init update-related global variables
|
||||||
@@ -118,8 +67,8 @@ func Main(version string, channel string, armVer string) {
|
|||||||
Context.tls.Reload()
|
Context.tls.Reload()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
cleanup()
|
Context.cleanup()
|
||||||
cleanupAlways()
|
Context.cleanupAlways()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +253,7 @@ func run(args options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
err := initDNSServer()
|
err := Context.initDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("%s", err)
|
log.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
@@ -312,7 +261,7 @@ func run(args options) {
|
|||||||
Context.autoHosts.Start()
|
Context.autoHosts.Start()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := startDNSServer()
|
err := Context.startDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -331,16 +280,16 @@ func run(args options) {
|
|||||||
|
|
||||||
// StartMods - initialize and start DNS after installation
|
// StartMods - initialize and start DNS after installation
|
||||||
func StartMods() error {
|
func StartMods() error {
|
||||||
err := initDNSServer()
|
err := Context.initDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.tls.Start()
|
Context.tls.Start()
|
||||||
|
|
||||||
err = startDNSServer()
|
err = Context.startDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeDNSServer()
|
Context.closeDNSServer()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -489,43 +438,6 @@ func configureLogger(args options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanup() {
|
|
||||||
log.Info("Stopping AdGuard Home")
|
|
||||||
|
|
||||||
if Context.web != nil {
|
|
||||||
Context.web.Close()
|
|
||||||
Context.web = nil
|
|
||||||
}
|
|
||||||
if Context.auth != nil {
|
|
||||||
Context.auth.Close()
|
|
||||||
Context.auth = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := stopDNSServer()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Couldn't stop DNS server: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if Context.dhcpServer != nil {
|
|
||||||
Context.dhcpServer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
Context.autoHosts.Close()
|
|
||||||
|
|
||||||
if Context.tls != nil {
|
|
||||||
Context.tls.Close()
|
|
||||||
Context.tls = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function is called before application exits
|
|
||||||
func cleanupAlways() {
|
|
||||||
if len(Context.pidFileName) != 0 {
|
|
||||||
_ = os.Remove(Context.pidFileName)
|
|
||||||
}
|
|
||||||
log.Info("Stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
// command-line arguments
|
// command-line arguments
|
||||||
type options struct {
|
type options struct {
|
||||||
verbose bool // is verbose logging enabled
|
verbose bool // is verbose logging enabled
|
||||||
@@ -734,7 +646,7 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err
|
|||||||
return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
|
return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHTTPProxy(req *http.Request) (*url.URL, error) {
|
func getHTTPProxy(*http.Request) (*url.URL, error) {
|
||||||
if len(config.ProxyURL) == 0 {
|
if len(config.ProxyURL) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ func TestHome(t *testing.T) {
|
|||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup()
|
ctx := &Context
|
||||||
cleanupAlways()
|
ctx.cleanup()
|
||||||
|
ctx.cleanupAlways()
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user