Compare commits

..

23 Commits

Author SHA1 Message Date
Andrey Meshkov
cc1060d428 homeContext refactoring 2020-09-08 15:16:36 +03:00
Artem Baskal
06594bde8f - client: Add link to 'update_failed' error toast
Close #2062

Squashed commit of the following:

commit a1a1d4fe74dd414f83477d972bc07062e2c890ab
Merge: 9535e109 84938c56
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 8 10:21:47 2020 +0300

    Merge branch 'master' into fix/2062

commit 9535e10934c57c2592df234a030bad183c0086cd
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Sep 7 13:59:57 2020 +0300

    Fix translation

commit e6f912d1d2793fd008c22b4418681abcc54896d0
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Sep 7 12:03:45 2020 +0300

    - client: Add link to 'update_failed' error toast
2020-09-08 13:18:19 +03:00
Artem Baskal
84938c5603 - client: Fix whois cell styles
Close #2069

* commit '15a82233f3dc8b5307e3edef2214f98ab408de64':
  - client: Fix whois cell styles
2020-09-08 10:20:59 +03:00
ArtemBaskal
15a82233f3 - client: Fix whois cell styles 2020-09-07 10:36:17 +03:00
Artem Baskal
8dc0108868 + client: Redesign query logs block/unblock buttons
Close #2050

Squashed commit of the following:

commit 3bc6a409034989b914306e1c33da274730ca623e
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Sep 4 20:58:09 2020 +0300

    Change dashboard block confirm message

commit d4d47c3557e2166ee04db25a71b782bfbfe3b865
Merge: e8865827 fc43e2ac
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Sep 4 14:56:34 2020 +0300

    Merge branch 'master' into feature/2050

commit e8865827879955b1ef62c9ff85798d07bfa4627d
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Sep 4 13:46:10 2020 +0300

    Rename classname

commit 648151c54e493c63622e014cb9cd1cb450f25478
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 19:09:21 2020 +0300

    Decrease arrow size

commit 4feadab707c613d31225dfa9443a9a836db37ba1
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 18:27:41 2020 +0300

    Rename button class

commit c3919d8ae8d1431657ce61afad2c20e5806f279a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 10:35:15 2020 +0300

    Review changes: extract variables

commit 0ac809584c391e41a1749a844bc1075e05a92345
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 10:13:57 2020 +0300

    Display dashboard button on hover

commit 1395287c2383e2248a2a5d39451403bd73141e55
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 21:24:04 2020 +0300

    Do not hide button on option open

commit 947f254b7aea26f289b66b66fac46dba11ea3952
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 21:20:19 2020 +0300

    Add buttons for mobile screen

commit df05697f87163a2b716d82653884e631f2fa6cf3
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 20:18:20 2020 +0300

    Change dashboard button styles

commit 16655f2d6b0d79d1fa027ec2310bb0268fffaf6a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 20:04:28 2020 +0300

    Change button styles, rename button options

commit 1ac22e875d8b26c16830bf6edb85dadcc19ff287
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 19:30:16 2020 +0300

    Review changes

commit c590119875439d85927bdd334658e003bc1f0563
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 17:58:08 2020 +0300

    Remove default query logs form values

commit 141329563417f5337f5659d5500f4cbe16d64bd2
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 17:41:23 2020 +0300

    Update blocking buttons options logic, fix button svg size

commit 9e4f39aa6cb8e134d80d496b8a248b2fe6aceb99
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 16:30:48 2020 +0300

    Fix button position

commit 8aabff7daccb87ae02c2302e62e296b3cfc17608
Merge: 415a0334 6b614295
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 17:29:55 2020 +0300

    Merge branch 'master' into feature/2050

commit 415a0334561733d92a0f7badd68101ef554dc689
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 17:05:51 2020 +0300

    Add blocking options

commit bc6aed92b6e12f27c2604501275b53bb8159d5bc
Merge: 0de4fb3a 40b74522
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 15:49:06 2020 +0300

    Merge branch 'feature/infinite_scroll_query_logs' into feature/2050

commit 40b745225112cf8d664220ed8f484b0aa16e997c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 15:46:27 2020 +0300

    Remove dynamic translation of toasts

commit 0de4fb3a4cd785c6b52e860e204c6e13d356b178
Merge: 1ab14471 f08fa7b8
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 15:07:30 2020 +0300

    Merge branch 'feature/infinite_scroll_query_logs' into feature/2050

... and 51 more commits
2020-09-05 10:22:47 +03:00
Andrey Meshkov
fc43e2ac6f Merge: - client: Fix superfluous character in de locale
Fix #2053

* commit '050e996a35b1f26f1c03af7eda75584afad9bfc1':
  - client: Fix superfluous character in de locale
2020-09-04 13:37:48 +03:00
Andrey Meshkov
1a6bd29462 Merge: - client: Fix top clients alignment
Fix #2039

* commit 'b54ce85d3d5f39957d0a2c3625c04e4936c40d94':
  - client: Fix top clients alignment
2020-09-04 13:37:28 +03:00
Andrey Meshkov
340052090c Merge: - client: Display service name for blocked services
Merge in DNS/adguard-home from fix/2038 to master

* commit '9e33bd52599c2039603d99f93db461cc6a6a23f4':
  - client: Display service name for blocked services
2020-09-04 13:37:05 +03:00
ArtemBaskal
b54ce85d3d - client: Fix top clients alignment 2020-09-03 20:57:22 +03:00
ArtemBaskal
9e33bd5259 - client: Display service name for blocked services 2020-09-03 20:35:20 +03:00
ArtemBaskal
050e996a35 - client: Fix superfluous character in de locale 2020-09-03 19:33:49 +03:00
Simon Zolin
07db05dd80 * makefile: test: use '-race' parameter on UNIX, don't use it on Windows
Squashed commit of the following:

commit b01379bb223dd28464e8f0b1e8878d3e6b314c26
Merge: 16c7ab79 4efc464e
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Sep 3 10:00:31 2020 +0300

    Merge remote-tracking branch 'origin/master' into fix-test

commit 16c7ab7949ee0a175f89285c1f1fbde0aa27081b
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 20:04:00 2020 +0300

    minor

commit af8002b09a3017955e9892db413fa62ce1e4ad81
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 19:56:01 2020 +0300

    * makefile: test: use '-race' parameter on UNIX, don't use it on Windows

commit b893358cbe5d1b7dc7db23b0b159ec436c20ee3e
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 19:36:48 2020 +0300

    test

commit c52a82b720f61f874d49708e9cc1b307d2f62839
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 17:47:51 2020 +0300

    - fix test
2020-09-03 10:10:54 +03:00
Simon Zolin
4efc464e98 - querylog: file rotation didn't work properly; fix entry searching algorithm
If AGH is restarted, file rotation timer is reset
which can lead to the situation when file rotation procedure is never started.

Squashed commit of the following:

commit 427ae91a512cd146ebfffad06ed24eb723cb9e7d
Merge: 067fac65 e56c746b
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 18:18:46 2020 +0300

    Merge remote-tracking branch 'origin/master' into qlogs-rotate

commit 067fac65b1a87d499900f4860ffa96ed8208967c
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 15:30:48 2020 +0300

    minor

commit c2059a15700e5696cb1bb5cd49129c6020d986f4
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 14:53:07 2020 +0300

    improve

commit a279438eaf1cf40b820652093fb56d56784de7d8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 18:49:14 2020 +0300

    minor

commit 26ac130f139f565de39200e484b3bd4a04afcfcc
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 13:54:27 2020 +0300

    rename

commit 0fad7b88dbeadcddd4d77536a18da72f3203ea80
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 13:05:36 2020 +0300

    + TestQLogSeek

commit fa6afc6d4dc592b1fef67c4a069ea50fae600a58
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 13:05:34 2020 +0300

    minor

commit 11e6ab9131e5c37467e8530a2db95a82bbb0603b
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Aug 31 19:45:47 2020 +0300

    fix tests

commit 7cbb89948df0e69b1bae8f8cde1879b5b1c4b1d6
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Aug 31 19:29:43 2020 +0300

    - querylog: fix entry searching algorithm

commit 745d44863d88b321bd7001f24a68620f7ef05819
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Aug 31 18:34:14 2020 +0300

    - querylog: file rotation didn't work properly

    If AGH is restarted, file rotation timer is reset
     which can lead to the situation when file rotation procedure is never started.
2020-09-02 19:42:26 +03:00
Artem Baskal
e56c746b60 - client: Do not redirect to login page from install and login pages
Close #2036

Squashed commit of the following:

commit 9880b80671973929b732bb45f767392627ddecc1
Merge: 55a51ea2 7b9cef3a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 16:34:43 2020 +0300

    Merge branch 'master' into fix/unauthorized_redirect_logic

commit 55a51ea2947a43c339c8e5111ba79e4d52b26c84
Merge: 170b7387 7931e506
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 15:54:38 2020 +0300

    Merge branch 'master' into fix/unauthorized_redirect_logic

commit 170b7387b06e6c9b30b50cc673f7457976007b0f
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Aug 25 17:13:02 2020 +0300

    - client: Do not redirect to login page from install and login pages
2020-09-02 17:51:38 +03:00
Artem Baskal
7b9cef3a08 - client: Fix location icon size
Close #2061

* commit 'f363c95ef58a52c134dcca1cd7c58b4dc2358405':
  - client: Fix location icon size
2020-09-02 16:29:20 +03:00
ArtemBaskal
f363c95ef5 - client: Fix location icon size 2020-09-02 16:09:59 +03:00
Simon Zolin
729f4b1766 Merge: * install: don't show an error about static IP if an interface is not selected
* commit '67bf027616df85911dca3caeef9b3d40d9e964ea':
  * install: don't show an error about static IP if an interface is not selected
2020-09-02 16:07:08 +03:00
Simon Zolin
67bf027616 * install: don't show an error about static IP if an interface is not selected 2020-09-02 15:50:09 +03:00
Simon Zolin
7931e50673 + DNS: add "ipset" configuration setting
Close #1191

Squashed commit of the following:

commit ba14b53f9e3d98ad8127aa3af1def0da4269e8c4
Merge: 362f4c44 6b614295
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 14:03:19 2020 +0300

    Merge remote-tracking branch 'origin/master' into 1191-ipset

commit 362f4c44915cb8946db2e80f9a3f5afd74fe5de1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 12:50:56 2020 +0300

    minor

commit 28e12459166fe3d13fb0dbe59ac11b7d86adb9b4
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 12:43:25 2020 +0300

    minor

commit bdbd7324501f6111bea1e91eda7d730c7ea57b11
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 18:40:04 2020 +0300

    move code, ipset-v6

commit 77f4d943e74b70b5bc5aea279875ab1e2fab2192
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 15:53:27 2020 +0300

    comment

commit 16401325bbefeba08e447257b12a8424b78c9475
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Aug 31 17:43:23 2020 +0300

    minor

commit c8410e9a519b87911bc50f504e8b4aaf8dce6e02
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Aug 31 15:30:52 2020 +0300

    + DNS: add "ipset" configuration setting
2020-09-02 14:13:45 +03:00
Artem Baskal
6b61429572 Pull request 743: + client: Query Logs Infinite Scroll
Merge in DNS/adguard-home from feature/infinite_scroll_query_logs to master

Squashed commit of the following:

commit 4407ef2e7c055066257da791fbd65e6b0a495729
Merge: 40b74522 0a4781be
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 16:20:23 2020 +0300

    Merge branch 'master' into feature/infinite_scroll_query_logs

commit 40b745225112cf8d664220ed8f484b0aa16e997c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 15:46:27 2020 +0300

    Remove dynamic translation of toasts

commit f08fa7b8c6a243f6b10e924aebccc183ce7814fd
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 13:59:53 2020 +0300

    Remove renderLimitIdx, update isEntireLog

commit 0f1b02616faaa5759c0a3f6d8257117fa22094d9
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 11:11:14 2020 +0300

    Rename variables

commit 0928570c689c1fa704af775382620d68893e7c1c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 11:06:50 2020 +0300

    Make query logs short polling function more expressive

commit 9e773cbd6c287a1c799fa2680f3462508462ea7a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 11:06:19 2020 +0300

    Fix Toast translation interface

commit f9c57033e5adc5788954cf086b2f114dd8938bcb
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Aug 31 17:01:36 2020 +0300

    Do not hide loader

commit b86ba48613437f5559a748ad9aa4cf79d15db082
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Aug 31 16:56:34 2020 +0300

    Add dynamic translation for all toasts

commit b9d1d9b447ca90a3c179e503fa5d4abd3516321e
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Aug 31 16:39:29 2020 +0300

    Prevent getting query logs recursion if query is not changed

commit e25189749f7912648cca4503cfa8d0ad898c4bb6
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Aug 31 10:13:20 2020 +0300

    Decrease page limit to 20

commit 8b248ac5276899de838abf2dc9a69e47599cfc12
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Aug 28 18:47:12 2020 +0300

    Return checkFilteredLogs

commit bf2d65c4a3dca0da6b15f632ae11042b7c8e2045
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Aug 28 18:33:51 2020 +0300

    Review changes

commit 01b5250f9d9136a1f334086d3e2f00d1a928b37b
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Aug 28 15:29:59 2020 +0300

    Remove checkFilteredLogs

commit 25b364c41e6a1489d930c8b3b39b1ab43723f29d
Merge: 1dc66034 2c666cbd
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Aug 28 14:28:47 2020 +0300

    Merge branch 'feature/infinite_scroll_query_logs' of ssh://bit.adguard.com:7999/dns/adguard-home into feature/infinite_scroll_query_logs

commit 1dc6603421cde9847e792bfe77ff6546e53fbc2a
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Aug 28 14:28:01 2020 +0300

    disregard maxFileScanEntries only if offset is set

commit bad741ed7f1dccf6959d43d000b8c0150f526f9e
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Aug 28 11:57:45 2020 +0300

    Fix search behavior when limit is specified

commit 2c666cbdde465cf17434126830dd99ceedfc4cbc
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Aug 27 18:50:28 2020 +0300

    Hide table ref loader during data loading

commit 8b4f7fe642ef9e87a979813dcdbd7817d64c27f9
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Aug 27 18:43:24 2020 +0300

    Repair search

commit 26fae1ae01a789999b8a2181d60b35663a20460a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Aug 27 17:59:27 2020 +0300

    Resetting initial render index, change loader position on search

commit e2c97ae1a288438267eef9aec71b979319674a71
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Aug 27 16:02:03 2020 +0300

    Change isScrolledIntoView

... and 32 more commits
2020-09-01 16:30:30 +03:00
Simon Zolin
0a4781be97 * urlfilter v0.12.2
Close #1950

* commit '268d90b5bc8f6e05633a043abb0d8a2fdc8736e5':
  * urlfilter v0.12.2
2020-09-01 15:50:50 +03:00
Simon Zolin
268d90b5bc * urlfilter v0.12.2 2020-09-01 15:37:03 +03:00
Andrey Meshkov
7d3a72e626 Fix NoCoin list source 2020-08-31 11:40:08 +03:00
109 changed files with 2428 additions and 2229 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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",

View File

@@ -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": "Намерен в списъците с домейни.",

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}

View File

@@ -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."
}
}

View File

@@ -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": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟",

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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"
}
}

View File

@@ -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>をお読みください。"
}
}

View File

@@ -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": "쿼리를 보려면 클릭합니다"
}
}

View File

@@ -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."
}
}

View File

@@ -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?",

View File

@@ -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ć."
}
}

View File

@@ -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"
}
}

View File

@@ -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?",

View File

@@ -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."
}
}

View File

@@ -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> о том, как это разрешить."
}
}

View File

@@ -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ť."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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?",

View File

@@ -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": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?",

View File

@@ -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."
}
}

View File

@@ -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"
}
}

View File

@@ -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>"
}
}

View File

@@ -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>。"
}
}

View File

@@ -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);
};

View File

@@ -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) {

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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 = [

View File

@@ -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;

View File

@@ -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',

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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}

View File

@@ -164,6 +164,10 @@
color: #9aa0ac;
}
.nav-icon--white {
color: #fff;
}
.header-brand-img {
height: 32px;
}

View 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;

View 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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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,
};
};

View 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;

View File

@@ -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);

View File

@@ -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,
};

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
&nbsp;

View File

@@ -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>;
},
},
]}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>
);

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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",

View File

@@ -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>
&nbsp;
</>
)}{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}&nbsp;<small>{`(${value})`}</small>
</div>
);
} else {
nameContainer = (
<div
className="logs__text logs__text--nowrap"
title={`${name} (${value})`}
>
{name}&nbsp;<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>
);
};

View File

@@ -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;
};

View 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>
&nbsp;
</>
)}{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}&nbsp;<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>;
};

View File

@@ -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,
},
);

View File

@@ -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] };

View File

@@ -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: {

View File

@@ -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.

View File

@@ -14,6 +14,7 @@
//
// This file contains code taken from gVisor.
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
// +build go1.12
package nclient4

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
View 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
View File

@@ -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
View File

@@ -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=

View File

@@ -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{}

View File

@@ -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
View 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")
}

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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" {

View File

@@ -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")
}

View File

@@ -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