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: Get current user info
|
||||
* Safe services
|
||||
* ipset
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
* 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
|
||||
|
||||
@@ -1882,3 +1888,25 @@ Check if host name is blocked by SB/PC service:
|
||||
sha256(host.com)[0..1] -> hashes[0],hashes[1],...
|
||||
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)
|
||||
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
|
||||
all: build
|
||||
|
||||
@@ -158,7 +164,7 @@ test:
|
||||
@echo Running JS unit-tests
|
||||
npm run test --prefix client
|
||||
@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
|
||||
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",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -10784,6 +10773,24 @@
|
||||
"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": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
@@ -12169,9 +12176,9 @@
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
||||
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
@@ -12187,6 +12194,18 @@
|
||||
"duplexify": "^3.6.0",
|
||||
"inherits": "^2.0.3",
|
||||
"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": {
|
||||
@@ -12358,9 +12377,9 @@
|
||||
}
|
||||
},
|
||||
"react-i18next": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz",
|
||||
"integrity": "sha512-lyOZSSQkif4H9HnHN3iEKVkryLI+WkdZSEw3VAZzinZLopfYRMHVY5YxCopdkXPLEHs6S5GjKYPh3+j0j336Fg==",
|
||||
"version": "11.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.2.tgz",
|
||||
"integrity": "sha512-Djj3K3hh5Tecla2CI9rLO3TZBYGMFrGilm0JY4cLofAQONCi5TK6nVmUPKoB59n1ZffgjfgJt6zlbE9aGF6Q0Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"html-parse-stringify2": "2.0.1"
|
||||
@@ -13281,10 +13300,13 @@
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz",
|
||||
"integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==",
|
||||
"dev": true
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
|
||||
"integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"serve-index": {
|
||||
"version": "1.9.1",
|
||||
@@ -14619,16 +14641,16 @@
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
|
||||
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
|
||||
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cacache": "^12.0.2",
|
||||
"find-cache-dir": "^2.1.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"schema-utils": "^1.0.0",
|
||||
"serialize-javascript": "^2.1.2",
|
||||
"serialize-javascript": "^4.0.0",
|
||||
"source-map": "^0.6.1",
|
||||
"terser": "^4.1.2",
|
||||
"webpack-sources": "^1.4.0",
|
||||
@@ -14688,15 +14710,6 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
@@ -14707,24 +14720,6 @@
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@@ -14764,22 +14759,15 @@
|
||||
"find-up": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"serialize-javascript": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -14794,12 +14782,6 @@
|
||||
"requires": {
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@nivo/line": "^0.49.1",
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
@@ -29,7 +28,7 @@
|
||||
"react": "^16.13.1",
|
||||
"react-click-outside": "^3.0.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-i18next": "^11.4.0",
|
||||
"react-i18next": "^11.7.2",
|
||||
"react-modal": "^3.11.2",
|
||||
"react-popper-tooltip": "^2.11.1",
|
||||
"react-redux": "^7.2.0",
|
||||
|
||||
@@ -139,8 +139,8 @@
|
||||
"page_table_footer_text": "Страница",
|
||||
"rows_table_footer_text": "редове",
|
||||
"updated_custom_filtering_toast": "Обновени местни правила за филтриране",
|
||||
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране",
|
||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране",
|
||||
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
|
||||
"plain_dns": "Обикновен DNS",
|
||||
"source_label": "Източник",
|
||||
"found_in_known_domain_db": "Намерен в списъците с домейни.",
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Stránka",
|
||||
"rows_table_footer_text": "řádky",
|
||||
"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_added_to_custom_filtering_toast": "Pravidlo přidáno do 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrováno pomocí {{filter}}",
|
||||
"query_log_confirm_clear": "Opravdu chcete vymazat celý protokol dotazů?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Původní odezva",
|
||||
"click_to_view_queries": "Klikněte pro zobrazení dotazů",
|
||||
"port_53_faq_link": "Port 53 je často obsazen službami \"DNSStubListener\" nebo \"systemd-resolved\". Přečtěte si <0>tento návod</0> o tom, jak to vyřešit."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Side",
|
||||
"rows_table_footer_text": "rækker",
|
||||
"updated_custom_filtering_toast": "De brugerdefinerede filtreringsregler er blevet opdateret",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel fjernet fra de brugerdefinerede filtreringsregler",
|
||||
"rule_added_to_custom_filtering_toast": "Regel tilføjet til 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtreret af {{filter}}",
|
||||
"query_log_confirm_clear": "Er du sikker på, at du vil rydde hele forespørgselsloggen?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Oprindeligt svar",
|
||||
"click_to_view_queries": "Klik for at se forespørgsler",
|
||||
"port_53_faq_link": "Port 53 optages ofte af \"DNSStubListener\" eller \"systemd-resolved\" tjenester. Læs <0>denne instruktion</0> om, hvordan du løser dette."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_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_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_deleted": "Statischer Lease „{{key}}” erfolgreich entfernt",
|
||||
"dhcp_new_static_lease": "Neuer statischer Lease",
|
||||
@@ -99,7 +99,7 @@
|
||||
"no_clients_found": "Keine Clients gefunden",
|
||||
"general_statistics": "Allgemeine Statistiken",
|
||||
"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_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",
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Seite",
|
||||
"rows_table_footer_text": "Reihen",
|
||||
"updated_custom_filtering_toast": "Die benutzerdefinierten Filterregeln wurden aktualisiert",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt",
|
||||
"rule_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt",
|
||||
"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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Gefiltert nach {{filter}}",
|
||||
"query_log_confirm_clear": "Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Ursprüngliche Antwort",
|
||||
"click_to_view_queries": "Anklicken, um Abfragen anzuzeigen",
|
||||
"port_53_faq_link": "Port 53 wird oft von Diensten wie „DNSStubListener” oder „systemresolved” belegt. Bitte lesen Sie <0>diese Anweisung</0>, wie dies behoben werden kann."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +194,10 @@
|
||||
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
||||
"unblock": "Unblock",
|
||||
"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",
|
||||
"date": "Date",
|
||||
"domain_name_table_header": "Domain name",
|
||||
@@ -213,8 +217,8 @@
|
||||
"page_table_footer_text": "Page",
|
||||
"rows_table_footer_text": "rows",
|
||||
"updated_custom_filtering_toast": "Updated the custom filtering rules",
|
||||
"rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules",
|
||||
"rule_added_to_custom_filtering_toast": "Rule added to 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtered by {{filter}}",
|
||||
"query_log_confirm_clear": "Are you sure you want to clear the entire query log?",
|
||||
@@ -359,7 +363,7 @@
|
||||
"fix": "Fix",
|
||||
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
||||
"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",
|
||||
"clients_title": "Clients",
|
||||
"clients_desc": "Configure devices connected to AdGuard Home",
|
||||
@@ -569,5 +573,6 @@
|
||||
"setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server",
|
||||
"original_response": "Original response",
|
||||
"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",
|
||||
"rows_table_footer_text": "filas",
|
||||
"updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado",
|
||||
"rule_added_to_custom_filtering_toast": "Regla añadida a 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}}",
|
||||
"query_log_response_status": "Estado: {{value}}",
|
||||
"query_log_filtered": "Filtrado por {{filter}}",
|
||||
"query_log_confirm_clear": "¿Está seguro de que desea borrar todo el registro de consultas?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Respuesta original",
|
||||
"click_to_view_queries": "Clic para ver las consultas",
|
||||
"port_53_faq_link": "El puerto 53 suele estar ocupado por los servicios \"DNSStubListener\" o \"systemd-resolved\". Por favor lee <0>esta instrucción</0> sobre cómo resolver esto."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,8 +202,8 @@
|
||||
"page_table_footer_text": "صفحه",
|
||||
"rows_table_footer_text": "سطر",
|
||||
"updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است",
|
||||
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد",
|
||||
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد",
|
||||
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد {{rule}}",
|
||||
"query_log_response_status": "وضعیت: {{value}}",
|
||||
"query_log_filtered": "فیلتر شده با {{filter}}",
|
||||
"query_log_confirm_clear": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟",
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Page",
|
||||
"rows_table_footer_text": "lignes",
|
||||
"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_added_to_custom_filtering_toast": "Règle ajoutée aux 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}}",
|
||||
"query_log_response_status": "Statut : {{value}}",
|
||||
"query_log_filtered": "Filtré par {{filter}}",
|
||||
"query_log_confirm_clear": "Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?",
|
||||
@@ -561,4 +561,4 @@
|
||||
"filter_category_other_desc": "Autres listes noires",
|
||||
"click_to_view_queries": "Cliquez pour voir les requêtes",
|
||||
"port_53_faq_link": "Le port 53 est souvent occupé par les services « DNSStubListener » ou « systemd-resolved ». Veuillez lire <0>cette instruction</0> pour savoir comment résoudre ce problème."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Stranica",
|
||||
"rows_table_footer_text": "redova",
|
||||
"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_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrirao {{filter}}",
|
||||
"query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Originalni odgovor",
|
||||
"click_to_view_queries": "Kliknite za pregled upita",
|
||||
"port_53_faq_link": "Port 53 često zauzimaju usluge \"DNSStubListener\" ili \"systemd-resolved\". Molimo pročitajte <0>ove upute</0> o tome kako to riješiti."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Halaman",
|
||||
"rows_table_footer_text": "baris",
|
||||
"updated_custom_filtering_toast": "Perbarui aturan penyaringan khusus",
|
||||
"rule_removed_from_custom_filtering_toast": "Aturan dihapus dari aturan penyaringan khusus",
|
||||
"rule_added_to_custom_filtering_toast": "Aturan ditambah ke 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Difilter oleh {{filter}}",
|
||||
"query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Respon asli",
|
||||
"click_to_view_queries": "Klik untuk lihat permintaan",
|
||||
"port_53_faq_link": "Port 53 sering ditempati oleh layanan \"DNSStubListener\" atau \"systemd-resolved\". Silakan baca <0>instruksi ini</0> tentang cara menyelesaikan ini."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Pagina",
|
||||
"rows_table_footer_text": "righe",
|
||||
"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_added_to_custom_filtering_toast": "Regola aggiunta alle 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrato da {{filter}}",
|
||||
"query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?",
|
||||
@@ -560,4 +560,4 @@
|
||||
"filter_category_regional_desc": "Liste focalizzare su pubblicità regionali e server traccianti",
|
||||
"filter_category_other_desc": "Altre liste di blocco",
|
||||
"click_to_view_queries": "Clicca per visualizzare query"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "ページ",
|
||||
"rows_table_footer_text": "行",
|
||||
"updated_custom_filtering_toast": "カスタム・フィルタリングルールを更新しました",
|
||||
"rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました",
|
||||
"rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました",
|
||||
"rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました {{rule}}",
|
||||
"query_log_response_status": "ステータス: {{value}}",
|
||||
"query_log_filtered": "{{filter}}によるフィルタ",
|
||||
"query_log_confirm_clear": "クエリ・ログ全体を消去してもよろしいですか?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "当初の応答",
|
||||
"click_to_view_queries": "クエリを表示するにはクリックしてください",
|
||||
"port_53_faq_link": "多くの場合、ポート53は \"DNSStubListener\" または \"systemd-resolved\" サービスによって利用されています。これを解決する方法については、<0>この手順</0>をお読みください。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "페이지",
|
||||
"rows_table_footer_text": "행",
|
||||
"updated_custom_filtering_toast": "사용자 정의 필터링 규칙 업데이트",
|
||||
"rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거",
|
||||
"rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙",
|
||||
"rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거 {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙 {{rule}}",
|
||||
"query_log_response_status": "상태: {{value}}",
|
||||
"query_log_filtered": "필터: {{filter}}",
|
||||
"query_log_confirm_clear": "정말로 모든 쿼리 로그를 비우시겠습니까?",
|
||||
@@ -563,4 +563,4 @@
|
||||
"filter_category_other_desc": "기타 차단 목록",
|
||||
"original_response": "원래 응답",
|
||||
"click_to_view_queries": "쿼리를 보려면 클릭합니다"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Pagina",
|
||||
"rows_table_footer_text": "rijen",
|
||||
"updated_custom_filtering_toast": "Aangepaste filter regels zijn bijgewerkt",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel verwijderd uit de aangepaste filterregels",
|
||||
"rule_added_to_custom_filtering_toast": "Regel toegevoegd aan 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Gefilterd door {{filter}}",
|
||||
"query_log_confirm_clear": "Weet u zeker dat u het hele query logboek wilt legen?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Oorspronkelijke reactie",
|
||||
"click_to_view_queries": "Klik om queries te bekijken",
|
||||
"port_53_faq_link": "Poort 53 wordt vaak gebruikt door services als DNSStubListener- of de systeem DNS-resolver. Lees a.u.b. <0>deze instructie</0> hoe dit is op te lossen."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,8 +207,8 @@
|
||||
"page_table_footer_text": "Side",
|
||||
"rows_table_footer_text": "rekker",
|
||||
"updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene",
|
||||
"rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene",
|
||||
"rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrert av {{filter}}",
|
||||
"query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?",
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Strona",
|
||||
"rows_table_footer_text": "wierszy",
|
||||
"updated_custom_filtering_toast": "Zaktualizowano niestandardowe reguły filtrowania",
|
||||
"rule_removed_from_custom_filtering_toast": "Reguła usunięta z niestandardowych reguł filtrowania",
|
||||
"rule_added_to_custom_filtering_toast": "Reguła dodana do 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrowane przez {{filter}}",
|
||||
"query_log_confirm_clear": "Czy na pewno chcesz wyczyścić cały dziennik zapytań?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Oryginalna odpowiedź",
|
||||
"click_to_view_queries": "Kliknij, aby wyświetlić zapytania",
|
||||
"port_53_faq_link": "Port 53 jest często zajęty przez usługi \"DNSStubListener\" lub \"systemd-resolved\". Przeczytaj <0>tę instrukcję</0> jak to rozwiązać."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Página",
|
||||
"rows_table_footer_text": "linhas",
|
||||
"updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas",
|
||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrado por {{filter}}",
|
||||
"query_log_confirm_clear": "Você tem certeza que deseja limpar o registro de consulta?",
|
||||
@@ -560,4 +560,4 @@
|
||||
"filter_category_regional_desc": "Listas focadas em anúncios regionais e servidores de rastreamento",
|
||||
"filter_category_other_desc": "Outras listas negras",
|
||||
"click_to_view_queries": "Clique para ver as consultas"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +168,8 @@
|
||||
"page_table_footer_text": "Página",
|
||||
"rows_table_footer_text": "linhas",
|
||||
"updated_custom_filtering_toast": "Regras de filtragem personalizadas actualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas",
|
||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrado por {{filter}}",
|
||||
"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",
|
||||
"rows_table_footer_text": "linii",
|
||||
"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_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate",
|
||||
"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}}",
|
||||
"query_log_response_status": "Statut: {{value}}",
|
||||
"query_log_filtered": "Filtrat de {{filter}}",
|
||||
"query_log_confirm_clear": "Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Răspuns original",
|
||||
"click_to_view_queries": "Clicați pentru a vizualiza interogări",
|
||||
"port_53_faq_link": "Portul 53 este adesea ocupat de serviciile \"DNSStubListener\" sau \"systemd-resolved\". Vă rugăm să citiți <0>această instrucțiune</0> despre cum să rezolvați aceasta."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Страница",
|
||||
"rows_table_footer_text": "строк",
|
||||
"updated_custom_filtering_toast": "Внесены изменения в пользовательские правила",
|
||||
"rule_removed_from_custom_filtering_toast": "Правило удалено из авторского списка правил фильтрации",
|
||||
"rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено",
|
||||
"rule_removed_from_custom_filtering_toast": "Пользовательское правило удалено: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено: {{rule}}",
|
||||
"query_log_response_status": "Статус: {{value}}",
|
||||
"query_log_filtered": "Отфильтровано с помощью {{filter}}",
|
||||
"query_log_confirm_clear": "Вы уверены, что хотите очистить весь журнал запросов?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Первоначальный ответ",
|
||||
"click_to_view_queries": "Нажмите, чтобы просмотреть запросы",
|
||||
"port_53_faq_link": "Порт 53 часто занят службами \"DNSStubListener\" или \"systemd-resolved\". Ознакомьтесь с <0>инструкцией</0> о том, как это разрешить."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Stránka",
|
||||
"rows_table_footer_text": "riadky",
|
||||
"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_added_to_custom_filtering_toast": "Pravidlo pridané do 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}}",
|
||||
"query_log_response_status": "Stav: {{value}}",
|
||||
"query_log_filtered": "Vyfiltrované pomocou {{filter}}",
|
||||
"query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Pôvodná odozva",
|
||||
"click_to_view_queries": "Kliknite pre zobrazenie dopytov",
|
||||
"port_53_faq_link": "Port 53 je často obsadený službami \"DNSStubListener\" alebo \"systemd-resolved\". Prečítajte si <0>tento návod</0> o tom, ako to vyriešiť."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Stran",
|
||||
"rows_table_footer_text": "vrstic",
|
||||
"updated_custom_filtering_toast": "Posodobljena pravila filtriranja po meri",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil filtriranja po meri",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano pravilom 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}}",
|
||||
"query_log_response_status": "Stanje: {{value}}",
|
||||
"query_log_filtered": "Filtriran z {{filter}}",
|
||||
"query_log_confirm_clear": "Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Izviren odgovor",
|
||||
"click_to_view_queries": "Kliknite za prikaz poizvedb",
|
||||
"port_53_faq_link": "Vrata 53 pogosto zasedajo storitve 'DNSStubListener' ali 'Sistemsko razrešene storitve'. Preberite <0>to navodilo</0> o tem, kako to rešiti."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "Stranica",
|
||||
"rows_table_footer_text": "redovi",
|
||||
"updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena 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}}",
|
||||
"query_log_response_status": "Stanje: {{value}}",
|
||||
"query_log_filtered": "Filtrirano od {{filter}}",
|
||||
"query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "Izvorni odgovor",
|
||||
"click_to_view_queries": "Kliknite da pogledate zahteve",
|
||||
"port_53_faq_link": "Port 53 je najčešće zauzet od \"DNSStubListener\" ili \"systemd-resolved\" usluga. Pročitajte <0>ovo uputstvo</0> kako da to rešite."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
"page_table_footer_text": "Sida",
|
||||
"rows_table_footer_text": "rader",
|
||||
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna",
|
||||
"rule_added_to_custom_filtering_toast": "Regel tillagd till 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}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrerat av {{filter}}",
|
||||
"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": "หน้า",
|
||||
"rows_table_footer_text": "ตาราง",
|
||||
"updated_custom_filtering_toast": "อัปเดตกฎการกรองที่กำหนดเอง",
|
||||
"rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว",
|
||||
"rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว",
|
||||
"rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว {{rule}}",
|
||||
"query_log_response_status": "สถานะ: {{value}}",
|
||||
"query_log_filtered": "กรองโดย {{filter}}",
|
||||
"query_log_confirm_clear": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?",
|
||||
|
||||
@@ -197,8 +197,8 @@
|
||||
"page_table_footer_text": "Sayfa",
|
||||
"rows_table_footer_text": "satır",
|
||||
"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_added_to_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarına eklendi",
|
||||
"rule_removed_from_custom_filtering_toast": "Özel filtreleme kurallarından kural kaldırıldı: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Özel filtreleme kurallarına kural eklendi: {{rule}}",
|
||||
"query_log_response_status": "Durum: {{value}}",
|
||||
"query_log_filtered": "{{filter}} tarafından filtrelendi",
|
||||
"query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?",
|
||||
@@ -507,4 +507,4 @@
|
||||
"allowed": "İzin verildi",
|
||||
"blocklist": "Engellenen listesi",
|
||||
"port_53_faq_link": "Port 53 genellikle \"DNSStubListener\" veya \"sistemd-resolved\" hizmetler tarafından kullanılır. Lütfen problemin nasıl çözüleceğine ilişkin <0>bu talimatı</0> okuyun."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@
|
||||
"page_table_footer_text": "Trang",
|
||||
"rows_table_footer_text": "hàng",
|
||||
"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_added_to_custom_filtering_toast": "Quy tắc đã được thêm vào 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}}",
|
||||
"query_log_response_status": "Trạng thái: {{value}}",
|
||||
"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?",
|
||||
@@ -445,4 +445,4 @@
|
||||
"blocked_threats": "Mối nguy hiểm đã chặn",
|
||||
"allowed": "Được phép",
|
||||
"safe_search": "Tìm kiếm an toàn"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "页",
|
||||
"rows_table_footer_text": "行",
|
||||
"updated_custom_filtering_toast": "自定义过滤规则已更新",
|
||||
"rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除",
|
||||
"rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中",
|
||||
"rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除 {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中 {{rule}}",
|
||||
"query_log_response_status": "状态: {{value}}",
|
||||
"query_log_filtered": "被 {{filter}} 过滤",
|
||||
"query_log_confirm_clear": "你确定想要清除全部查询日志吗?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "原始响应",
|
||||
"click_to_view_queries": "点击查看查询",
|
||||
"port_53_faq_link": "53端口常被DNSStubListener或systemdn解析的服务占用。请阅读<0>这份关于如何解决这一问题的说明</0>"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"page_table_footer_text": "頁面",
|
||||
"rows_table_footer_text": "列",
|
||||
"updated_custom_filtering_toast": "已更新自訂的過濾規則",
|
||||
"rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除",
|
||||
"rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中",
|
||||
"rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除 {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中 {{rule}}",
|
||||
"query_log_response_status": "狀態:{{value}}",
|
||||
"query_log_filtered": "被 {{filter}} 過濾",
|
||||
"query_log_confirm_clear": "您確定您想要清除整個查詢記錄嗎?",
|
||||
@@ -564,4 +564,4 @@
|
||||
"original_response": "原始的回應",
|
||||
"click_to_view_queries": "點擊以檢視查詢",
|
||||
"port_53_faq_link": "連接埠 53 常被 \"DNSStubListener\" 或 \"systemd-resolved\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明</0>。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@ import { createAction } from 'redux-actions';
|
||||
import i18next from 'i18next';
|
||||
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 {
|
||||
CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME,
|
||||
BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, GETTING_STARTED_LINK,
|
||||
} from '../helpers/constants';
|
||||
import { areEqualVersions } from '../helpers/version';
|
||||
import { getTlsStatus } from './encryption';
|
||||
import apiClient from '../api/Api';
|
||||
import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts';
|
||||
import { getFilteringStatus, setRules } from './filtering';
|
||||
|
||||
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
||||
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||
@@ -181,7 +185,14 @@ export const getUpdate = () => async (dispatch, getState) => {
|
||||
|
||||
dispatch(getUpdateRequest());
|
||||
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());
|
||||
};
|
||||
|
||||
@@ -541,3 +552,44 @@ export const removeStaticLease = (config) => async (dispatch) => {
|
||||
};
|
||||
|
||||
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 { addErrorToast } from './toasts';
|
||||
import { HTML_PAGES } from '../helpers/constants';
|
||||
|
||||
export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST');
|
||||
export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE');
|
||||
@@ -11,7 +12,8 @@ export const processLogin = (values) => async (dispatch) => {
|
||||
dispatch(processLoginRequest());
|
||||
try {
|
||||
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);
|
||||
dispatch(processLoginSuccess());
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,9 +3,7 @@ import { createAction } from 'redux-actions';
|
||||
import apiClient from '../api/Api';
|
||||
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
||||
import {
|
||||
DEFAULT_LOGS_FILTER,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT,
|
||||
} from '../helpers/constants';
|
||||
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 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 totalData = total || { logs };
|
||||
|
||||
const needToGetAdditionalLogs = (logs.length < TABLE_DEFAULT_PAGE_SIZE
|
||||
|| totalData.logs.length < TABLE_DEFAULT_PAGE_SIZE)
|
||||
&& oldest !== '';
|
||||
const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
|
||||
const currentQuery = queryForm && queryForm.values.search;
|
||||
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());
|
||||
|
||||
try {
|
||||
@@ -54,7 +59,7 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
||||
filter,
|
||||
});
|
||||
if (additionalLogs.oldest.length > 0) {
|
||||
return await checkFilteredLogs(additionalLogs, filter, dispatch, {
|
||||
return await shortPollQueryLogs(additionalLogs, filter, dispatch, getState, {
|
||||
logs: [...totalData.logs, ...additionalLogs.logs],
|
||||
oldest: additionalLogs.oldest,
|
||||
});
|
||||
@@ -71,31 +76,25 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
||||
return totalData;
|
||||
};
|
||||
|
||||
export const setLogsPagination = createAction('LOGS_PAGINATION');
|
||||
export const setLogsPage = createAction('SET_LOG_PAGE');
|
||||
export const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS');
|
||||
|
||||
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||
|
||||
export const getLogs = (config) => async (dispatch, getState) => {
|
||||
export const getLogs = () => async (dispatch, getState) => {
|
||||
dispatch(getLogsRequest());
|
||||
try {
|
||||
const { isFiltered, filter, page } = getState().queryLogs;
|
||||
const { isFiltered, filter, oldest } = getState().queryLogs;
|
||||
const data = await getLogsWithParams({
|
||||
...config,
|
||||
older_than: oldest,
|
||||
filter,
|
||||
});
|
||||
|
||||
if (isFiltered) {
|
||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
||||
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||
dispatch(getLogsSuccess(updatedData));
|
||||
dispatch(setLogsPagination({
|
||||
page,
|
||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||
}));
|
||||
} else {
|
||||
dispatch(getLogsSuccess(data));
|
||||
}
|
||||
@@ -111,7 +110,7 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
|
||||
*
|
||||
* @param filter
|
||||
* @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
|
||||
*/
|
||||
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 setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
|
||||
|
||||
export const setFilteredLogs = (filter) => async (dispatch) => {
|
||||
export const setFilteredLogs = (filter) => async (dispatch, getState) => {
|
||||
dispatch(setFilteredLogsRequest());
|
||||
try {
|
||||
const data = await getLogsWithParams({
|
||||
older_than: '',
|
||||
filter,
|
||||
});
|
||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
||||
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||
|
||||
dispatch(setFilteredLogsSuccess({
|
||||
...updatedData,
|
||||
filter,
|
||||
}));
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setFilteredLogsFailure(error));
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import axios from 'axios';
|
||||
|
||||
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';
|
||||
|
||||
class Api {
|
||||
baseUrl = BASE_URL;
|
||||
|
||||
async makeRequest(path, method = 'POST', config) {
|
||||
const url = `${this.baseUrl}/${path}`;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `${this.baseUrl}/${path}`,
|
||||
url,
|
||||
method,
|
||||
...config,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorPath = `${this.baseUrl}/${path}`;
|
||||
const errorPath = url;
|
||||
if (error.response) {
|
||||
if (error.response.status === 403) {
|
||||
const loginPageUrl = window.location.href.replace(R_PATH_LAST_PART, '/login.html');
|
||||
const { pathname } = document.location;
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@@ -530,6 +536,8 @@ class Api {
|
||||
|
||||
getQueryLog(params) {
|
||||
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);
|
||||
return this.makeRequest(url, method);
|
||||
}
|
||||
|
||||
@@ -66,3 +66,12 @@ body {
|
||||
.select--no-warning {
|
||||
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 Dashboard from '../../containers/Dashboard';
|
||||
import Logs from '../../containers/Logs';
|
||||
import SetupGuide from '../../containers/SetupGuide';
|
||||
import Settings from '../../containers/Settings';
|
||||
import Dns from '../../containers/Dns';
|
||||
@@ -38,6 +37,7 @@ import DnsAllowlist from '../../containers/DnsAllowlist';
|
||||
import DnsRewrites from '../../containers/DnsRewrites';
|
||||
import CustomRules from '../../containers/CustomRules';
|
||||
import Services from '../Filters/Services';
|
||||
import Logs from '../Logs';
|
||||
|
||||
|
||||
const ROUTES = [
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
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 Cell from '../ui/Cell';
|
||||
|
||||
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
|
||||
import { IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
|
||||
import { formatClientCell } from '../../helpers/formatClientCell';
|
||||
import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
|
||||
import { toggleClientBlock } from '../../actions/access';
|
||||
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
|
||||
|
||||
const getClientsPercentColor = (percent) => {
|
||||
if (percent > 50) {
|
||||
@@ -20,126 +23,132 @@ const getClientsPercentColor = (percent) => {
|
||||
return STATUS_COLORS.red;
|
||||
};
|
||||
|
||||
const countCell = (dnsQueries) => function cell(row) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(dnsQueries, value);
|
||||
const CountCell = (row) => {
|
||||
const { value, original: { ip } } = row;
|
||||
const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual);
|
||||
|
||||
const percent = getPercent(numDnsQueries, value);
|
||||
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 buttonProps = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND
|
||||
? {
|
||||
className: 'btn-outline-danger',
|
||||
text: 'block',
|
||||
type: 'block',
|
||||
const renderBlockingButton = (ip) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const processingSet = useSelector((state) => state.access.processingSet);
|
||||
const disallowed_clients = useSelector(
|
||||
(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 (
|
||||
<div className="table__action button__action">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonProps.className}`}
|
||||
onClick={() => handleClick(buttonProps.type, ip)}
|
||||
disabled={processing}
|
||||
>
|
||||
<Trans>{buttonProps.text}</Trans>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
const onClick = () => toggleClientStatus(type, ip);
|
||||
|
||||
return <div className="table__action pl-4">
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingSet}
|
||||
>
|
||||
<Trans>{text}</Trans>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const clientCell = (t, toggleClientStatus, processing, disallowedClients) => function cell(row) {
|
||||
const { value } = row;
|
||||
const ipMatchListStatus = getIpMatchListStatus(value, disallowedClients);
|
||||
const ClientCell = (row) => {
|
||||
const { value, original: { info } } = row;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="logs__row logs__row--overflow logs__row--column">
|
||||
{formatClientCell(row, true, false)}
|
||||
</div>
|
||||
{ipMatchListStatus !== IP_MATCH_LIST_STATUS.CIDR
|
||||
&& renderBlockingButton(ipMatchListStatus, value, toggleClientStatus, processing)}
|
||||
</>
|
||||
);
|
||||
return <>
|
||||
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
|
||||
{renderFormattedClientCell(value, info, true)}
|
||||
{renderBlockingButton(value)}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
const Clients = ({
|
||||
t,
|
||||
refreshButton,
|
||||
topClients,
|
||||
subtitle,
|
||||
dnsQueries,
|
||||
toggleClientStatus,
|
||||
processingAccessSet,
|
||||
disallowedClients,
|
||||
}) => (
|
||||
<Card
|
||||
title={t('top_clients')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
|
||||
const disallowedClients = useSelector((state) => state.access.disallowed_clients, shallowEqual);
|
||||
|
||||
return <Card
|
||||
title={t('top_clients')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<ReactTable
|
||||
data={topClients.map(({
|
||||
name: ip, count, info, blocked,
|
||||
}) => ({
|
||||
ip,
|
||||
count,
|
||||
info,
|
||||
blocked,
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
sortMethod: sortIp,
|
||||
Cell: clientCell(t, toggleClientStatus, processingAccessSet, disallowedClients),
|
||||
},
|
||||
{
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'count',
|
||||
minWidth: 180,
|
||||
maxWidth: 200,
|
||||
Cell: countCell(dnsQueries),
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_clients_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
className="-highlight card-table-overflow--limited clients__table"
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
data={topClients.map(({
|
||||
name: ip, count, info, blocked,
|
||||
}) => ({
|
||||
ip,
|
||||
count,
|
||||
info,
|
||||
blocked,
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
sortMethod: sortIp,
|
||||
Cell: ClientCell,
|
||||
},
|
||||
{
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'count',
|
||||
minWidth: 180,
|
||||
maxWidth: 200,
|
||||
Cell: CountCell,
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_clients_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
className="-highlight card-table-overflow--limited clients__table"
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { ip } = rowInfo.original;
|
||||
const { ip } = rowInfo.original;
|
||||
|
||||
return getIpMatchListStatus(ip, disallowedClients)
|
||||
=== IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' };
|
||||
}}
|
||||
return getIpMatchListStatus(ip, disallowedClients) === IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' };
|
||||
}}
|
||||
/>
|
||||
</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,
|
||||
</Card>;
|
||||
};
|
||||
|
||||
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',
|
||||
count: numDnsQueries,
|
||||
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',
|
||||
count: numBlockedFiltering,
|
||||
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>],
|
||||
},
|
||||
{
|
||||
label: 'stats_malware_phishing',
|
||||
count: numReplacedSafebrowsing,
|
||||
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',
|
||||
count: numReplacedParental,
|
||||
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',
|
||||
count: numReplacedSafesearch,
|
||||
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',
|
||||
|
||||
@@ -10,7 +10,6 @@ import BlockedDomains from './BlockedDomains';
|
||||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Loading from '../ui/Loading';
|
||||
import { BLOCK_ACTIONS } from '../../helpers/constants';
|
||||
import './Dashboard.css';
|
||||
|
||||
const Dashboard = ({
|
||||
@@ -19,7 +18,6 @@ const Dashboard = ({
|
||||
getStatsConfig,
|
||||
dashboard,
|
||||
toggleProtection,
|
||||
toggleClientBlock,
|
||||
stats,
|
||||
access,
|
||||
}) => {
|
||||
@@ -50,14 +48,6 @@ const Dashboard = ({
|
||||
</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
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm"
|
||||
@@ -122,7 +112,6 @@ const Dashboard = ({
|
||||
clients={dashboard.clients}
|
||||
autoClients={dashboard.autoClients}
|
||||
refreshButton={refreshButton}
|
||||
toggleClientStatus={toggleClientStatus}
|
||||
processingAccessSet={access.processingSet}
|
||||
disallowedClients={access.disallowed_clients}
|
||||
/>
|
||||
@@ -157,7 +146,6 @@ Dashboard.propTypes = {
|
||||
getStatsConfig: PropTypes.func.isRequired,
|
||||
toggleProtection: PropTypes.func.isRequired,
|
||||
getClients: PropTypes.func.isRequired,
|
||||
toggleClientBlock: PropTypes.func.isRequired,
|
||||
getAccessList: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -11,25 +11,10 @@ import {
|
||||
checkWhiteList,
|
||||
checkSafeSearch,
|
||||
checkSafeBrowsing,
|
||||
checkParental,
|
||||
checkParental, getFilterName,
|
||||
} from '../../../helpers/helpers';
|
||||
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) => {
|
||||
if (checkNotFilteredNotFound(reason)) {
|
||||
return t('check_not_found');
|
||||
@@ -101,7 +86,12 @@ const Info = ({
|
||||
ip_addrs,
|
||||
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)
|
||||
|| checkSafeBrowsing(reason)
|
||||
|| checkParental(reason);
|
||||
|
||||
@@ -48,7 +48,7 @@ class Table extends Component {
|
||||
accessor: 'url',
|
||||
minWidth: 200,
|
||||
Cell: ({ value }) => (
|
||||
<div className="logs__row o-hidden">
|
||||
<div className="logs__row">
|
||||
{isValidAbsolutePath(value) ? value
|
||||
: <a
|
||||
href={value}
|
||||
|
||||
@@ -164,6 +164,10 @@
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.nav-icon--white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-brand-img {
|
||||
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 { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import getIconTooltip from './getIconTooltip';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
LONG_TIME_FORMAT,
|
||||
@@ -9,15 +10,20 @@ import {
|
||||
} from '../../../helpers/constants';
|
||||
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||
import IconTooltip from './IconTooltip';
|
||||
|
||||
const getDomainCell = (props) => {
|
||||
const {
|
||||
row, t, isDetailed, dnssec_enabled,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
tracker, type, answer_dnssec, client_proto, domain, time,
|
||||
} = row.original;
|
||||
const DomainCell = ({
|
||||
answer_dnssec,
|
||||
service_name,
|
||||
client_proto,
|
||||
domain,
|
||||
time,
|
||||
tracker,
|
||||
type,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
@@ -44,14 +50,18 @@ const getDomainCell = (props) => {
|
||||
protocol,
|
||||
};
|
||||
|
||||
if (service_name) {
|
||||
requestDetailsObj.check_service = service_name;
|
||||
}
|
||||
|
||||
const sourceData = getSourceData(tracker);
|
||||
|
||||
const knownTrackerDataObj = {
|
||||
name_table_header: tracker?.name,
|
||||
category_label: hasTracker && captitalizeWords(tracker.category),
|
||||
source_label: sourceData
|
||||
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
|
||||
className="link--green">{sourceData.name}</a>,
|
||||
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
|
||||
className="link--green">{sourceData.name}</a>,
|
||||
};
|
||||
|
||||
const renderGrid = (content, idx) => {
|
||||
@@ -72,51 +82,43 @@ const getDomainCell = (props) => {
|
||||
|
||||
const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
|
||||
|
||||
const trackerHint = getIconTooltip({
|
||||
className: privacyIconClass,
|
||||
tooltipClass: 'pt-4 pb-5 px-5 mw-75',
|
||||
xlinkHref: 'privacy',
|
||||
contentItemClass: 'key-colon',
|
||||
renderContent,
|
||||
place: 'bottom',
|
||||
});
|
||||
|
||||
const valueClass = classNames('w-100', {
|
||||
const valueClass = classNames('w-100 text-truncate', {
|
||||
'px-2 d-flex justify-content-center flex-column': isDetailed,
|
||||
});
|
||||
|
||||
const details = [ip, protocol].filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<div className="logs__row o-hidden">
|
||||
{dnssec_enabled && getIconTooltip({
|
||||
className: lockIconClass,
|
||||
tooltipClass: 'py-4 px-5 pb-45',
|
||||
canShowTooltip: answer_dnssec,
|
||||
xlinkHref: 'lock',
|
||||
columnClass: 'w-100',
|
||||
content: 'validated_with_dnssec',
|
||||
placement: 'bottom',
|
||||
})}
|
||||
{trackerHint}
|
||||
<div className={valueClass}>
|
||||
<div className="text-truncate" title={domain}>{domain}</div>
|
||||
{details && isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={details}>{details}</div>}
|
||||
</div>
|
||||
return <div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell">
|
||||
{dnssec_enabled && <IconTooltip
|
||||
className={lockIconClass}
|
||||
tooltipClass='py-4 px-5 pb-45'
|
||||
canShowTooltip={!!answer_dnssec}
|
||||
xlinkHref='lock'
|
||||
columnClass='w-100'
|
||||
content='validated_with_dnssec'
|
||||
placement='bottom'
|
||||
/>}
|
||||
<IconTooltip className={privacyIconClass} tooltipClass='pt-4 pb-5 px-5 mw-75'
|
||||
xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent}
|
||||
place='bottom' />
|
||||
<div className={valueClass}>
|
||||
<div className="text-truncate" title={domain}>{service_name || domain}</div>
|
||||
{details && isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={details}>{details}</div>}
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
};
|
||||
|
||||
getDomainCell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
isDetailed: PropTypes.bool.isRequired,
|
||||
toggleBlocking: PropTypes.func.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
dnssec_enabled: PropTypes.bool.isRequired,
|
||||
DomainCell.propTypes = {
|
||||
answer_dnssec: propTypes.bool.isRequired,
|
||||
client_proto: propTypes.string.isRequired,
|
||||
domain: propTypes.string.isRequired,
|
||||
time: propTypes.string.isRequired,
|
||||
type: propTypes.string.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 'react-popper-tooltip/dist/styles.css';
|
||||
import './IconTooltip.css';
|
||||
import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants';
|
||||
|
||||
const getIconTooltip = ({
|
||||
const IconTooltip = ({
|
||||
className,
|
||||
contentItemClass,
|
||||
columnClass,
|
||||
triggerClass,
|
||||
canShowTooltip = true,
|
||||
xlinkHref,
|
||||
title,
|
||||
placement,
|
||||
tooltipClass,
|
||||
content,
|
||||
trigger,
|
||||
onVisibilityChange,
|
||||
renderContent = content ? React.Children.map(
|
||||
processContent(content),
|
||||
(item, idx) => <div key={idx} className={contentItemClass}>
|
||||
@@ -36,6 +40,10 @@ const getIconTooltip = ({
|
||||
className={tooltipClassName}
|
||||
content={tooltipContent}
|
||||
placement={placement}
|
||||
triggerClass={triggerClass}
|
||||
trigger={trigger}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
|
||||
>
|
||||
{xlinkHref && <svg className={className}>
|
||||
<use xlinkHref={`#${xlinkHref}`} />
|
||||
@@ -43,20 +51,20 @@ const getIconTooltip = ({
|
||||
</Tooltip>;
|
||||
};
|
||||
|
||||
getIconTooltip.propTypes = {
|
||||
IconTooltip.propTypes = {
|
||||
className: PropTypes.string,
|
||||
trigger: PropTypes.string,
|
||||
triggerClass: PropTypes.string,
|
||||
contentItemClass: PropTypes.string,
|
||||
columnClass: PropTypes.string,
|
||||
tooltipClass: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
placement: PropTypes.string,
|
||||
canShowTooltip: PropTypes.string,
|
||||
canShowTooltip: PropTypes.bool,
|
||||
xlinkHref: PropTypes.string,
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.array,
|
||||
]),
|
||||
content: PropTypes.node,
|
||||
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 {
|
||||
response_status, search,
|
||||
} = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
} = useSelector((state) => state?.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
|
||||
const [
|
||||
debouncedSearch,
|
||||
@@ -171,14 +171,14 @@ const Form = (props) => {
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
query, label, disabled,
|
||||
QUERY, LABEL, disabled,
|
||||
}) => (
|
||||
<option
|
||||
key={label}
|
||||
value={query}
|
||||
key={LABEL}
|
||||
value={QUERY}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t(label)}
|
||||
{t(LABEL)}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
@@ -197,5 +197,4 @@ Form.propTypes = {
|
||||
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
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 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">
|
||||
<h1 className="page-title page-title--large">
|
||||
@@ -29,7 +40,6 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
|
||||
Filters.propTypes = {
|
||||
filter: PropTypes.object.isRequired,
|
||||
refreshLogs: PropTypes.func.isRequired,
|
||||
processingGetLogs: PropTypes.bool.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 {
|
||||
--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-f3: #F3F3F3;
|
||||
--gray-8: #888;
|
||||
--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 {
|
||||
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 {
|
||||
.logs__text {
|
||||
padding: 0 1px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -54,237 +42,6 @@
|
||||
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 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
@@ -302,132 +59,24 @@
|
||||
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 {
|
||||
white-space: pre-wrap !important;
|
||||
overflow-wrap: break-word;
|
||||
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 {
|
||||
color: var(--green79);
|
||||
}
|
||||
|
||||
.row--detailed {
|
||||
height: 4.9rem
|
||||
}
|
||||
|
||||
.w-90 {
|
||||
max-width: 90% !important;
|
||||
}
|
||||
|
||||
.h-85 {
|
||||
height: 85% !important;
|
||||
}
|
||||
|
||||
.pt-45 {
|
||||
padding-top: 1.25rem !important;
|
||||
}
|
||||
|
||||
.pb-45 {
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
|
||||
.py-45 {
|
||||
padding-top: 1.25rem !important;
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
|
||||
.mh-100 {
|
||||
max-height: 100% !important;
|
||||
}
|
||||
@@ -493,14 +142,6 @@
|
||||
}
|
||||
|
||||
@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 {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
@@ -517,38 +158,258 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.logs__table .rt-tr {
|
||||
height: 3.125rem;
|
||||
@media screen and (max-width: 767.98px) {
|
||||
.logs__table .logs__cell--response,
|
||||
.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 {
|
||||
--size: 2.5rem;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
margin-left: 0.9375rem;
|
||||
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 PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
@@ -8,34 +7,32 @@ import queryString from 'query-string';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
SMALL_SCREEN_SIZE,
|
||||
} from '../../helpers/constants';
|
||||
import Loading from '../ui/Loading';
|
||||
import Filters from './Filters';
|
||||
import Table from './Table';
|
||||
import Disabled from './Disabled';
|
||||
import { getFilteringStatus } from '../../actions/filtering';
|
||||
import { getClients } from '../../actions';
|
||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||
import {
|
||||
getLogsConfig,
|
||||
refreshFilteredLogs,
|
||||
resetFilteredLogs,
|
||||
setFilteredLogs,
|
||||
toggleDetailedLogs,
|
||||
} from '../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../actions/toasts';
|
||||
import InfiniteTable from './InfiniteTable';
|
||||
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]) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isTitle = value === 'title';
|
||||
const isButton = key === buttonType;
|
||||
const isButton = key.startsWith(BUTTON_PREFIX);
|
||||
const isBoolean = typeof value === 'boolean';
|
||||
const isHidden = isBoolean && value === false;
|
||||
|
||||
@@ -48,21 +45,20 @@ const processContent = (data, buttonType) => Object.entries(data)
|
||||
keyClass = '';
|
||||
}
|
||||
|
||||
return isHidden ? null : <Fragment key={key}>
|
||||
return isHidden ? null : <div key={key}>
|
||||
<div
|
||||
className={classNames(`key__${key}`, keyClass, {
|
||||
'font-weight-bold': isBoolean && value === true,
|
||||
})}>
|
||||
className={classNames(`key__${key}`, keyClass, {
|
||||
'font-weight-bold': isBoolean && value === true,
|
||||
})}>
|
||||
<Trans>{isButton ? value : key}</Trans>
|
||||
</div>
|
||||
<div className={`value__${key} text-pre text-truncate`}>
|
||||
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
||||
</div>
|
||||
</Fragment>;
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
||||
const Logs = (props) => {
|
||||
const Logs = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
@@ -71,7 +67,14 @@ const Logs = (props) => {
|
||||
search: search_url_param = '',
|
||||
} = 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 response_status = filter?.response_status || response_status_url_param;
|
||||
@@ -82,6 +85,7 @@ const Logs = (props) => {
|
||||
const [isModalOpened, setModalOpened] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const closeModal = () => setModalOpened(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -94,44 +98,11 @@ const Logs = (props) => {
|
||||
})();
|
||||
}, [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 mediaQueryHandler = (e) => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
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,
|
||||
});
|
||||
dispatch(toggleDetailedLogs(false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,7 +120,6 @@ const Logs = (props) => {
|
||||
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||
dispatch(getFilteringStatus());
|
||||
dispatch(getClients());
|
||||
try {
|
||||
@@ -169,6 +139,7 @@ const Logs = (props) => {
|
||||
mediaQuery.removeEventListener('change', mediaQueryHandler);
|
||||
} catch (e1) {
|
||||
try {
|
||||
// Safari 13.1 do not support mediaQuery.addEventListener('change', handler)
|
||||
mediaQuery.removeListener(mediaQueryHandler);
|
||||
} catch (e2) {
|
||||
console.error(e2);
|
||||
@@ -179,99 +150,53 @@ const Logs = (props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshLogs = async () => {
|
||||
setIsLoading(true);
|
||||
await Promise.all([
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
||||
dispatch(refreshFilteredLogs()),
|
||||
]);
|
||||
dispatch(addSuccessToast('query_log_updated'));
|
||||
setIsLoading(false);
|
||||
};
|
||||
const renderPage = () => <>
|
||||
<Filters
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
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 (
|
||||
<>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && (
|
||||
<>
|
||||
<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,
|
||||
return <>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && renderPage()}
|
||||
{!enabled && !processingGetConfig && <Disabled />}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
|
||||
@@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
|
||||
<div key={key} title={t(key)}>
|
||||
{icon && (
|
||||
<Fragment>
|
||||
<svg className="logs__whois-icon text-muted-dark icons">
|
||||
<svg className="logs__whois-icon text-muted-dark icons icon--24">
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
|
||||
|
||||
@@ -77,12 +77,12 @@ const StaticLeases = ({
|
||||
title={t('delete_table_action')}
|
||||
disabled={processingDeleting}
|
||||
onClick={() => handleDelete(ip, mac, hostname)}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>;
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>;
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -1,71 +1,56 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { FAILURE_TOAST_TIMEOUT, SUCCESS_TOAST_TIMEOUT } from '../../helpers/constants';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { TOAST_TIMEOUTS } from '../../helpers/constants';
|
||||
import { removeToast } from '../../actions';
|
||||
|
||||
class Toast extends Component {
|
||||
state = {
|
||||
timerId: null,
|
||||
const Toast = ({
|
||||
id,
|
||||
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() {
|
||||
this.setRemoveToastTimeout();
|
||||
}
|
||||
useEffect(() => {
|
||||
setRemoveToastTimeout();
|
||||
}, []);
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearRemoveToastTimeout = () => clearTimeout(this.state.timerId);
|
||||
|
||||
setRemoveToastTimeout = () => {
|
||||
const timeout = this.props.type === 'success' ? SUCCESS_TOAST_TIMEOUT : FAILURE_TOAST_TIMEOUT;
|
||||
|
||||
const timerId = setTimeout(() => {
|
||||
this.props.removeToast(this.props.id);
|
||||
}, timeout);
|
||||
|
||||
this.setState({ timerId });
|
||||
};
|
||||
|
||||
showMessage(t, type, message) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div className={`toast toast--${type}`}
|
||||
onMouseOver={clearRemoveToastTimeout}
|
||||
onMouseOut={setRemoveToastTimeout}>
|
||||
<p className="toast__content">
|
||||
<Trans
|
||||
i18nKey={message}
|
||||
{...options}
|
||||
/>
|
||||
</p>
|
||||
<button className="toast__dismiss" onClick={removeCurrentToast}>
|
||||
<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 = {
|
||||
t: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
message: 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 PropTypes from 'prop-types';
|
||||
import { useSelector, shallowEqual } from 'react-redux';
|
||||
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.css';
|
||||
|
||||
const Toasts = (props) => (
|
||||
<TransitionGroup className="toasts">
|
||||
{props.toasts.notices?.map((toast) => {
|
||||
const { id } = toast;
|
||||
return (
|
||||
<CSSTransition
|
||||
key={id}
|
||||
timeout={500}
|
||||
classNames="toast"
|
||||
>
|
||||
<Toast removeToast={props.removeToast} {...toast} />
|
||||
</CSSTransition>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
);
|
||||
const Toasts = () => {
|
||||
const toasts = useSelector((state) => state.toasts, shallowEqual);
|
||||
|
||||
Toasts.propTypes = {
|
||||
toasts: PropTypes.object,
|
||||
removeToast: PropTypes.func,
|
||||
return <TransitionGroup className="toasts">
|
||||
{toasts.notices?.map((toast) => {
|
||||
const { id } = toast;
|
||||
return <CSSTransition
|
||||
key={id}
|
||||
timeout={TOAST_TRANSITION_TIMEOUT}
|
||||
classNames="toast"
|
||||
>
|
||||
<Toast {...toast} />
|
||||
</CSSTransition>;
|
||||
})}
|
||||
</TransitionGroup>;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { toasts } = state;
|
||||
const props = { toasts };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(Toasts);
|
||||
export default Toasts;
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
.card-table-overflow--limited {
|
||||
overflow-y: auto;
|
||||
max-height: 280px;
|
||||
max-height: 17.5rem;
|
||||
}
|
||||
|
||||
.card-table-overflow--limited.clients__table {
|
||||
max-height: 18rem;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
@@ -118,14 +122,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card .red {
|
||||
.card .logs__cell--red {
|
||||
background-color: #fff4f2;
|
||||
}
|
||||
|
||||
.card .green {
|
||||
.card .logs__cell--green {
|
||||
background-color: #f1faf3;
|
||||
}
|
||||
|
||||
.card .blue {
|
||||
.card .logs__row--blue {
|
||||
background-color: #ecf7ff;
|
||||
}
|
||||
|
||||
@@ -344,6 +344,14 @@ const Icons = () => (
|
||||
<path d="M60 54.5h8v40h-8zM60 35.5h8v8h-8z" />
|
||||
</svg>
|
||||
</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>
|
||||
);
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
opacity: 0.8;
|
||||
background-color: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.loading:after {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import './Loading.css';
|
||||
|
||||
const Loading = ({ className }) => (
|
||||
<div className={classNames('loading', className)} />
|
||||
);
|
||||
const Loading = ({ className, text }) => {
|
||||
const { t } = useTranslation();
|
||||
return <div className={classNames('loading', className)}>{t(text)}</div>;
|
||||
};
|
||||
|
||||
Loading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.rt-tr-group.red {
|
||||
.rt-tr-group.logs__row--red {
|
||||
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);
|
||||
}
|
||||
|
||||
.rt-tr-group.blue {
|
||||
.rt-tr-group.logs__row--blue {
|
||||
background-color: #e5effd;
|
||||
}
|
||||
|
||||
.rt-tr-group.yellow {
|
||||
.rt-tr-group.logs__row--yellow {
|
||||
background-color: var(--yellow-pale);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const Tooltip = ({
|
||||
trigger = 'hover',
|
||||
delayShow = SHOW_TOOLTIP_DELAY,
|
||||
delayHide = HIDE_TOOLTIP_DELAY,
|
||||
onVisibilityChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const touchEventsAvailable = 'ontouchstart' in window;
|
||||
@@ -34,33 +35,48 @@ const Tooltip = ({
|
||||
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 (
|
||||
<TooltipTrigger
|
||||
placement={placement}
|
||||
trigger={triggerValue}
|
||||
delayHide={delayHideValue}
|
||||
delayShow={delayShowValue}
|
||||
tooltip={({ tooltipRef, getTooltipProps }) => (
|
||||
<div
|
||||
{...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className,
|
||||
})}
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>
|
||||
)}
|
||||
tooltip={renderTooltip}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
>
|
||||
{({ getTriggerProps, triggerRef }) => (
|
||||
<span
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
className: triggerClass,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
{renderTrigger}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
};
|
||||
@@ -76,10 +92,11 @@ Tooltip.propTypes = {
|
||||
).isRequired,
|
||||
placement: propTypes.string,
|
||||
trigger: propTypes.string,
|
||||
delayHide: propTypes.string,
|
||||
delayShow: propTypes.string,
|
||||
delayHide: propTypes.number,
|
||||
delayShow: propTypes.number,
|
||||
className: propTypes.string,
|
||||
triggerClass: propTypes.string,
|
||||
onVisibilityChange: propTypes.func,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { toggleProtection, getClients } from '../actions';
|
||||
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
|
||||
import { toggleClientBlock, getAccessList } from '../actions/access';
|
||||
import { getAccessList } from '../actions/access';
|
||||
import Dashboard from '../components/Dashboard';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@@ -16,7 +16,6 @@ const mapDispatchToProps = {
|
||||
getStats,
|
||||
getStatsConfig,
|
||||
setStatsConfig,
|
||||
toggleClientBlock,
|
||||
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
|
||||
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 = {
|
||||
avg_processing_time: 'average_processing_time',
|
||||
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 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 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_FILTER_TIMEOUT = 500;
|
||||
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 SHOW_TOOLTIP_DELAY = 200;
|
||||
export const MODAL_OPEN_TIMEOUT = 150;
|
||||
@@ -307,9 +313,7 @@ export const DEFAULT_LOGS_FILTER = {
|
||||
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
export const TABLE_DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
export const TABLE_FIRST_PAGE = 0;
|
||||
export const QUERY_LOGS_PAGE_LIMIT = 20;
|
||||
|
||||
export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
@@ -327,85 +331,93 @@ export const FILTERED_STATUS = {
|
||||
|
||||
export const RESPONSE_FILTER = {
|
||||
ALL: {
|
||||
query: 'all',
|
||||
label: 'all_queries',
|
||||
QUERY: 'all',
|
||||
LABEL: 'all_queries',
|
||||
},
|
||||
FILTERED: {
|
||||
query: 'filtered',
|
||||
label: 'filtered',
|
||||
QUERY: 'filtered',
|
||||
LABEL: 'filtered',
|
||||
},
|
||||
PROCESSED: {
|
||||
query: 'processed',
|
||||
label: 'show_processed_responses',
|
||||
QUERY: 'processed',
|
||||
LABEL: 'show_processed_responses',
|
||||
},
|
||||
BLOCKED: {
|
||||
query: 'blocked',
|
||||
label: 'show_blocked_responses',
|
||||
QUERY: 'blocked',
|
||||
LABEL: 'show_blocked_responses',
|
||||
},
|
||||
BLOCKED_THREATS: {
|
||||
query: 'blocked_safebrowsing',
|
||||
label: 'blocked_threats',
|
||||
QUERY: 'blocked_safebrowsing',
|
||||
LABEL: 'blocked_threats',
|
||||
},
|
||||
BLOCKED_ADULT_WEBSITES: {
|
||||
query: 'blocked_parental',
|
||||
label: 'blocked_adult_websites',
|
||||
QUERY: 'blocked_parental',
|
||||
LABEL: 'blocked_adult_websites',
|
||||
},
|
||||
ALLOWED: {
|
||||
query: 'whitelisted',
|
||||
label: 'allowed',
|
||||
QUERY: 'whitelisted',
|
||||
LABEL: 'allowed',
|
||||
},
|
||||
REWRITTEN: {
|
||||
query: 'rewritten',
|
||||
label: 'rewritten',
|
||||
QUERY: 'rewritten',
|
||||
LABEL: 'rewritten',
|
||||
},
|
||||
SAFE_SEARCH: {
|
||||
query: 'safe_search',
|
||||
label: 'safe_search',
|
||||
QUERY: 'safe_search',
|
||||
LABEL: 'safe_search',
|
||||
},
|
||||
};
|
||||
|
||||
export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER)
|
||||
.reduce((acc, { query }) => {
|
||||
acc[query] = query;
|
||||
.reduce((acc, { QUERY }) => {
|
||||
acc[QUERY] = QUERY;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const QUERY_STATUS_COLORS = {
|
||||
BLUE: 'blue',
|
||||
GREEN: 'green',
|
||||
RED: 'red',
|
||||
WHITE: 'white',
|
||||
YELLOW: 'yellow',
|
||||
};
|
||||
|
||||
export const FILTERED_STATUS_TO_META_MAP = {
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||
label: RESPONSE_FILTER.ALLOWED.label,
|
||||
color: 'green',
|
||||
LABEL: RESPONSE_FILTER.ALLOWED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.GREEN,
|
||||
},
|
||||
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
|
||||
label: RESPONSE_FILTER.PROCESSED.label,
|
||||
color: 'white',
|
||||
LABEL: RESPONSE_FILTER.PROCESSED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.WHITE,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
|
||||
label: RESPONSE_FILTER.BLOCKED.label,
|
||||
color: 'red',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.RED,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
|
||||
label: RESPONSE_FILTER.SAFE_SEARCH.label,
|
||||
color: 'yellow',
|
||||
LABEL: RESPONSE_FILTER.SAFE_SEARCH.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: {
|
||||
label: RESPONSE_FILTER.BLOCKED.label,
|
||||
color: 'red',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.RED,
|
||||
},
|
||||
[FILTERED_STATUS.REWRITE]: {
|
||||
label: RESPONSE_FILTER.REWRITTEN.label,
|
||||
color: 'blue',
|
||||
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||
},
|
||||
[FILTERED_STATUS.REWRITE_HOSTS]: {
|
||||
label: RESPONSE_FILTER.REWRITTEN.label,
|
||||
color: 'blue',
|
||||
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: {
|
||||
label: RESPONSE_FILTER.BLOCKED_THREATS.label,
|
||||
color: 'yellow',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_PARENTAL]: {
|
||||
label: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.label,
|
||||
color: 'yellow',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -519,3 +531,20 @@ export const DHCP_DESCRIPTION_PLACEHOLDERS = {
|
||||
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",
|
||||
"categoryId": "security",
|
||||
"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": {
|
||||
"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 {
|
||||
CHECK_TIMEOUT,
|
||||
CUSTOM_FILTERING_RULES_ID,
|
||||
DEFAULT_DATE_FORMAT_OPTIONS,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
@@ -96,7 +97,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
|
||||
filterId,
|
||||
rule,
|
||||
status,
|
||||
serviceName: service_name,
|
||||
service_name,
|
||||
originalAnswer: original_answer,
|
||||
originalResponse: processResponse(original_answer),
|
||||
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 gateway_ip {string}
|
||||
@@ -803,3 +828,29 @@ export const enrichWithConcatenatedIpAddresses = (interfaces) => Object.entries(
|
||||
acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses);
|
||||
return acc;
|
||||
}, 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 * 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(
|
||||
{
|
||||
[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.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||
[actions.toggleDetailedLogs]: (state, { payload }) => ({
|
||||
@@ -34,14 +14,7 @@ const queryLogs = handleActions(
|
||||
|
||||
[actions.setFilteredLogsSuccess]: (state, { 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]);
|
||||
|
||||
return {
|
||||
@@ -49,10 +22,8 @@ const queryLogs = handleActions(
|
||||
oldest,
|
||||
filter,
|
||||
isFiltered,
|
||||
pages,
|
||||
total,
|
||||
logs: logsSlice,
|
||||
allLogs: logs,
|
||||
logs,
|
||||
isEntireLog: logs.length < 1,
|
||||
processingGetLogs: false,
|
||||
};
|
||||
},
|
||||
@@ -67,29 +38,13 @@ const queryLogs = handleActions(
|
||||
[actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||
[actions.getLogsSuccess]: (state, { payload }) => {
|
||||
const {
|
||||
logs, oldest, older_than, page, pageSize, initial,
|
||||
logs, oldest, older_than,
|
||||
} = 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 {
|
||||
...state,
|
||||
oldest,
|
||||
pages,
|
||||
total,
|
||||
allLogs,
|
||||
logs: logsSlice,
|
||||
logs: older_than ? [...state.logs, ...logs] : logs,
|
||||
isEntireLog: logs.length < 1,
|
||||
processingGetLogs: false,
|
||||
};
|
||||
@@ -126,7 +81,7 @@ const queryLogs = handleActions(
|
||||
...state, processingAdditionalLogs: false, processingGetLogs: false,
|
||||
}),
|
||||
[actions.getAdditionalLogsSuccess]: (state) => ({
|
||||
...state, processingAdditionalLogs: false, processingGetLogs: false,
|
||||
...state, processingAdditionalLogs: false, processingGetLogs: false, isEntireLog: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -135,18 +90,15 @@ const queryLogs = handleActions(
|
||||
processingGetConfig: false,
|
||||
processingSetConfig: false,
|
||||
processingAdditionalLogs: false,
|
||||
logs: [],
|
||||
interval: 1,
|
||||
allLogs: [],
|
||||
page: 0,
|
||||
pages: 0,
|
||||
total: 0,
|
||||
logs: [],
|
||||
enabled: true,
|
||||
oldest: '',
|
||||
filter: DEFAULT_LOGS_FILTER,
|
||||
isFiltered: false,
|
||||
anonymize_client_ip: false,
|
||||
isDetailed: true,
|
||||
isEntireLog: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -5,16 +5,18 @@ import {
|
||||
addErrorToast, addNoticeToast, addSuccessToast,
|
||||
} from '../actions/toasts';
|
||||
import { removeToast } from '../actions';
|
||||
import { TOAST_TYPES } from '../helpers/constants';
|
||||
|
||||
const toasts = handleActions({
|
||||
[addErrorToast]: (state, { payload }) => {
|
||||
const message = payload.error.toString();
|
||||
console.error(message);
|
||||
console.error(payload.error);
|
||||
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message,
|
||||
type: 'error',
|
||||
options: payload.options,
|
||||
type: TOAST_TYPES.ERROR,
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||
@@ -24,7 +26,7 @@ const toasts = handleActions({
|
||||
const successToast = {
|
||||
id: nanoid(),
|
||||
message: payload,
|
||||
type: 'success',
|
||||
type: TOAST_TYPES.SUCCESS,
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
@@ -34,7 +36,8 @@ const toasts = handleActions({
|
||||
const noticeToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'notice',
|
||||
options: payload.options,
|
||||
type: TOAST_TYPES.NOTICE,
|
||||
};
|
||||
|
||||
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: {
|
||||
MainRoot: path.resolve(__dirname, '../'),
|
||||
ClientRoot: path.resolve(__dirname, './src'),
|
||||
// TODO: change to '@hot-loader/react-dom' when v16.13.1 is released
|
||||
// https://stackoverflow.com/a/62671689/12942752
|
||||
'react-dom': 'react-dom',
|
||||
// TODO: uncomment when v16.13.1 is released https://stackoverflow.com/a/62671689/12942752
|
||||
// 'react-dom': '@hot-loader/react-dom',
|
||||
},
|
||||
},
|
||||
module: {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
// +build go1.12
|
||||
|
||||
// Package nclient4 is a small, minimum-functionality client for DHCPv4.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
//
|
||||
// This file contains code taken from gVisor.
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
// +build go1.12
|
||||
|
||||
package nclient4
|
||||
|
||||
@@ -82,6 +82,11 @@ type FilteringConfig struct {
|
||||
EnableDNSSEC bool `yaml:"enable_dnssec"` // Set DNSSEC flag in outcoming DNS request
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
@@ -51,6 +51,8 @@ type Server struct {
|
||||
stats stats.Stats
|
||||
access *accessCtx
|
||||
|
||||
ipset ipsetCtx
|
||||
|
||||
tableHostToIP map[string]net.IP // "hostname -> IP" table for internal addresses (DHCP)
|
||||
tableHostToIPLock sync.Mutex
|
||||
|
||||
@@ -168,7 +170,7 @@ func (s *Server) startInternal() error {
|
||||
|
||||
// Prepare the object
|
||||
func (s *Server) Prepare(config *ServerConfig) error {
|
||||
// 1. Initialize the server configuration
|
||||
// Initialize the server configuration
|
||||
// --
|
||||
if config != nil {
|
||||
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()
|
||||
|
||||
// 3. Prepare DNS servers settings
|
||||
// Initialize IPSET configuration
|
||||
// --
|
||||
s.ipset.init(s.conf.IPSETList)
|
||||
|
||||
// Prepare DNS servers settings
|
||||
// --
|
||||
err := s.prepareUpstreamSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Create DNS proxy configuration
|
||||
// Create DNS proxy configuration
|
||||
// --
|
||||
var proxyConfig proxy.Config
|
||||
proxyConfig, err = s.createProxyConfig()
|
||||
@@ -203,11 +209,11 @@ func (s *Server) Prepare(config *ServerConfig) error {
|
||||
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()
|
||||
|
||||
// 5. Initialize DNS access module
|
||||
// Initialize DNS access module
|
||||
// --
|
||||
s.access = &accessCtx{}
|
||||
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
|
||||
}
|
||||
|
||||
// 6. Register web handlers if necessary
|
||||
// Register web handlers if necessary
|
||||
// --
|
||||
if !webRegistered && s.conf.HTTPRegister != nil {
|
||||
webRegistered = true
|
||||
s.registerHandlers()
|
||||
}
|
||||
|
||||
// 7. Create the main DNS proxy instance
|
||||
// Create the main DNS proxy instance
|
||||
// --
|
||||
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
||||
return nil
|
||||
|
||||
@@ -49,6 +49,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
|
||||
processUpstream,
|
||||
processDNSSECAfterResponse,
|
||||
processFilteringAfterResponse,
|
||||
s.ipset.process,
|
||||
processQueryLogsAndStats,
|
||||
}
|
||||
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 (
|
||||
github.com/AdguardTeam/dnsproxy v0.31.1
|
||||
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/fsnotify/fsnotify v1.4.9
|
||||
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/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
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.11.2/go.mod h1:aMuejlNxpWppOVjiEV87X6z0eMf7wsXHTAIWQuylfZY=
|
||||
github.com/AdguardTeam/urlfilter v0.12.2 h1:5ZkH/+AWNBK8cCfbcgOL1MIrpPJ2NJXDs00KjYJr2WE=
|
||||
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/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
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/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
||||
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
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
u := Context.auth.UserFind(user, pass)
|
||||
u := a.UserFind(user, pass)
|
||||
return u
|
||||
}
|
||||
return User{}
|
||||
|
||||
@@ -47,12 +47,6 @@ type configuration struct {
|
||||
RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
|
||||
DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060
|
||||
|
||||
// The value for SetGCPercent.
|
||||
// SetGCPercent sets the garbage collection target percentage:
|
||||
// a collection is triggered when the ratio of freshly allocated data
|
||||
// to live data remaining after the previous collection reaches this percentage.
|
||||
MemGCPercent uint8 `yaml:"mem_gc_percentage"`
|
||||
|
||||
// TTL for a web session (in hours)
|
||||
// An active session is automatically refreshed once a day.
|
||||
WebSessionTTLHours uint32 `yaml:"web_session_ttl"`
|
||||
|
||||
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)
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"dns_addresses": getDNSAddresses(),
|
||||
"dns_addresses": Context.getDNSAddresses(),
|
||||
"http_port": config.BindPort,
|
||||
"dns_port": config.DNS.Port,
|
||||
"running": isRunning(),
|
||||
"running": Context.isRunning(),
|
||||
"version": versionString,
|
||||
"language": config.Language,
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ func getVersionResp(info update.VersionInfo) []byte {
|
||||
// Complete an update procedure
|
||||
func finishUpdate() {
|
||||
log.Info("Stopping all tasks")
|
||||
cleanup()
|
||||
cleanupAlways()
|
||||
Context.cleanup()
|
||||
Context.cleanupAlways()
|
||||
|
||||
exeName := "AdGuardHome"
|
||||
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
|
||||
// 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
|
||||
func initDNSServer() error {
|
||||
func (c *homeContext) initDNSServer() error {
|
||||
var err error
|
||||
baseDir := Context.getDataDir()
|
||||
baseDir := c.getDataDir()
|
||||
|
||||
statsConf := stats.Config{
|
||||
Filename: filepath.Join(baseDir, "stats.db"),
|
||||
@@ -34,7 +34,7 @@ func initDNSServer() error {
|
||||
ConfigModified: onConfigModified,
|
||||
HTTPRegister: httpRegister,
|
||||
}
|
||||
Context.stats, err = stats.New(statsConf)
|
||||
c.stats, err = stats.New(statsConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't initialize statistics module")
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func initDNSServer() error {
|
||||
ConfigModified: onConfigModified,
|
||||
HTTPRegister: httpRegister,
|
||||
}
|
||||
Context.queryLog = querylog.New(conf)
|
||||
c.queryLog = querylog.New(conf)
|
||||
|
||||
filterConf := config.DNS.DnsfilterConf
|
||||
bindhost := config.DNS.BindHost
|
||||
@@ -56,93 +56,39 @@ func initDNSServer() error {
|
||||
bindhost = "127.0.0.1"
|
||||
}
|
||||
filterConf.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
|
||||
filterConf.AutoHosts = &Context.autoHosts
|
||||
filterConf.AutoHosts = &c.autoHosts
|
||||
filterConf.ConfigModified = onConfigModified
|
||||
filterConf.HTTPRegister = httpRegister
|
||||
Context.dnsFilter = dnsfilter.New(&filterConf, nil)
|
||||
c.dnsFilter = dnsfilter.New(&filterConf, nil)
|
||||
|
||||
p := dnsforward.DNSCreateParams{
|
||||
DNSFilter: Context.dnsFilter,
|
||||
Stats: Context.stats,
|
||||
QueryLog: Context.queryLog,
|
||||
DNSFilter: c.dnsFilter,
|
||||
Stats: c.stats,
|
||||
QueryLog: c.queryLog,
|
||||
}
|
||||
if Context.dhcpServer != nil {
|
||||
p.DHCPServer = Context.dhcpServer
|
||||
if c.dhcpServer != nil {
|
||||
p.DHCPServer = c.dhcpServer
|
||||
}
|
||||
Context.dnsServer = dnsforward.NewServer(p)
|
||||
dnsConfig := generateServerConfig()
|
||||
err = Context.dnsServer.Prepare(&dnsConfig)
|
||||
c.dnsServer = dnsforward.NewServer(p)
|
||||
dnsConfig := c.generateServerConfig()
|
||||
err = c.dnsServer.Prepare(&dnsConfig)
|
||||
if err != nil {
|
||||
closeDNSServer()
|
||||
c.closeDNSServer()
|
||||
return fmt.Errorf("dnsServer.Prepare: %s", err)
|
||||
}
|
||||
|
||||
Context.rdns = InitRDNS(Context.dnsServer, &Context.clients)
|
||||
Context.whois = initWhois(&Context.clients)
|
||||
c.rdns = InitRDNS(c.dnsServer, &c.clients)
|
||||
c.whois = initWhois(&c.clients)
|
||||
|
||||
Context.filters.Init()
|
||||
c.filters.Init()
|
||||
return nil
|
||||
}
|
||||
|
||||
func isRunning() bool {
|
||||
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
|
||||
func (c *homeContext) isRunning() bool {
|
||||
return c.dnsServer != nil && c.dnsServer.IsRunning()
|
||||
}
|
||||
|
||||
// nolint (gocyclo)
|
||||
// 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) {
|
||||
func (c *homeContext) onDNSRequest(d *proxy.DNSContext) {
|
||||
ip := dnsforward.GetIPString(d.Addr)
|
||||
if ip == "" {
|
||||
// This would be quite weird if we get here
|
||||
@@ -151,25 +97,25 @@ func onDNSRequest(d *proxy.DNSContext) {
|
||||
|
||||
ipAddr := net.ParseIP(ip)
|
||||
if !ipAddr.IsLoopback() {
|
||||
Context.rdns.Begin(ip)
|
||||
c.rdns.Begin(ip)
|
||||
}
|
||||
if isPublicIP(ipAddr) {
|
||||
Context.whois.Begin(ip)
|
||||
if util.IsPublicIP(ipAddr) {
|
||||
c.whois.Begin(ip)
|
||||
}
|
||||
}
|
||||
|
||||
func generateServerConfig() dnsforward.ServerConfig {
|
||||
func (c *homeContext) generateServerConfig() dnsforward.ServerConfig {
|
||||
newconfig := dnsforward.ServerConfig{
|
||||
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},
|
||||
FilteringConfig: config.DNS.FilteringConfig,
|
||||
ConfigModified: onConfigModified,
|
||||
HTTPRegister: httpRegister,
|
||||
OnDNSRequest: onDNSRequest,
|
||||
OnDNSRequest: c.onDNSRequest,
|
||||
}
|
||||
|
||||
tlsConf := tlsConfigSettings{}
|
||||
Context.tls.WriteDiskConfig(&tlsConf)
|
||||
c.tls.WriteDiskConfig(&tlsConf)
|
||||
if tlsConf.Enabled {
|
||||
newconfig.TLSConfig = tlsConf.TLSConfig
|
||||
if tlsConf.PortDNSOverTLS != 0 {
|
||||
@@ -179,17 +125,17 @@ func generateServerConfig() dnsforward.ServerConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
newconfig.TLSv12Roots = Context.tlsRoots
|
||||
newconfig.TLSCiphers = Context.tlsCiphers
|
||||
newconfig.TLSv12Roots = c.tlsRoots
|
||||
newconfig.TLSCiphers = c.tlsCiphers
|
||||
newconfig.TLSAllowUnencryptedDOH = tlsConf.AllowUnencryptedDOH
|
||||
|
||||
newconfig.FilterHandler = applyAdditionalFiltering
|
||||
newconfig.GetCustomUpstreamByClient = Context.clients.FindUpstreams
|
||||
newconfig.FilterHandler = c.applyAdditionalFiltering
|
||||
newconfig.GetCustomUpstreamByClient = c.clients.FindUpstreams
|
||||
return newconfig
|
||||
}
|
||||
|
||||
// Get the list of DNS addresses the server is listening on
|
||||
func getDNSAddresses() []string {
|
||||
func (c *homeContext) getDNSAddresses() []string {
|
||||
dnsAddresses := []string{}
|
||||
|
||||
if config.DNS.BindHost == "0.0.0.0" {
|
||||
@@ -209,7 +155,7 @@ func getDNSAddresses() []string {
|
||||
}
|
||||
|
||||
tlsConf := tlsConfigSettings{}
|
||||
Context.tls.WriteDiskConfig(&tlsConf)
|
||||
c.tls.WriteDiskConfig(&tlsConf)
|
||||
if tlsConf.Enabled && len(tlsConf.ServerName) != 0 {
|
||||
|
||||
if tlsConf.PortHTTPS != 0 {
|
||||
@@ -231,75 +177,75 @@ func getDNSAddresses() []string {
|
||||
}
|
||||
|
||||
// If a client has his own settings, apply them
|
||||
func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
||||
Context.dnsFilter.ApplyBlockedServices(setts, nil, true)
|
||||
func (c *homeContext) applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
||||
c.dnsFilter.ApplyBlockedServices(setts, nil, true)
|
||||
|
||||
if len(clientAddr) == 0 {
|
||||
return
|
||||
}
|
||||
setts.ClientIP = clientAddr
|
||||
|
||||
c, ok := Context.clients.Find(clientAddr)
|
||||
cl, ok := c.clients.Find(clientAddr)
|
||||
if !ok {
|
||||
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 {
|
||||
Context.dnsFilter.ApplyBlockedServices(setts, c.BlockedServices, false)
|
||||
if cl.UseOwnBlockedServices {
|
||||
c.dnsFilter.ApplyBlockedServices(setts, cl.BlockedServices, false)
|
||||
}
|
||||
|
||||
setts.ClientName = c.Name
|
||||
setts.ClientTags = c.Tags
|
||||
setts.ClientName = cl.Name
|
||||
setts.ClientTags = cl.Tags
|
||||
|
||||
if !c.UseOwnSettings {
|
||||
if !cl.UseOwnSettings {
|
||||
return
|
||||
}
|
||||
|
||||
setts.FilteringEnabled = c.FilteringEnabled
|
||||
setts.SafeSearchEnabled = c.SafeSearchEnabled
|
||||
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
|
||||
setts.ParentalEnabled = c.ParentalEnabled
|
||||
setts.FilteringEnabled = cl.FilteringEnabled
|
||||
setts.SafeSearchEnabled = cl.SafeSearchEnabled
|
||||
setts.SafeBrowsingEnabled = cl.SafeBrowsingEnabled
|
||||
setts.ParentalEnabled = cl.ParentalEnabled
|
||||
}
|
||||
|
||||
func startDNSServer() error {
|
||||
if isRunning() {
|
||||
func (c *homeContext) startDNSServer() error {
|
||||
if c.isRunning() {
|
||||
return fmt.Errorf("unable to start forwarding DNS server: Already running")
|
||||
}
|
||||
|
||||
enableFilters(false)
|
||||
|
||||
Context.clients.Start()
|
||||
c.clients.Start()
|
||||
|
||||
err := Context.dnsServer.Start()
|
||||
err := c.dnsServer.Start()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
||||
}
|
||||
|
||||
Context.dnsFilter.Start()
|
||||
Context.filters.Start()
|
||||
Context.stats.Start()
|
||||
Context.queryLog.Start()
|
||||
c.dnsFilter.Start()
|
||||
c.filters.Start()
|
||||
c.stats.Start()
|
||||
c.queryLog.Start()
|
||||
|
||||
const topClientsNumber = 100 // the number of clients to get
|
||||
topClients := Context.stats.GetTopClientsIP(topClientsNumber)
|
||||
topClients := c.stats.GetTopClientsIP(topClientsNumber)
|
||||
for _, ip := range topClients {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
if !ipAddr.IsLoopback() {
|
||||
Context.rdns.Begin(ip)
|
||||
c.rdns.Begin(ip)
|
||||
}
|
||||
if isPublicIP(ipAddr) {
|
||||
Context.whois.Begin(ip)
|
||||
if util.IsPublicIP(ipAddr) {
|
||||
c.whois.Begin(ip)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func reconfigureDNSServer() error {
|
||||
newconfig := generateServerConfig()
|
||||
err := Context.dnsServer.Reconfigure(&newconfig)
|
||||
func (c *homeContext) reconfigureDNSServer() error {
|
||||
newconfig := c.generateServerConfig()
|
||||
err := c.dnsServer.Reconfigure(&newconfig)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
||||
}
|
||||
@@ -307,43 +253,42 @@ func reconfigureDNSServer() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopDNSServer() error {
|
||||
if !isRunning() {
|
||||
func (c *homeContext) stopDNSServer() error {
|
||||
if !c.isRunning() {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := Context.dnsServer.Stop()
|
||||
err := c.dnsServer.Stop()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Couldn't stop forwarding DNS server")
|
||||
}
|
||||
|
||||
closeDNSServer()
|
||||
c.closeDNSServer()
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeDNSServer() {
|
||||
func (c *homeContext) closeDNSServer() {
|
||||
// DNS forward module must be closed BEFORE stats or queryLog because it depends on them
|
||||
if Context.dnsServer != nil {
|
||||
Context.dnsServer.Close()
|
||||
Context.dnsServer = nil
|
||||
if c.dnsServer != nil {
|
||||
c.dnsServer.Close()
|
||||
c.dnsServer = nil
|
||||
}
|
||||
|
||||
if Context.dnsFilter != nil {
|
||||
Context.dnsFilter.Close()
|
||||
Context.dnsFilter = nil
|
||||
if c.dnsFilter != nil {
|
||||
c.dnsFilter.Close()
|
||||
c.dnsFilter = nil
|
||||
}
|
||||
|
||||
if Context.stats != nil {
|
||||
Context.stats.Close()
|
||||
Context.stats = nil
|
||||
if c.stats != nil {
|
||||
c.stats.Close()
|
||||
c.stats = nil
|
||||
}
|
||||
|
||||
if Context.queryLog != nil {
|
||||
Context.queryLog.Close()
|
||||
Context.queryLog = nil
|
||||
if c.queryLog != nil {
|
||||
c.queryLog.Close()
|
||||
c.queryLog = nil
|
||||
}
|
||||
|
||||
Context.filters.Close()
|
||||
|
||||
c.filters.Close()
|
||||
log.Debug("Closed all DNS modules")
|
||||
}
|
||||
|
||||
110
home/home.go
110
home/home.go
@@ -3,7 +3,6 @@ package home
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -13,9 +12,7 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -30,9 +27,6 @@ import (
|
||||
|
||||
"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/golibs/log"
|
||||
)
|
||||
|
||||
@@ -49,52 +43,6 @@ var (
|
||||
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
|
||||
func Main(version string, channel string, armVer string) {
|
||||
// Init update-related global variables
|
||||
@@ -119,8 +67,8 @@ func Main(version string, channel string, armVer string) {
|
||||
Context.tls.Reload()
|
||||
|
||||
default:
|
||||
cleanup()
|
||||
cleanupAlways()
|
||||
Context.cleanup()
|
||||
Context.cleanupAlways()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
@@ -210,11 +158,6 @@ func run(args options) {
|
||||
log.Info("Configuration file is OK")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if config.MemGCPercent != 0 {
|
||||
debug.SetGCPercent(int(config.MemGCPercent))
|
||||
log.Debug("Set SetGCPercent=%d", config.MemGCPercent)
|
||||
}
|
||||
}
|
||||
|
||||
// 'clients' module uses 'dnsfilter' module's static data (dnsfilter.BlockedSvcKnown()),
|
||||
@@ -310,7 +253,7 @@ func run(args options) {
|
||||
}
|
||||
|
||||
if !Context.firstRun {
|
||||
err := initDNSServer()
|
||||
err := Context.initDNSServer()
|
||||
if err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
@@ -318,7 +261,7 @@ func run(args options) {
|
||||
Context.autoHosts.Start()
|
||||
|
||||
go func() {
|
||||
err := startDNSServer()
|
||||
err := Context.startDNSServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -337,16 +280,16 @@ func run(args options) {
|
||||
|
||||
// StartMods - initialize and start DNS after installation
|
||||
func StartMods() error {
|
||||
err := initDNSServer()
|
||||
err := Context.initDNSServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Context.tls.Start()
|
||||
|
||||
err = startDNSServer()
|
||||
err = Context.startDNSServer()
|
||||
if err != nil {
|
||||
closeDNSServer()
|
||||
Context.closeDNSServer()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -495,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
|
||||
type options struct {
|
||||
verbose bool // is verbose logging enabled
|
||||
@@ -740,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...)
|
||||
}
|
||||
|
||||
func getHTTPProxy(req *http.Request) (*url.URL, error) {
|
||||
func getHTTPProxy(*http.Request) (*url.URL, error) {
|
||||
if len(config.ProxyURL) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user