Compare commits

...

406 Commits
v0.1 ... v0.91

Author SHA1 Message Date
Eugene Bujak
89753c4efb Bump version to 0.91 2018-11-27 12:44:37 +03:00
Andrey Meshkov
8e57243275 Merge pull request #112 in DNS/adguard-dns from fix_block_button to master
* commit 'e08c5efd999895c1f782aca01b45b669e9c850e0':
  Fix ru translation
  Fix block button
2018-11-26 18:38:44 +03:00
Ildar Kamalov
e08c5efd99 Fix ru translation 2018-11-26 18:33:49 +03:00
Ildar Kamalov
c17c282901 Fix block button 2018-11-26 18:17:34 +03:00
Eugene Bujak
8966383ca3 Merge pull request #111 in DNS/adguard-dns from fix/config_ratelimit to master
* commit '82da886df5428335719f9e861cf59672f962e5e8':
  Add ratelimit and refuse_any to config file
2018-11-26 17:01:21 +03:00
Andrey Meshkov
82da886df5 Add ratelimit and refuse_any to config file 2018-11-26 17:00:46 +03:00
Andrey Meshkov
afe234759f Merge pull request #108 in DNS/adguard-dns from add_languages to master
* commit 'd1f5f781c9690129514a3f2629b1729b911dfdcf':
  Update locales
  Add new languages
2018-11-26 16:22:25 +03:00
Ildar Kamalov
d1f5f781c9 Merge branch 'master' into add_languages 2018-11-26 15:44:07 +03:00
Andrey Meshkov
f95bea325b Merge pull request #109 in DNS/adguard-dns from fix/453 to master
* commit '69cc597b87a23443d57cf35551f03a3846b64797':
  Fix #453: Added upstream examples
  Added upstream examples
2018-11-26 15:40:15 +03:00
Ildar Kamalov
d8c97cbabe Update locales 2018-11-26 15:26:38 +03:00
Eugene Bujak
c995726f78 Merge pull request #110 in DNS/adguard-dns from bugfix/424 to master
* commit 'd2a0d0333241d8f31718e4e8765f57d9ec68f7df':
  Allow specifying absolute paths in -c command line option.
2018-11-26 15:20:38 +03:00
Eugene Bujak
d2a0d03332 Allow specifying absolute paths in -c command line option.
Closes #424.
2018-11-26 15:18:56 +03:00
Andrey Meshkov
69cc597b87 Fix #453: Added upstream examples 2018-11-26 15:12:04 +03:00
Ildar Kamalov
15f8cfce64 Add new languages 2018-11-26 15:00:56 +03:00
Andrey Meshkov
939c902fb0 Added upstream examples 2018-11-26 15:00:17 +03:00
Eugene Bujak
d9a65631b9 Merge pull request #107 in DNS/adguard-dns from fix/460 to master
* commit 'c500345d16586e5a7c917611d52261beac2ac501':
  Fix #460
2018-11-26 14:47:05 +03:00
Eugene Bujak
093bd164d6 Merge pull request #106 in DNS/adguard-dns from fix/426_refuseany to master
* commit 'a0482fc201a735d6868b301d5b55f47c9f6dbfaf':
  Fix #426 Added refuseany (enabled by default) Added ratelimit and refuseany to the config file (for manual editing only)
2018-11-26 14:47:00 +03:00
Andrey Meshkov
c500345d16 Fix #460 2018-11-26 14:09:43 +03:00
Andrey Meshkov
a0482fc201 Fix #426
Added refuseany (enabled by default)
Added ratelimit and refuseany to the config file (for manual editing only)
2018-11-26 13:38:17 +03:00
Andrey Meshkov
a6c9210461 Merge pull request #103 in DNS/adguard-dns from feature/426 to master
* commit 'f94c63ed5b4fe028a74ce074b66afcbad7b426f4':
  Set default ratelimit to 30/sec
  Fix #426: add ratelimit to directives
  Added ratelimit plugin
2018-11-26 12:38:04 +03:00
Ildar Kamalov
4ae91f0c1b Merge pull request #104 in DNS/adguard-dns from fix/442 to master
* commit '903c1da993c24ab4a338a3cee22c947a8fcc2e19':
  Remove change_language call on loading app
  Pass two-letter language code
2018-11-26 12:26:45 +03:00
Ildar Kamalov
903c1da993 Remove change_language call on loading app 2018-11-26 11:48:06 +03:00
Eugene Bujak
dcbf083d5b Merge pull request #105 in DNS/adguard-dns from fix/265_restrictmoderate to master
* commit '1fa250bb35e9e44b091be36a9f08bb12705cc518':
  Use restrictmoderate.youtube.com #265
2018-11-26 11:11:49 +03:00
Andrey Meshkov
1fa250bb35 Use restrictmoderate.youtube.com #265 2018-11-26 11:05:39 +03:00
Ildar Kamalov
18f210eef5 Pass two-letter language code 2018-11-26 10:55:42 +03:00
Andrey Meshkov
f94c63ed5b Set default ratelimit to 30/sec 2018-11-25 23:32:28 +03:00
Andrey Meshkov
e4998651fe Fix #426: add ratelimit to directives 2018-11-25 23:17:49 +03:00
Andrey Meshkov
668dcebf13 Added ratelimit plugin 2018-11-25 23:11:36 +03:00
Andrey Meshkov
cdd2e8ecb4 Merge pull request #99 in DNS/adguard-dns from feature/442 to master
* commit '63f20bc3979c61f71afcad1e9c7bbcdbe783a98e': (27 commits)
  Request language from server
  i18n -— fiz broken change_language APO method
  Allow querying and changeing i18n language via API.
  Update readme and translations
  Add ru locale
  Move translations script to scripts folder
  Fix footer layout
  Add oneskyapp scripts for download and upload translations
  Fix minor mistake
  Translate Copyright text on footer
  Fix wrong translate key
  Fix wrong check update button text
  Convert i18n resource to key type
  Translate missing toast message when toggle query log feature
  create locales/en.js and change to key type. Next may be use auto load translate resource
  Tranlsate missing toast message
  add missing translate
  Update missing trasnlate
  Fix eslint
  Add translate PopoverFilter
  ...
2018-11-22 18:46:21 +03:00
Ildar Kamalov
63f20bc397 Request language from server 2018-11-22 17:56:57 +03:00
Eugene Bujak
83544ab0f6 i18n -— fiz broken change_language APO method 2018-11-22 13:15:38 +03:00
Eugene Bujak
2139bb9c79 Allow querying and changeing i18n language via API. 2018-11-21 20:44:20 +03:00
Ildar Kamalov
0530f5dff2 Update readme and translations 2018-11-21 11:43:55 +03:00
Andrey Meshkov
4e27ad0c8e Merge pull request #102 in DNS/adguard-dns from fix/improve_ratelimit to master
* commit '166bc72ff34f839d205166b3d560e97e8d6cf208':
  Fix tests
  Fix binary search in the whitelist
  Added ratelimit whitelist and tests
2018-11-21 00:24:34 +03:00
Andrey Meshkov
166bc72ff3 Fix tests 2018-11-21 00:23:09 +03:00
Andrey Meshkov
25f20bd5a7 Fix binary search in the whitelist 2018-11-21 00:18:13 +03:00
Andrey Meshkov
345e4dc89a Added ratelimit whitelist and tests 2018-11-20 23:51:18 +03:00
Ildar Kamalov
1ae6af44d1 Add ru locale 2018-11-19 19:23:57 +03:00
Ildar Kamalov
3779407291 Move translations script to scripts folder 2018-11-19 09:57:45 +03:00
Ildar Kamalov
ced5499083 Fix footer layout 2018-11-16 12:46:18 +03:00
Ildar Kamalov
5bf38041c5 Add oneskyapp scripts for download and upload translations 2018-11-16 12:45:41 +03:00
Hoàng Rio
25f469efd7 Fix minor mistake 2018-11-12 08:32:49 +07:00
hoangrio
3d3e8e7dbc Translate Copyright text on footer 2018-11-10 21:52:28 +07:00
hoangrio
346fa6e921 Fix wrong translate key 2018-11-10 14:11:10 +07:00
Eugene Bujak
54ee16634c Merge pull request #96 in DNS/adguard-dns from gomodules to master
* commit '3c427ba2956a0ceae80ed27976d1a3615494a80a':
  Drop support for go 1.10 or lower.
  Introduce go module support
2018-11-09 19:33:13 +03:00
Eugene Bujak
3c427ba295 Drop support for go 1.10 or lower. 2018-11-09 19:31:38 +03:00
Eugene Bujak
a6e4c48567 Introduce go module support 2018-11-09 18:19:55 +03:00
hoangrio
628323761a Merge remote-tracking branch 'upstream/master' into i18n 2018-11-09 21:40:23 +07:00
hoangrio
e1276d089b Fix wrong check update button text 2018-11-09 21:12:46 +07:00
Hoàng Rio
d47a23269d Convert i18n resource to key type 2018-11-09 13:51:28 +07:00
Hoàng Rio
beab9a1be0 Merge branch 'i18n' into i18n_wip 2018-11-09 10:03:06 +07:00
Hoàng Rio
82bc5965f4 Translate missing toast message when toggle query log feature 2018-11-09 10:02:27 +07:00
Hoàng Rio
8d209773b3 Merge branch 'i18n' into i18n_wip 2018-11-08 13:29:47 +07:00
Hoàng Rio
67c8abcb8e Merge branch 'master' into i18n 2018-11-08 13:29:15 +07:00
Eugene Bujak
bd39509458 Merge pull request #95 in DNS/adguard-dns from feature/265 to master
* commit 'fc7d93b920b196e654502e1a2b720d56bae05711':
  Force safe search on Youtube
2018-11-06 15:11:06 +03:00
Andrey Meshkov
fc7d93b920 Force safe search on Youtube 2018-11-06 15:03:42 +03:00
Andrey Meshkov
4a357f1345 Merge pull request #93 in DNS/adguard-dns from fix/414 to master
* commit '914eb612cd0da015b98c151b7ac603fb4126a2c3':
  Add bootstrap DNS to readme
  Fix review comments
  Close test upstream
  Added bootstrap DNS to the config file DNS healthcheck now uses the upstream package methods
  goimports files
  Added CoreDNS plugin setup and replaced forward
  Added factory method for creating DNS upstreams
  Added health-check method
  Added persistent connections cache
  Upstream plugin prototype
2018-11-06 12:27:08 +03:00
Hoàng Rio
3693047270 create locales/en.js and change to key type. Next may be use auto load translate resource 2018-11-06 14:55:26 +07:00
Hoàng Rio
92fbbc8cc5 Tranlsate missing toast message 2018-11-06 09:51:00 +07:00
Andrey Meshkov
914eb612cd Add bootstrap DNS to readme 2018-11-06 01:20:53 +03:00
Andrey Meshkov
cc40826299 Fix review comments 2018-11-06 01:14:28 +03:00
Andrey Meshkov
2e879896ff Close test upstream 2018-11-06 00:52:27 +03:00
Andrey Meshkov
451922b858 Added bootstrap DNS to the config file
DNS healthcheck now uses the upstream package methods
2018-11-06 00:47:59 +03:00
Andrey Meshkov
7f018234f6 goimports files 2018-11-05 23:52:11 +03:00
Andrey Meshkov
efdd1c1ff2 Added CoreDNS plugin setup and replaced forward 2018-11-05 23:49:31 +03:00
Andrey Meshkov
9bc4bf66ed Added factory method for creating DNS upstreams 2018-11-05 22:11:13 +03:00
Andrey Meshkov
a6022fc198 Added health-check method 2018-11-05 21:19:01 +03:00
Andrey Meshkov
d6f560ecaf Added persistent connections cache 2018-11-05 20:40:10 +03:00
Hoàng Rio
839c2ebdd4 add missing translate 2018-11-05 11:46:18 +07:00
Hoàng Rio
9cd7a37646 Update missing trasnlate 2018-11-05 11:26:32 +07:00
Hoàng Rio
cd75c406c1 Merge remote-tracking branch 'upstream/master' into i18n 2018-11-05 11:23:16 +07:00
Eugene Bujak
2449075bca Revert "Cache DNS lookups when resolving safebrowsing or parental servers, also cache replacement hostnames as well."
This reverts commit a5d1053520.

This cache had unintended side effects.
2018-11-02 12:15:30 +03:00
Hoàng Rio
4c9a84dda0 Fix eslint 2018-11-02 13:34:38 +07:00
Hoàng Rio
262e9acc03 Add translate PopoverFilter 2018-11-02 13:09:08 +07:00
Andrey Meshkov
484c0ceaff Upstream plugin prototype 2018-11-01 14:45:32 +03:00
Hoàng Rio
e399a5fe37 Merge branch 'master' into i18n 2018-10-31 08:19:43 +07:00
Andrey Meshkov
19e30dbccc Merge pull request #92 in DNS/adguard-dns from feature/371 to master
* commit '49ff0d2b9ae4982d4352b668b8d7085ed72f2d4e':
  Added green background for whitelist rows
  Fix rule text in the Reason
  Added filter name to popover
  Fix review comments: NextFilterId collisions
  Added more logging to the plugin
  Added filterId to the querylog Updated the openapi.yaml accordingly Some minor refactoring/renaming Fix other review comments
  Fix review comments Fixed coredns plugin tests Check that user filter is not empty
  Fix #371 #421
2018-10-30 18:20:34 +03:00
Ildar Kamalov
49ff0d2b9a Added green background for whitelist rows 2018-10-30 18:10:05 +03:00
Andrey Meshkov
800002f83d Merge branch 'feature/371' of ssh://bit.adguard.com:7999/dns/adguard-dns into feature/371 2018-10-30 17:41:12 +03:00
Andrey Meshkov
73e20d1dd0 Fix rule text in the Reason 2018-10-30 17:37:11 +03:00
Ildar Kamalov
9bb788ecb5 Merge branch 'feature/371' of ssh://bit.adguard.com:7999/dns/adguard-dns into feature/371 2018-10-30 17:28:08 +03:00
Ildar Kamalov
f3fa497af3 Added filter name to popover 2018-10-30 17:27:47 +03:00
Andrey Meshkov
54bdacdde2 Fix review comments: NextFilterId collisions 2018-10-30 17:16:20 +03:00
Andrey Meshkov
0e065a2e61 Added more logging to the plugin 2018-10-30 12:57:16 +03:00
Andrey Meshkov
591065aa3a Added filterId to the querylog
Updated the openapi.yaml accordingly
Some minor refactoring/renaming
Fix other review comments
2018-10-30 12:24:59 +03:00
Andrey Meshkov
760e3596b6 Fix review comments
Fixed coredns plugin tests
Check that user filter is not empty
2018-10-30 11:01:09 +03:00
Hoàng Rio
21b8b233f8 Merge branch 'master' into i18n 2018-10-30 13:07:54 +07:00
Andrey Meshkov
32d4e80c93 Fix #371 #421
Filters are now saved to a file
Also, they're loaded from the file on startup
Filter ID is not passed to the CoreDNS plugin config (server-side AG DNS must be changed accordingly)
Some minor refactoring, unused functions removed
2018-10-30 02:17:24 +03:00
Andrey Meshkov
30f3eb446c Merge pull request #443 from AdguardTeam/fix/sync_with_bitbucket
Fix/sync with bitbucket
2018-10-29 13:50:41 +00:00
Andrey Meshkov
f711d6558f Merge pull request #90 in DNS/adguard-dns from fix/437 to master
* commit 'c39831abbcc3942bc7af9f8fd6c816b96770ee3e':
  Fix sorting method
  Move tiny-version-compare module to helpers
  Fix tooltips over-lapping
  Fix IP address sorting
2018-10-29 16:34:02 +03:00
Andrey Meshkov
abd1d306dc Merge pull request #91 in DNS/adguard-dns from fix/383 to master
* commit '1e1ce606c552d83daa4272a301d373561182820b':
  gofmt instead of goland's format
  Add ErrAlreadyExists
  Moved hosts-syntax matching to DnsFilter
  Fix gitignore
2018-10-29 16:33:53 +03:00
Andrey Meshkov
1e1ce606c5 gofmt instead of goland's format 2018-10-29 16:29:30 +03:00
Andrey Meshkov
abb51ddb8a Add ErrAlreadyExists 2018-10-29 16:17:18 +03:00
Andrey Meshkov
2b2a797cf7 Moved hosts-syntax matching to DnsFilter 2018-10-29 15:46:58 +03:00
Ildar Kamalov
c39831abbc Fix sorting method 2018-10-29 15:24:35 +03:00
Ildar Kamalov
9173b0ee7a Move tiny-version-compare module to helpers 2018-10-29 14:38:01 +03:00
Ildar Kamalov
c427034e27 Fix tooltips over-lapping
Closes #439
2018-10-29 13:13:32 +03:00
Ildar Kamalov
3cd3b93511 Fix IP address sorting
Closes #437
2018-10-29 13:12:04 +03:00
Ildar Kamalov
41c9a89516 Merge pull request #89 in DNS/adguard-dns from fix/433 to master
* commit '90ed48e9fbc140ef3ab09f4ae006a4b4d433e518':
  Fix graph corruption on Edge browser
2018-10-29 10:05:59 +03:00
Hoàng Rio
9863c1f1ac Fix i18n in setting page not update when click change language button at footer 2018-10-29 11:58:25 +07:00
Hoàng Rio
79468ab1bc Merge branch 'i18n' of https://github.com/abpvn/AdGuardHome into i18n 2018-10-29 10:26:41 +07:00
Hoàng Rio
4b821f0bd7 Add switch language button on footer 2018-10-29 10:26:19 +07:00
hoangrio
54b0f073e8 Merge remote-tracking branch 'upstream/master' into i18n 2018-10-28 13:05:52 +07:00
Ildar Kamalov
90ed48e9fb Fix graph corruption on Edge browser
Closes #433
2018-10-27 21:17:48 +03:00
Ildar Kamalov
7a68c3dfc6 Merge pull request #431 from abpvn/master
Allow click footer link when loading
2018-10-26 14:58:50 +03:00
Andrey Meshkov
234ab23557 Fix gitignore 2018-10-26 14:13:31 +03:00
Hoàng Rio
234e29697f Add translate for Popover 2018-10-26 10:58:18 +07:00
Hoàng Rio
4590564fea Complete translate client to Vietnamese 2018-10-26 09:44:23 +07:00
hoangrio
bfb7a252ad Fix i18n config for use : character 2018-10-25 23:02:04 +07:00
hoangrio
1d12e35dac Merge branch 'master' into i18n 2018-10-25 22:22:10 +07:00
hoangnd
3854a7acf9 Add translate Setting page 2018-10-25 17:33:44 +07:00
hoangnd
3be7366ae1 Fix toast z-index after allow click link on footer on loading 2018-10-25 16:44:23 +07:00
hoangnd
e1069f6bd1 Translate dashboard to Vietnamese with i18n 2018-10-25 14:28:52 +07:00
hoangnd
f8ee8a7907 Allow click footer link when loading 2018-10-25 12:55:35 +07:00
Andrey Meshkov
b6bc613c87 Merge pull request #411 from mustakimali/master
Formatted 'Rules count' number with thousands separator
2018-10-23 18:30:40 +01:00
Mo Mustakim Ali
e466a09e20 Update index.js 2018-10-23 15:58:07 +01:00
Mo Mustakim Ali
98bf5322a3 Using culture-specific approach instead of regex 2018-10-23 13:48:51 +01:00
A.J. Ruckman
b3ae247520 use keyed fields 2018-10-22 22:51:51 +03:00
A.J. Ruckman
b3840b5790 minor tweaks 2018-10-22 22:51:51 +03:00
Mustakim Ali
0c4646201f Fixed linter issue 2018-10-20 17:03:36 +01:00
Mustakim Ali
66b83a5fb5 Formatted 'Rules count' number with ',' separator
147500 => 147,500
2018-10-20 16:50:25 +01:00
Mustakim Ali
12706d4a97 Added missing JSDoc 2018-10-20 16:49:02 +01:00
Eugene Bujak
50d2c0a8d3 v0.9-hotfix1 2018-10-18 16:00:51 +03:00
Erik Rogers
4ad29ee65d Add Dockerfiles 2018-10-18 15:34:29 +03:00
Eugene Bujak
b2998d77f0 Merge pull request #88 in DNS/adguard-dns from bugfix/381 to master
* commit 'a528ed9f947d42f4324cd4f2263a015d34d7341f':
  Stop requiring current working directory to be the location of AdGuardHome.
2018-10-17 20:46:40 +03:00
Eugene Bujak
a528ed9f94 Stop requiring current working directory to be the location of AdGuardHome.
Fixes #381.
2018-10-17 20:43:26 +03:00
Eugene Bujak
a1bc008190 Merge pull request #87 in DNS/adguard-dns from feature/378 to master
* commit 'd3a6a8625406bfc452c545ed2f5eff6340bb86c5':
  Makefile -- add support for providing different GOPATH in command line.
  Do not use port 8618, it's a leftover from a time when we had two binaries.
2018-10-17 19:01:50 +03:00
Eugene Bujak
d3a6a86254 Makefile -- add support for providing different GOPATH in command line. 2018-10-17 18:57:47 +03:00
Eugene Bujak
5437a9d3a6 Do not use port 8618, it's a leftover from a time when we had two binaries.
Should fix 378 but needs testing from users having the problem since couldn't reproduce it here yet.
2018-10-17 18:55:27 +03:00
Ildar Kamalov
bdfb141d36 Fix logo 2018-10-17 14:44:42 +03:00
Andrey Meshkov
550dc3b129 fix gh language 2018-10-17 13:14:45 +03:00
Eugene Bujak
bacc465ebd README -- fix broken links, part deux. 2018-10-17 02:33:01 +03:00
Eugene Bujak
e606d63525 README -- fix broken links 2018-10-17 00:01:22 +03:00
Eugene Bujak
dbde07eea2 version.json -- point to actual files 2018-10-17 00:00:07 +03:00
Eugene Bujak
fc2f01f933 Release v0.9 2018-10-16 15:39:27 +03:00
Eugene Bujak
a267dbf625 Merge pull request #85 in DNS/adguard-dns from feature/361 to master
* commit 'c46fcce87ddc71c201079dba3df6267a80493a27':
  README -- Add whotracks.me to acknowledgments
  README -- add acknowledgements and fix release badge
2018-10-16 15:17:02 +03:00
Eugene Bujak
c46fcce87d README -- Add whotracks.me to acknowledgments 2018-10-15 22:21:42 +03:00
Eugene Bujak
af9c47c40a README -- add acknowledgements and fix release badge 2018-10-15 22:04:38 +03:00
Eugene Bujak
59323b2008 Merge pull request #84 in DNS/adguard-dns from avg_time_fix to master
* commit 'f0823f119573ac17f0bc1cacd10b527227b128c3':
  Fixup of previous commit.
2018-10-15 19:36:10 +03:00
Eugene Bujak
f0823f1195 Fixup of previous commit. 2018-10-15 19:34:31 +03:00
Eugene Bujak
dca9aebccb Merge pull request #83 in DNS/adguard-dns from avg_time_fix to master
* commit '1ed9faa0c22c1ae9ec2043c80e697133688cea40':
  Fix API returning wrong average request time
2018-10-15 19:31:33 +03:00
Eugene Bujak
1ed9faa0c2 Fix API returning wrong average request time 2018-10-15 19:30:10 +03:00
Andrey Meshkov
0d44e3ccdc Merge pull request #82 in DNS/adguard-dns from fix/update_readme to master
* commit 'cf9414c107a06de8b277d417387cd6a835c94b9d':
  Update the README
2018-10-15 17:29:04 +03:00
Andrey Meshkov
cf9414c107 Update the README 2018-10-15 17:20:57 +03:00
Eugene Bujak
47ce0f3e98 Merge pull request #81 in DNS/adguard-dns from hotfix to master
* commit '2e1acc2bac2b30219065afeeb66390d7f2b83f2c':
  Hotfix -- fix broken links in readme
2018-10-15 16:33:50 +03:00
Eugene Bujak
2e1acc2bac Hotfix -- fix broken links in readme 2018-10-15 16:32:38 +03:00
Eugene Bujak
96142b4164 Merge pull request #80 in DNS/adguard-dns from rename to master
* commit '1af11c4e45e13bcbeb9fe37f7867760f09ae8c2b':
  Name migration -- rename config from AdguardDNS.yaml to AdGuardHome.yaml
  Rename from 'Adguard DNS' to 'AdGuard Home'.
2018-10-15 16:15:37 +03:00
Eugene Bujak
1af11c4e45 Name migration -- rename config from AdguardDNS.yaml to AdGuardHome.yaml
It's done only if user didn't specify it in parameters and target filename doesn't exist yet.
2018-10-15 16:13:03 +03:00
Eugene Bujak
3e2a3afc52 Rename from 'Adguard DNS' to 'AdGuard Home'. 2018-10-15 16:02:19 +03:00
Andrey Meshkov
40d1b18b28 Merge pull request #79 in DNS/adguard-dns from fix/370 to master
* commit 'c2ba8de2062775decf09c94360d303e742e4ff44':
  Fix column max width
2018-10-15 15:06:20 +03:00
Eugene Bujak
a126a3868c Merge pull request #78 in DNS/adguard-dns from fix/368 to master
* commit 'aa691a068add3a29dd06b65d595b633750120eeb':
  Fix #368
2018-10-15 15:00:03 +03:00
Ildar Kamalov
c2ba8de206 Fix column max width 2018-10-15 14:57:36 +03:00
Andrey Meshkov
82269bcf33 Merge pull request #77 in DNS/adguard-dns from fix/365 to master
* commit 'bdaea88bf0b79d84da717393a447f71bd78e3cff':
  Add "FAQ" link to the header
  more trackers
  fix url
  tooltip
  capitalize category name
  fix gitignore
  remove extra file
  Add trackers, rework some text
  Fix some UI issues, rename DNS->Home
2018-10-15 14:46:18 +03:00
Andrey Meshkov
aa691a068a Fix #368 2018-10-15 14:31:21 +03:00
Ildar Kamalov
bdaea88bf0 Add "FAQ" link to the header
Closes #370
2018-10-15 13:49:07 +03:00
Andrey Meshkov
8c11449d23 more trackers 2018-10-15 00:08:38 +03:00
Andrey Meshkov
0e04954673 fix url 2018-10-15 00:00:18 +03:00
Andrey Meshkov
1059669b57 tooltip 2018-10-14 23:50:13 +03:00
Andrey Meshkov
c9736ec0fa capitalize category name 2018-10-14 23:42:25 +03:00
Andrey Meshkov
dcbee729fb fix gitignore 2018-10-14 23:26:00 +03:00
Andrey Meshkov
c35f260e53 remove extra file 2018-10-14 23:25:40 +03:00
Andrey Meshkov
2f61b42e90 Add trackers, rework some text 2018-10-14 23:24:11 +03:00
Andrey Meshkov
e67695df8b Fix some UI issues, rename DNS->Home 2018-10-14 17:49:07 +03:00
Andrey Meshkov
e356540872 fix crash 2018-10-12 20:36:57 +03:00
Andrey Meshkov
3d01c3512e Merge pull request #76 in DNS/adguard-dns from single_binary to master
* commit '880ad362a849cc53729128b614a4d4cc1d696750':
  Makefile -- use variable for target binary
  coredns plugin -- remove debug logging
  single binary -- coredns also tries to parse arguments, it kills itself on unknown flags
  Update makefile to build only one binary instead of two
  WIP -- single binary -- works, replies to DNS, but need to check what got broken
2018-10-12 19:58:33 +03:00
Andrey Meshkov
49567219dc Merge pull request #74 in DNS/adguard-dns from feature/363 to master
* commit 'd2e5692694e4537038434b56020f4e6c9e782955':
  Remove debug logging added by previous commit.
  If running from terminal, ask for username/password if config file does not exists
2018-10-12 19:56:41 +03:00
Eugene Bujak
3f85625dc6 Merge pull request #75 in DNS/adguard-dns from feature/364 to master
* commit 'd0d98ba762e9482f91a385488d22865ea00ff2a5':
  Add whotracksme info popover
  trackers module
  Added a script for updating the whotracksme database
2018-10-12 19:56:33 +03:00
Ildar Kamalov
d0d98ba762 Add whotracksme info popover
Closes #364
2018-10-12 19:52:19 +03:00
Eugene Bujak
d2e5692694 Remove debug logging added by previous commit.
Closes #363.
2018-10-12 19:49:17 +03:00
Eugene Bujak
9d030f38b7 If running from terminal, ask for username/password if config file does not exists 2018-10-12 19:43:09 +03:00
Andrey Meshkov
5fb603f6c9 trackers module 2018-10-12 17:32:38 +03:00
Andrey Meshkov
11e8853a34 Added a script for updating the whotracksme database 2018-10-12 17:32:38 +03:00
Eugene Bujak
880ad362a8 Makefile -- use variable for target binary 2018-10-12 17:11:57 +03:00
Eugene Bujak
5192e95a0d coredns plugin -- remove debug logging 2018-10-12 17:11:57 +03:00
Eugene Bujak
ac6e0add31 single binary -- coredns also tries to parse arguments, it kills itself on unknown flags 2018-10-12 17:11:57 +03:00
Eugene Bujak
7ff89baf45 Update makefile to build only one binary instead of two 2018-10-12 17:11:57 +03:00
Eugene Bujak
bad88961e9 WIP -- single binary -- works, replies to DNS, but need to check what got broken 2018-10-12 17:11:57 +03:00
Andrey Meshkov
838406353b Merge pull request #73 in DNS/adguard-dns from fix/362 to master
* commit '1cdbe3f879566f8e08ad0fa88daad5b568750820':
  Fix sorting issue and show loader
2018-10-12 17:05:58 +03:00
Ildar Kamalov
1cdbe3f879 Fix sorting issue and show loader
Closes #362
2018-10-12 16:58:48 +03:00
Andrey Meshkov
47a9c6555e Merge pull request #72 in DNS/adguard-dns from feature/360 to master
* commit '35368619b0d7ed0fb838d749cd7b2f63ae9c9aa3':
  Fix footer links
  Add progress bar to the stats tables
  Add new logo
  Replace the main Statistics graph with 4 blocks instead
  Clean static folder on build
2018-10-12 16:38:03 +03:00
Ildar Kamalov
35368619b0 Fix footer links
Closes #360
2018-10-12 16:27:59 +03:00
Ildar Kamalov
6ca881ee86 Add progress bar to the stats tables 2018-10-12 16:02:01 +03:00
Ildar Kamalov
1233901822 Add new logo 2018-10-12 15:23:57 +03:00
Ildar Kamalov
bc11f872fa Replace the main Statistics graph with 4 blocks instead 2018-10-12 15:23:21 +03:00
Ildar Kamalov
599426a1f9 Clean static folder on build 2018-10-12 15:20:59 +03:00
Eugene Bujak
fb2d90832c Merge pull request #68 in DNS/adguard-dns from feature/357 to master
* commit '8d13770b0d4b6d8fde0a61bbfdaa7bf9817142f6':
  Remove unneeded debug prints
  API filtering/add_url -- accept JSON instead of name=value lines
  Remove unused module
  Send json for addFilter request
  Fix params
  Add name field to the filter subscription dialog
2018-10-11 18:35:30 +03:00
Eugene Bujak
8d13770b0d Remove unneeded debug prints 2018-10-11 18:33:56 +03:00
Eugene Bujak
751be05a31 API filtering/add_url -- accept JSON instead of name=value lines 2018-10-11 18:33:56 +03:00
Ildar Kamalov
33958c5a25 Remove unused module 2018-10-11 18:33:56 +03:00
Ildar Kamalov
1e18235c1d Send json for addFilter request 2018-10-11 18:33:56 +03:00
Ildar Kamalov
4b8ee9ce83 Fix params 2018-10-11 18:33:56 +03:00
Ildar Kamalov
5be66e7dc7 Add name field to the filter subscription dialog 2018-10-11 18:33:56 +03:00
Eugene Bujak
8cf898e8d9 Merge pull request #70 in DNS/adguard-dns from hotfix to master
* commit '4995c1a1a8411445390a77235f2ebfbc97b12ff3':
  Hotfix -- fix querylog verifier that got broken by 5ae2a32d6e
2018-10-11 18:12:28 +03:00
Eugene Bujak
4995c1a1a8 Hotfix -- fix querylog verifier that got broken by 5ae2a32d6e 2018-10-11 18:06:33 +03:00
Eugene Bujak
557c2268dc Merge pull request #67 in DNS/adguard-dns from feature/333 to master
* commit '383f1c2fb38437e682ec9fc6623672730dae4581':
  Hide badge if core is not running
  Add client requests for toggle protection
  API backend -- implement ability to turn toggle all protection in one go, helpful to temporarily disable all kinds of filtering
2018-10-11 16:23:01 +03:00
Eugene Bujak
aa7b99d78c Merge pull request #69 in DNS/adguard-dns from fix/blocked_ttl_10 to master
* commit 'e7b6ab47501cf14d3a7b5cf212c88bde0c6604d7':
  Change blocked ttl to 10 sec, we don't need it to be large in a home network
2018-10-11 15:11:43 +03:00
Andrey Meshkov
e7b6ab4750 Change blocked ttl to 10 sec, we don't need it to be large in a home network 2018-10-11 13:24:06 +03:00
Ildar Kamalov
383f1c2fb3 Hide badge if core is not running 2018-10-11 11:08:07 +03:00
Ildar Kamalov
3a74dfdfa4 Add client requests for toggle protection
Closes #333
2018-10-11 10:57:36 +03:00
Eugene Bujak
413228e6ec API backend -- implement ability to turn toggle all protection in one go, helpful to temporarily disable all kinds of filtering 2018-10-10 20:13:03 +03:00
Eugene Bujak
c3df81bb8d Merge pull request #66 in DNS/adguard-dns from bugfix/356 to master
* commit 'e689c7d940e9a20bc13f024e18b86f3c1e5ba759':
  Do not lose filter name when saving to yaml
  coredns querylog -- since we read entire querylog json once at startup, fill querylog cache from it and then rotate it on each incoming DNS query
  Cache DNS lookups when resolving safebrowsing or parental servers, also cache replacement hostnames as well.
2018-10-10 19:50:20 +03:00
Eugene Bujak
e689c7d940 Do not lose filter name when saving to yaml 2018-10-10 19:49:18 +03:00
Eugene Bujak
5ae2a32d6e coredns querylog -- since we read entire querylog json once at startup, fill querylog cache from it and then rotate it on each incoming DNS query 2018-10-10 19:44:07 +03:00
Eugene Bujak
a5d1053520 Cache DNS lookups when resolving safebrowsing or parental servers, also cache replacement hostnames as well. 2018-10-10 19:10:38 +03:00
Eugene Bujak
e2295c1a11 Merge pull request #65 in DNS/adguard-dns from fix/strings to master
* commit 'd591ea6264bb287a6e57f815a0bc75b7b920bb87':
  Makefile -- run npm build whenever any .js file changes inside client/
  fix strings
  Fix strings
  Fix strings
2018-10-10 17:57:36 +03:00
Eugene Bujak
d591ea6264 Makefile -- run npm build whenever any .js file changes inside client/ 2018-10-10 17:56:48 +03:00
Andrey Meshkov
ee8759f063 tMerge branch 'master' into fix/strings 2018-10-10 17:55:03 +03:00
Andrey Meshkov
151944bc27 fix strings 2018-10-10 17:54:21 +03:00
Andrey Meshkov
a6172d1966 Fix strings 2018-10-10 17:32:36 +03:00
Andrey Meshkov
90bef94500 Fix strings 2018-10-10 17:24:12 +03:00
Eugene Bujak
f5deff63ba Merge pull request #64 in DNS/adguard-dns from remove_old_stats to master
* commit '903b20dcab855f0dec7af3b305e8362fa4255b8a':
  Remove dead code
2018-10-10 16:19:30 +03:00
Eugene Bujak
903b20dcab Remove dead code 2018-10-10 15:47:08 +03:00
Eugene Bujak
945bd24f67 Merge pull request #63 in DNS/adguard-dns from hotfix to master
* commit 'ae9964c445e200d22b159a47bfc6c00af990653e':
  Makefile -- fix build failure on systems where /bin/sh is not alias to /bin/bash
2018-10-10 01:26:21 +03:00
Eugene Bujak
ae9964c445 Makefile -- fix build failure on systems where /bin/sh is not alias to /bin/bash 2018-10-10 01:23:32 +03:00
Andrey Meshkov
3a5ecb9fc1 Merge pull request #62 in DNS/adguard-dns from hotfix to master
* commit 'c499c435c3c91960abd695b4919554937a3df5d1':
  Fixup of previous merge -- fix build failure.
2018-10-10 01:13:38 +03:00
Eugene Bujak
c499c435c3 Fixup of previous merge -- fix build failure. 2018-10-10 01:13:00 +03:00
Andrey Meshkov
2113bb5436 Merge pull request #60 in DNS/adguard-dns from feature/342 to master
* commit '8503f76747d47160d5d8239e275a7d6d8965dacd':
  Add default disabled hosts filters.
  Change default filter URL to github-hosted version
2018-10-10 01:05:58 +03:00
Eugene Bujak
8503f76747 Add default disabled hosts filters. 2018-10-10 00:59:37 +03:00
Eugene Bujak
c2be5917ef Change default filter URL to github-hosted version 2018-10-10 00:44:39 +03:00
Eugene Bujak
a54984f688 Merge pull request #58 in DNS/adguard-dns from feature/348 to master
* commit '5533b434dac98ba610028ca98327992d18078da0':
  coredns plugin -- give out to browser last entries from querylog file, not first
  Update .gitignore to ignore non-gzipped querylog
  Makefile -- Fix bug introduced by 93c451cb0c
  coredns plugin -- Increase querylog given out to web UI from 1000 to 5000.
2018-10-10 00:35:58 +03:00
Eugene Bujak
5533b434da coredns plugin -- give out to browser last entries from querylog file, not first 2018-10-10 00:23:15 +03:00
Eugene Bujak
4984c55bce Update .gitignore to ignore non-gzipped querylog 2018-10-10 00:05:48 +03:00
Eugene Bujak
9b489c8ddb Makefile -- Fix bug introduced by 93c451cb0c
make would always run webpack, even if output was generated already.
2018-10-10 00:05:48 +03:00
Eugene Bujak
eb5f66ad9e coredns plugin -- Increase querylog given out to web UI from 1000 to 5000. 2018-10-09 22:53:19 +03:00
Eugene Bujak
75d74a017b Merge pull request #56 in DNS/adguard-dns from feature/348 to master
* commit 'ca794aed6344cf0f5570bb359c731a96ca90739f':
  querylog file -- disable gzip compression
  Implement online stats calculation in coredns plugin instead of scraping prometheus.
2018-10-09 21:16:03 +03:00
Eugene Bujak
93c451cb0c Merge pull request #57 in DNS/adguard-dns from feature/354 to master
* commit '0545aeff3fb8368238e8c61d43589ef8b4a6d8e8':
  Fix variable
  Add hash to the static JS/CSS
  Fix tooltip width
  Fix default filtering for query log
  Fix textarea width
2018-10-09 21:15:26 +03:00
Ildar Kamalov
0545aeff3f Fix variable 2018-10-09 11:00:48 +03:00
Ildar Kamalov
814005021c Add hash to the static JS/CSS
Closes #354
2018-10-09 10:25:21 +03:00
Eugene Bujak
ca794aed63 querylog file -- disable gzip compression 2018-10-09 05:02:16 +03:00
Eugene Bujak
37f6d38c49 Implement online stats calculation in coredns plugin instead of scraping prometheus. 2018-10-09 04:45:05 +03:00
Konstantin 🦄 Zamyakin
165722585f Merge pull request #55 in DNS/adguard-dns from hotfix to master
* commit '7dea729656fe60aefcd81b7c1b808866d3c334a7':
  Fix build failure of coredns plugin introduced by previous merge
2018-10-08 21:30:47 +03:00
Eugene Bujak
7dea729656 Fix build failure of coredns plugin introduced by previous merge 2018-10-08 20:35:22 +03:00
Eugene Bujak
16b1a343a0 Merge pull request #54 in DNS/adguard-dns from feature/348 to master
* commit 'a15f21ca1cd0acabf77a4e9750dd77b2b870a6f4':
  code review -- move constants into named constants
  coredns plugin -- Cache /querylog API result
  coredns plugin -- Final fix for deadlock during coredns reload
  coredns plugin -- change rlock to lock when loading top stats to avoid doing it in parallel
  coredns plugin -- Fix deadlock during coredns reload
  stats -- Clamp number of rotations to sane value and prevent from going into (very long) loop
  querylog API -- when manually generating json, don't forget to escape strings
  coredns plugin -- don't reload from querylog on SIGUSR, we already have it in memory
  Fix some lint warnings
  coredns plugin -- Calculate top for domains, clients and blocked both from querylog and running requests.
  Fix more race conditions found by race detector
  Querylog -- Omit empty fields when writing json
  Querylog -- Read from querylog files when answering to /querylog API, it now survives restarts.
  querylog -- Add querylog files to gitignore
  if coredns unexpectedly quits, restart it
  Fix race conditions found by go's race detector
  Querylog -- Implement file writing and update /querylog handler for changed structures.
2018-10-08 20:15:06 +03:00
Eugene Bujak
a15f21ca1c code review -- move constants into named constants 2018-10-08 20:04:36 +03:00
Eugene Bujak
a15c59e24e coredns plugin -- Cache /querylog API result 2018-10-08 19:51:43 +03:00
Ildar Kamalov
5718f55b9a Fix tooltip width 2018-10-08 18:55:30 +03:00
Ildar Kamalov
6de0871f2c Fix default filtering for query log 2018-10-08 18:44:12 +03:00
Ildar Kamalov
6a90efe957 Fix textarea width 2018-10-08 18:42:55 +03:00
Eugene Bujak
763dcc46e9 coredns plugin -- Final fix for deadlock during coredns reload 2018-10-08 17:49:08 +03:00
Eugene Bujak
3109529dbb coredns plugin -- change rlock to lock when loading top stats to avoid doing it in parallel 2018-10-08 17:14:11 +03:00
Eugene Bujak
2c84cd6448 coredns plugin -- Fix deadlock during coredns reload 2018-10-08 17:07:45 +03:00
Eugene Bujak
0440ef016a stats -- Clamp number of rotations to sane value and prevent from going into (very long) loop 2018-10-08 05:55:33 +03:00
Eugene Bujak
182fa37e5f querylog API -- when manually generating json, don't forget to escape strings 2018-10-08 05:07:02 +03:00
Eugene Bujak
ea1125f57d coredns plugin -- don't reload from querylog on SIGUSR, we already have it in memory 2018-10-08 04:24:37 +03:00
Eugene Bujak
4ecb84f9ad Fix some lint warnings 2018-10-07 23:43:24 +03:00
Eugene Bujak
a2434d4574 coredns plugin -- Calculate top for domains, clients and blocked both from querylog and running requests.
This moves the functionality from frontend to coredns plugin.
2018-10-07 23:42:17 +03:00
Eugene Bujak
3b1faa1365 Fix more race conditions found by race detector 2018-10-07 21:24:22 +03:00
Eugene Bujak
dc1042c3e9 Querylog -- Omit empty fields when writing json 2018-10-07 02:21:47 +03:00
Eugene Bujak
a63fe958ae Querylog -- Read from querylog files when answering to /querylog API, it now survives restarts. 2018-10-07 02:21:33 +03:00
Eugene Bujak
0ee112e8a0 querylog -- Add querylog files to gitignore 2018-10-07 02:21:27 +03:00
Eugene Bujak
656d092ad6 if coredns unexpectedly quits, restart it 2018-10-07 02:21:27 +03:00
Eugene Bujak
2244c21b76 Fix race conditions found by go's race detector 2018-10-07 02:21:27 +03:00
Eugene Bujak
2c33905a79 Querylog -- Implement file writing and update /querylog handler for changed structures. 2018-10-07 02:21:12 +03:00
Eugene Bujak
16fd1359cd Merge pull request #53 in DNS/adguard-dns from bugfix/shoult_not_happen_spam to master
* commit '3a7a80f15f0180836077b4f63e504a659133adbb':
  coredns plugin -- fix SHOULD NOT HAPPEN spam when incoming request is for root servers
  Makefile -- update pprof plugin to survive coredns reloads
2018-10-05 17:12:24 +03:00
Eugene Bujak
3a7a80f15f coredns plugin -- fix SHOULD NOT HAPPEN spam when incoming request is for root servers 2018-10-05 07:36:03 +03:00
Eugene Bujak
5b9a5fff97 Makefile -- update pprof plugin to survive coredns reloads 2018-10-05 07:25:44 +03:00
Eugene Bujak
3f8450337f Merge pull request #52 in DNS/adguard-dns from feature/persistent-stats to master
* commit '19e76b693803220dffcd0a1fb1fe1e654309a11a':
  Add API call to reset stats
  Periodically flush stats.json
  Web UI -- persistent stats by writing them into stats.json at exit
2018-10-04 14:53:05 +03:00
Eugene Bujak
19e76b6938 Add API call to reset stats 2018-10-04 14:29:17 +03:00
Eugene Bujak
856e26edcf Periodically flush stats.json 2018-10-04 14:29:17 +03:00
Eugene Bujak
51ec58b0ce Web UI -- persistent stats by writing them into stats.json at exit 2018-10-04 14:29:16 +03:00
Eugene Bujak
c6eabb5b67 Merge pull request #51 in DNS/adguard-dns from feature/regexp_leak to master
* commit '1cc1e3749df6ccefb741232d7949fd5893d84f66':
  dnsfilter -- lazily initialize safebrowsing and parental lookup cache
  dnsfilter -- avoid using regexps when simple suffix match is enough.
2018-10-04 13:52:31 +03:00
Eugene Bujak
1cc1e3749d dnsfilter -- lazily initialize safebrowsing and parental lookup cache 2018-10-04 13:38:52 +03:00
Eugene Bujak
cb97a254a5 dnsfilter -- avoid using regexps when simple suffix match is enough.
This covers 96.98% of all adguard dns rules.
2018-10-04 13:19:43 +03:00
Eugene Bujak
9e939e5754 Merge pull request #49 in DNS/adguard-dns from features/memleak-test to master
* commit '3aac7e7bc9b4bb3ecff697b7748499a14bc64a0d':
  Add a test to demonstrate huge memory usage due from having too many regexps
2018-10-04 12:51:09 +03:00
Eugene Bujak
b72d6f68e6 Merge pull request #47 in DNS/adguard-dns from feature/349 to master
* commit '57ade2c3c3804d24857a45a8ab31c10534154dc7':
  Increase querylog size from 1000 to 10000 -- that'll use 32MB of memory.
  Web UI API -- Give out 24-hour stat instead of last 3 minutes.
2018-10-04 12:16:43 +03:00
Eugene Bujak
3aac7e7bc9 Add a test to demonstrate huge memory usage due from having too many regexps 2018-10-04 02:06:23 +03:00
Eugene Bujak
57ade2c3c3 Increase querylog size from 1000 to 10000 -- that'll use 32MB of memory. 2018-10-03 22:44:57 +03:00
Eugene Bujak
7d7360c700 Web UI API -- Give out 24-hour stat instead of last 3 minutes. 2018-10-03 22:44:50 +03:00
Ildar Kamalov
8c76e17b1b Merge pull request #46 in DNS/adguard-dns from feature/332 to master
* commit '991574f236ba691548839104a4218d749fbef10a':
  Fix row original
  Add query log filtering
2018-10-03 12:38:14 +03:00
Ildar Kamalov
991574f236 Fix row original 2018-10-02 18:30:34 +03:00
Ildar Kamalov
d7596fe860 Add query log filtering
Closes #322
2018-10-02 18:14:41 +03:00
Eugene Bujak
0c3c8dba9b Merge pull request #43 in DNS/adguard-dns from feature/341 to master
* commit 'e20bfe9d08d6c60c8f37ec49dcda2f446bdf0ce5':
  Replace line endings on save
  Add "block" and "unblock" buttons to the Query Log
2018-09-28 20:07:38 +03:00
Eugene Bujak
04e9f74435 Merge pull request #45 in DNS/adguard-dns from less-chatty to master
* commit '7b7f7138806b0b743d4fb1c4fef3c40f513be8b4':
  Be less noisy during long periods of time
2018-09-28 20:04:25 +03:00
Eugene Bujak
7b7f713880 Be less noisy during long periods of time 2018-09-28 18:08:26 +03:00
Ildar Kamalov
e20bfe9d08 Replace line endings on save 2018-09-28 17:47:34 +03:00
Ildar Kamalov
c40f7b4d5c Add "block" and "unblock" buttons to the Query Log 2018-09-28 16:30:52 +03:00
Eugene Bujak
d7039d9222 Merge pull request #42 in DNS/adguard-dns from feature/344 to master
* commit '2c720350006f607958540a672d2fa4cf927010bb':
  Add list of upstream servers
2018-09-26 18:55:28 +03:00
Eugene Bujak
3282a45978 Merge pull request #41 in DNS/adguard-dns from feature/346 to master
* commit '98994916b58faddb210b0776bdd7b5b6de43a8dc':
  Code review request -- set safebrowsing default to disabled
  web backend -- generate corefile with blocked_ttl config parameter
  coredns plugin -- Add option "blocked_ttl" that can change default nxdomain response TTL
  Makefile -- avoid stale copy of this repo inside build/gopath
2018-09-26 18:52:30 +03:00
Eugene Bujak
98994916b5 Code review request -- set safebrowsing default to disabled 2018-09-26 18:41:45 +03:00
Eugene Bujak
f1ae5d78d2 web backend -- generate corefile with blocked_ttl config parameter
Closes #346.
2018-09-26 18:38:35 +03:00
Ildar Kamalov
2c72035000 Add list of upstream servers
Closes #344
2018-09-26 18:38:06 +03:00
Eugene Bujak
c7790a8d9f coredns plugin -- Add option "blocked_ttl" that can change default nxdomain response TTL 2018-09-26 18:38:06 +03:00
Eugene Bujak
c9e10c9de7 Makefile -- avoid stale copy of this repo inside build/gopath 2018-09-26 18:38:06 +03:00
Eugene Bujak
de7b2d5e6b Merge pull request #40 in DNS/adguard-dns from feature/347 to master
* commit 'ff86d6b7dc31e463651c11f02330630e35676e05':
  Set default servers to tls://1.1.1.1 and tls://1.0.0.1
2018-09-26 18:00:34 +03:00
Eugene Bujak
ff86d6b7dc Set default servers to tls://1.1.1.1 and tls://1.0.0.1
Also add support for tls:// in webUI API
2018-09-26 17:47:23 +03:00
Eugene Bujak
3afd8fccc7 Merge pull request #39 in DNS/adguard-dns from feature/333 to master
* commit '2cf22898dd1418d1659340a95c94c8c9a6a7cf04':
  Add button to the dashboard page for enable/disable filtering
2018-09-26 17:24:30 +03:00
Ildar Kamalov
2cf22898dd Add button to the dashboard page for enable/disable filtering
Closes #333
2018-09-26 17:12:31 +03:00
Eugene Bujak
381b96a4b1 Merge pull request #38 in DNS/adguard-dns from bugfix/344 to master
* commit 'a65a40c6beb00176f46e7187ba0c4b678b17f6d8':
  Update /status to return currently set upstream DNS servers.
  web UI -- Fix engrish when checking upstream DNS servers succeeds
2018-09-25 20:54:35 +03:00
Eugene Bujak
a65a40c6be Update /status to return currently set upstream DNS servers. 2018-09-25 19:53:36 +03:00
Eugene Bujak
da62fac76e web UI -- Fix engrish when checking upstream DNS servers succeeds 2018-09-25 19:52:50 +03:00
Eugene Bujak
6a53dd0f00 Merge pull request #37 in DNS/adguard-dns from bugfix/333 to master
* commit '09a39cce03f69b1f9801e66d763d7b5208411336':
  Allow disabling of filtering but keeping querylog, safebrowsing, safesearch and parental working.
  Makefile -- make it a bit less noisy during build and much less noisy during clean
2018-09-25 19:44:07 +03:00
Eugene Bujak
09a39cce03 Allow disabling of filtering but keeping querylog, safebrowsing, safesearch and parental working. 2018-09-25 19:26:26 +03:00
Eugene Bujak
50b188a086 Makefile -- make it a bit less noisy during build and much less noisy during clean 2018-09-25 19:25:54 +03:00
Eugene Bujak
dd8396cec1 Merge pull request #36 in DNS/adguard-dns from bugfix/343 to master
* commit 'ea320f5ee35fda7744e86a1bd77b948e534eeb1e':
  Fix test failures introduced by previous commit afd1fe21f6.
2018-09-25 19:14:59 +03:00
Eugene Bujak
ea320f5ee3 Fix test failures introduced by previous commit afd1fe21f6. 2018-09-25 19:12:50 +03:00
Eugene Bujak
afd1fe21f6 Merge pull request #35 in DNS/adguard-dns from bugfix/343 to master
* commit '119d38fa8e8b5c5193fe20ad215a6daac833354b':
  Add trace() for debugging
  coredns -- don't try to be smart and replace 127.0.0.1 with NXDOMAIN yet -- need research on that first
  Fix 'index out of range' panic when adding a filter URL that has empty line in contents
  web UI -- Fix description of hosts rule syntax, it's other way around
2018-09-25 18:44:41 +03:00
Eugene Bujak
119d38fa8e Add trace() for debugging 2018-09-25 18:34:34 +03:00
Eugene Bujak
620212ad37 coredns -- don't try to be smart and replace 127.0.0.1 with NXDOMAIN yet -- need research on that first 2018-09-25 18:34:01 +03:00
Eugene Bujak
bd0fa4cc4f Fix 'index out of range' panic when adding a filter URL that has empty line in contents 2018-09-25 18:23:02 +03:00
Eugene Bujak
b0549a8e5b web UI -- Fix description of hosts rule syntax, it's other way around 2018-09-25 18:22:41 +03:00
Eugene Bujak
92399b8ebf Merge pull request #34 in DNS/adguard-dns from better-builds to master
* commit 'd8fbb2cd3b688b3890d60bc3923db50696dd9d59':
  Remove leftover from old internal repo
  Rewrite Makefile
2018-09-21 20:16:56 +03:00
Eugene Bujak
d8fbb2cd3b Remove leftover from old internal repo 2018-09-21 20:06:33 +03:00
Eugene Bujak
469b93eaa4 Rewrite Makefile
* fixes building outdated coredns plugin from inside GOPATH
 * make clean now cleans all build output, including node_modules and webpack output
 * smarter invocation of `npm install` -- only if package.json or package-lock.json changed
 * use separate gopath because coredns build system requires custom checkout of prometheus dependency
2018-09-21 20:01:55 +03:00
Ildar Kamalov
92b681cb41 Merge pull request #33 in DNS/adguard-dns from feature/321 to master
* commit '1c1b952d485e572eb2320b641429e07757b2d65f':
  Fix message checking
  Check upstream length in component
  Add a test upstreams button
2018-09-21 19:06:25 +03:00
Ildar Kamalov
1c1b952d48 Fix message checking 2018-09-21 18:57:27 +03:00
Ildar Kamalov
c2a2b3ea6a Check upstream length in component 2018-09-21 18:50:06 +03:00
Ildar Kamalov
f727f999f9 Add a test upstreams button
Closes #321
2018-09-21 18:08:39 +03:00
Eugene Bujak
02b28f4511 Merge pull request #32 in DNS/adguard-dns from feature/338 to master
* commit '43fcf4117db0e1e26085a0cc20a574edc8bd6255':
  Add update check
2018-09-21 15:32:11 +03:00
Ildar Kamalov
43fcf4117d Add update check
Closes #338
2018-09-21 15:20:41 +03:00
Eugene Bujak
68422b8399 Merge pull request #31 in DNS/adguard-dns from feature/338 to master
* commit 'c3f6a96f2f36f751d1315d83d65bff9396ea71d4':
  Add API endpoint to fetch version.json from github.io
2018-09-21 12:13:11 +03:00
Eugene Bujak
c3f6a96f2f Add API endpoint to fetch version.json from github.io 2018-09-20 20:02:25 +03:00
Eugene Bujak
2c2b951fd6 Merge pull request #30 in DNS/adguard-dns from feature/339 to master
* commit 'fba70b8b730187b8a60e724c1495ecc72e6f2d5e':
  Add version.json -- contains v0.1
2018-09-20 18:42:09 +03:00
Eugene Bujak
fba70b8b73 Add version.json -- contains v0.1 2018-09-20 18:35:09 +03:00
Eugene Bujak
38cfe95280 Merge pull request #29 in DNS/adguard-dns from readme to master
* commit 'a76fd7618abce834aa4eced0200e3ecfe77298e0':
  Proofreading by @vbagirov
  Update readme.
2018-09-20 18:27:15 +03:00
Eugene Bujak
a76fd7618a Proofreading by @vbagirov 2018-09-20 18:15:52 +03:00
Eugene Bujak
8d23e29190 Update readme.
* Link directly to binary downloads
 * Move "how to run" section right after binaries.
 * Add "How does AdGuard DNS work" and "How is this different from public AdGuard DNS servers" sections.
 * Update phrasing to emphasize that it's "your AdGuard DNS" server.
 * Remove yarn from `brew install` line.
 * Mention terminal for bash commands.
 * Add 32-bit ARM and 32-bit Intel binary links.
2018-09-20 15:21:02 +03:00
Eugene Bujak
a185161ad4 Merge pull request #28 in DNS/adguard-dns from feature/321 to master
* commit 'e733c1950484969c553b92e80ced6e585cc07bea':
  Implement API to test for upstream DNS servers.
2018-09-20 13:38:12 +03:00
Ildar Kamalov
81c7dbbc16 Merge pull request #27 in DNS/adguard-dns from feature/316 to master
* commit '0e173d2f705752c335c7fa78fdd5c578420e8a43':
  add progress bar and filters notifications
2018-09-19 19:14:19 +03:00
Eugene Bujak
e733c19504 Implement API to test for upstream DNS servers. 2018-09-19 19:12:09 +03:00
Ildar Kamalov
0e173d2f70 add progress bar and filters notifications 2018-09-19 18:58:55 +03:00
Eugene Bujak
0292d2b32b Merge pull request #26 in DNS/adguard-dns from basicauth to master
* commit 'ba56d6c01d7b241829d3115e1ce87241737cf571':
  Reorganize config file.
  Update README to explain config file settings
  Implement simple basic auth.
2018-09-19 15:54:49 +03:00
Eugene Bujak
ba56d6c01d Reorganize config file. 2018-09-19 15:51:44 +03:00
Eugene Bujak
b8213bf88a Update README to explain config file settings 2018-09-19 15:51:28 +03:00
Eugene Bujak
4548eb8d11 Implement simple basic auth.
Closes #326.
2018-09-18 20:59:41 +03:00
Eugene Bujak
a2f06aadc0 Merge pull request #25 in DNS/adguard-dns from feature/331 to master
* commit 'df12038f33b7acf6a3e9329054f1b5ad4cb02cd8':
  Add refresh button to querylog page
2018-09-17 19:03:45 +03:00
Ildar Kamalov
df12038f33 Add refresh button to querylog page
Closes #331
2018-09-17 17:44:32 +03:00
Eugene Bujak
c2aa39efe5 Merge pull request #23 in DNS/adguard-dns from gometalinter to master
* commit '076c9de68e73bbddc63cf6f7212818c91f3e5c08':
  Fix many lint warnings found by gometalinter
2018-09-17 11:26:17 +03:00
Eugene Bujak
5d046c5c16 Merge pull request #24 in DNS/adguard-dns from parental_fullhash to master
* commit 'dcbe3dd4051f421553eb7782af67378f3d9ce85b':
  dnsfilter -- compare full hashes when parsing parental lookup result.
2018-09-17 01:50:09 +03:00
Eugene Bujak
ae50a2f827 Merge pull request #20 in DNS/adguard-dns from feature/315 to master
* commit 'ded02d112c0e5b6d9585ec5506f24746abffdff3':
  Add console error
  Fix timeout
  Handle settings errors
  Show toast on failed request
  Fix clear interval
  Add alert on failed requests
2018-09-17 01:44:48 +03:00
Eugene Bujak
dcbe3dd405 dnsfilter -- compare full hashes when parsing parental lookup result.
Closes #337.
2018-09-17 01:42:01 +03:00
Ildar Kamalov
ded02d112c Add console error 2018-09-14 21:31:20 +03:00
Eugene Bujak
076c9de68e Fix many lint warnings found by gometalinter 2018-09-14 18:40:05 +03:00
Ildar Kamalov
d237df6389 Fix timeout 2018-09-14 16:43:27 +03:00
Ildar Kamalov
22a5abb7b8 Handle settings errors 2018-09-14 16:41:34 +03:00
Ildar Kamalov
828bb40084 Show toast on failed request 2018-09-14 15:37:35 +03:00
Eugene Bujak
548010e002 Merge pull request #22 in DNS/adguard-dns from fixtravis to master
* commit '5c6aa910efc9d90c5435a9eda19866a7d48be032':
  Fix a missed argument that breaks go test (which invokes go vet and fails if that fails)
2018-09-14 14:48:30 +03:00
Eugene Bujak
5c6aa910ef Fix a missed argument that breaks go test (which invokes go vet and fails if that fails) 2018-09-14 14:47:27 +03:00
Eugene Bujak
b9999f155e Merge pull request #21 in DNS/adguard-dns from add_url_verifier to master
* commit '3b44efc8e3f8fac534fbec37e1537ee4bd646141':
  /add_url -- it fetches the URL and checks if contents are valid filter, fails if it is not, and returns number of rules if it is
2018-09-14 11:23:51 +03:00
Eugene Bujak
3b44efc8e3 /add_url -- it fetches the URL and checks if contents are valid filter, fails if it is not, and returns number of rules if it is 2018-09-14 04:33:54 +03:00
Ildar Kamalov
9258fada47 Fix clear interval
Closes #315
2018-09-12 15:38:54 +03:00
Ildar Kamalov
6c70d8ca37 Add alert on failed requests 2018-09-12 12:58:55 +03:00
Eugene Bujak
5554643cd0 Merge pull request #19 in DNS/adguard-dns from version to master
* commit '7c71d4b44597ca7b22d3db60960c67b6e23ff2a6':
  web interface -- avoid having 'v.v0.1', saying 'version v0.1' seems more natural than that
2018-09-11 19:08:27 +03:00
Eugene Bujak
7c71d4b445 web interface -- avoid having 'v.v0.1', saying 'version v0.1' seems more natural than that 2018-09-11 19:04:49 +03:00
Eugene Bujak
3a92520764 Merge pull request #18 in DNS/adguard-dns from nonfqdn to master
* commit 'aa2e5500e72864727a0dcd196f37e84931cfa30a':
  coredns plugin -- do not filter out non-FQDN's -- otherwise it breaks serving /etc/hosts
2018-09-11 18:00:06 +03:00
Eugene Bujak
aa2e5500e7 coredns plugin -- do not filter out non-FQDN's -- otherwise it breaks serving /etc/hosts 2018-09-11 17:57:20 +03:00
Eugene Bujak
e2cf9ffd84 Merge pull request #17 in DNS/adguard-dns from feature/328 to master
* commit 'd8a3ee36764e4c3e33f5c73a3c5f9e73cdd5ec13':
  change graph stats to 24 hours
2018-09-11 15:05:47 +03:00
Ildar Kamalov
d8a3ee3676 change graph stats to 24 hours
Closes #328
2018-09-11 12:40:01 +03:00
Eugene Bujak
46e447589c Merge pull request #16 in DNS/adguard-dns from footer to master
* commit '97161ab4f0694367458933bc6ba44efe4d5e0509':
  web interface -- Update footer from placeholder to actual values.
2018-09-10 21:17:57 +03:00
Eugene Bujak
97161ab4f0 web interface -- Update footer from placeholder to actual values. 2018-09-10 21:14:28 +03:00
Eugene Bujak
3901dda39c Merge pull request #14 in DNS/adguard-dns from dnsfilter_recursion to master
* commit 'd49e3769a105f4dee639b9dec1112b123b7a23aa':
  dnsfilter -- do not check lookup hosts against themselves to avoid recursion
  Add support for serving /etc/hosts
  Makefile -- Fix cross-compilation
2018-09-10 20:52:20 +03:00
Eugene Bujak
d49e3769a1 dnsfilter -- do not check lookup hosts against themselves to avoid recursion 2018-09-10 20:43:22 +03:00
Eugene Bujak
c1e16cc584 Add support for serving /etc/hosts 2018-09-10 20:43:22 +03:00
Eugene Bujak
9c1dc6d373 Makefile -- Fix cross-compilation 2018-09-10 17:46:42 +03:00
Eugene Bujak
fced9178b8 Merge pull request #13 in DNS/adguard-dns from fix_coredns_build to master
* commit 'd34049b5135ff5dac2015dd93a5a624abdfe2cb4':
  travis -- Fix go test failure.
2018-09-10 16:10:44 +03:00
Eugene Bujak
d34049b513 travis -- Fix go test failure. 2018-09-10 15:57:47 +03:00
Eugene Bujak
43dbef8935 Merge pull request #12 in DNS/adguard-dns from fix_coredns_build to master
* commit 'f6d7d6a37ac9f8a184794d08c5b8a43bdc24e75a':
  Fix coredns build failure.
2018-09-10 15:33:05 +03:00
Eugene Bujak
f6d7d6a37a Fix coredns build failure. 2018-09-10 15:27:46 +03:00
Eugene Bujak
b54f9a7a36 Merge pull request #11 in DNS/adguard-dns from feature/313 to master
* commit '9e6ed7f273e996b2a89d9eda2302117adcc2389d':
  fix filtered reason overflow
  fix table cell overflow in filters
2018-09-07 19:12:05 +03:00
Ildar Kamalov
9e6ed7f273 fix filtered reason overflow 2018-09-07 19:06:09 +03:00
Ildar Kamalov
04faff3e2c fix table cell overflow in filters 2018-09-07 18:58:48 +03:00
Eugene Bujak
5fdaf7cb66 Merge pull request #10 in DNS/adguard-dns from feature/313 to master
* commit '76f98e2950463d4e176f9a5e2bf2c2c1c8aee51c':
  fix default page size in query logs
2018-09-07 18:44:34 +03:00
Ildar Kamalov
76f98e2950 fix default page size in query logs
Closes #313
2018-09-07 18:41:53 +03:00
Konstantin 🦄 Zamyakin
ba836220b8 Merge pull request #9 in DNS/adguard-dns from consistent-stats to master
* commit '31893410892bd047c9f6ea8f602717e6996c9491':
  web interface -- Make refresh buttons reload all data, not just counters
  web interface -- change text from 'general counters' to 'general statistics'
  Fixup of previous commit -- errand keystroke crept in
  API /stats_top -- sort top entries by value
  API /stats_top -- show only top entries for last 3 minutes
2018-09-07 18:21:46 +03:00
Eugene Bujak
3189341089 web interface -- Make refresh buttons reload all data, not just counters 2018-09-07 18:05:10 +03:00
Eugene Bujak
4ba8293c06 web interface -- change text from 'general counters' to 'general statistics' 2018-09-07 18:05:02 +03:00
Eugene Bujak
7094ed4f28 Fixup of previous commit -- errand keystroke crept in 2018-09-07 17:59:24 +03:00
Eugene Bujak
f623c3d909 API /stats_top -- sort top entries by value 2018-09-07 17:50:03 +03:00
Eugene Bujak
8198b65f29 API /stats_top -- show only top entries for last 3 minutes 2018-09-07 17:49:33 +03:00
Konstantin 🦄 Zamyakin
38b3fe6718 Merge pull request #8 in DNS/adguard-dns from parental_metrics to master
* commit '9682dc6bc19ea940cf71911f6281450c7027eb16':
  travis -- npm installation of dependencies belongs in install section
  makefile -- use npm --prefix syntax instead of doing cd into subdir
  travis -- don't use slow master or tip builds, just specify 1.x for latest go version
  travis -- move dependency installation to install section, simplify go test invocation to test all subdirs in one go
  dnsfilter -- small code cleanup
  coredns plugin metrics -- deduplicate code
  dnsfilter metrics -- parental cache hits were counted as safebrowsing cache hits
2018-09-07 17:11:21 +03:00
Eugene Bujak
9682dc6bc1 travis -- npm installation of dependencies belongs in install section 2018-09-07 16:14:43 +03:00
Eugene Bujak
659b530381 makefile -- use npm --prefix syntax instead of doing cd into subdir 2018-09-07 16:14:25 +03:00
Eugene Bujak
1b5748e328 travis -- don't use slow master or tip builds, just specify 1.x for latest go version 2018-09-07 16:13:23 +03:00
Eugene Bujak
ebf2380af4 travis -- move dependency installation to install section, simplify go test invocation to test all subdirs in one go 2018-09-07 16:13:03 +03:00
Eugene Bujak
6fc50cd743 dnsfilter -- small code cleanup 2018-09-07 16:10:43 +03:00
Eugene Bujak
3b9aaff861 coredns plugin metrics -- deduplicate code 2018-09-07 16:10:11 +03:00
Eugene Bujak
c572c7b0e9 dnsfilter metrics -- parental cache hits were counted as safebrowsing cache hits 2018-09-07 15:46:38 +03:00
Eugene Bujak
74275bebdc Merge pull request #7 in DNS/adguard-dns from metrics to master
* commit '1f0fdef8d6b2ce324e7009bb3f95626d87438d61':
  Fix invalid element order for historical stats -- in API declaration values are from oldest to newest, not other way around.
  Rewrite how historical stats are stored and calculated.
  coredns plugin -- convert logic into switch, logging unexpected non-covered cases
  After filters were redownloaded and deemed to be fresh, tell coredns server to reload
  coredns plugin -- on server reload, metrics disappeared, therefore they must be registered on each reload instead of once
  coredns plugin -- give feedback how many rules were in rulefile
  dnsfilter -- Update tests to check for expected filter/nofilter reason as well.
  Remove debug logging during checks if coredns is alive
  Be more atomic during writing of files -- this prevents other processes from seeing empty or impartial files
  Start coredns on launch before we serve HTTP -- this checks if port is available
  Move starting of coredns server into separate function
  sometimes answer can be empty, therefore question could be lost -- pass both to querylog
  Reduce binary size of coredns by 60% by removing orchestration plugins like kubernetes, route53, trace, etcd and federation
  Fix registration of metrics if querylog is enabled
2018-09-06 14:44:40 +03:00
Eugene Bujak
1f0fdef8d6 Fix invalid element order for historical stats -- in API declaration values are from oldest to newest, not other way around. 2018-09-06 02:20:51 +03:00
Eugene Bujak
04562dece3 Rewrite how historical stats are stored and calculated.
Closes #310.
2018-09-06 02:11:36 +03:00
Eugene Bujak
c7a5275d42 coredns plugin -- convert logic into switch, logging unexpected non-covered cases 2018-09-06 02:09:57 +03:00
Eugene Bujak
fe397943d6 After filters were redownloaded and deemed to be fresh, tell coredns server to reload 2018-09-06 02:09:05 +03:00
Eugene Bujak
876854d403 coredns plugin -- on server reload, metrics disappeared, therefore they must be registered on each reload instead of once 2018-09-06 02:08:49 +03:00
Eugene Bujak
c143e3d57f coredns plugin -- give feedback how many rules were in rulefile 2018-09-06 02:07:23 +03:00
Eugene Bujak
1102963fa0 dnsfilter -- Update tests to check for expected filter/nofilter reason as well. 2018-09-06 02:06:40 +03:00
Eugene Bujak
f2621c4a9a Remove debug logging during checks if coredns is alive 2018-09-06 02:04:16 +03:00
Eugene Bujak
859f1590dd Be more atomic during writing of files -- this prevents other processes from seeing empty or impartial files 2018-09-06 02:03:03 +03:00
Eugene Bujak
0ce40fd46e Start coredns on launch before we serve HTTP -- this checks if port is available 2018-09-06 02:00:57 +03:00
Eugene Bujak
33fbccf0ba Move starting of coredns server into separate function 2018-09-06 02:00:44 +03:00
Eugene Bujak
e122d9138b sometimes answer can be empty, therefore question could be lost -- pass both to querylog 2018-09-05 21:25:11 +03:00
Eugene Bujak
606bed9d20 Reduce binary size of coredns by 60% by removing orchestration plugins like kubernetes, route53, trace, etcd and federation 2018-09-05 21:23:08 +03:00
Eugene Bujak
3b11648e14 Fix registration of metrics if querylog is enabled 2018-09-05 21:21:46 +03:00
Konstantin 🦄 Zamyakin
e62050fb7e Merge pull request #6 in DNS/adguard-dns from feature/319 to master
* commit 'fa8bc570827cae708266901754712fb0c9a1f1ea':
  add reason status
  fix tooltip styles
  add client column and tooltip to blocked requests
2018-09-05 19:30:33 +03:00
Maxim Topchu
fa8bc57082 add reason status 2018-09-05 18:20:38 +03:00
Ildar Kamalov
0e99a65687 fix tooltip styles 2018-09-04 12:43:13 +03:00
Maxim Topchu
bed92f89f0 add client column and tooltip to blocked requests 2018-09-03 15:55:20 +03:00
Konstantin 🦄 Zamyakin
f12ef5d504 Merge pull request #5 in DNS/adguard-dns from querylog_client to master
* commit '379e14c28b18c0632e42760aca94aa7b72b05885':
  coredns plugin -- forgot to pass client's IP to querylog, fix that.
2018-08-31 21:08:20 +03:00
Eugene Bujak
379e14c28b coredns plugin -- forgot to pass client's IP to querylog, fix that.
Closes #311
2018-08-31 19:59:04 +03:00
Maxim Topchu
2ca1a0e586 fix link and name 2018-08-31 18:48:14 +03:00
Eugene Bujak
30553c6a9a Show 50 top blocked/requestsed/clients instead of 3.
Closes #312
2018-08-31 18:21:07 +03:00
Eugene Bujak
7bf513b638 Readme -- remove yarn from prerequisites since it's no longer needed 2018-08-31 18:13:45 +03:00
Konstantin Zamyakin
c4fefa10b0 Travis -- fix build on OSX, simplify travis and get rid of yarn 2018-08-31 18:11:45 +03:00
Eugene Bujak
3af62e463a travis -- install lts/* version of node via travis configuration instead of brew 2018-08-31 13:30:41 +03:00
Eugene Bujak
ad91ba8f43 travis -- upgrade nodejs on osx 2018-08-31 13:07:37 +03:00
Eugene Bujak
f054dcede4 travis -- build binaries as a test as well 2018-08-30 19:47:29 +03:00
Eugene Bujak
d53f9bafe9 Makefile -- replace sed with perl, add check that plugin.cfg has dnsfilter 2018-08-30 19:05:15 +03:00
Eugene Bujak
0421e1f4f8 Add travis testing 2018-08-30 18:18:18 +03:00
132 changed files with 35243 additions and 11783 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
client/* linguist-vendored

17
.gitignore vendored
View File

@@ -1,7 +1,20 @@
/AdguardDNS
/AdguardDNS.yaml
.DS_Store
.vscode
.idea
debug
/AdGuardHome
/AdGuardHome.yaml
/data/
/build/
/client/node_modules/
/coredns
/Corefile
/dnsfilter.txt
/querylog.json
/querylog.json.1
/scripts/translations/node_modules
/scripts/translations/oneskyapp.json
# Test output
dnsfilter/dnsfilter.TestLotsOfRules*.pprof
tests/top-1m.csv

26
.travis.yml Normal file
View File

@@ -0,0 +1,26 @@
language: go
sudo: false
go:
- 1.11.x
- 1.x
cache:
directories:
- $HOME/.cache/go-build
- $HOME/gopath/pkg/mod
- $HOME/Library/Caches/go-build
os:
- linux
- osx
install:
- go get -v -d -t ./...
- npm --prefix client install
- go env
script:
- (cd `go env GOPATH`/src/github.com/prometheus/client_golang && git checkout -q v0.8.0)
- go test ./...
- make

48
Dockerfile.arm Normal file
View File

@@ -0,0 +1,48 @@
FROM easypi/alpine-arm:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.91"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info
ARG ADGUARD_ARCH="linux_arm"
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
# AdGuard release info
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
# AdGuard directory
ARG ADGUARD_DIR="/data/adguard"
ENV ADGUARD_DIR ${ADGUARD_DIR}
# Update CA certs and download AdGuard binaries
RUN apk --no-cache --update add ca-certificates \
&& cd /tmp \
&& wget ${ADGUARD_RELEASE} \
&& tar xvf ${ADGUARD_ARCHIVE} \
&& mkdir -p "${ADGUARD_DIR}" \
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
&& rm -rf "AdGuardHome" \
&& rm ${ADGUARD_ARCHIVE}
# Expose DNS port 53
EXPOSE 53
# Expose UI port 3000
ARG ADGUARD_UI_HOST="0.0.0.0"
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
ARG ADGUARD_UI_PORT="3000"
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
EXPOSE ${ADGUARD_UI_PORT}
# Run AdGuardHome
WORKDIR ${ADGUARD_DIR}
VOLUME ${ADGUARD_DIR}
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}

48
Dockerfile.linux Normal file
View File

@@ -0,0 +1,48 @@
FROM alpine:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.91"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info
ARG ADGUARD_ARCH="linux_386"
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
# AdGuard release info
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
# AdGuard directory
ARG ADGUARD_DIR="/data/adguard"
ENV ADGUARD_DIR ${ADGUARD_DIR}
# Update CA certs and download AdGuard binaries
RUN apk --no-cache --update add ca-certificates \
&& cd /tmp \
&& wget ${ADGUARD_RELEASE} \
&& tar xvf ${ADGUARD_ARCHIVE} \
&& mkdir -p "${ADGUARD_DIR}" \
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
&& rm -rf "AdGuardHome" \
&& rm ${ADGUARD_ARCHIVE}
# Expose DNS port 53
EXPOSE 53
# Expose UI port 3000
ARG ADGUARD_UI_HOST="0.0.0.0"
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
ARG ADGUARD_UI_PORT="3000"
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
EXPOSE ${ADGUARD_UI_PORT}
# Run AdGuardHome
WORKDIR ${ADGUARD_DIR}
VOLUME ${ADGUARD_DIR}
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}

48
Dockerfile.linux64 Normal file
View File

@@ -0,0 +1,48 @@
FROM alpine:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.91"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info
ARG ADGUARD_ARCH="linux_amd64"
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
# AdGuard release info
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
# AdGuard directory
ARG ADGUARD_DIR="/data/adguard"
ENV ADGUARD_DIR ${ADGUARD_DIR}
# Update CA certs and download AdGuard binaries
RUN apk --no-cache --update add ca-certificates \
&& cd /tmp \
&& wget ${ADGUARD_RELEASE} \
&& tar xvf ${ADGUARD_ARCHIVE} \
&& mkdir -p "${ADGUARD_DIR}" \
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
&& rm -rf "AdGuardHome" \
&& rm ${ADGUARD_ARCHIVE}
# Expose DNS port 53
EXPOSE 53
# Expose UI port 3000
ARG ADGUARD_UI_HOST="0.0.0.0"
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
ARG ADGUARD_UI_PORT="3000"
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
EXPOSE ${ADGUARD_UI_PORT}
# Run AdGuardHome
WORKDIR ${ADGUARD_DIR}
VOLUME ${ADGUARD_DIR}
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}

View File

@@ -1,33 +1,32 @@
GIT_VERSION := $(shell git describe --abbrev=4 --dirty --always --tags)
GOPATH := $(shell go env GOPATH)
NATIVE_GOOS = $(shell unset GOOS; go env GOOS)
NATIVE_GOARCH = $(shell unset GOARCH; go env GOARCH)
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
mkfile_dir := $(patsubst %/,%,$(dir $(mkfile_path)))
STATIC := build/static/bundle.css build/static/bundle.js build/static/index.html
GOPATH := $(shell go env GOPATH)
JSFILES = $(shell find client -path client/node_modules -prune -o -type f -name '*.js')
STATIC = build/static/index.html
TARGET=AdGuardHome
.PHONY: all build clean
all: build
build: AdguardDNS coredns
build: $(TARGET)
$(STATIC):
yarn --cwd client install
yarn --cwd client run build-prod
client/node_modules: client/package.json client/package-lock.json
npm --prefix client install
touch client/node_modules
AdguardDNS: $(STATIC) *.go
echo mkfile_dir = $(mkfile_dir)
go get -v -d .
GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go get -v github.com/gobuffalo/packr/...
PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -o AdguardDNS
$(STATIC): $(JSFILES) client/node_modules
npm --prefix client run build-prod
coredns: coredns_plugin/*.go dnsfilter/*.go
echo mkfile_dir = $(mkfile_dir)
go get -v -d github.com/coredns/coredns
cd $(GOPATH)/src/github.com/coredns/coredns && grep -q 'dnsfilter:' plugin.cfg || sed -E -i.bak $$'s|^log:log|log:log\\\ndnsfilter:github.com/AdguardTeam/AdguardDNS/coredns_plugin|g' plugin.cfg
cd $(GOPATH)/src/github.com/coredns/coredns && GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go generate
cd $(GOPATH)/src/github.com/coredns/coredns && go get -v -d .
cd $(GOPATH)/src/github.com/coredns/coredns && go build -o $(mkfile_dir)/coredns
$(TARGET): $(STATIC) *.go coredns_plugin/*.go dnsfilter/*.go
GOPATH=$(GOPATH) GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)" -o $(TARGET)
clean:
rm -vf coredns AdguardDNS
$(MAKE) cleanfast
rm -rf build
rm -rf client/node_modules
cleanfast:
rm -f $(TARGET)

198
README.md
View File

@@ -1,66 +1,202 @@
# Self-hosted AdGuard DNS
&nbsp;
<p align="center">
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_home.svg" width="300px" alt="AdGuard Home" />
</p>
<h3 align="center">Privacy protection center for you and your devices</h3>
<p align="center">
Free and open source, powerful network-wide ads & trackers blocking DNS server.
</p>
AdGuard DNS is an ad-filtering DNS server with built-in phishing protection and optional family-friendly protection.
<p align="center">
<a href="https://adguard.com/">AdGuard.com</a> |
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
<a href="https://reddit.com/r/Adguard">Reddit</a> |
<a href="https://twitter.com/AdGuard">Twitter</a>
<br /><br />
<a href="https://travis-ci.org/AdguardTeam/AdGuardHome">
<img src="https://travis-ci.org/AdguardTeam/AdGuardHome.svg" alt="Build status" />
</a>
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
</a>
</p>
This repository describes how to set up and run your self-hosted instance of AdGuard DNS -- it comes with a web dashboard that can be accessed from browser to control the DNS server and change its settings, it also allows you to add your filters in both AdGuard and hosts format.
<br />
If this seems too complicated, you can always use AdGuard DNS servers that provide same functionality — https://adguard.com/en/adguard-dns/overview.html
<p align="center">
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_home.gif" width="800" />
</p>
<hr />
# AdGuard Home
AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.
## How does AdGuard Home work?
AdGuard Home operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
## How is this different from public AdGuard DNS servers?
Running your own AdGuard Home server allows you to do much more than using a public DNS server.
* Choose what exactly will the server block or not block;
* Monitor your network activity;
* Add your own custom filtering rules;
In the future, AdGuard Home is supposed to become more than just a DNS server.
## Installation
Go to https://github.com/AdguardTeam/AdguardDNS/releases and download the binaries for your platform:
### Mac
Download file `AdguardDNS_*_darwin_amd64.tar.gz`, then unpack it and follow [how to run](#How-to-run) instructions below.
### Linux
Download file `AdguardDNS_*_linux_amd64.tar.gz`, then unpack it and follow [how to run](#How-to-run) instructions below.
Download this file: [AdGuardHome_v0.91_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_MacOS.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
## How to build your own
### Linux 64-bit Intel
Download this file: [AdGuardHome_v0.91_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_amd64.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Linux 32-bit Intel
Download this file: [AdGuardHome_v0.91_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_386.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Raspberry Pi (32-bit ARM)
Download this file: [AdGuardHome_v0.91_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_arm.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
## How to run
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with `sudo` in terminal:
```bash
sudo ./AdGuardHome
```
Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service.
### Running without superuser
You can run AdGuard Home without superuser privileges, but you need to instruct it to use a different port rather than 53. You can do that by editing `AdGuardHome.yaml` and finding these two lines:
```yaml
coredns:
port: 53
```
You can change port 53 to anything above 1024 to avoid requiring superuser privileges.
If the file does not exist, create it in the same folder, type these two lines down and save.
### Additional configuration
Upon the first execution, a file named `AdGuardHome.yaml` will be created, with default values written in it. You can modify the file while your AdGuard Home service is not running. Otherwise, any changes to the file will be lost because the running program will overwrite them.
Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possible parameters that you can configure are listed below:
* `bind_host` — Web interface IP address to listen on
* `bind_port` — Web interface IP port to listen on
* `auth_name` — Web interface optional authorization username
* `auth_pass` — Web interface optional authorization password
* `coredns` — CoreDNS configuration section
* `port` — DNS server port to listen on
* `filtering_enabled` — Filtering of DNS requests based on filter lists
* `safebrowsing_enabled` — Filtering of DNS requests based on safebrowsing
* `safesearch_enabled` — Enforcing "Safe search" option for search engines, when possible
* `parental_enabled` — Parental control-based DNS requests filtering
* `parental_sensitivity` — Age group for parental control-based filtering, must be either 3, 10, 13 or 17
* `querylog_enabled` — Query logging (also used to calculate top 50 clients, blocked domains and requested domains for statistic purposes)
* `bootstrap_dns` — DNS server used for initial hostnames resolution in case if upstream is DoH or DoT with a hostname
* `upstream_dns` — List of upstream DNS servers
* `filters` — List of filters, each filter has the following values:
* `ID` - filter ID (must be unique)
* `url` — URL pointing to the filter contents (filtering rules)
* `enabled` — Current filter's status (enabled/disabled)
* `user_rules` — User-specified filtering rules
Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values.
## How to build from source
### Prerequisites
You will need:
* [go](https://golang.org/dl/)
* [node.js](https://nodejs.org/en/download/)
* [yarn](https://yarnpkg.com/en/docs/install)
You can either install it from these websites or use [brew.sh](https://brew.sh/) if you're on Mac:
* [go](https://golang.org/dl/) v1.11 or later.
* [node.js](https://nodejs.org/en/download/)
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:
```bash
brew install go node yarn
brew install go node
```
### Building
Open Terminal and execute these commands:
```bash
git clone https://github.com/AdguardTeam/AdguardDNS
cd AdguardDNS
git clone https://github.com/AdguardTeam/AdGuardHome
cd AdGuardHome
make
```
## How to run
### How to update translations
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with sudo:
```bash
sudo ./AdguardDNS
Before updating translations you need to install dependencies:
```
cd scripts/translations
npm install
```
Now open the browser and point it to http://localhost:3000/ to control AdGuard DNS server.
Create file `oneskyapp.json` in `scripts/translations` folder.
## Running without superuser
You can run it without superuser privileges, but you need to instruct it to use other port rather than 53. You can do that by opening `AdguardDNS.yaml` and adding this line:
```yaml
coredns:
port: 53535
Example of `oneskyapp.json`
```
{
"url": "https://platform.api.onesky.io/1/projects/",
"projectId": <PROJECT ID>,
"apiKey": <API KEY>,
"secretKey": <SECRET KEY>
}
```
If the file does not exist, create it and put these two lines down.
#### Upload translations
```
node upload.js
```
#### Download translations
```
node download.js
```
## Contributing
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdguardDNS/pulls
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations
Here is a direct link to AdGuard Home project: http://translate.adguard.com/collaboration/project?id=153384
## Reporting issues
If you come across any problem, or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdguardDNS/issues) and click on the New issue button.
If you run into any problem or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdGuardHome/issues) and click on the `New issue` button.
## Acknowledgments
This software wouldn't have been possible without:
* [Go](https://golang.org/dl/) and it's libraries:
* [CoreDNS](https://coredns.io)
* [packr](https://github.com/gobuffalo/packr)
* [gcache](https://github.com/bluele/gcache)
* [miekg's dns](https://github.com/miekg/dns)
* [go-yaml](https://github.com/go-yaml/yaml)
* [Node.js](https://nodejs.org/) and it's libraries:
* [React.js](https://reactjs.org)
* [Tabler](https://github.com/tabler/tabler)
* And many more node.js packages.
* [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
For a full list of all node.js packages in use, please take a look at [client/package.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json) file.

228
app.go
View File

@@ -1,6 +1,7 @@
package main
import (
"bufio"
"fmt"
"log"
"net"
@@ -10,20 +11,31 @@ import (
"strconv"
"github.com/gobuffalo/packr"
"golang.org/x/crypto/ssh/terminal"
)
// VersionString will be set through ldflags, contains current version
var VersionString = "undefined"
func main() {
log.Printf("AdGuard DNS web interface backend, version %s\n", VersionString)
log.Printf("AdGuard Home web interface backend, version %s\n", VersionString)
box := packr.NewBox("build/static")
{
executable, err := os.Executable()
if err != nil {
panic(err)
}
config.ourBinaryDir = filepath.Dir(executable)
executableName := filepath.Base(executable)
if executableName == "AdGuardHome" {
// Binary build
config.ourBinaryDir = filepath.Dir(executable)
} else {
// Most likely we're debugging -- using current working directory in this case
workDir, _ := os.Getwd()
config.ourBinaryDir = workDir
}
log.Printf("Current working directory is %s", config.ourBinaryDir)
}
// config can be specified, which reads options from there, but other command line flags have to override config values
@@ -96,11 +108,19 @@ func main() {
if configFilename != nil {
config.ourConfigFilename = *configFilename
}
// parse from config file
err := parseConfig()
err := askUsernamePasswordIfPossible()
if err != nil {
log.Fatal(err)
}
// parse from config file
err = parseConfig()
if err != nil {
log.Fatal(err)
}
// override bind host/port from the console
if bindHost != nil {
config.BindHost = *bindHost
}
@@ -109,20 +129,212 @@ func main() {
}
}
err := writeConfig()
// Eat all args so that coredns can start happily
if len(os.Args) > 1 {
os.Args = os.Args[:1]
}
// Do the upgrade if necessary
err := upgradeConfig()
if err != nil {
log.Fatal(err)
}
// Save the updated config
err = writeConfig()
if err != nil {
log.Fatal(err)
}
// Load filters from the disk
for i := range config.Filters {
filter := &config.Filters[i]
err = filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
}
}
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
runStatsCollectors()
runFilterRefreshers()
runFiltersUpdatesTimer()
http.Handle("/", http.FileServer(box))
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
registerControlHandlers()
err = startDNSServer()
if err != nil {
log.Fatal(err)
}
URL := fmt.Sprintf("http://%s", address)
log.Println("Go to " + URL)
log.Fatal(http.ListenAndServe(address, nil))
}
func getInput() (string, error) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
text := scanner.Text()
err := scanner.Err()
return text, err
}
func promptAndGet(prompt string) (string, error) {
for {
fmt.Printf(prompt)
input, err := getInput()
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
return "", err
}
if len(input) != 0 {
return input, nil
}
// try again
}
}
func promptAndGetPassword(prompt string) (string, error) {
for {
fmt.Printf(prompt)
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
fmt.Printf("\n")
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
return "", err
}
if len(password) != 0 {
return string(password), nil
}
// try again
}
}
func askUsernamePasswordIfPossible() error {
configfile := config.ourConfigFilename
if !filepath.IsAbs(configfile) {
configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
}
_, err := os.Stat(configfile)
if !os.IsNotExist(err) {
// do nothing, file exists
trace("File %s exists, won't ask for password", configfile)
return nil
}
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
return nil // do nothing
}
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
return nil // do nothing
}
fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
yesno, err := promptAndGet("Please type 'yes' or 'no': ")
if err != nil {
return err
}
if yesno[0] != 'y' && yesno[0] != 'Y' {
return nil
}
username, err := promptAndGet("Please enter the username: ")
if err != nil {
return err
}
password, err := promptAndGetPassword("Please enter the password: ")
if err != nil {
return err
}
password2, err := promptAndGetPassword("Please enter password again: ")
if err != nil {
return err
}
if password2 != password {
fmt.Printf("Passwords do not match! Aborting\n")
os.Exit(1)
}
config.AuthName = username
config.AuthPass = password
return nil
}
// Performs necessary upgrade operations if needed
func upgradeConfig() error {
if config.SchemaVersion == SchemaVersion {
// No upgrade, do nothing
return nil
}
if config.SchemaVersion > SchemaVersion {
// Unexpected -- the config file is newer than we expect
return fmt.Errorf("configuration file is supposed to be used with a newer version of AdGuard Home, schema=%d", config.SchemaVersion)
}
// Perform upgrade operations for each consecutive version upgrade
for oldVersion, newVersion := config.SchemaVersion, config.SchemaVersion+1; newVersion <= SchemaVersion; {
err := upgradeConfigSchema(oldVersion, newVersion)
if err != nil {
log.Fatal(err)
}
// Increment old and new versions
oldVersion++
newVersion++
}
// Save the current schema version
config.SchemaVersion = SchemaVersion
return nil
}
// Upgrade from oldVersion to newVersion
func upgradeConfigSchema(oldVersion int, newVersion int) error {
if oldVersion == 0 && newVersion == 1 {
log.Printf("Updating schema from %d to %d", oldVersion, newVersion)
// The first schema upgrade:
// Added "ID" field to "filter" -- we need to populate this field now
// Added "config.ourDataDir" -- where we will now store filters contents
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy
// Set the filter ID
log.Printf("Seting ID=%d for filter %s", NextFilterId, filter.URL)
filter.ID = NextFilterId
NextFilterId++
// Forcibly update the filter
_, err := filter.update(true)
if err != nil {
log.Fatal(err)
}
// Saving it to the filters dir now
err = filter.save()
if err != nil {
log.Fatal(err)
}
}
// No more "dnsfilter.txt", filters are now loaded from config.ourDataDir/filters/
dnsFilterPath := filepath.Join(config.ourBinaryDir, "dnsfilter.txt")
_, err := os.Stat(dnsFilterPath)
if !os.IsNotExist(err) {
log.Printf("Deleting %s as we don't need it anymore", dnsFilterPath)
err = os.Remove(dnsFilterPath)
if err != nil {
log.Printf("Cannot remove %s due to %s", dnsFilterPath, err)
}
}
}
return nil
}

9
client/.eslintrc vendored
View File

@@ -13,6 +13,13 @@
"commonjs": true
},
"settings": {
"react": {
"pragma": "React",
"version": "16.4"
}
},
"rules": {
"indent": ["error", 4, {
"SwitchCase": 1,
@@ -43,6 +50,6 @@
}],
"no-console": ["warn", { "allow": ["warn", "error"] }],
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"import/prefer-default-export": "off",
"import/prefer-default-export": "off"
}
}

16687
client/package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

16
client/package.json vendored
View File

@@ -3,31 +3,36 @@
"version": "0.1.0",
"private": true,
"scripts": {
"build-dev": "NODE_ENV=development webpack --config webpack.dev.js",
"watch": "NODE_ENV=development webpack --config webpack.dev.js --watch",
"build-prod": "NODE_ENV=production webpack --config webpack.prod.js",
"build-dev": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js",
"watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js --watch",
"build-prod": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.prod.js",
"lint": "eslint frontend/"
},
"dependencies": {
"@nivo/line": "^0.42.1",
"@nivo/line": "^0.49.1",
"axios": "^0.18.0",
"classnames": "^2.2.6",
"date-fns": "^1.29.0",
"file-saver": "^1.3.8",
"i18next": "^12.0.0",
"i18next-browser-languagedetector": "^2.2.3",
"lodash": "^4.17.10",
"nanoid": "^1.2.3",
"prop-types": "^15.6.1",
"react": "^16.4.0",
"react-click-outside": "^3.0.1",
"react-dom": "^16.4.0",
"react-i18next": "^8.2.0",
"react-modal": "^3.4.5",
"react-redux": "^5.0.7",
"react-redux-loading-bar": "^4.0.7",
"react-router-dom": "^4.2.2",
"react-table": "^6.8.6",
"react-transition-group": "^2.4.0",
"redux": "^4.0.0",
"redux-actions": "^2.4.0",
"redux-thunk": "^2.3.0",
"svg-url-loader": "^2.3.2",
"tabler-react": "^1.10.0",
"whatwg-fetch": "2.0.3"
},
"devDependencies": {
@@ -41,6 +46,7 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-runtime": "6.26.0",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^1.1.11",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico">
<title>AdGuard DNS</title>
<title>AdGuard Home</title>
</head>
<body>
<noscript>

View File

@@ -0,0 +1,128 @@
{
"back": "Back",
"dashboard": "Dashboard",
"settings": "Settings",
"filters": "Filters",
"query_log": "Query Log",
"faq": "FAQ",
"version": "version",
"address": "address",
"on": "ON",
"off": "OFF",
"copyright": "Copyright",
"homepage": "Homepage",
"report_an_issue": "Report an issue",
"enable_protection": "Enable protection",
"enabled_protection": "Enabled protection",
"disable_protection": "Disable protection",
"disabled_protection": "Disabled protection",
"refresh_statics": "Refresh statistics",
"dns_query": "DNS Queries",
"blocked_by": "Blocked by",
"stats_malware_phishing": "Blocked malware\/phishing",
"stats_adult": "Blocked adult websites",
"stats_query_domain": "Top queried domains",
"for_last_24_hours": "for the last 24 hours",
"no_domains_found": "No domains found",
"requests_count": "Requests count",
"top_blocked_domains": "Top blocked domains",
"top_clients": "Top clients",
"no_clients_found": "No clients found",
"general_statistics": "General statistics",
"number_of_dns_query_24_hours": "A number of DNS quieries processed for the last 24 hours",
"number_of_dns_query_blocked_24_hours": "A number of DNS requests blocked by adblock filters and hosts blocklists",
"number_of_dns_query_blocked_24_hours_by_sec": "A number of DNS requests blocked by the AdGuard browsing security module",
"number_of_dns_query_blocked_24_hours_adult": "A number of adult websites blocked",
"enforced_save_search": "Enforced safe search",
"number_of_dns_query_to_safe_search": "A number of DNS requests to search engines for which Safe Search was enforced",
"average_processing_time": "Average processing time",
"average_processing_time_hint": "Average time in milliseconds on processing a DNS request",
"block_domain_use_filters_and_hosts": "Block domains using filters and hosts files",
"filters_block_toggle_hint": "You can setup blocking rules in the <a href='#filters'>Filters<\/a> settings.",
"use_adguard_browsing_sec": "Use AdGuard browsing security web service",
"use_adguard_browsing_sec_hint": "AdGuard Home will check if domain is blacklisted by the browsing security web service. It will use privacy-friendly lookup API to perform the check: only a short prefix of the domain name SHA256 hash is sent to the server.",
"use_adguard_parental": "Use AdGuard parental control web service",
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
"enforce_safe_search": "Enforce safe search",
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, and Yandex.",
"no_servers_specified": "No servers specified",
"no_settings": "No settings",
"general_settings": "General settings",
"upstream_dns": "Upstream DNS servers",
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream. Use tls:\/\/ prefix for DNS over TLS servers.",
"test_upstream_btn": "Test upstreams",
"apply_btn": "Apply",
"disabled_filtering_toast": "Disabled filtering",
"enabled_filtering_toast": "Enabled filtering",
"disabled_safe_browsing_toast": "Disabled safebrowsing",
"enabled_safe_browsing_toast": "Enabled safebrowsing",
"disabled_parental_toast": "Disabled parental control",
"enabled_parental_toast": "Enabled parental control",
"disabled_safe_search_toast": "Disabled safe search",
"enabled_save_search_toast": "Enabled safe search",
"enabled_table_header": "Enabled",
"name_table_header": "Name",
"filter_url_table_header": "Filter URL",
"rules_count_table_header": "Rules count",
"last_time_updated_table_header": "Last time updated",
"actions_table_header": "Actions",
"delete_table_action": "Delete",
"filters_and_hosts": "Filters and hosts blocklists",
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
"no_filters_added": "No filters added",
"add_filter_btn": "Add filter",
"cancel_btn": "Cancel",
"enter_name_hint": "Enter name",
"enter_url_hint": "Enter URL",
"check_updates_btn": "Check updates",
"new_filter_btn": "New filter subscription",
"enter_valid_filter_url": "Enter a valid URL to a filter subscription or a hosts file.",
"custom_filter_rules": "Custom filtering rules",
"custom_filter_rules_hint": "Enter one rule on a line. You can use either adblock rules or hosts files syntax.",
"examples_title": "Examples",
"example_meaning_filter_block": "block access to the example.org domain and all its subdomains",
"example_meaning_filter_whitelist": "unblock access to the example.org domain and all its subdomains",
"example_meaning_host_block": "AdGuard Home will now return 127.0.0.1 address for the example.org domain (but not its subdomains).",
"example_comment": "! Here goes a comment",
"example_comment_meaning": "just a comment",
"example_comment_hash": "# Also a comment",
"example_upstream_regular": "regular DNS (over UDP)",
"example_upstream_dot": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
"example_upstream_tcp": "regular DNS (over TCP)",
"all_filters_up_to_date_toast": "All filters are already up-to-date",
"updated_upstream_dns_toast": "Updated the upstream DNS servers",
"dns_test_ok_toast": "Specified DNS servers are working correctly",
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
"unblock_btn": "Unblock",
"block_btn": "Block",
"time_table_header": "Time",
"domain_name_table_header": "Domain name",
"type_table_header": "Type",
"response_table_header": "Response",
"empty_response_status": "Empty",
"show_all_filter_type": "Show all",
"show_filtered_type": "Show filtered",
"no_logs_found": "No logs found",
"disabled_log_btn": "Disable log",
"download_log_file_btn": "Download log file",
"refresh_btn": "Refresh",
"enabled_log_btn": "Enable log",
"last_dns_queries": "Last 5000 DNS queries",
"previous_btn": "Previous",
"next_btn": "Next",
"loading_table_status": "Loading...",
"page_table_footer_text": "Page",
"of_table_footer_text": "of",
"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",
"query_log_disabled_toast": "Query log disabled",
"query_log_enabled_toast": "Query log enabled",
"source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category",
"rule_label": "Rule",
"filter_label": "Filter"
}

View File

@@ -0,0 +1,124 @@
{
"back": "Atr\u00e1s",
"dashboard": "Tablero de rendimiento",
"settings": "Ajustes",
"filters": "Filtros",
"query_log": "Log de consulta",
"faq": "FAQ",
"version": "versi\u00f3n",
"address": "direcci\u00f3n",
"on": "Activado",
"off": "Desactivado",
"copyright": "Copyright",
"homepage": "P\u00e1gina de inicio",
"report_an_issue": "Reportar el error",
"enable_protection": "Activar la protecci\u00f3n",
"enabled_protection": "Protecci\u00f3n activada",
"disable_protection": "Desactivar protecci\u00f3n",
"disabled_protection": "Protecci\u00f3n desactivada",
"refresh_statics": "Renovas estad\u00edsticas",
"dns_query": "DNS Queries",
"blocked_by": "Bloqueado por",
"stats_malware_phishing": "Malware\/phishing bloqueado",
"stats_adult": "Contenido adulto bloqueado",
"stats_query_domain": "Dominios m\u00e1s solicitados",
"for_last_24_hours": "en las \u00faltimas 24 horas",
"no_domains_found": "No dominios encontrados",
"requests_count": "N\u00famero de solicitudes",
"top_blocked_domains": "Dominios m\u00e1s bloqueados",
"top_clients": "Clientes m\u00e1s populares",
"no_clients_found": "No hay clientes",
"general_statistics": "Estad\u00edstica general",
"number_of_dns_query_24_hours": "Una serie de consultas DNS procesadas durante las \u00faltimas 24 horas",
"number_of_dns_query_blocked_24_hours": "El n\u00famero de solicitudes de DNS bloqueadas por los filtros y listas de block",
"number_of_dns_query_blocked_24_hours_by_sec": "El n\u00famero de solicitudes de DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "El n\u00famero de sitios para adultos bloqueados",
"enforced_save_search": "B\u00fasqueda segura forzada",
"number_of_dns_query_to_safe_search": "Una serie de solicitudes de DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la B\u00fasqueda Segura",
"average_processing_time": "Tiempo medio de tratamiento",
"average_processing_time_hint": "Tiempo medio en milisegundos al procesar una solicitud DNS",
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en los ajustes <a href='#filters'>Filtros<\/a>.",
"use_adguard_browsing_sec": "Usar el servicio web de Seguridad de navegaci\u00f3n de AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 la API de b\u00fasqueda de privacidad para realizar la comprobaci\u00f3n: s\u00f3lo se env\u00eda al servidor un prefijo corto del hash del nombre de dominio SHA256.",
"use_adguard_parental": "Usar Control Parental de AdGuard ",
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API de privacidad que el servicio web de seguridad de navegaci\u00f3n.",
"enforce_safe_search": "Enforzar b\u00fasqueda segura",
"enforce_save_search_hint": "AdGuard Home puede hacer cumplir la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, Youtube, Bing y Yandex.",
"no_servers_specified": "No hay servidores especificados",
"no_settings": "No hay ajustes",
"general_settings": "Ajustes generales",
"upstream_dns": "Servidores DNS upstream",
"upstream_dns_hint": "Si mantiene este campo vac\u00edo, AdGuard Home utilizar\u00e1 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> como upstream. Utilice el prefijo tls:\/\/ para DNS sobre servidores TLS.",
"test_upstream_btn": "Probar upstream",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Desactivar filtrado",
"enabled_filtering_toast": "Filtrado activado",
"disabled_safe_browsing_toast": "Navegaci\u00f3n segura desactivada",
"enabled_safe_browsing_toast": "Navegaci\u00f3n segura activada",
"disabled_parental_toast": "Control parental desactivado",
"enabled_parental_toast": "Control parental activado",
"disabled_safe_search_toast": "B\u00fasqueda segura desactivada",
"enabled_save_search_toast": "B\u00fasqueda segura activada",
"enabled_table_header": "Activado",
"name_table_header": "Nombre",
"filter_url_table_header": "Filtro URL",
"rules_count_table_header": "N\u00famero de reglas",
"last_time_updated_table_header": "\u00daltima actualizaci\u00f3n",
"actions_table_header": "Acciones",
"delete_table_action": "Eliminar",
"filters_and_hosts": "Filtros y hosts blocklists",
"filters_and_hosts_hint": "AdGuard Home entiende reglas b\u00e1sicas de bloqueo y la sintaxis de los archivos de hosts.",
"no_filters_added": "No hay filtros agregados",
"add_filter_btn": "Agregar filtro",
"cancel_btn": "Cancelar",
"enter_name_hint": "Ingresar nombre",
"enter_url_hint": "Ingresar URL",
"check_updates_btn": "Revisar si hay actualizaciones",
"new_filter_btn": "Nueva suscripci\u00f3n al filtro",
"enter_valid_filter_url": "Ingrese el URL v\u00e1lido para suscribirse o un archivo hosts.",
"custom_filter_rules": "Personalizar reglas del filtrado",
"custom_filter_rules_hint": "Introduzca una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o sintaxis de archivos de hosts.",
"examples_title": "Ejemplos",
"example_meaning_filter_block": "bloquear acceso para el dominio ejemplo.org\ny todos sus subdominios ",
"example_meaning_filter_whitelist": "desbloquear el acceso para el dominio ejemplo.org y sus subdominios",
"example_meaning_host_block": "AdGuard Home regresar\u00e1 la direcci\u00f3n 127.0.0.1 para el dominio ejemplo.org (pero no para sus subdominios).",
"example_comment": "! Aqu\u00ed va el comentario",
"example_comment_meaning": "solo un comentario",
"example_comment_hash": "# Tambi\u00e9n un comentario",
"all_filters_up_to_date_toast": "Todos los filtros son actualizados",
"updated_upstream_dns_toast": "Servidores DNS upstream son actualizados",
"dns_test_ok_toast": "Servidores DNS especificados funcionan correctamente",
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no puede ser usado, por favor, revise si lo ha escrito correctamente",
"unblock_btn": "Desbloquear",
"block_btn": "Bloquear",
"time_table_header": "Tiempo",
"domain_name_table_header": "Nombre de dominio",
"type_table_header": "Tipo",
"response_table_header": "Respuesta",
"empty_response_status": "Vac\u00edo",
"show_all_filter_type": "Mostrar todo",
"show_filtered_type": "Mostrar filtrados",
"no_logs_found": "No se han encontrado registros",
"disabled_log_btn": "Desactivar registro",
"download_log_file_btn": "Descargar el archivo de registro",
"refresh_btn": "Renovar",
"enabled_log_btn": "Activar registro",
"last_dns_queries": "\u00daltimas 500 solicitudes de DNS",
"previous_btn": "Anterior",
"next_btn": "Siguiente",
"loading_table_status": "Cargando...",
"page_table_footer_text": "P\u00e1gina",
"of_table_footer_text": "de",
"rows_table_footer_text": "filas",
"updated_custom_filtering_toast": "Actualizadas las reglas de filtrado personalizadas",
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizadas",
"rule_added_to_custom_filtering_toast": "Regla a\u00f1adida a las reglas de filtrado personalizadas",
"query_log_disabled_toast": "Log de consulta desactivado",
"query_log_enabled_toast": "Log de consulta activado",
"source_label": "Fuente",
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
"category_label": "Categor\u00eda",
"rule_label": "Regla",
"filter_label": "Filtro"
}

View File

@@ -0,0 +1,124 @@
{
"back": "Retour",
"dashboard": "Tableau de bord",
"settings": "Param\u00e8tres",
"filters": "Filtres",
"query_log": "Journal des requ\u00eates\u001c",
"faq": "FAQ",
"version": "version",
"address": "addresse",
"on": "Activ\u00e9",
"off": "\u00c9teint",
"copyright": "Copyright",
"homepage": "Page d'accueil",
"report_an_issue": "Signaler un probl\u00e8me",
"enable_protection": "Activer la protection",
"enabled_protection": "Protection activ\u00e9e",
"disable_protection": "D\u00e9sactiver la protection",
"disabled_protection": "Protection d\u00e9sactiv\u00e9e",
"refresh_statics": "Renouveler les statistiques",
"dns_query": "Requ\u00eates\u001c DNS",
"blocked_by": "Bloqu\u00e9 par",
"stats_malware_phishing": "Tentative de malware\/hamme\u00e7onnage bloqu\u00e9e",
"stats_adult": "Sites \u00e0 contenu adulte bloqu\u00e9s",
"stats_query_domain": "Domaines les plus recherch\u00e9s",
"for_last_24_hours": "pendant les derni\u00e8res 24 heures",
"no_domains_found": "Pas de domaines trouv\u00e9s",
"requests_count": "Nombre de requ\u00eates",
"top_blocked_domains": "Les domaines les plus fr\u00e9quemment bloqu\u00e9s",
"top_clients": "Meilleurs clients",
"no_clients_found": "Pas de clients trouv\u00e9s",
"general_statistics": "Statistiques g\u00e9n\u00e9rales",
"number_of_dns_query_24_hours": "Un nombre de requ\u00eates DNS quieries trait\u00e9es pendant les 24 heures derni\u00e8res",
"number_of_dns_query_blocked_24_hours": "Un nombre de requ\u00eates DNS bloqu\u00e9es par les filtres adblock et les listes de blocage des hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Un nombre de requ\u00eates DNS bloqu\u00e9es par le module S\u00e9curit\u00e9 de navigation d'AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Un nombre de sites \u00e0 contenu adulte bloqu\u00e9s",
"enforced_save_search": "Recherche s\u00e9curis\u00e9e renforc\u00e9e",
"number_of_dns_query_to_safe_search": "Un nombre de requ\u00eates DNS faites avec la Recherche securis\u00e9e",
"average_processing_time": "Temps moyen de traitement",
"average_processing_time_hint": "Temps moyen (en millisecondes) de traitement d'une requ\u00eate DNS",
"block_domain_use_filters_and_hosts": "Bloquez les domaines \u00e0 l'aide des filtres et fichiers hosts",
"filters_block_toggle_hint": "Vous pouvez configurer les r\u00e8gles de filtrage dans les param\u00e8tres des <a href='#filters'>Filtres<\/a>.",
"use_adguard_browsing_sec": "Utilisez le service S\u00e9curit\u00e9 de navigation d'AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home va v\u00e9rifier si le domaine est dans la liste noire du service de s\u00e9curit\u00e9 de navigation. Pour cela il va utiliser un lookup API discret : le pr\u00e9fixe court du hash du nom de domaine SHA256 sera envoy\u00e9 au serveur.",
"use_adguard_parental": "Utiliser le contr\u00f4le parental d'AdGuard",
"use_adguard_parental_hint": "AdGuard Home va v\u00e9rifier s'il y a du contenu pour adultes sur le domaine. Ce sera fait par aide du m\u00eame API discret que celui utilis\u00e9 par le service de S\u00e9curit\u00e9 de navigation.",
"enforce_safe_search": "Renforcer la recherche s\u00e9curis\u00e9e",
"enforce_save_search_hint": "AdGuard Home peut renforcer la Recherche s\u00e9curis\u00e9e dans les moteurs de recherche suivants : Google, Youtube, Bing et Yandex.",
"no_servers_specified": "Pas de serveurs sp\u00e9cifi\u00e9s",
"no_settings": "Pas de param\u00e8tres",
"general_settings": "Param\u00e8tres g\u00e9n\u00e9raux",
"upstream_dns": "Serveurs DNS upstream",
"upstream_dns_hint": "Si vous laisses ce champ vide, AdGuard Home va utiliser <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> somme upstream. Utilisez le pr\u00e9fixe tls:\/\/ pour DNS via les serveurs TLS .",
"test_upstream_btn": "Tester les upstreams",
"apply_btn": "Appliquer",
"disabled_filtering_toast": "Filtrage d\u00e9sactiv\u00e9",
"enabled_filtering_toast": "Filtrage activ\u00e9",
"disabled_safe_browsing_toast": "Surfing s\u00e9curis\u00e9 d\u00e9sactiv\u00e9",
"enabled_safe_browsing_toast": "Surfing s\u00e9curis\u00e9 activ\u00e9",
"disabled_parental_toast": "Contr\u00f4le parental d\u00e9sactiv\u00e9",
"enabled_parental_toast": "Contr\u00f4le parental activ\u00e9",
"disabled_safe_search_toast": "Recherche s\u00e9curis\u00e9e d\u00e9sactiv\u00e9e",
"enabled_save_search_toast": "Recherche s\u00e9curis\u00e9e activ\u00e9e",
"enabled_table_header": "Activ\u00e9",
"name_table_header": "Nom",
"filter_url_table_header": "URL du filtre",
"rules_count_table_header": "Nombre des r\u00e8gles",
"last_time_updated_table_header": "Derni\u00e8re mise \u00e0 jour",
"actions_table_header": "Actions",
"delete_table_action": "Supprimer",
"filters_and_hosts": "Listes de blocage des filtres et hosts",
"filters_and_hosts_hint": "AdGuard Home comprend les r\u00e8gles basiques de blocage ainsi que la syntaxe des fichiers hosts.",
"no_filters_added": "Aucun filtre ajout\u00e9",
"add_filter_btn": "Ajouter filtre",
"cancel_btn": "Annuler",
"enter_name_hint": "Saisir nom",
"enter_url_hint": "Saisir URL",
"check_updates_btn": "V\u00e9rifier les mises \u00e0 jour",
"new_filter_btn": "Abonnement \u00e0 un nouveau filtre",
"enter_valid_filter_url": "Saisir un URL valide pour s'abonner au filtre ou \u00e0 un fichier host.",
"custom_filter_rules": "R\u00e8gles de filtrage d'utilisateur",
"custom_filter_rules_hint": "Saisissez la r\u00e8gle en une ligne. C'est possible d'utiliser les r\u00e8gles de blocage ou la syntaxe des fichiers hosts.",
"examples_title": "Exemples",
"example_meaning_filter_block": "bloquer l'acc\u00e9s au domaine exemple.org et \u00e0 tous ses sous-domaines",
"example_meaning_filter_whitelist": "d\u00e9bloquer l'acc\u00e9s au domaine exemple.org et \u00e0 tous ses sous-domaines",
"example_meaning_host_block": "AdGuard Home va retourner l'adresse 127.0.0.1 au domaine example.org (mais pas aux sous-domaines).",
"example_comment": "! Voici comment ajouter une d\u00e9scription",
"example_comment_meaning": "commentaire",
"example_comment_hash": "# Et comme \u00e7a aussi on peut laisser des commentaires",
"all_filters_up_to_date_toast": "Tous les filtres sont mis \u00e0 jour",
"updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis \u00e0 jour",
"dns_test_ok_toast": "Les serveurs DNS sp\u00e9cifi\u00e9s fonctionnent de mani\u00e8re incorrecte",
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
"unblock_btn": "D\u00e9bloquer",
"block_btn": "Bloquer",
"time_table_header": "Temps",
"domain_name_table_header": "Nom de domaine",
"type_table_header": "Type",
"response_table_header": "R\u00e9ponse",
"empty_response_status": "Vide",
"show_all_filter_type": "Montrer tout",
"show_filtered_type": "Montrer les sites filtr\u00e9s",
"no_logs_found": "Aucun journal trouv\u00e9",
"disabled_log_btn": "D\u00e9sactiver le journal",
"download_log_file_btn": "T\u00e9l\u00e9charger le fichier de journal",
"refresh_btn": "Actualiser",
"enabled_log_btn": "Activer le journal",
"last_dns_queries": "5000 derni\u00e8res requ\u00eates DNS",
"previous_btn": "Pr\u00e9c\u00e9dent",
"next_btn": "Suivant",
"loading_table_status": "Chargement en cours ...",
"page_table_footer_text": "Page",
"of_table_footer_text": "de",
"rows_table_footer_text": "lignes",
"updated_custom_filtering_toast": "R\u00e8gles de filtrage d'utilisateur mises \u00e0 jour",
"rule_removed_from_custom_filtering_toast": "R\u00e8gle retir\u00e9e des r\u00e8gles d'utilisateur",
"rule_added_to_custom_filtering_toast": "R\u00e8gle ajout\u00e9e aux r\u00e8gles d'utilisateur",
"query_log_disabled_toast": "Journal de requ\u00eates d\u00e9sactiv\u00e9",
"query_log_enabled_toast": "Journal de requ\u00eates activ\u00e9",
"source_label": "Source",
"found_in_known_domain_db": "Trouv\u00e9 dans la base de donn\u00e9es des domaines connus",
"category_label": "Cat\u00e9gorie",
"rule_label": "R\u00e8gle",
"filter_label": "Filtre"
}

View File

@@ -0,0 +1,124 @@
{
"back": "\u623b\u308b",
"dashboard": "\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9",
"settings": "\u8a2d\u5b9a",
"filters": "\u30d5\u30a3\u30eb\u30bf",
"query_log": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0",
"faq": "FAQ",
"version": "\u30d0\u30fc\u30b8\u30e7\u30f3",
"address": "\u30a2\u30c9\u30ec\u30b9",
"on": "ON",
"off": "OFF",
"copyright": "\u8457\u4f5c\u6a29",
"homepage": "\u30db\u30fc\u30e0\u30da\u30fc\u30b8",
"report_an_issue": "\u554f\u984c\u3092\u5831\u544a\u3059\u308b",
"enable_protection": "\u4fdd\u8b77\u3092\u6709\u52b9\u306b\u3059\u308b",
"enabled_protection": "\u4fdd\u8b77\u6709\u52b9",
"disable_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3059\u308b",
"disabled_protection": "\u4fdd\u8b77\u7121\u52b9",
"refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u66f4\u65b0\u3059\u308b",
"dns_query": "DNS\u30af\u30a8\u30ea",
"blocked_by": "\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u6e08\u307f",
"stats_malware_phishing": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30de\u30eb\u30a6\u30a7\u30a2\uff0f\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0",
"stats_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8",
"stats_query_domain": "\u983b\u7e41\u306b\u554f\u3044\u5408\u308f\u305b\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
"for_last_24_hours": "\u904e\u53bb24\u6642\u9593\u5185",
"no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
"requests_count": "\u30ea\u30af\u30a8\u30b9\u30c8\u6570",
"top_blocked_domains": "\u6700\u3082\u30d6\u30ed\u30c3\u30af\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
"top_clients": "\u30c8\u30c3\u30d7\u30af\u30e9\u30a4\u30a2\u30f3\u30c8",
"no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
"general_statistics": "\u4e00\u822c\u7d71\u8a08",
"number_of_dns_query_24_hours": "\u904e\u53bb24\u6642\u9593\u306b\u51e6\u7406\u3055\u308c\u305fDNS\u30af\u30a8\u30ea\u306e\u6570",
"number_of_dns_query_blocked_24_hours": "\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30e2\u30b8\u30e5\u30fc\u30eb\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"number_of_dns_query_blocked_24_hours_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u6210\u4eba\u5411\u3051\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306e\u6570",
"enforced_save_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u9069\u7528\u6e08\u307f",
"number_of_dns_query_to_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u304c\u9069\u7528\u3055\u308c\u305f\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u306b\u5bfe\u3059\u308bDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"average_processing_time": "\u5e73\u5747\u51e6\u7406\u6642\u9593",
"average_processing_time_hint": "DNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u51e6\u7406\u306b\u304b\u304b\u308b\u5e73\u5747\u6642\u9593\uff08\u30df\u30ea\u79d2\u5358\u4f4d\uff09",
"block_domain_use_filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3057\u3066\u30c9\u30e1\u30a4\u30f3\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
"filters_block_toggle_hint": "<a href='#filters'>\u30d5\u30a3\u30eb\u30bf<\/a>\u306e\u8a2d\u5b9a\u3067\u30d6\u30ed\u30c3\u30ad\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002",
"use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
"use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u30fc\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002",
"use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30ebWeb\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
"use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002",
"enforce_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3059\u308b",
"enforce_save_search_hint": "AdGuard Home\u306f\u3001Google\u3001Youtube\u3001Bing\u3001Yandex\u306e\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u3067\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3067\u304d\u307e\u3059\u3002",
"no_servers_specified": "\u30b5\u30fc\u30d0\u30fc\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093",
"no_settings": "\u8a2d\u5b9a\u306a\u3057",
"general_settings": "\u4e00\u822c\u8a2d\u5b9a",
"upstream_dns": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc",
"upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u7a7a\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306f<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u3092\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u3068\u3057\u3066\u4f7f\u7528\u3057\u307e\u3059\u3002 TLS\u30b5\u30fc\u30d0\u30fc\u7d4c\u7531\u306eDNS\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
"test_upstream_btn": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u30b5\u30fc\u30d0\u30fc\u30c6\u30b9\u30c8",
"apply_btn": "\u9069\u7528",
"disabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u7121\u52b9",
"enabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6709\u52b9",
"disabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u7121\u52b9",
"enabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u6709\u52b9",
"disabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7121\u52b9",
"enabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u6709\u52b9",
"disabled_safe_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u7121\u52b9",
"enabled_save_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u6709\u52b9",
"enabled_table_header": "\u6709\u52b9",
"name_table_header": "\u540d\u79f0",
"filter_url_table_header": "\u30d5\u30a3\u30eb\u30bf\u306eURL",
"rules_count_table_header": "\u30eb\u30fc\u30eb\u6570",
"last_time_updated_table_header": "\u6700\u7d42\u66f4\u65b0\u65e5",
"actions_table_header": "\u64cd\u4f5c",
"delete_table_action": "\u524a\u9664",
"filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8",
"filters_and_hosts_hint": "AdGuard Home\u306f\u3001\u57fa\u672c\u7684\u306a\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u69cb\u6587\u3092\u7406\u89e3\u3057\u3066\u3044\u307e\u3059\u3002",
"no_filters_added": "\u30d5\u30a3\u30eb\u30bf\u306f\u8ffd\u52a0\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f",
"add_filter_btn": "\u30d5\u30a3\u30eb\u30bf\u3092\u8ffd\u52a0\u3059\u308b",
"cancel_btn": "\u30ad\u30e3\u30f3\u30bb\u30eb",
"enter_name_hint": "\u540d\u79f0\u3092\u5165\u529b",
"enter_url_hint": "URL\u3092\u5165\u529b",
"check_updates_btn": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u78ba\u8a8d\u3059\u308b",
"new_filter_btn": "\u65b0\u3057\u3044\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3",
"enter_valid_filter_url": "\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3\u304a\u3088\u3073\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u6709\u52b9\u306aURL\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
"custom_filter_rules": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb",
"custom_filter_rules_hint": "1\u3064\u306e\u884c\u306b1\u3064\u306e\u30eb\u30fc\u30eb\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3084\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u69cb\u6587\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002",
"examples_title": "\u4f8b",
"example_meaning_filter_block": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
"example_meaning_filter_whitelist": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u306e\u30d6\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3059\u308b",
"example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u306f\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3057\u305f\u3002",
"example_comment": "! \u3053\u3053\u306b\u306f\u30b3\u30e1\u30f3\u30c8\u304c\u5165\u308a\u307e\u3059",
"example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8",
"example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8",
"all_filters_up_to_date_toast": "\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u3059\u3079\u3066\u6700\u65b0\u3067\u3059",
"updated_upstream_dns_toast": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
"dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u30fc\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059",
"dns_test_not_ok_toast": "\u30b5\u30fc\u30d0 \"{{key}}\": \u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b63\u3057\u304f\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044",
"unblock_btn": "\u30d6\u30ed\u30c3\u30af\u89e3\u9664",
"block_btn": "\u30d6\u30ed\u30c3\u30af",
"time_table_header": "\u6642\u523b",
"domain_name_table_header": "\u30c9\u30e1\u30a4\u30f3\u540d",
"type_table_header": "\u7a2e\u985e",
"response_table_header": "\u5fdc\u7b54",
"empty_response_status": "\u7a7a",
"show_all_filter_type": "\u5168\u3066\u8868\u793a",
"show_filtered_type": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6e08\u307f\u3092\u8868\u793a",
"no_logs_found": "\u30ed\u30b0\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
"disabled_log_btn": "\u30ed\u30b0\u3092\u7121\u52b9\u306b\u3059\u308b",
"download_log_file_btn": "\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9",
"refresh_btn": "\u66f4\u65b0",
"enabled_log_btn": "\u30ed\u30b0\u3092\u6709\u52b9\u306b\u3059\u308b",
"last_dns_queries": "\u6700\u7d42\uff15\uff10\uff10\uff10\u672c\u306eDNS\u30af\u30a8\u30ea",
"previous_btn": "\u524d",
"next_btn": "\u6b21\u3078",
"loading_table_status": "\u8aad\u307f\u8fbc\u307f\u4e2d\u2026",
"page_table_footer_text": "\u30da\u30fc\u30b8",
"of_table_footer_text": "\uff0f",
"rows_table_footer_text": "\u884c",
"updated_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
"rule_removed_from_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u304b\u3089\u30eb\u30fc\u30eb\u3092\u9664\u53bb\u3057\u307e\u3057\u305f",
"rule_added_to_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u306b\u30eb\u30fc\u30eb\u3092\u8ffd\u52a0\u3057\u307e\u3057\u305f",
"query_log_disabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u7121\u52b9",
"query_log_enabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u6709\u52b9",
"source_label": "\u30bd\u30fc\u30b9",
"found_in_known_domain_db": "\u65e2\u77e5\u306e\u30c9\u30e1\u30a4\u30f3\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u3002",
"category_label": "\u30ab\u30c6\u30b4\u30ea",
"rule_label": "\u30eb\u30fc\u30eb",
"filter_label": "\u30d5\u30a3\u30eb\u30bf"
}

View File

@@ -0,0 +1,124 @@
{
"back": "Voltar",
"dashboard": "Painel",
"settings": "Configura\u00e7\u00f5es",
"filters": "Filtros",
"query_log": "Registro de consultas",
"faq": "FAQ",
"version": "vers\u00e3o",
"address": "endere\u00e7o",
"on": "ON",
"off": "OFF",
"copyright": "Copyright",
"homepage": "P\u00e1gina inicial",
"report_an_issue": "Reportar um problema",
"enable_protection": "Ativar prote\u00e7\u00e3o",
"enabled_protection": "Prote\u00e7\u00e3o ativada",
"disable_protection": "Desativar prote\u00e7\u00e3o",
"disabled_protection": "Prote\u00e7\u00e3o desativada",
"refresh_statics": "Atualizar estat\u00edsticas",
"dns_query": "Consultas de DNS",
"blocked_by": "Bloqueador por",
"stats_malware_phishing": "Bloqueado malware\/phishing",
"stats_adult": "Bloqueado sites adultos",
"stats_query_domain": "Principais dom\u00ednios consultados",
"for_last_24_hours": "nas \u00faltimas 24 horas",
"no_domains_found": "Nenhum dom\u00ednio encontrado",
"requests_count": "Contagem de solicita\u00e7\u00f5es",
"top_blocked_domains": "Principais dom\u00ednios bloqueados",
"top_clients": "Principais clientes",
"no_clients_found": "Nenhuma cliente encontrado",
"general_statistics": "Estat\u00edsticas gerais",
"number_of_dns_query_24_hours": "O n\u00famero de consultas DNS processadas nas \u00faltimas 24 horas",
"number_of_dns_query_blocked_24_hours": "V\u00e1rias solicita\u00e7\u00f5es DNS bloqueadas por filtros de bloqueio de an\u00fancios e listas de bloqueio de hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "V\u00e1rias solicita\u00e7\u00f5es de DNS bloqueadas pelo m\u00f3dulo de seguran\u00e7a da navega\u00e7\u00e3o do AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "V\u00e1rios sites adultos bloqueados",
"enforced_save_search": "For\u00e7ar pesquisa segura",
"number_of_dns_query_to_safe_search": "V\u00e1rias solicita\u00e7\u00f5es de DNS para motores de busca para os quais a pesquisa segura foi aplicada",
"average_processing_time": "Tempo m\u00e9dio de processamento",
"average_processing_time_hint": "Tempo m\u00e9dio em milissegundos no processamento de uma solicita\u00e7\u00e3o DNS",
"block_domain_use_filters_and_hosts": "Bloquear dom\u00ednios usando arquivos de filtros e hosts",
"filters_block_toggle_hint": "Voc\u00ea pode configurar as regras de bloqueio nas configura\u00e7\u00f5es de <a href='#filters'>Filtros<\/a>.",
"use_adguard_browsing_sec": "Usar o servi\u00e7o de seguran\u00e7a da navega\u00e7\u00e3o do AdGuard",
"use_adguard_browsing_sec_hint": "O AdGuard Home ir\u00e1 verificar se o dom\u00ednio est\u00e1 na lista negra do servi\u00e7o de seguran\u00e7a da navega\u00e7\u00e3o. Ele usar\u00e1 a API de pesquisa de privacidade para executar a verifica\u00e7\u00e3o: apenas um prefixo curto do hash do nome de dom\u00ednio SHA256 \u00e9 enviado para o servidor.",
"use_adguard_parental": "Usar o servi\u00e7o de controle parental do AdGuard",
"use_adguard_parental_hint": "O AdGuard Home ir\u00e1 verificar se o dom\u00ednio cont\u00e9m conte\u00fado adulto. Ele usa a mesma API amig\u00e1vel de privacidade que o servi\u00e7o de seguran\u00e7a da navega\u00e7\u00e3o.",
"enforce_safe_search": "For\u00e7ar pesquisa segura",
"enforce_save_search_hint": "O AdGuard Home pode for\u00e7ar a pesquisa segura nos seguintes motores de busca: Google, Youtube, Bing e Yandex.",
"no_servers_specified": "Nenhum servidor especificado",
"no_settings": "N\u00e3o configurado",
"general_settings": "Configura\u00e7\u00f5es gerais",
"upstream_dns": "Servidores DNS upstream",
"upstream_dns_hint": "Se voc\u00ea deixar este campo vazio, o AdGuard Home ir\u00e1 usar o<a href='https:\/\/1.1.1.1\/' target='_blank'>DNS da Cloudflare<\/a> como upstream. Use o prefixo tls:\/\/ para servidores DNS com TLS.",
"test_upstream_btn": "Testar upstreams",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Filtragem desativada",
"enabled_filtering_toast": "Filtragem ativada",
"disabled_safe_browsing_toast": "Navega\u00e7\u00e3o segura desativada",
"enabled_safe_browsing_toast": "Navega\u00e7\u00e3o segura ativada",
"disabled_parental_toast": "Controle parental desativado",
"enabled_parental_toast": "Controle parental ativado",
"disabled_safe_search_toast": "Pesquisa segura desativada",
"enabled_save_search_toast": "Pesquisa segura ativada",
"enabled_table_header": "Ativado",
"name_table_header": "Nome",
"filter_url_table_header": "URL do filtro",
"rules_count_table_header": "Quantidade de regras",
"last_time_updated_table_header": "\u00daltima atualiza\u00e7\u00e3o",
"actions_table_header": "A\u00e7\u00f5es",
"delete_table_action": "Excluir",
"filters_and_hosts": "Filtros e listas de bloqueio de hosts",
"filters_and_hosts_hint": "O AdGuard Home entende regras b\u00e1sicas de bloqueio de an\u00fancios e a sintaxe de arquivos de hosts.",
"no_filters_added": "Nenhum filtro adicionado",
"add_filter_btn": "Adicionar filtro",
"cancel_btn": "Cancelar",
"enter_name_hint": "Digite o nome",
"enter_url_hint": "Digite a URL",
"check_updates_btn": "Verificar atualiza\u00e7\u00f5es",
"new_filter_btn": "Nova inscri\u00e7\u00e3o de filtro",
"enter_valid_filter_url": "Digite a URL v\u00e1lida para efetuar a inscri\u00e7\u00e3o de filtro ou um arquivo de hosts.",
"custom_filter_rules": "Regras de filtragem personalizadas",
"custom_filter_rules_hint": "Digite uma regra por linha. Voc\u00ea pode usar regras de bloqueio de an\u00fancios ou a sintaxe de arquivos de hosts.",
"examples_title": "Exemplos",
"example_meaning_filter_block": "bloqueia o acesso ao dom\u00ednio exemplo.org e a todos os seus subdom\u00ednios",
"example_meaning_filter_whitelist": "desbloqueia o acesso ao dom\u00ednio exemplo.org e a todos os seus subdom\u00ednios",
"example_meaning_host_block": "O AdGuard Home ir\u00e1 retornar o endere\u00e7o 127.0.0.1 para o dom\u00ednio exemplo.org (exceto seus subdom\u00ednios).",
"example_comment": "! Aqui vai um coment\u00e1rio",
"example_comment_meaning": "apenas um coment\u00e1rio",
"example_comment_hash": "# Tamb\u00e9m um coment\u00e1rio",
"all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados",
"updated_upstream_dns_toast": "Atualizado os servidores DNS upstream",
"dns_test_ok_toast": "Os servidores DNS especificados est\u00e3o funcionando corretamente",
"dns_test_not_ok_toast": "O servidor \"{{key}}\": n\u00e3o p\u00f4de ser utilizado. Por favor, verifique se voc\u00ea escreveu corretamente",
"unblock_btn": "Desbloquear",
"block_btn": "Bloquear",
"time_table_header": "Data",
"domain_name_table_header": "Nome de dom\u00ednio",
"type_table_header": "Tipo",
"response_table_header": "Resposta",
"empty_response_status": "Vazio",
"show_all_filter_type": "Mostrar todos",
"show_filtered_type": "Mostrar filtrados",
"no_logs_found": "Nenhum registro encontrado",
"disabled_log_btn": "Desativar registros",
"download_log_file_btn": "Baixar arquivo de registros",
"refresh_btn": "Atualizar",
"enabled_log_btn": "Ativar registros",
"last_dns_queries": "\u00daltimas 5000 consultas DNS",
"previous_btn": "Anterior",
"next_btn": "Pr\u00f3ximo",
"loading_table_status": "Carregando",
"page_table_footer_text": "P\u00e1gina",
"of_table_footer_text": "de",
"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 \u00e0s regras de filtragem personalizadas",
"query_log_disabled_toast": "Registros de consultas desativado",
"query_log_enabled_toast": "Registros de consultas ativado",
"source_label": "Fonte",
"found_in_known_domain_db": "Encontrado no banco de dados de dom\u00ednios conhecidos.",
"category_label": "Categoria",
"rule_label": "Regra",
"filter_label": "Filtro"
}

View File

@@ -0,0 +1,128 @@
{
"back": "\u041d\u0430\u0437\u0430\u0434",
"dashboard": "\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
"settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"filters": "\u0424\u0438\u043b\u044c\u0442\u0440\u044b",
"query_log": "\u0416\u0443\u0440\u043d\u0430\u043b",
"faq": "FAQ",
"version": "\u0432\u0435\u0440\u0441\u0438\u044f",
"address": "\u0430\u0434\u0440\u0435\u0441",
"on": "\u0412\u043a\u043b",
"off": "\u0412\u044b\u043a\u043b",
"copyright": "\u0412\u0441\u0435 \u043f\u0440\u0430\u0432\u0430 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u044b",
"homepage": "\u0413\u043b\u0430\u0432\u043d\u0430\u044f",
"report_an_issue": "\u0421\u043e\u043e\u0431\u0449\u0438\u0442\u044c \u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435",
"enable_protection": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0449\u0438\u0442\u0443",
"enabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430 \u0432\u043a\u043b.",
"disable_protection": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0449\u0438\u0442\u0443",
"disabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430 \u0432\u044b\u043a\u043b.",
"refresh_statics": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443",
"dns_query": "DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b",
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e ",
"stats_malware_phishing": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0432\u0440\u0435\u0434\u043e\u043d\u043e\u0441\u043d\u044b\u0435 \u0438 \u0444\u0438\u0448\u0438\u043d\u0433\u043e\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b",
"stats_adult": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \"\u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0435\" \u0441\u0430\u0439\u0442\u044b",
"stats_query_domain": "\u0427\u0430\u0441\u0442\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u043e\u043c\u0435\u043d\u044b",
"for_last_24_hours": "\u0437\u0430 24 \u0447\u0430\u0441\u0430",
"no_domains_found": "\u0414\u043e\u043c\u0435\u043d\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
"requests_count": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432",
"top_blocked_domains": "\u0427\u0430\u0441\u0442\u043e \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u0434\u043e\u043c\u0435\u043d\u044b",
"top_clients": "\u0427\u0430\u0441\u0442\u044b\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u044b",
"no_clients_found": "\u041a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e",
"general_statistics": "\u041e\u0431\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430",
"number_of_dns_query_24_hours": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0437\u0430 24 \u0447\u0430\u0441\u0430",
"number_of_dns_query_blocked_24_hours": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u043c\u0438 \u0438 \u0431\u043b\u043e\u043a-\u0441\u043f\u0438\u0441\u043a\u0430\u043c\u0438",
"number_of_dns_query_blocked_24_hours_by_sec": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u043c \u0410\u043d\u0442\u0438\u0444\u0438\u0448\u0438\u043d\u0433\u0430 AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \"\u0441\u0430\u0439\u0442\u043e\u0432 \u0434\u043b\u044f \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0445\"",
"enforced_save_search": "\u041f\u0440\u0438\u043c\u0435\u043d\u0435\u043d \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a",
"number_of_dns_query_to_safe_search": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 DNS \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u043e\u0432\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u044b\u043b \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a",
"average_processing_time": "\u0421\u0440\u0435\u0434\u043d\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430",
"average_processing_time_hint": "\u0421\u0440\u0435\u0434\u043d\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 DNS \u0432 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445",
"block_domain_use_filters_and_hosts": "\u0411\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432 \u0438 \u0444\u0430\u0439\u043b\u043e\u0432 \u0445\u043e\u0441\u0442\u043e\u0432",
"filters_block_toggle_hint": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0432 <a href='#filters'> \"\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0445\"<\/a>.",
"use_adguard_browsing_sec": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0443\u044e \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044e AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442, \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u0432 \u0432\u0435\u0431-\u0441\u043b\u0443\u0436\u0431\u0443 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430. \u041e\u043d \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c API, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443: \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0438\u043c\u0435\u043d\u0438 \u0434\u043e\u043c\u0435\u043d\u0430 SHA256.",
"use_adguard_parental": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043c\u043e\u0434\u0443\u043b\u044c \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard ",
"use_adguard_parental_hint": "AdGuard Home \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442, \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b 18+. \u041e\u043d \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0442\u043e\u0442 \u0436\u0435 API \u0434\u043b\u044f \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438, \u0447\u0442\u043e \u0438 \u0432\u0435\u0431-\u0441\u043b\u0443\u0436\u0431\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430.",
"enforce_safe_search": "\u0423\u0441\u0438\u043b\u0438\u0442\u044c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a",
"enforce_save_search_hint": "AdGuard Home \u043c\u043e\u0436\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442\u044c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0445: Google, Youtube, Bing \u0438 Yandex.",
"no_servers_specified": "\u041d\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432",
"no_settings": "\u041d\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a",
"general_settings": "\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"upstream_dns": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b",
"upstream_dns_hint": "\u0415\u0441\u043b\u0438 \u0432\u044b \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0442\u043e AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 upstream. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 tls:\/\/ \u0434\u043b\u044f DNS \u0447\u0435\u0440\u0435\u0437 \u0441\u0435\u0440\u0432\u0435\u0440\u044b TLS.",
"test_upstream_btn": "\u0422\u0435\u0441\u0442 upstream \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432",
"apply_btn": "\u041f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c",
"disabled_filtering_toast": "\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u0432\u044b\u043a\u043b.",
"enabled_filtering_toast": "\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u0432\u043a\u043b.",
"disabled_safe_browsing_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u0432\u044b\u043a\u043b.",
"enabled_safe_browsing_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u0432\u043a\u043b.",
"disabled_parental_toast": "\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u0432\u044b\u043a\u043b.",
"enabled_parental_toast": "\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u0432\u043a\u043b.",
"disabled_safe_search_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u0432\u044b\u043a\u043b.",
"enabled_save_search_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u0432\u043a\u043b.",
"enabled_table_header": "\u0412\u043a\u043b.",
"name_table_header": "\u0418\u043c\u044f",
"filter_url_table_header": "URL \u0444\u0438\u043b\u044c\u0442\u0440\u0430",
"rules_count_table_header": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0430\u0432\u0438\u043b:",
"last_time_updated_table_header": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435",
"actions_table_header": "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f",
"delete_table_action": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c",
"filters_and_hosts": "\u0424\u0438\u043b\u044c\u0442\u0440\u044b \u0438 \u0447\u0435\u0440\u043d\u044b\u0435 \u0441\u043f\u0438\u0441\u043a\u0438 hosts",
"filters_and_hosts_hint": "AdGuard Home \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0435\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0438 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441 \u0444\u0430\u0439\u043b\u043e\u0432 hosts.",
"no_filters_added": "\u0424\u0438\u043b\u044c\u0442\u0440\u044b \u043d\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b",
"add_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440",
"cancel_btn": "\u041e\u0442\u043c\u0435\u043d\u0430",
"enter_name_hint": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f",
"enter_url_hint": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 URL",
"check_updates_btn": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f",
"new_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430",
"enter_valid_filter_url": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 URL \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u043d\u0430 \u0444\u0438\u043b\u044c\u0442\u0440 \u0438\u043b\u0438 \u0444\u0430\u0439\u043b hosts.",
"custom_filter_rules": "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439 \u0444\u0438\u043B\u044C\u0442\u0440",
"custom_filter_rules_hint": "\u0412\u0432\u043e\u0434\u0438\u0442\u0435 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u043f\u0440\u0430\u0432\u0438\u043b\u0443 \u043d\u0430 \u0441\u0442\u0440\u043e\u0447\u043a\u0443. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0438\u043b\u0438 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441 \u0444\u0430\u0439\u043b\u043e\u0432 hosts.",
"examples_title": "\u041f\u0440\u0438\u043c\u0435\u0440\u044b",
"example_meaning_filter_block": "\u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u043e\u043c\u0435\u043d\u0443 example.org \u0438 \u0432\u0441\u0435\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u0430\u043c",
"example_meaning_filter_whitelist": "\u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u043e\u043c\u0435\u043d\u0443 example.org \u0438 \u0432\u0441\u0435\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u0430\u043c",
"example_meaning_host_block": "\u0422\u0435\u043f\u0435\u0440\u044c AdGuard Home \u0432\u0435\u0440\u043d\u0435\u0442 127.0.0.1 \u0434\u043b\u044f \u0434\u043e\u043c\u0435\u043d\u0430 example.org (\u043d\u043e \u043d\u0435 \u0434\u043b\u044f \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u043e\u0432).",
"example_comment": "! \u0422\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435",
"example_comment_meaning": "\u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439",
"example_comment_hash": "# \u0418 \u0432\u043e\u0442 \u0442\u0430\u043a \u0442\u043e\u0436\u0435",
"example_upstream_regular": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 UDP)",
"example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/a>",
"example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/a>",
"example_upstream_tcp": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 TCP)",
"all_filters_up_to_date_toast": "\u0412\u0441\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
"updated_upstream_dns_toast": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
"dns_test_ok_toast": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b DNS \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e",
"dns_test_not_ok_toast": "\u0421\u0435\u0440\u0432\u0435\u0440 \"{{key}}\": \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f",
"unblock_btn": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c",
"block_btn": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c",
"time_table_header": "\u0412\u0440\u0435\u043c\u044f",
"domain_name_table_header": "\u0414\u043e\u043c\u0435\u043d",
"type_table_header": "\u0422\u0438\u043f",
"response_table_header": "\u041e\u0442\u0432\u0435\u0442",
"empty_response_status": "\u041f\u0443\u0441\u0442\u043e",
"show_all_filter_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0435",
"show_filtered_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435",
"no_logs_found": "\u041b\u043e\u0433\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
"disabled_log_btn": "\u0416\u0443\u0440\u043d\u0430\u043b \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 \u0432\u044b\u043a\u043b.",
"download_log_file_btn": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043e\u0442\u0447\u0451\u0442",
"refresh_btn": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c",
"enabled_log_btn": "\u0416\u0443\u0440\u043d\u0430\u043b \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 \u0432\u043a\u043b.",
"last_dns_queries": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5000 DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432",
"previous_btn": "\u041d\u0430\u0437\u0430\u0434",
"next_btn": "\u0412\u043f\u0435\u0440\u0451\u0434",
"loading_table_status": "\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430...",
"page_table_footer_text": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430",
"of_table_footer_text": "\u0438\u0437",
"rows_table_footer_text": "\u0441\u0442\u0440\u043e\u043a",
"updated_custom_filtering_toast": "\u0412\u043d\u0435\u0441\u0435\u043d\u044b \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430",
"rule_removed_from_custom_filtering_toast": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043e \u0438\u0437 \u0430\u0432\u0442\u043e\u0440\u0441\u043a\u043e\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430 \u043f\u0440\u0430\u0432\u0438\u043b \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438",
"rule_added_to_custom_filtering_toast": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e",
"query_log_disabled_toast": "\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432\u044b\u043a\u043b.",
"query_log_enabled_toast": "\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432\u043a\u043b.",
"source_label": "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a",
"found_in_known_domain_db": "\u041d\u0430\u0439\u0434\u0435\u043d \u0432 \u0431\u0430\u0437\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432.",
"category_label": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
"rule_label": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e",
"filter_label": "\u0424\u0438\u043b\u044c\u0442\u0440"
}

View File

@@ -0,0 +1,124 @@
{
"back": "Tiilbaka",
"dashboard": "Kontrollpanel",
"settings": "Inst\u00e4llningar",
"filters": "Filter",
"query_log": "F\u00f6rfr\u00e5gningslogg",
"faq": "FAQ",
"version": "version",
"address": "adress",
"on": "P\u00c5",
"off": "AV",
"copyright": "Copyright",
"homepage": "Hemsida",
"report_an_issue": "Rapportera ett problem",
"enable_protection": "Koppla p\u00e5 skydd",
"enabled_protection": "Kopplade p\u00e5 skydd",
"disable_protection": "Koppla bort skydd",
"disabled_protection": "Kopplade bort skydd",
"refresh_statics": "Uppdatera statistik",
"dns_query": "DNS-f\u00f6rfr\u00e5gningar",
"blocked_by": "Blockerat av",
"stats_malware_phishing": "Blockerad skadekod\/phising",
"stats_adult": "Blockerade vuxensajter",
"stats_query_domain": "Mest efters\u00f6kta dom\u00e4ner",
"for_last_24_hours": "under de senaste 24 timamrna",
"no_domains_found": "Inga dom\u00e4ner hittade",
"requests_count": "F\u00f6rfr\u00e5gningsantal",
"top_blocked_domains": "Flest blockerade dom\u00e4ner",
"top_clients": "Toppklienter",
"no_clients_found": "Inga hitatde klienter",
"general_statistics": "Allm\u00e4n statistik",
"number_of_dns_query_24_hours": "Ett antal DNS-f\u00f6rfr\u00e5gningar utf\u00f6rdes under de senaste 244 timamrna",
"number_of_dns_query_blocked_24_hours": "Ett antal DNS-f\u00f6rfr\u00e5gningar blockerades av annonsfilter och v\u00e4rdens bloceringsklistor",
"number_of_dns_query_blocked_24_hours_by_sec": "Ett antal DNS-f\u00f6rfr\u00e5gningar blockerades av AdGuards modul f\u00f6r surfs\u00e4kerhet",
"number_of_dns_query_blocked_24_hours_adult": "Ett anta vuxensajter blockerades",
"enforced_save_search": "Aktivering av S\u00e4ker surf",
"number_of_dns_query_to_safe_search": "Ett antal DNS-f\u00f6rfr\u00e5gningar genomf\u00f6rdes p\u00e5 s\u00f6kmotorer med S\u00e4ker surf aktiverat",
"average_processing_time": "Genomsnittlig processtid",
"average_processing_time_hint": "Genomsnittlig processtid i millisekunder f\u00f6r DNS-f\u00f6rfr\u00e5gning",
"block_domain_use_filters_and_hosts": "Blockera dom\u00e4ner med filter- och v\u00e4rdfiler",
"filters_block_toggle_hint": "Du kan st\u00e4lla in egna blockerings regler i <a href='#filters'>Filterinst\u00e4llningar<\/a>.",
"use_adguard_browsing_sec": "Amv\u00e4nd AdGuards webbservice f\u00f6r surfs\u00e4kerhet",
"use_adguard_browsing_sec_hint": "AdGuard Home kommer att kontrollera om en dom\u00e4n \u00e4r svartlistad i webbservicens surfs\u00e4kerhet. Med en integritetsv\u00e4nlig metod g\u00f6rs en API-lookup f\u00f6r att kontrollera : endast en kort prefix i dom\u00e4nnamnet SHA256 hash skickas till servern.",
"use_adguard_parental": "Anv\u00e4nda AdGuards webbservice f\u00f6r f\u00e4r\u00e4ldrakontroll",
"use_adguard_parental_hint": "AdGuard Home kommer att kontrollera dom\u00e4ner f\u00f6r inneh\u00e5ll av vuxenmaterial . Samma integritetsv\u00e4nliga metod f\u00f6r API-lookup som till\u00e4mpas i webbservicens surfs\u00e4kerhet anv\u00e4nds.",
"enforce_safe_search": "Till\u00e4mpa S\u00e4ker surf",
"enforce_save_search_hint": "AdGuard Home kan framtvinga s\u00e4ker surf i f\u00f6ljande s\u00f6kmoterer: Google, Youtube, Bing, och Yandex.",
"no_servers_specified": "Inga servrar angivna",
"no_settings": "Inga inst\u00e4llningar",
"general_settings": "Allm\u00e4nna inst\u00e4llningar",
"upstream_dns": "Upstream DNS-servrar",
"upstream_dns_hint": "Om du l\u00e5ter f\u00e4ltet vara tomt kommer AdGuard Home att anv\u00e4nda <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> f\u00f6r upstream. Anv\u00e4nd tls:\/\/ prefix f\u00f6r DNS \u00f6ver TLS-servrar.",
"test_upstream_btn": "Testa uppstr\u00f6mmar",
"apply_btn": "Till\u00e4mpa",
"disabled_filtering_toast": "Filtrering bortkopplad",
"enabled_filtering_toast": "Filtrering inkopplad",
"disabled_safe_browsing_toast": "S\u00e4ker surfning bortkopplat",
"enabled_safe_browsing_toast": "S\u00e4ker surfning inkopplat",
"disabled_parental_toast": "F\u00f6r\u00e4ldrakontroll bortkopplat",
"enabled_parental_toast": "F\u00f6r\u00e4ldrakontroll inkopplat",
"disabled_safe_search_toast": "S\u00e4ker webbs\u00f6kning bortkopplat",
"enabled_save_search_toast": "S\u00e4ker webbs\u00f6kning inkopplat",
"enabled_table_header": "Inkopplat",
"name_table_header": "Namn",
"filter_url_table_header": "Filtrerar URL",
"rules_count_table_header": "Regelantal",
"last_time_updated_table_header": "Uppdaterades senast",
"actions_table_header": "\u00c5tg\u00e4rder",
"delete_table_action": "Ta bort",
"filters_and_hosts": "Filtrerings- och v\u00e4rdlistor f\u00f6r blockering",
"filters_and_hosts_hint": "AdGuard till\u00e4mpar grundl\u00e4ggande annonsblockeringsregler och v\u00e4rdfiltersyntaxer",
"no_filters_added": "Inga filter tillagda",
"add_filter_btn": "L\u00e4gg till filter",
"cancel_btn": "Avbryt",
"enter_name_hint": "Skriv in namn",
"enter_url_hint": "Skriv in URL",
"check_updates_btn": "S\u00f6k efter uppdateringar",
"new_filter_btn": "Nytt filterabonemang",
"enter_valid_filter_url": "Skriv in en giltigt URL till ett filterabonnemang eller v\u00e4rdfil.",
"custom_filter_rules": "Egna filterregler",
"custom_filter_rules_hint": "Skriv en regel per rad. Du kan anv\u00e4nda antingen annonsblockeringsregler eller v\u00e4rdfilssyntax.",
"examples_title": "Exempel",
"example_meaning_filter_block": "blockera \u00e5tkomst till dom\u00e4n example.org domain och alla dess subdom\u00e4ner",
"example_meaning_filter_whitelist": "avblockera \u00e5tkomst till dom\u00e4n example.org domain och alla dess subdom\u00e4ner",
"example_meaning_host_block": "AdGuard Home kommer nu att returnera adress 127.0.0.1 f\u00f6r dom\u00e4nexemplet example.org (dock utan dess subdom\u00e4ner).",
"example_comment": "! H\u00e4r kommer en kommentar",
"example_comment_meaning": "Endast en kommentar",
"example_comment_hash": "# Ocks\u00e5 en kommentar",
"all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella",
"updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar",
"dns_test_ok_toast": "Angivna DNS servrar fungerar korrekt",
"dns_test_not_ok_toast": "Server \"{{key}}\": kunde inte anv\u00e4ndas. Var sn\u00e4ll och kolla att du skrivit in r\u00e4tt",
"unblock_btn": "Avblockera",
"block_btn": "Blockera",
"time_table_header": "Tid",
"domain_name_table_header": "Dom\u00e4nnamn",
"type_table_header": "Typ",
"response_table_header": "Svar",
"empty_response_status": "Empty",
"show_all_filter_type": "Visa alla",
"show_filtered_type": "Visa filtrerade",
"no_logs_found": "Inga logga funna",
"disabled_log_btn": "Koppla bort logg",
"download_log_file_btn": "Ladda ner loggfil",
"refresh_btn": "L\u00e4s in igen",
"enabled_log_btn": "Koppla in logg",
"last_dns_queries": "De senaste 5000 DNS-anropen",
"previous_btn": "F\u00f6reg\u00e5ende",
"next_btn": "N\u00e4sta",
"loading_table_status": "L\u00e4ser in...",
"page_table_footer_text": "Sida",
"of_table_footer_text": "av",
"rows_table_footer_text": "rader",
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
"rule_removed_from_custom_filtering_toast": "Regel borttagen fr\u00e5n de egna filterreglerna",
"rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna",
"query_log_disabled_toast": "F\u00f6rfr\u00e5gningsloggen bortkopplad",
"query_log_enabled_toast": "F\u00f6rfr\u00e5gningsloggen inkopplad",
"source_label": "K\u00e4lla",
"found_in_known_domain_db": "Hittad i dom\u00e4ndatabas.",
"category_label": "Kategori",
"rule_label": "Regel",
"filter_label": "Filter"
}

View File

@@ -0,0 +1,124 @@
{
"back": "Quay l\u1ea1i",
"dashboard": "T\u1ed5ng quan",
"settings": "C\u00e0i \u0111\u1eb7t",
"filters": "B\u1ed9 l\u1ecdc",
"query_log": "L\u1ecbch s\u1eed truy v\u1ea5n",
"faq": "H\u1ecfi \u0111\u00e1p",
"version": "phi\u00ean b\u1ea3n",
"address": "\u0111\u1ecba ch\u1ec9",
"on": "\u0110ang b\u1eadt",
"off": "\u0110ang t\u1eaft",
"copyright": "B\u1ea3n quy\u1ec1n",
"homepage": "Trang ch\u1ee7",
"report_an_issue": "B\u00e1o l\u1ed7i",
"enable_protection": "B\u1eadt b\u1ea3o v\u1ec7",
"enabled_protection": "\u0110\u00e3 b\u1eadt b\u1ea3o v\u1ec7",
"disable_protection": "T\u1eaft b\u1ea3o v\u1ec7",
"disabled_protection": "\u0110\u00e3 t\u1eaft b\u1ea3o v\u1ec7",
"refresh_statics": "L\u00e0m m\u1edbi th\u1ed1ng k\u00ea",
"dns_query": "Truy v\u1ea5n DNS",
"blocked_by": "Ch\u1eb7n b\u1edfi",
"stats_malware_phishing": "M\u00e3 \u0111\u1ed9c\/l\u1eeba \u0111\u1ea3o \u0111\u00e3 ch\u1eb7n",
"stats_adult": "Website ng\u01b0\u1eddi l\u1edbn \u0111\u00e3 ch\u1eb7n",
"stats_query_domain": "T\u00ean mi\u1ec1n truy v\u1ea5n nhi\u1ec1u",
"for_last_24_hours": "trong 24 gi\u1edd qua",
"no_domains_found": "Kh\u00f4ng c\u00f3 t\u00ean mi\u1ec1n n\u00e0o",
"requests_count": "S\u1ed1 l\u1ea7n y\u00eau c\u1ea7u",
"top_blocked_domains": "T\u00ean mi\u1ec1n ch\u1eb7n nhi\u1ec1u",
"top_clients": "Client d\u00f9ng nhi\u1ec1u",
"no_clients_found": "Kh\u00f4ng c\u00f3 client n\u00e0o",
"general_statistics": "Th\u1ed1ng k\u00ea chung",
"number_of_dns_query_24_hours": "S\u1ed1 y\u00eau c\u1ea7u DNS \u0111\u00e3 x\u1eed l\u00fd trong 24 gi\u1edd qua",
"number_of_dns_query_blocked_24_hours": "S\u1ed1 y\u00eau c\u1ea7u DNS b\u1ecb ch\u1eb7n b\u1edfi b\u1ed9 l\u1ecdc qu\u1ea3ng c\u00e1o v\u00e0 danh s\u00e1ch ch\u1eb7n host",
"number_of_dns_query_blocked_24_hours_by_sec": "S\u1ed1 y\u00eau c\u1ea7u DNS b\u1ecb ch\u1eb7n b\u1edfi ch\u1ebf \u0111\u1ed9 b\u1ea3o v\u1ec7 duy\u1ec7t web AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "S\u1ed1 website ng\u01b0\u1eddi l\u1edbn \u0111\u00e3 ch\u1eb7n",
"enforced_save_search": "T\u00ecm ki\u1ebfm an to\u00e0n",
"number_of_dns_query_to_safe_search": "S\u1ed1 y\u00eau c\u1ea7u DNS t\u1edbi c\u00f4ng c\u1ee5 t\u00ecm ki\u1ebfm \u0111\u00e3 chuy\u1ec3n th\u00e0nh t\u00ecm ki\u1ebfm an to\u00e0n",
"average_processing_time": "Th\u1eddi gian x\u1eed l\u00fd trung b\u00ecnh",
"average_processing_time_hint": "Th\u1eddi gian trung b\u00ecnh cho m\u1ed9t y\u00eau c\u1ea7u DNS t\u00ednh b\u1eb1ng mili gi\u00e2y",
"block_domain_use_filters_and_hosts": "Ch\u1eb7n t\u00ean mi\u1ec1n s\u1eed d\u1ee5ng c\u00e1c b\u1ed9 l\u1ecdc v\u00e0 file hosts",
"filters_block_toggle_hint": "B\u1ea1n c\u00f3 th\u1ec3 thi\u1ebft l\u1eadp quy t\u1eafc ch\u1eb7n t\u1ea1i c\u00e0i \u0111\u1eb7t <a href='#filters'>B\u1ed9 l\u1ecdc<\/a>.",
"use_adguard_browsing_sec": "S\u1eed d\u1ee5ng d\u1ecbch v\u1ee5 b\u1ea3o v\u1ec7 duy\u1ec7t web AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home s\u1ebd ki\u1ec3m tra t\u00ean mi\u1ec1n v\u1edbi d\u1ecbch v\u1ee5 b\u1ea3o v\u1ec7 duy\u1ec7t web. T\u00ednh n\u0103ng s\u1eed d\u1ee5ng m\u1ed9t API th\u00e2n thi\u1ec7n v\u1edbi quy\u1ec1n ri\u00eang t\u01b0: ch\u1ec9 m\u1ed9t ph\u1ea7n ng\u1eafn ti\u1ec1n t\u1ed1 m\u00e3 b\u0103m SHA256 \u0111\u01b0\u1ee3c g\u1eedi \u0111\u1ebfn m\u00e1y ch\u1ee7",
"use_adguard_parental": "S\u1eed d\u1ee5ng d\u1ecbch v\u1ee5 qu\u1ea3n l\u00fd c\u1ee7a ph\u1ee5 huynh AdGuard",
"use_adguard_parental_hint": "AdGuard Home s\u1ebd ki\u1ec3m tra n\u1ebfu t\u00ean mi\u1ec1n ch\u1ee9a t\u1eeb kho\u00e1 ng\u01b0\u1eddi l\u1edbn. T\u00ednh n\u0103ng s\u1eed d\u1ee5ng API th\u00e2n thi\u1ec7n v\u1edbi quy\u1ec1n ri\u00eang t\u01b0 t\u01b0\u01a1ng t\u1ef1 v\u1edbi d\u1ecbch v\u1ee5 b\u1ea3o v\u1ec7 duy\u1ec7t web",
"enforce_safe_search": "B\u1eaft bu\u1ed9c t\u00ecm ki\u1ebfm an to\u00e0n",
"enforce_save_search_hint": "AdGuard Home c\u00f3 th\u1ec3 b\u1eaft bu\u1ed9c t\u00ecm ki\u1ebfm an to\u00e0n v\u1edbi c\u00e1c d\u1ecbch v\u1ee5 t\u00ecm ki\u1ebfm: Google, Youtube, Bing, Yandex.",
"no_servers_specified": "Kh\u00f4ng c\u00f3 m\u00e1y ch\u1ee7 n\u00e0o \u0111\u01b0\u1ee3c li\u1ec7t k\u00ea",
"no_settings": "Kh\u00f4ng c\u00f3 c\u00e0i \u0111\u1eb7t n\u00e0o",
"general_settings": "C\u00e0i \u0111\u1eb7t chung",
"upstream_dns": "M\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",
"upstream_dns_hint": "N\u1ebfu b\u1ea1n \u0111\u1ec3 tr\u1ed1ng m\u1ee5c n\u00e0y, AdGuard Home s\u1ebd s\u1eed d\u1ee5ng <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u0111\u1ec3 t\u00ecm ki\u1ebfm. S\u1eed d\u1ee5ng ti\u1ec1n t\u1ed1 tls:\/\/ cho c\u00e1c m\u00e1y ch\u1ee7 DNS d\u1ef1a tr\u00ean TLS.",
"test_upstream_btn": "Ki\u1ec3m tra",
"apply_btn": "\u00c1p d\u1ee5ng",
"disabled_filtering_toast": "\u0110\u00e3 t\u1eaft ch\u1eb7n qu\u1ea3ng c\u00e1o",
"enabled_filtering_toast": "\u0110\u00e3 b\u1eadt ch\u1eb7n qu\u1ea3ng c\u00e1o",
"disabled_safe_browsing_toast": "\u0110\u00e3 t\u1eaft b\u1ea3o v\u1ec7 duy\u1ec7t web",
"enabled_safe_browsing_toast": "\u0110\u00e3 b\u1eadt b\u1ea3o v\u1ec7 duy\u1ec7t web",
"disabled_parental_toast": "\u0110\u00e3 t\u1eaft qu\u1ea3n l\u00fd c\u1ee7a ph\u1ee5 huynh",
"enabled_parental_toast": "\u0110\u00e3 b\u1eadt qu\u1ea3n l\u00fd c\u1ee7a ph\u1ee5 huynh",
"disabled_safe_search_toast": "\u0110\u00e3 t\u1eaft t\u00ecm ki\u1ebfm an to\u00e0n",
"enabled_save_search_toast": "\u0110\u00e3 b\u1eadt t\u00ecm ki\u1ebfm an to\u00e0n",
"enabled_table_header": "K\u00edch ho\u1ea1t",
"name_table_header": "T\u00ean",
"filter_url_table_header": "URL b\u1ed9 l\u1ecdc",
"rules_count_table_header": "S\u1ed1 quy t\u1eafc",
"last_time_updated_table_header": "C\u1eadp nh\u1eadt cu\u1ed1i",
"actions_table_header": "Thao t\u00e1c",
"delete_table_action": "Xo\u00e1",
"filters_and_hosts": "Danh s\u00e1ch b\u1ed9 l\u1ecdc v\u00e0 hosts",
"filters_and_hosts_hint": "AdGuard home hi\u1ec3u c\u00e1c quy t\u1eafc ch\u1eb7n qu\u1ea3ng c\u00e1o \u0111\u01a1n gi\u1ea3n v\u00e0 c\u00fa ph\u00e1p file hosts",
"no_filters_added": "Kh\u00f4ng c\u00f3 b\u1ed9 l\u1ecdc n\u00e0o \u0111\u01b0\u1ee3c th\u00eam",
"add_filter_btn": "Th\u00eam b\u1ed9 l\u1ecdc",
"cancel_btn": "Hu\u1ef7",
"enter_name_hint": "Nh\u1eadp t\u00ean",
"enter_url_hint": "Nh\u1eadp URL",
"check_updates_btn": "Ki\u1ec3m tra c\u1eadp nh\u1eadt",
"new_filter_btn": "\u0110\u0103ng k\u00fd b\u1ed9 l\u1ecdc m\u1edbi",
"enter_valid_filter_url": "Nh\u1eadp URL h\u1ee3p l\u1ec7 c\u1ee7a b\u1ed9 l\u1ecdc ho\u1eb7c file hosts",
"custom_filter_rules": "Quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
"custom_filter_rules_hint": "Nh\u1eadp m\u1ed7i quy t\u1eafc 1 d\u00f2ng. C\u00f3 th\u1ec3 s\u1eed d\u1ee5ng quy t\u1eafc ch\u1eb7n qu\u1ea3ng c\u00e1o ho\u1eb7c c\u00fa ph\u00e1p file host",
"examples_title": "V\u00ed d\u1ee5",
"example_meaning_filter_block": "Ch\u1eb7n truy c\u1eadp t\u1edbi t\u00ean mi\u1ec1n example.org v\u00e0 t\u1ea5t c\u1ea3 t\u00ean mi\u1ec1n con",
"example_meaning_filter_whitelist": "Kh\u00f4ng ch\u1eb7n truy c\u1eadp t\u1edbi t\u00ean mi\u1ec1n example.org v\u00e0 t\u1ea5t c\u1ea3 t\u00ean mi\u1ec1n con",
"example_meaning_host_block": "AdGuard Home s\u1ebd ph\u1ea3n h\u1ed3i \u0111\u1ecba ch\u1ec9 IP 127.0.0.1 cho t\u00ean mi\u1ec1n example.org (kh\u00f4ng \u00e1p d\u1ee5ng t\u00ean mi\u1ec1n con)",
"example_comment": "! \u0110\u00e2y l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_comment_meaning": "Ch\u1ec9 l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_comment_hash": "# C\u0169ng l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"all_filters_up_to_date_toast": "T\u1ea5t c\u1ea3 b\u1ed9 l\u1ecdc \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1eadp nh\u1eadt",
"updated_upstream_dns_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt m\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",
"dns_test_ok_toast": "M\u00e1y ch\u1ee7 DNS c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng",
"dns_test_not_ok_toast": "M\u00e1y ch\u1ee7 '{{key}}': kh\u00f4ng th\u1ec3 s\u1eed d\u1ee5ng, vui l\u00f2ng ki\u1ec3m tra b\u1ea1n \u0111\u00e3 \u0111i\u1ec1n ch\u00ednh x\u00e1c",
"unblock_btn": "B\u1ecf ch\u1eb7n",
"block_btn": "Ch\u1eb7n",
"time_table_header": "Th\u1eddi gian",
"domain_name_table_header": "T\u00ean mi\u1ec1n",
"type_table_header": "Lo\u1ea1i",
"response_table_header": "Ph\u1ea3n h\u1ed3i",
"empty_response_status": "R\u1ed7ng",
"show_all_filter_type": "Hi\u1ec7n t\u1ea5t c\u1ea3",
"show_filtered_type": "Ch\u1ec9 hi\u1ec7n \u0111\u00e3 ch\u1eb7n",
"no_logs_found": "Kh\u00f4ng c\u00f3 l\u1ecbch s\u1eed truy v\u1ea5n",
"disabled_log_btn": "T\u1eaft l\u1ecbch s\u1eed truy v\u1ea5n",
"download_log_file_btn": "T\u1ea3i t\u1eadp tin l\u1ecbch s\u1eed truy v\u1ea5n",
"refresh_btn": "L\u00e0m m\u1edbi",
"enabled_log_btn": "B\u1eadt l\u1ecbch s\u1eed truy v\u1ea5n",
"last_dns_queries": "5000 truy v\u1ea5n DNS g\u1ea7n nh\u1ea5t",
"previous_btn": "Trang tr\u01b0\u1edbc",
"next_btn": "Trang sau",
"loading_table_status": "\u0110ang t\u1ea3i...",
"page_table_footer_text": "Trang",
"of_table_footer_text": "c\u1ee7a",
"rows_table_footer_text": "h\u00e0ng",
"updated_custom_filtering_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
"rule_removed_from_custom_filtering_toast": "Quy t\u1eafc \u0111\u00e3 \u0111\u01b0\u1ee3c xo\u00e1 kh\u1ecfi quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
"rule_added_to_custom_filtering_toast": "Quy t\u1eafc \u0111\u00e3 \u0111\u01b0\u1ee3c th\u00eam v\u00e0o quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
"query_log_disabled_toast": "\u0110\u00e3 t\u1eaft l\u1ecbch s\u1eed truy v\u1ea5n",
"query_log_enabled_toast": "\u0110\u00e3 b\u1eadt l\u1ecbch s\u1eed truy v\u1ea5n",
"source_label": "Ngu\u1ed3n",
"found_in_known_domain_db": "T\u00ecm th\u1ea5y trong c\u01a1 s\u1edf d\u1eef li\u1ec7u t\u00ean mi\u1ec1n",
"category_label": "Th\u1ec3 lo\u1ea1i",
"rule_label": "Quy t\u1eafc",
"filter_label": "B\u1ed9 l\u1ecdc"
}

View File

@@ -1,50 +1,71 @@
import { createAction } from 'redux-actions';
import round from 'lodash/round';
import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { normalizeHistory, normalizeFilteringStatus } from '../helpers/helpers';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
import Api from '../api/Api';
const apiClient = new Api();
export const addErrorToast = createAction('ADD_ERROR_TOAST');
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
export const removeToast = createAction('REMOVE_TOAST');
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
export const toggleSetting = (settingKey, status) => async (dispatch) => {
switch (settingKey) {
case 'filtering':
if (status) {
await apiClient.disableFiltering();
} else {
await apiClient.enableFiltering();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safebrowsing':
if (status) {
await apiClient.disableSafebrowsing();
} else {
await apiClient.enableSafebrowsing();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'parental':
if (status) {
await apiClient.disableParentalControl();
} else {
await apiClient.enableParentalControl();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safesearch':
if (status) {
await apiClient.disableSafesearch();
} else {
await apiClient.enableSafesearch();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
default:
break;
let successMessage = '';
try {
// TODO move setting keys to constants
switch (settingKey) {
case 'filtering':
if (status) {
successMessage = 'disabled_filtering_toast';
await apiClient.disableFiltering();
} else {
successMessage = 'enabled_filtering_toast';
await apiClient.enableFiltering();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safebrowsing':
if (status) {
successMessage = 'disabled_safe_browsing_toast';
await apiClient.disableSafebrowsing();
} else {
successMessage = 'enabled_safe_browsing_toast';
await apiClient.enableSafebrowsing();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'parental':
if (status) {
successMessage = 'disabled_parental_toast';
await apiClient.disableParentalControl();
} else {
successMessage = 'enabled_parental_toast';
await apiClient.enableParentalControl();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safesearch':
if (status) {
successMessage = 'disabled_safe_search_toast';
await apiClient.disableSafesearch();
} else {
successMessage = 'enabled_save_search_toast';
await apiClient.enableSafesearch();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
default:
break;
}
dispatch(addSuccessToast(successMessage));
} catch (error) {
dispatch(addErrorToast({ error }));
}
};
@@ -73,11 +94,51 @@ export const initSettings = settingsList => async (dispatch) => {
};
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
}
};
export const getFilteringRequest = createAction('GET_FILTERING_REQUEST');
export const getFilteringFailure = createAction('GET_FILTERING_FAILURE');
export const getFilteringSuccess = createAction('GET_FILTERING_SUCCESS');
export const getFiltering = () => async (dispatch) => {
dispatch(getFilteringRequest());
try {
const filteringStatus = await apiClient.getFilteringStatus();
dispatch(getFilteringSuccess(filteringStatus.enabled));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getFilteringFailure());
}
};
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
export const toggleProtection = status => async (dispatch) => {
dispatch(toggleProtectionRequest());
let successMessage = '';
try {
if (status) {
successMessage = 'disabled_protection';
await apiClient.disableGlobalProtection();
} else {
successMessage = 'enabled_protection';
await apiClient.enableGlobalProtection();
}
dispatch(addSuccessToast(successMessage));
dispatch(toggleProtectionSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleProtectionFailure());
}
};
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
@@ -88,7 +149,7 @@ export const getDnsStatus = () => async (dispatch) => {
const dnsStatus = await apiClient.getGlobalStatus();
dispatch(dnsStatusSuccess(dnsStatus));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
}
};
@@ -103,7 +164,7 @@ export const enableDns = () => async (dispatch) => {
await apiClient.startGlobalFiltering();
dispatch(enableDnsSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(enableDnsFailure());
}
};
@@ -118,8 +179,8 @@ export const disableDns = () => async (dispatch) => {
await apiClient.stopGlobalFiltering();
dispatch(disableDnsSuccess());
} catch (error) {
console.error(error);
dispatch(disableDnsFailure());
dispatch(disableDnsFailure(error));
dispatch(addErrorToast({ error }));
}
};
@@ -139,30 +200,45 @@ export const getStats = () => async (dispatch) => {
dispatch(getStatsSuccess(processedStats));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getStatsFailure());
}
};
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
export const getVersion = () => async (dispatch) => {
dispatch(getVersionRequest());
try {
const newVersion = await apiClient.getGlobalVersion();
dispatch(getVersionSuccess(newVersion));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getVersionFailure());
}
};
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
export const getTopStats = () => async (dispatch, getState) => {
dispatch(getTopStatsRequest());
try {
const timer = setInterval(async () => {
const state = getState();
const timer = setInterval(async () => {
if (state.dashboard.isCoreRunning) {
if (state.dashboard.isCoreRunning) {
clearInterval(timer);
try {
const stats = await apiClient.getGlobalStatsTop();
dispatch(getTopStatsSuccess(stats));
clearInterval(timer);
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTopStatsFailure(error));
}
}, 100);
} catch (error) {
console.error(error);
dispatch(getTopStatsFailure());
}
}
}, 100);
};
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
@@ -171,19 +247,19 @@ export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const getLogs = () => async (dispatch, getState) => {
dispatch(getLogsRequest());
try {
const timer = setInterval(async () => {
const state = getState();
const timer = setInterval(async () => {
if (state.dashboard.isCoreRunning) {
const logs = await apiClient.getQueryLog();
if (state.dashboard.isCoreRunning) {
clearInterval(timer);
try {
const logs = normalizeLogs(await apiClient.getQueryLog());
dispatch(getLogsSuccess(logs));
clearInterval(timer);
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error));
}
}, 100);
} catch (error) {
console.error(error);
dispatch(getLogsFailure());
}
}
}, 100);
};
export const toggleLogStatusRequest = createAction('TOGGLE_LOGS_REQUEST');
@@ -193,16 +269,20 @@ export const toggleLogStatusSuccess = createAction('TOGGLE_LOGS_SUCCESS');
export const toggleLogStatus = queryLogEnabled => async (dispatch) => {
dispatch(toggleLogStatusRequest());
let toggleMethod;
let successMessage;
if (queryLogEnabled) {
toggleMethod = apiClient.disableQueryLog.bind(apiClient);
successMessage = 'query_log_disabled_toast';
} else {
toggleMethod = apiClient.enableQueryLog.bind(apiClient);
successMessage = 'query_log_enabled_toast';
}
try {
await toggleMethod();
dispatch(addSuccessToast(successMessage));
dispatch(toggleLogStatusSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(toggleLogStatusFailure());
}
};
@@ -214,10 +294,14 @@ export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
export const setRules = rules => async (dispatch) => {
dispatch(setRulesRequest());
try {
await apiClient.setRules(rules);
const replacedLineEndings = rules
.replace(/^\n/g, '')
.replace(/\n\s*\n/g, '\n');
await apiClient.setRules(replacedLineEndings);
dispatch(addSuccessToast('updated_custom_filtering_toast'));
dispatch(setRulesSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(setRulesFailure());
}
};
@@ -232,7 +316,7 @@ export const getFilteringStatus = () => async (dispatch) => {
const status = await apiClient.getFilteringStatus();
dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) }));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getFilteringStatusFailure());
}
};
@@ -258,7 +342,7 @@ export const toggleFilterStatus = url => async (dispatch, getState) => {
dispatch(toggleFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(toggleFilterFailure());
}
};
@@ -269,13 +353,27 @@ export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
export const refreshFilters = () => async (dispatch) => {
dispatch(refreshFiltersRequest);
dispatch(showLoading());
try {
await apiClient.refreshFilters();
const refreshText = await apiClient.refreshFilters();
dispatch(refreshFiltersSuccess);
if (refreshText.includes('OK')) {
if (refreshText.includes('OK 0')) {
dispatch(addSuccessToast('all_filters_up_to_date_toast'));
} else {
dispatch(addSuccessToast(refreshText.replace(/OK /g, '')));
}
} else {
dispatch(addErrorToast({ error: refreshText }));
}
dispatch(getFilteringStatus());
dispatch(hideLoading());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(refreshFiltersFailure());
dispatch(hideLoading());
}
};
@@ -292,7 +390,7 @@ export const getStatsHistory = () => async (dispatch) => {
const normalizedHistory = normalizeHistory(statsHistory);
dispatch(getStatsHistorySuccess(normalizedHistory));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getStatsHistoryFailure());
}
};
@@ -301,14 +399,14 @@ export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
export const addFilter = url => async (dispatch) => {
export const addFilter = (url, name) => async (dispatch) => {
dispatch(addFilterRequest());
try {
await apiClient.addFilter(url);
await apiClient.addFilter(url, name);
dispatch(addFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(addFilterFailure());
}
};
@@ -325,7 +423,7 @@ export const removeFilter = url => async (dispatch) => {
dispatch(removeFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure());
}
};
@@ -344,7 +442,7 @@ export const downloadQueryLog = () => async (dispatch) => {
data = await apiClient.downloadQueryLog();
dispatch(downloadQueryLogSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(downloadQueryLogFailure());
}
return data;
@@ -359,9 +457,68 @@ export const setUpstream = url => async (dispatch) => {
dispatch(setUpstreamRequest());
try {
await apiClient.setUpstream(url);
dispatch(addSuccessToast('updated_upstream_dns_toast'));
dispatch(setUpstreamSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(setUpstreamFailure());
}
};
export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
export const testUpstream = servers => async (dispatch) => {
dispatch(testUpstreamRequest());
try {
const upstreamResponse = await apiClient.testUpstream(servers);
const testMessages = Object.keys(upstreamResponse).map((key) => {
const message = upstreamResponse[key];
if (message !== 'OK') {
dispatch(addErrorToast({ error: t('dns_test_not_ok_toast', { key }) }));
}
return message;
});
if (testMessages.every(message => message === 'OK')) {
dispatch(addSuccessToast('dns_test_ok_toast'));
}
dispatch(testUpstreamSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(testUpstreamFailure());
}
};
export const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST');
export const changeLanguageFailure = createAction('CHANGE_LANGUAGE_FAILURE');
export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');
export const changeLanguage = lang => async (dispatch) => {
dispatch(changeLanguageRequest());
try {
await apiClient.changeLanguage(lang);
dispatch(changeLanguageSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(changeLanguageFailure());
}
};
export const getLanguageRequest = createAction('GET_LANGUAGE_REQUEST');
export const getLanguageFailure = createAction('GET_LANGUAGE_FAILURE');
export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS');
export const getLanguage = () => async (dispatch) => {
dispatch(getLanguageRequest());
try {
const language = await apiClient.getCurrentLanguage();
dispatch(getLanguageSuccess(language));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getLanguageFailure());
}
};

View File

@@ -1,18 +1,22 @@
import axios from 'axios';
import startOfToday from 'date-fns/start_of_today';
import endOfToday from 'date-fns/end_of_today';
import subHours from 'date-fns/sub_hours';
import dateFormat from 'date-fns/format';
export default class Api {
baseUrl = 'control';
async makeRequest(path, method = 'POST', config) {
const response = await axios({
url: `${this.baseUrl}/${path}`,
method,
...config,
});
return response.data;
try {
const response = await axios({
url: `${this.baseUrl}/${path}`,
method,
...config,
});
return response.data;
} catch (error) {
console.error(error);
throw new Error(`${this.baseUrl}/${path} | ${error.response.data} | ${error.response.status}`);
}
}
// Global methods
@@ -27,6 +31,10 @@ export default class Api {
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' };
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstream_dns', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
restartGlobalFiltering() {
const { path, method } = this.GLOBAL_RESTART;
@@ -51,13 +59,12 @@ export default class Api {
getGlobalStatsHistory() {
const { path, method } = this.GLOBAL_STATS_HISTORY;
const format = 'YYYY-MM-DDTHH:mm:ssZ';
const todayStart = dateFormat(startOfToday(), format);
const todayEnd = dateFormat(endOfToday(), format);
const dateNow = Date.now();
const config = {
params: {
start_time: todayStart,
end_time: todayEnd,
start_time: dateFormat(subHours(dateNow, 24), format),
end_time: dateFormat(dateNow, format),
time_unit: 'hours',
},
};
@@ -104,6 +111,30 @@ export default class Api {
return this.makeRequest(path, method, config);
}
testUpstream(servers) {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = {
data: servers,
header: { 'Content-Type': 'text/plain' },
};
return this.makeRequest(path, method, config);
}
getGlobalVersion() {
const { path, method } = this.GLOBAL_VERSION;
return this.makeRequest(path, method);
}
enableGlobalProtection() {
const { path, method } = this.GLOBAL_ENABLE_PROTECTION;
return this.makeRequest(path, method);
}
disableGlobalProtection() {
const { path, method } = this.GLOBAL_DISABLE_PROTECTION;
return this.makeRequest(path, method);
}
// Filtering
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
@@ -136,13 +167,13 @@ export default class Api {
return this.makeRequest(path, method);
}
addFilter(url) {
addFilter(url, name) {
const { path, method } = this.FILTERING_ADD_FILTER;
const parameter = 'url';
const requestBody = `${parameter}=${url}`;
const config = {
data: requestBody,
header: { 'Content-Type': 'text/plain' },
data: {
name,
url,
},
};
return this.makeRequest(path, method, config);
}
@@ -253,4 +284,22 @@ export default class Api {
const { path, method } = this.SAFESEARCH_DISABLE;
return this.makeRequest(path, method);
}
// Language
CURRENT_LANGUAGE = { path: 'i18n/current_language', method: 'GET' };
CHANGE_LANGUAGE = { path: 'i18n/change_language', method: 'POST' };
getCurrentLanguage() {
const { path, method } = this.CURRENT_LANGUAGE;
return this.makeRequest(path, method);
}
changeLanguage(lang) {
const { path, method } = this.CHANGE_LANGUAGE;
const parameters = {
data: lang,
headers: { 'Content-Type': 'text/plain' },
};
return this.makeRequest(path, method, parameters);
}
}

View File

@@ -17,3 +17,10 @@ body {
min-height: calc(100vh - 117px);
}
}
.loading-bar {
position: absolute;
z-index: 103;
height: 3px;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}

View File

@@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react';
import { HashRouter, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import LoadingBar from 'react-redux-loading-bar';
import 'react-table/react-table.css';
import '../ui/Tabler.css';
@@ -13,23 +14,58 @@ import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters';
import Logs from '../../containers/Logs';
import Footer from '../ui/Footer';
import Toasts from '../Toasts';
import Status from '../ui/Status';
import Update from '../ui/Update';
import i18n from '../../i18n';
class App extends Component {
componentDidMount() {
this.props.getDnsStatus();
this.props.getVersion();
}
componentDidUpdate(prevProps) {
if (this.props.dashboard.language !== prevProps.dashboard.language) {
this.setLanguage();
}
}
handleStatusChange = () => {
this.props.enableDns();
};
setLanguage = () => {
const { processing, language } = this.props.dashboard;
if (!processing) {
if (language) {
i18n.changeLanguage(language);
}
}
i18n.on('languageChanged', (lang) => {
this.props.changeLanguage(lang);
});
}
render() {
const { dashboard } = this.props;
const updateAvailable =
!dashboard.processingVersions &&
dashboard.isCoreRunning &&
dashboard.isUpdateAvailable;
return (
<HashRouter hashType='noslash'>
<Fragment>
{updateAvailable &&
<Update
announcement={dashboard.announcement}
announcementUrl={dashboard.announcementUrl}
/>
}
<LoadingBar className="loading-bar" updateTime={1000} />
<Route component={Header} />
<div className="container container--wrap">
{!dashboard.processing && !dashboard.isCoreRunning &&
@@ -49,6 +85,7 @@ class App extends Component {
}
</div>
<Footer />
<Toasts />
</Fragment>
</HashRouter>
);
@@ -60,6 +97,9 @@ App.propTypes = {
enableDns: PropTypes.func,
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
error: PropTypes.string,
getVersion: PropTypes.func,
changeLanguage: PropTypes.func,
};
export default App;

View File

@@ -1,34 +1,79 @@
import React from 'react';
import React, { Component } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import map from 'lodash/map';
import { withNamespaces, Trans } from 'react-i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import Popover from '../ui/Popover';
const Clients = props => (
<Card title="Top blocked domains" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<ReactTable
data={map(props.topBlockedDomains, (value, prop) => (
{ ip: prop, domain: value }
))}
columns={[{
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Domain name',
accessor: 'domain',
}]}
showPagination={false}
noDataText="No domains found"
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
import { getTrackerData } from '../../helpers/trackers/trackers';
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
Clients.propTypes = {
class BlockedDomains extends Component {
columns = [{
Header: 'IP',
accessor: 'ip',
Cell: (row) => {
const { value } = row;
const trackerData = getTrackerData(value);
return (
<div className="logs__row">
<div className="logs__text" title={value}>
{value}
</div>
{trackerData && <Popover data={trackerData} />}
</div>
);
},
}, {
Header: <Trans>requests_count</Trans>,
accessor: 'domain',
maxWidth: 190,
Cell: ({ value }) => {
const {
blockedFiltering,
replacedSafebrowsing,
replacedParental,
} = this.props;
const blocked = blockedFiltering + replacedSafebrowsing + replacedParental;
const percent = getPercent(blocked, value);
return (
<Cell value={value} percent={percent} color={STATUS_COLORS.red} />
);
},
}];
render() {
const { t } = this.props;
return (
<Card title={ t('top_blocked_domains') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}>
<ReactTable
data={map(this.props.topBlockedDomains, (value, prop) => (
{ ip: prop, domain: value }
))}
columns={this.columns}
showPagination={false}
noDataText={ t('no_domains_found') }
minRows={6}
className="-striped -highlight card-table-overflow stats__table"
/>
</Card>
);
}
}
BlockedDomains.propTypes = {
topBlockedDomains: PropTypes.object.isRequired,
refreshButton: PropTypes.node,
blockedFiltering: PropTypes.number.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired,
replacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
t: PropTypes.func,
};
export default Clients;
export default withNamespaces()(BlockedDomains);

View File

@@ -1,34 +1,67 @@
import React from 'react';
import React, { Component } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import map from 'lodash/map';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
const Clients = props => (
<Card title="Top clients" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<ReactTable
data={map(props.topClients, (value, prop) => (
{ ip: prop, count: value }
))}
columns={[{
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Request count',
accessor: 'count',
}]}
showPagination={false}
noDataText="No clients found"
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class Clients extends Component {
getPercentColor = (percent) => {
if (percent > 50) {
return STATUS_COLORS.green;
} else if (percent > 10) {
return STATUS_COLORS.yellow;
}
return STATUS_COLORS.red;
}
columns = [{
Header: 'IP',
accessor: 'ip',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
}, {
Header: <Trans>requests_count</Trans>,
accessor: 'count',
Cell: ({ value }) => {
const percent = getPercent(this.props.dnsQueries, value);
const percentColor = this.getPercentColor(percent);
return (
<Cell value={value} percent={percent} color={percentColor} />
);
},
}];
render() {
const { t } = this.props;
return (
<Card title={ t('top_clients') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}>
<ReactTable
data={map(this.props.topClients, (value, prop) => (
{ ip: prop, count: value }
))}
columns={this.columns}
showPagination={false}
noDataText={ t('no_clients_found') }
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
}
}
Clients.propTypes = {
topClients: PropTypes.object.isRequired,
refreshButton: PropTypes.node,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
t: PropTypes.func,
};
export default Clients;
export default withNamespaces()(Clients);

View File

@@ -1,17 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
import Tooltip from '../ui/Tooltip';
const tooltipType = 'tooltip-custom--narrow';
const Counters = props => (
<Card title="General counters" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<Card title={ props.t('general_statistics') } subtitle={ props.t('for_last_24_hours') } bodyType="card-table" refresh={props.refreshButton}>
<table className="table card-table">
<tbody>
<tr>
<td>
DNS Queries
<Tooltip text="A number of DNS quieries processed in the last 3 minutes" />
<Trans>dns_query</Trans>
<Tooltip text={ props.t('number_of_dns_query_24_hours') } type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -21,8 +24,8 @@ const Counters = props => (
</tr>
<tr>
<td>
Blocked by filters
<Tooltip text="A number of DNS requests blocked by filters" />
<Trans>blocked_by</Trans> <a href="#filters"><Trans>filters</Trans></a>
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours') } type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -32,8 +35,8 @@ const Counters = props => (
</tr>
<tr>
<td>
Blocked malware/phishing
<Tooltip text="A number of DNS requests blocked" />
<Trans>stats_malware_phishing</Trans>
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours_by_sec') } type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -43,8 +46,8 @@ const Counters = props => (
</tr>
<tr>
<td>
Blocked adult websites
<Tooltip text="A number of adult websites blocked" />
<Trans>stats_adult</Trans>
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours_adult') } type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -54,8 +57,8 @@ const Counters = props => (
</tr>
<tr>
<td>
Enforced safe search
<Tooltip text="A number of DNS requests to search engines for which Safe Search was enforced" />
<Trans>enforced_save_search</Trans>
<Tooltip text={ props.t('number_of_dns_query_to_safe_search') } type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -65,8 +68,8 @@ const Counters = props => (
</tr>
<tr>
<td>
Average processing time
<Tooltip text="Average time in milliseconds on processing a DNS request" />
<Trans>average_processing_time</Trans>
<Tooltip text={ props.t('average_processing_time_hint') } type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -86,7 +89,8 @@ Counters.propTypes = {
replacedParental: PropTypes.number.isRequired,
replacedSafesearch: PropTypes.number.isRequired,
avgProcessingTime: PropTypes.number.isRequired,
refreshButton: PropTypes.node,
refreshButton: PropTypes.node.isRequired,
t: PropTypes.func,
};
export default Counters;
export default withNamespaces()(Counters);

View File

@@ -0,0 +1,22 @@
.stats__table .popover__body {
left: 0;
transform: none;
}
.stats__table .popover__body:after {
left: 13px;
}
.stats__table .rt-tr-group:first-child .popover__body,
.stats__table .rt-tr-group:nth-child(2) .popover__body {
top: calc(100% + 5px);
bottom: initial;
z-index: 1;
}
.stats__table .rt-tr-group:first-child .popover__body:after,
.stats__table .rt-tr-group:nth-child(2) .popover__body:after {
top: -11px;
border-top: 6px solid transparent;
border-bottom: 6px solid #585965;
}

View File

@@ -1,34 +1,81 @@
import React from 'react';
import React, { Component } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import map from 'lodash/map';
import { withNamespaces, Trans } from 'react-i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import Popover from '../ui/Popover';
const QueriedDomains = props => (
<Card title="Top queried domains" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<ReactTable
data={map(props.topQueriedDomains, (value, prop) => (
{ ip: prop, count: value }
))}
columns={[{
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Request count',
accessor: 'count',
}]}
showPagination={false}
noDataText="No domains found"
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
import { getTrackerData } from '../../helpers/trackers/trackers';
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class QueriedDomains extends Component {
getPercentColor = (percent) => {
if (percent > 10) {
return STATUS_COLORS.red;
} else if (percent > 5) {
return STATUS_COLORS.yellow;
}
return STATUS_COLORS.green;
}
columns = [{
Header: 'IP',
accessor: 'ip',
Cell: (row) => {
const { value } = row;
const trackerData = getTrackerData(value);
return (
<div className="logs__row">
<div className="logs__text" title={value}>
{value}
</div>
{trackerData && <Popover data={trackerData} />}
</div>
);
},
}, {
Header: <Trans>requests_count</Trans>,
accessor: 'count',
maxWidth: 190,
Cell: ({ value }) => {
const percent = getPercent(this.props.dnsQueries, value);
const percentColor = this.getPercentColor(percent);
return (
<Cell value={value} percent={percent} color={percentColor} />
);
},
}];
render() {
const { t } = this.props;
return (
<Card title={ t('stats_query_domain') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}>
<ReactTable
data={map(this.props.topQueriedDomains, (value, prop) => (
{ ip: prop, count: value }
))}
columns={this.columns}
showPagination={false}
noDataText={ t('no_domains_found') }
minRows={6}
className="-striped -highlight card-table-overflow stats__table"
/>
</Card>
);
}
}
QueriedDomains.propTypes = {
topQueriedDomains: PropTypes.object.isRequired,
refreshButton: PropTypes.node,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
t: PropTypes.func,
};
export default QueriedDomains;
export default withNamespaces()(QueriedDomains);

View File

@@ -1,61 +1,110 @@
import React from 'react';
import { ResponsiveLine } from '@nivo/line';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
import Line from '../ui/Line';
const Statistics = props => (
<Card title="Statistics" subtitle="Today" bodyType="card-graph" refresh={props.refreshButton}>
{props.history ?
<ResponsiveLine
data={props.history}
margin={{
top: 50,
right: 40,
bottom: 80,
left: 80,
}}
minY="auto"
stacked={false}
curve='monotoneX'
axisBottom={{
orient: 'bottom',
tickSize: 5,
tickPadding: 5,
tickRotation: -45,
legend: 'time',
legendOffset: 50,
legendPosition: 'center',
}}
axisLeft={{
orient: 'left',
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'count',
legendOffset: -40,
legendPosition: 'center',
}}
enableArea={true}
dotSize={10}
dotColor="inherit:darker(0.3)"
dotBorderWidth={2}
dotBorderColor="#ffffff"
dotLabel="y"
dotLabelYOffset={-12}
animate={true}
motionStiffness={90}
motionDamping={15}
/>
:
<h2 className="text-muted">Empty data</h2>
}
</Card>
);
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class Statistics extends Component {
render() {
const {
dnsQueries,
blockedFiltering,
replacedSafebrowsing,
replacedParental,
} = this.props;
const filteringData = [this.props.history[1]];
const queriesData = [this.props.history[2]];
const parentalData = [this.props.history[3]];
const safebrowsingData = [this.props.history[4]];
return (
<div className="row">
<div className="col-sm-6 col-lg-3">
<Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-blue">
{dnsQueries}
</div>
<div className="card-title-stats">
<Trans>dns_query</Trans>
</div>
</div>
<div className="card-chart-bg">
<Line data={queriesData} color={STATUS_COLORS.blue}/>
</div>
</Card>
</div>
<div className="col-sm-6 col-lg-3">
<Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-red">
{blockedFiltering}
</div>
<div className="card-value card-value-percent text-red">
{getPercent(dnsQueries, blockedFiltering)}
</div>
<div className="card-title-stats">
<Trans>blocked_by</Trans><a href="#filters"> <Trans>filters</Trans></a>
</div>
</div>
<div className="card-chart-bg">
<Line data={filteringData} color={STATUS_COLORS.red}/>
</div>
</Card>
</div>
<div className="col-sm-6 col-lg-3">
<Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-green">
{replacedSafebrowsing}
</div>
<div className="card-value card-value-percent text-green">
{getPercent(dnsQueries, replacedSafebrowsing)}
</div>
<div className="card-title-stats">
<Trans>stats_malware_phishing</Trans>
</div>
</div>
<div className="card-chart-bg">
<Line data={safebrowsingData} color={STATUS_COLORS.green}/>
</div>
</Card>
</div>
<div className="col-sm-6 col-lg-3">
<Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-yellow">
{replacedParental}
</div>
<div className="card-value card-value-percent text-yellow">
{getPercent(dnsQueries, replacedParental)}
</div>
<div className="card-title-stats">
<Trans>stats_adult</Trans>
</div>
</div>
<div className="card-chart-bg">
<Line data={parentalData} color={STATUS_COLORS.yellow}/>
</div>
</Card>
</div>
</div>
);
}
}
Statistics.propTypes = {
history: PropTypes.array.isRequired,
refreshButton: PropTypes.node,
dnsQueries: PropTypes.number.isRequired,
blockedFiltering: PropTypes.number.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired,
replacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
};
export default Statistics;
export default withNamespaces()(Statistics);

View File

@@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import 'whatwg-fetch';
import { Trans, withNamespaces } from 'react-i18next';
import Statistics from './Statistics';
import Counters from './Counters';
@@ -10,31 +11,47 @@ import BlockedDomains from './BlockedDomains';
import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading';
import './Dashboard.css';
class Dashboard extends Component {
componentDidMount() {
this.getAllStats();
}
getAllStats = () => {
this.props.getStats();
this.props.getStatsHistory();
this.props.getTopStats();
}
getToggleFilteringButton = () => {
const { protectionEnabled } = this.props.dashboard;
const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection';
const buttonClass = protectionEnabled ? 'btn-gray' : 'btn-success';
return (
<button type="button" className={`btn btn-sm mr-2 ${buttonClass}`} onClick={() => this.props.toggleProtection(protectionEnabled)}>
<Trans>{buttonText}</Trans>
</button>
);
}
render() {
const { dashboard } = this.props;
const { dashboard, t } = this.props;
const dashboardProcessing =
dashboard.processing ||
dashboard.processingStats ||
dashboard.processingStatsHistory ||
dashboard.processingTopStats;
const disableButton = <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={() => this.props.disableDns()}>Disable DNS</button>;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.props.getStats()}>Refresh statistics</button>;
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.props.getStats()}></button>;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}><Trans>refresh_statics</Trans></button>;
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.getAllStats()} />;
return (
<Fragment>
<PageTitle title="Dashboard">
<PageTitle title={ t('dashboard') }>
<div className="page-title__actions">
{disableButton}
{this.getToggleFilteringButton()}
{refreshFullButton}
</div>
</PageTitle>
@@ -46,6 +63,10 @@ class Dashboard extends Component {
<Statistics
history={dashboard.statsHistory}
refreshButton={refreshButton}
dnsQueries={dashboard.stats.dns_queries}
blockedFiltering={dashboard.stats.blocked_filtering}
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing}
replacedParental={dashboard.stats.replaced_parental}
/>
</div>
}
@@ -66,12 +87,14 @@ class Dashboard extends Component {
<Fragment>
<div className="col-lg-6">
<Clients
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topClients={dashboard.topStats.top_clients}
/>
</div>
<div className="col-lg-6">
<QueriedDomains
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topQueriedDomains={dashboard.topStats.top_queried_domains}
/>
@@ -80,6 +103,9 @@ class Dashboard extends Component {
<BlockedDomains
refreshButton={refreshButton}
topBlockedDomains={dashboard.topStats.top_blocked_domains}
blockedFiltering={dashboard.stats.blocked_filtering}
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing}
replacedParental={dashboard.stats.replaced_parental}
/>
</div>
</Fragment>
@@ -95,9 +121,11 @@ Dashboard.propTypes = {
getStats: PropTypes.func,
getStatsHistory: PropTypes.func,
getTopStats: PropTypes.func,
disableDns: PropTypes.func,
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
getFiltering: PropTypes.func,
toggleProtection: PropTypes.func,
t: PropTypes.func,
};
export default Dashboard;
export default withNamespaces()(Dashboard);

View File

@@ -1,8 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
export default class UserRules extends Component {
class UserRules extends Component {
handleChange = (e) => {
const { value } = e.currentTarget;
this.props.handleRulesChange(value);
@@ -14,44 +15,42 @@ export default class UserRules extends Component {
};
render() {
const { t } = this.props;
return (
<Card
title="Custom filtering rules"
subtitle="Enter one rule on a line. You can use either adblock rules or hosts files syntax."
title={ t('custom_filter_rules') }
subtitle={ t('custom_filter_rules_hint') }
>
<form onSubmit={this.handleSubmit}>
<textarea className="form-control" value={this.props.userRules} onChange={this.handleChange} />
<textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} />
<div className="card-actions">
<button
className="btn btn-success btn-standart"
type="submit"
onClick={this.handleSubmit}
>
Apply...
<Trans>apply_btn</Trans>
</button>
</div>
</form>
<hr/>
<div className="list leading-loose">
Examples:
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>||example.org^</code> - block access to the example.org domain
and all its subdomains
<code>||example.org^</code> - { t('example_meaning_filter_block') }
</li>
<li>
<code> @@||example.org^</code> - unblock access to the example.org
domain and all its subdomains
<code> @@||example.org^</code> - { t('example_meaning_filter_whitelist') }
</li>
<li>
<code>example.org 127.0.0.1</code> - AdGuard DNS will now return
127.0.0.1 address for the example.org domain (but not its subdomains).
<code>127.0.0.1 example.org</code> - { t('example_meaning_host_block') }
</li>
<li>
<code>! Here goes a comment</code> - just a comment
<code>{ t('example_comment') }</code> - { t('example_comment_meaning') }
</li>
<li>
<code># Also a comment</code> - just a comment
<code>{ t('example_comment_hash') }</code> - { t('example_comment_meaning') }
</li>
</ol>
</div>
@@ -64,4 +63,7 @@ UserRules.propTypes = {
userRules: PropTypes.string,
handleRulesChange: PropTypes.func,
handleRulesSubmit: PropTypes.func,
t: PropTypes.func,
};
export default withNamespaces()(UserRules);

View File

@@ -1,6 +1,7 @@
import React, { Component } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Modal from '../ui/Modal';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
@@ -33,57 +34,61 @@ class Filters extends Component {
};
columns = [{
Header: 'Enabled',
Header: this.props.t('enabled_table_header'),
accessor: 'enabled',
Cell: this.renderCheckbox,
width: 90,
className: 'text-center',
}, {
Header: 'Filter name',
Header: this.props.t('name_table_header'),
accessor: 'name',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
}, {
Header: 'Host file URL',
Header: this.props.t('filter_url_table_header'),
accessor: 'url',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><a href={value} target='_blank' rel='noopener noreferrer' className="link logs__text">{value}</a></div>),
}, {
Header: 'Rules count',
Header: this.props.t('rules_count_table_header'),
accessor: 'rulesCount',
className: 'text-center',
Cell: props => props.value.toLocaleString(),
}, {
Header: 'Last time update',
Header: this.props.t('last_time_updated_table_header'),
accessor: 'lastUpdated',
className: 'text-center',
}, {
Header: 'Actions',
Header: this.props.t('actions_table_header'),
accessor: 'url',
Cell: ({ value }) => (<span className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
Cell: ({ value }) => (<span title={ this.props.t('delete_table_action') } className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
className: 'text-center',
width: 75,
width: 80,
sortable: false,
},
];
render() {
const { t } = this.props;
const { filters, userRules } = this.props.filtering;
return (
<div>
<PageTitle title="Filters" />
<PageTitle title={ t('filters') } />
<div className="content">
<div className="row">
<div className="col-md-12">
<Card
title="Blocking filters and hosts files"
subtitle="AdGuard DNS understands basic adblock rules and hosts files syntax."
title={ t('filters_and_hosts') }
subtitle={ t('filters_and_hosts_hint') }
>
<ReactTable
data={filters}
columns={this.columns}
showPagination={false}
noDataText="No filters added"
noDataText={ t('no_filters_added') }
minRows={4} // TODO find out what to show if rules.length is 0
/>
<div className="card-actions">
<button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}>Add filter</button>
<button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}>Check updates</button>
<button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}><Trans>add_filter_btn</Trans></button>
<button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}><Trans>check_updates_btn</Trans></button>
</div>
</Card>
</div>
@@ -101,8 +106,8 @@ class Filters extends Component {
toggleModal={this.props.toggleFilteringModal}
addFilter={this.props.addFilter}
isFilterAdded={this.props.filtering.isFilterAdded}
title="New filter subscription"
inputDescription="Enter valid URL or file path of the filter into field above. You will be subscribed to that filter."
title={ t('new_filter_btn') }
inputDescription={ t('enter_valid_filter_url') }
/>
</div>
);
@@ -124,7 +129,8 @@ Filters.propTypes = {
toggleFilteringModal: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
t: PropTypes.func,
};
export default Filters;
export default withNamespaces()(Filters);

View File

@@ -67,13 +67,17 @@
}
.nav-version {
padding: 16px 0;
font-size: 0.85rem;
padding: 7px 0;
font-size: 0.80rem;
text-align: right;
}
.nav-version__value {
font-weight: 600;
}
.header-brand-img {
height: 26px;
height: 32px;
}
@media screen and (min-width: 992px) {
@@ -84,6 +88,8 @@
.nav-tabs .nav-link {
width: auto;
border-bottom: 1px solid transparent;
font-size: 13px;
white-space: nowrap;
}
.mobile-menu {
@@ -103,7 +109,16 @@
.nav-version {
padding: 0;
font-size: 0.9rem;
}
}
@media screen and (min-width: 1280px) {
.nav-tabs .nav-link {
font-size: 14px;
}
.nav-version {
font-size: 0.85rem;
}
}

View File

@@ -3,6 +3,8 @@ import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import enhanceWithClickOutside from 'react-click-outside';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import { REPOSITORY } from '../../helpers/constants';
class Menu extends Component {
handleClickOutside = () => {
@@ -15,44 +17,50 @@ class Menu extends Component {
render() {
const menuClass = classnames({
'col-lg mobile-menu': true,
'col-lg-6 mobile-menu': true,
'mobile-menu--active': this.props.isMenuOpen,
});
return (
<Fragment>
<div className={menuClass}>
<ul className="nav nav-tabs border-0 flex-column flex-lg-row">
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
<li className="nav-item border-bottom d-lg-none" onClick={this.toggleMenu}>
<div className="nav-link nav-link--back">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/></svg>
Back
<Trans>back</Trans>
</div>
</li>
<li className="nav-item">
<NavLink to="/" exact={true} className="nav-link">
<svg className="nav-icon" fill="none" height="24" stroke="#9aa0ac" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/></svg>
Dashboard
<Trans>dashboard</Trans>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/settings" className="nav-link">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/></svg>
Settings
<Trans>settings</Trans>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/filters" className="nav-link">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/></svg>
Filters
<Trans>filters</Trans>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/logs" className="nav-link">
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/></svg>
Query Log
<Trans>query_log</Trans>
</NavLink>
</li>
<li className="nav-item">
<a href={`${REPOSITORY.URL}/wiki`} className="nav-link" target="_blank" rel="noopener noreferrer">
<svg className="nav-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#66b574" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
<Trans>faq</Trans>
</a>
</li>
</ul>
</div>
</Fragment>
@@ -66,4 +74,4 @@ Menu.propTypes = {
toggleMenuOpen: PropTypes.func,
};
export default enhanceWithClickOutside(Menu);
export default withNamespaces()(enhanceWithClickOutside(Menu));

View File

@@ -1,11 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
export default function Version(props) {
function Version(props) {
const { dnsVersion, dnsAddress, dnsPort } = props;
return (
<div className="nav-version">
v.{dnsVersion} / address: {dnsAddress}:{dnsPort}
<div className="nav-version__text">
<Trans>version</Trans>: <span className="nav-version__value">{dnsVersion}</span>
</div>
<div className="nav-version__text">
<Trans>address</Trans>: <span className="nav-version__value">{dnsAddress}:{dnsPort}</span>
</div>
</div>
);
}
@@ -15,3 +21,5 @@ Version.propTypes = {
dnsAddress: PropTypes.string,
dnsPort: PropTypes.number,
};
export default withNamespaces()(Version);

View File

@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import Menu from './Menu';
import Version from './Version';
@@ -26,8 +27,8 @@ class Header extends Component {
const { dashboard } = this.props;
const badgeClass = classnames({
'badge dns-status': true,
'badge-success': dashboard.isCoreRunning,
'badge-danger': !dashboard.isCoreRunning,
'badge-success': dashboard.protectionEnabled,
'badge-danger': !dashboard.protectionEnabled,
});
return (
@@ -42,9 +43,9 @@ class Header extends Component {
<Link to="/" className="nav-link pl-0 pr-1">
<img src={logo} alt="" className="header-brand-img" />
</Link>
{!dashboard.proccessing &&
{!dashboard.proccessing && dashboard.isCoreRunning &&
<span className={badgeClass}>
{dashboard.isCoreRunning ? 'ON' : 'OFF'}
<Trans>{dashboard.protectionEnabled ? 'on' : 'off'}</Trans>
</span>
}
</div>
@@ -72,4 +73,4 @@ Header.propTypes = {
location: PropTypes.object,
};
export default Header;
export default withNamespaces()(Header);

View File

@@ -1 +1 @@
<svg viewBox="0 0 118 26" xmlns="http://www.w3.org/2000/svg"><path fill="#232323" d="M92.535 18.314l-.897-2.259h-4.47l-.849 2.259h-3.034L88.13 6.809h2.708l4.796 11.505h-3.1zm-3.1-8.434l-1.468 3.949h2.904L89.435 9.88zm-6.607 4.095c0 .693-.117 1.324-.35 1.893a4.115 4.115 0 0 1-1.004 1.463 4.63 4.63 0 0 1-1.574.95c-.614.228-1.297.341-2.047.341-.761 0-1.447-.113-2.056-.34a4.468 4.468 0 0 1-1.55-.951 4.126 4.126 0 0 1-.978-1.463 5.038 5.038 0 0 1-.343-1.893V6.809H75.7v6.939c0 .314.041.612.123.893.081.282.206.534.375.756.169.222.392.398.669.528s.612.195 1.003.195c.392 0 .726-.065 1.003-.195a1.83 1.83 0 0 0 .677-.528 2.1 2.1 0 0 0 .376-.756c.076-.281.114-.58.114-.893v-6.94h2.79v7.167zm-11.446 3.64a8.898 8.898 0 0 1-1.982.715 10.43 10.43 0 0 1-2.472.276c-.924 0-1.775-.146-2.553-.439a5.895 5.895 0 0 1-2.006-1.235 5.63 5.63 0 0 1-1.314-1.909c-.315-.742-.473-1.568-.473-2.478 0-.92.16-1.755.482-2.502a5.567 5.567 0 0 1 1.33-1.91 5.893 5.893 0 0 1 1.99-1.21 7.044 7.044 0 0 1 2.463-.423c.913 0 1.762.138 2.545.414.783.277 1.419.648 1.908 1.114l-1.762 1.998a3.05 3.05 0 0 0-1.076-.772c-.446-.2-.952-.3-1.517-.3-.49 0-.941.09-1.354.268a3.256 3.256 0 0 0-1.077.747 3.39 3.39 0 0 0-.71 1.138 3.977 3.977 0 0 0-.253 1.438c0 .53.077 1.018.229 1.463.152.444.378.826.677 1.145.299.32.669.569 1.11.748.44.178.943.268 1.508.268.326 0 .636-.025.93-.073.294-.05.566-.128.816-.236v-2.096h-2.203V11.52h4.764v6.094zm46.107-5.086c0 1.007-.188 1.877-.563 2.608a5.262 5.262 0 0 1-1.484 1.804 6.199 6.199 0 0 1-2.08 1.04 8.459 8.459 0 0 1-2.35.333h-4.306V6.809h4.176c.816 0 1.62.095 2.414.284.794.19 1.501.504 2.121.943.62.438 1.12 1.026 1.5 1.763.382.736.572 1.646.572 2.73zm-2.904 0c0-.65-.106-1.19-.318-1.617a2.724 2.724 0 0 0-.848-1.024 3.4 3.4 0 0 0-1.208-.544 5.955 5.955 0 0 0-1.394-.163h-1.387v6.728h1.321c.5 0 .982-.057 1.444-.17.462-.115.87-.301 1.224-.562a2.78 2.78 0 0 0 .848-1.04c.212-.433.318-.97.318-1.608zm-55.226 0c0 1.007-.188 1.877-.563 2.608a5.262 5.262 0 0 1-1.484 1.804 6.199 6.199 0 0 1-2.08 1.04 8.459 8.459 0 0 1-2.35.333h-4.306V6.809h4.176c.816 0 1.62.095 2.414.284.794.19 1.501.504 2.121.943.62.438 1.12 1.026 1.5 1.763.382.736.572 1.646.572 2.73zm-2.904 0c0-.65-.106-1.19-.318-1.617a2.724 2.724 0 0 0-.848-1.024 3.4 3.4 0 0 0-1.207-.544 5.955 5.955 0 0 0-1.395-.163H51.3v6.728h1.321c.5 0 .982-.057 1.444-.17.462-.115.87-.301 1.224-.562a2.78 2.78 0 0 0 .848-1.04c.212-.433.318-.97.318-1.608zm-11.86 5.785l-.897-2.259h-4.47l-.848 2.259h-3.034L40.19 6.809h2.708l4.796 11.505h-3.1zm-3.1-8.434l-1.467 3.949h2.903L41.496 9.88zm61.203 8.434l-2.496-4.566h-.946v4.566h-2.74V6.809h4.404c.555 0 1.096.057 1.623.17.528.114 1 .306 1.42.577.418.271.752.629 1.003 1.073.25.444.375.996.375 1.657 0 .78-.212 1.436-.636 1.966-.425.531-1.012.91-1.762 1.138l3.018 4.924h-3.263zm-.114-7.979c0-.27-.057-.49-.171-.658a1.172 1.172 0 0 0-.44-.39 1.919 1.919 0 0 0-.604-.187 4.469 4.469 0 0 0-.645-.049H99.24v2.681h1.321c.228 0 .462-.018.701-.056.24-.038.457-.106.653-.204.196-.097.356-.238.481-.422s.188-.422.188-.715z"/><path fill="#68bc71" d="M12.651 0C8.697 0 3.927.93 0 2.977c0 4.42-.054 15.433 12.651 22.958C25.357 18.41 25.303 7.397 25.303 2.977 21.376.93 16.606 0 12.651 0z"/><path fill="#67b279" d="M12.638 25.927C-.054 18.403 0 7.396 0 2.977 3.923.932 8.687.002 12.638 0v25.927z"/><path fill="#fff" d="M12.19 17.305l7.65-10.311c-.56-.45-1.052-.133-1.323.113h-.01l-6.379 6.636-2.403-2.892c-1.147-1.325-2.705-.314-3.07-.047l5.535 6.5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41"><g fill-rule="evenodd"><path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z"/><path fill-rule="nonzero" d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"/><path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,89 @@
.logs__row {
position: relative;
display: flex;
align-items: center;
min-height: 26px;
}
.logs__row--overflow {
overflow: hidden;
}
.logs__row .list-unstyled {
margin-bottom: 0;
overflow: hidden;
}
.logs__text,
.logs__row .list-unstyled li {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.logs__row .tooltip-custom {
top: 0;
margin-left: 0;
margin-right: 5px;
}
.logs__action {
position: absolute;
top: 10px;
right: 15px;
background-color: #fff;
border-radius: 4px;
transition: opacity 0.2s ease, visibility 0.2s ease;
visibility: hidden;
opacity: 0;
}
.logs__table .rt-td {
position: relative;
}
.logs__table .rt-tr:hover .logs__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 input:focus,
.logs__table .rt-thead.-filters select:focus {
border-color: #1991eb;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
}

View File

@@ -1,21 +1,26 @@
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { saveAs } from 'file-saver/FileSaver';
import escapeRegExp from 'lodash/escapeRegExp';
import endsWith from 'lodash/endsWith';
import { Trans, withNamespaces } from 'react-i18next';
import { formatTime } from '../../helpers/helpers';
import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import Loading from '../ui/Loading';
import { normalizeLogs } from '../../helpers/helpers';
import PopoverFiltered from '../ui/PopoverFilter';
import Popover from '../ui/Popover';
import './Logs.css';
const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
class Logs extends Component {
componentDidMount() {
// get logs on initialization if queryLogIsEnabled
if (this.props.dashboard.queryLogEnabled) {
this.props.getLogs();
}
this.getLogs();
this.props.getFilteringStatus();
}
componentDidUpdate(prevProps) {
@@ -25,46 +30,225 @@ class Logs extends Component {
}
}
getLogs = () => {
// get logs on initialization if queryLogIsEnabled
if (this.props.dashboard.queryLogEnabled) {
this.props.getLogs();
}
}
renderTooltip(isFiltered, rule, filter) {
if (rule) {
return (isFiltered && <PopoverFiltered rule={rule} filter={filter}/>);
}
return '';
}
toggleBlocking = (type, domain) => {
const { userRules } = this.props.filtering;
const { t } = this.props;
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
const baseRule = `||${domain}^$important`;
const baseUnblocking = `@@${baseRule}`;
const blockingRule = type === 'block' ? baseUnblocking : baseRule;
const unblockingRule = type === 'block' ? baseRule : baseUnblocking;
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
if (userRules.match(preparedBlockingRule)) {
this.props.setRules(userRules.replace(`${blockingRule}`, ''));
this.props.addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
} else if (!userRules.match(preparedUnblockingRule)) {
this.props.setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
this.props.addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
}
this.props.getFilteringStatus();
}
renderBlockingButton(isFiltered, domain) {
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
const buttonText = isFiltered ? 'unblock_btn' : 'block_btn';
const buttonType = isFiltered ? 'unblock' : 'block';
return (
<div className="logs__action">
<button
type="button"
className={`btn btn-sm ${buttonClass}`}
onClick={() => this.toggleBlocking(buttonType, domain)}
>
<Trans>{buttonText}</Trans>
</button>
</div>
);
}
renderLogs(logs) {
const { t } = this.props;
const columns = [{
Header: 'Time',
Header: t('time_table_header'),
accessor: 'time',
maxWidth: 150,
maxWidth: 110,
filterable: false,
Cell: ({ value }) => (<div className="logs__row"><span className="logs__text" title={value}>{formatTime(value)}</span></div>),
}, {
Header: 'Domain name',
Header: t('domain_name_table_header'),
accessor: 'domain',
Cell: (row) => {
const response = row.value;
const trackerData = getTrackerData(response);
return (
<div className="logs__row" title={response}>
<div className="logs__text">
{response}
</div>
{trackerData && <Popover data={trackerData}/>}
</div>
);
},
}, {
Header: 'Type',
Header: t('type_table_header'),
accessor: 'type',
maxWidth: 100,
maxWidth: 60,
}, {
Header: 'Response',
Header: t('response_table_header'),
accessor: 'response',
Cell: (row) => {
const responses = row.value;
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
const parsedFilteredReason = reason.replace('Filtered', 'Filtered by ');
const rule = row && row.original && row.original.rule;
const { filterId } = row.original;
const { filters } = this.props.filtering;
let filterName = '';
if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') {
if (filterId === 0) {
filterName = t('custom_filter_rules');
} else {
const filterItem = Object.keys(filters)
.filter(key => filters[key].id === filterId);
filterName = filters[filterItem].name;
}
}
if (isFiltered) {
return (
<div className="logs__row">
<span className="logs__text" title={parsedFilteredReason}>
{parsedFilteredReason}
</span>
{this.renderTooltip(isFiltered, rule, filterName)}
</div>
);
}
if (responses.length > 0) {
const liNodes = responses.map((response, index) =>
(<li key={index}>{response}</li>));
return (<ul className="list-unstyled">{liNodes}</ul>);
(<li key={index} title={response}>{response}</li>));
const isRenderTooltip = reason === 'NotFilteredWhiteList';
return (
<div className="logs__row">
<ul className="list-unstyled">{liNodes}</ul>
{this.renderTooltip(isRenderTooltip, rule, filterName)}
</div>
);
}
return 'Empty';
return (
<div className="logs__row">
<span><Trans>empty_response_status</Trans></span>
{this.renderTooltip(isFiltered, rule, filterName)}
</div>
);
},
}];
filterMethod: (filter, row) => {
if (filter.value === 'filtered') {
// eslint-disable-next-line no-underscore-dangle
return row._original.reason.indexOf('Filtered') === 0 || row._original.reason === 'NotFilteredWhiteList';
}
return true;
},
Filter: ({ filter, onChange }) =>
<select
onChange={event => onChange(event.target.value)}
className="form-control"
value={filter ? filter.value : 'all'}
>
<option value="all">{ t('show_all_filter_type') }</option>
<option value="filtered">{ t('show_filtered_type') }</option>
</select>,
}, {
Header: t('Client'),
accessor: 'client',
maxWidth: 250,
Cell: (row) => {
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
return (
<Fragment>
<div className="logs__row">
{row.value}
</div>
{this.renderBlockingButton(isFiltered, row.original.domain)}
</Fragment>
);
},
},
];
if (logs) {
const normalizedLogs = normalizeLogs(logs);
return (<ReactTable
data={normalizedLogs}
className='logs__table'
filterable
data={logs}
columns={columns}
showPagination={false}
showPagination={true}
defaultPageSize={50}
minRows={7}
noDataText="No logs found"
// Text
previousText={ t('previous_btn') }
nextText={ t('next_btn') }
loadingText={ t('loading_table_status') }
pageText={ t('page_table_footer_text') }
ofText={ t('of_table_footer_text') }
rowsText={ t('rows_table_footer_text') }
noDataText={ t('no_logs_found') }
defaultFilterMethod={(filter, row) => {
const id = filter.pivotId || filter.id;
return row[id] !== undefined ?
String(row[id]).indexOf(filter.value) !== -1 : true;
}}
defaultSorted={[
{
id: 'time',
desc: true,
},
]}
getTrProps={(_state, rowInfo) => {
// highlight filtered requests
if (!rowInfo) {
return {};
}
if (rowInfo.original.reason.indexOf('Filtered') === 0) {
return {
className: 'red',
};
} else if (rowInfo.original.reason === 'NotFilteredWhiteList') {
return {
className: 'green',
};
}
return {
className: '',
};
}}
/>);
}
return undefined;
@@ -79,34 +263,53 @@ class Logs extends Component {
};
renderButtons(queryLogEnabled) {
return (<div className="card-actions-top">
<button
className="btn btn-success btn-standart mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
>{queryLogEnabled ? 'Disable log' : 'Enable log'}</button>
{queryLogEnabled &&
<button
className="btn btn-primary btn-standart"
type="submit"
onClick={this.handleDownloadButton}
>Download log file</button> }
</div>);
if (queryLogEnabled) {
return (
<Fragment>
<button
className="btn btn-gray btn-sm mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
><Trans>disabled_log_btn</Trans></button>
<button
className="btn btn-primary btn-sm mr-2"
type="submit"
onClick={this.handleDownloadButton}
><Trans>download_log_file_btn</Trans></button>
<button
className="btn btn-outline-primary btn-sm"
type="submit"
onClick={this.getLogs}
><Trans>refresh_btn</Trans></button>
</Fragment>
);
}
return (
<button
className="btn btn-success btn-sm mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
><Trans>enabled_log_btn</Trans></button>
);
}
render() {
const { queryLogs, dashboard } = this.props;
const { queryLogs, dashboard, t } = this.props;
const { queryLogEnabled } = dashboard;
return (
<div>
<PageTitle title="Query Log" subtitle="DNS queries log" />
<Fragment>
<PageTitle title={ t('query_log') } subtitle={ t('last_dns_queries') }>
<div className="page-title__actions">
{this.renderButtons(queryLogEnabled)}
</div>
</PageTitle>
<Card>
{this.renderButtons(queryLogEnabled)}
{queryLogEnabled && queryLogs.processing && <Loading />}
{queryLogEnabled && !queryLogs.processing &&
{queryLogEnabled && queryLogs.getLogsProcessing && <Loading />}
{queryLogEnabled && !queryLogs.getLogsProcessing &&
this.renderLogs(queryLogs.logs)}
</Card>
</div>
</Fragment>
);
}
}
@@ -117,6 +320,12 @@ Logs.propTypes = {
dashboard: PropTypes.object,
toggleLogStatus: PropTypes.func,
downloadQueryLog: PropTypes.func,
getFilteringStatus: PropTypes.func,
filtering: PropTypes.object,
userRules: PropTypes.string,
setRules: PropTypes.func,
addSuccessToast: PropTypes.func,
t: PropTypes.func,
};
export default Logs;
export default withNamespaces()(Logs);

View File

@@ -10,3 +10,11 @@
padding-left: 20px;
padding-right: 20px;
}
.form-control--textarea {
min-height: 110px;
}
.form-control--textarea-large {
min-height: 240px;
}

View File

@@ -1,8 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
export default class Upstream extends Component {
class Upstream extends Component {
handleChange = (e) => {
const { value } = e.currentTarget;
this.props.handleUpstreamChange(value);
@@ -13,31 +15,66 @@ export default class Upstream extends Component {
this.props.handleUpstreamSubmit();
};
handleTest = () => {
this.props.handleUpstreamTest();
}
render() {
const testButtonClass = classnames({
'btn btn-primary btn-standart mr-2': true,
'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream,
});
const { t } = this.props;
return (
<Card
title="Upstream DNS servers"
subtitle="If you keep this field empty, AdGuard will use <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> as an upstream."
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<form>
<textarea
className="form-control"
value={this.props.upstream}
className="form-control form-control--textarea"
value={this.props.upstreamDns}
onChange={this.handleChange}
/>
<div className="card-actions">
<button
className={testButtonClass}
type="button"
onClick={this.handleTest}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
className="btn btn-success btn-standart"
type="submit"
onClick={this.handleSubmit}
>
Apply
<Trans>apply_btn</Trans>
</button>
</div>
</form>
<hr/>
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { t('example_upstream_regular') }
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_dot') }} />
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_doh') }} />
</li>
<li>
<code>tcp://1.1.1.1</code> - { t('example_upstream_tcp') }
</li>
</ol>
</div>
</div>
</div>
</Card>
@@ -46,7 +83,12 @@ export default class Upstream extends Component {
}
Upstream.propTypes = {
upstream: PropTypes.string,
upstreamDns: PropTypes.string,
processingTestUpstream: PropTypes.bool,
handleUpstreamChange: PropTypes.func,
handleUpstreamSubmit: PropTypes.func,
handleUpstreamTest: PropTypes.func,
t: PropTypes.func,
};
export default withNamespaces()(Upstream);

View File

@@ -1,5 +1,6 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next';
import Upstream from './Upstream';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
@@ -7,27 +8,27 @@ import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import './Settings.css';
export default class Settings extends Component {
class Settings extends Component {
settings = {
filtering: {
enabled: false,
title: 'Block domains using filters and hosts files',
subtitle: 'You can setup blocking rules in the <a href="#filters">Filters</a> settings.',
title: 'block_domain_use_filters_and_hosts',
subtitle: 'filters_block_toggle_hint',
},
safebrowsing: {
enabled: false,
title: 'Use AdGuard browsing security web service',
subtitle: 'AdGuard DNS will check if domain is blacklisted by the browsing security web service (sb.adtidy.org). It will use privacy-safe lookup API to do the check.',
title: 'use_adguard_browsing_sec',
subtitle: 'use_adguard_browsing_sec_hint',
},
parental: {
enabled: false,
title: 'Use AdGuard parental control web service',
subtitle: 'AdGuard DNS will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.',
title: 'use_adguard_parental',
subtitle: 'use_adguard_parental_hint',
},
safesearch: {
enabled: false,
title: 'Enforce safe search',
subtitle: 'AdGuard DNS can enforce safe search in the major search engines: Google, Bing, Yandex.',
title: 'enforce_safe_search',
subtitle: 'enforce_save_search_hint',
},
};
@@ -36,11 +37,19 @@ export default class Settings extends Component {
}
handleUpstreamChange = (value) => {
this.props.handleUpstreamChange({ upstream: value });
this.props.handleUpstreamChange({ upstreamDns: value });
};
handleUpstreamSubmit = () => {
this.props.setUpstream(this.props.settings.upstream);
this.props.setUpstream(this.props.dashboard.upstreamDns);
};
handleUpstreamTest = () => {
if (this.props.dashboard.upstreamDns.length > 0) {
this.props.testUpstream(this.props.dashboard.upstreamDns);
} else {
this.props.addErrorToast({ error: this.props.t('no_servers_specified') });
}
};
renderSettings = (settings) => {
@@ -56,29 +65,32 @@ export default class Settings extends Component {
});
}
return (
<div>No settings</div>
<div><Trans>no_settings</Trans></div>
);
}
render() {
const { settings, upstream } = this.props;
const { settings, t } = this.props;
const { upstreamDns } = this.props.dashboard;
return (
<Fragment>
<PageTitle title="Settings" />
<PageTitle title={ t('settings') } />
{settings.processing && <Loading />}
{!settings.processing &&
<div className="content">
<div className="row">
<div className="col-md-12">
<Card title="General settings" bodyType="card-body box-body--settings">
<Card title={ t('general_settings') } bodyType="card-body box-body--settings">
<div className="form">
{this.renderSettings(settings.settingsList)}
</div>
</Card>
<Upstream
upstream={upstream}
upstreamDns={upstreamDns}
processingTestUpstream={settings.processingTestUpstream}
handleUpstreamChange={this.handleUpstreamChange}
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
/>
</div>
</div>
@@ -97,4 +109,7 @@ Settings.propTypes = {
handleUpstreamChange: PropTypes.func,
setUpstream: PropTypes.func,
upstream: PropTypes.string,
t: PropTypes.func,
};
export default withNamespaces()(Settings);

View File

@@ -0,0 +1,60 @@
.toasts {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 103;
width: 345px;
}
.toast {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
padding: 16px;
font-weight: 600;
color: #ffffff;
border-radius: 4px;
background-color: rgba(236, 53, 53, 0.75);
}
.toast--success {
background-color: rgba(90, 173, 99, 0.75);
}
.toast:last-child {
margin-bottom: 0;
}
.toast__content {
flex: 1 1 auto;
margin: 0 12px 0 0;
text-overflow: ellipsis;
overflow: hidden;
}
.toast__dismiss {
display: block;
flex: 0 0 auto;
padding: 0;
background: transparent;
border: 0;
cursor: pointer;
}
.toast-enter {
opacity: 0.01;
}
.toast-enter-active {
opacity: 1;
transition: all 0.3s ease-out;
}
.toast-exit {
opacity: 1;
}
.toast-exit-active {
opacity: 0.01;
transition: all 0.3s ease-out;
}

View File

@@ -0,0 +1,39 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
class Toast extends Component {
componentDidMount() {
const timeout = this.props.type === 'error' ? 30000 : 5000;
setTimeout(() => {
this.props.removeToast(this.props.id);
}, timeout);
}
shouldComponentUpdate() {
return false;
}
render() {
return (
<div className={`toast toast--${this.props.type}`}>
<p className="toast__content">
<Trans>{this.props.message}</Trans>
</p>
<button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}>
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
</button>
</div>
);
}
}
Toast.propTypes = {
id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
removeToast: PropTypes.func.isRequired,
};
export default withNamespaces()(Toast);

View File

@@ -0,0 +1,42 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import * as actionCreators from '../../actions';
import Toast from './Toast';
import './Toast.css';
const Toasts = props => (
<TransitionGroup className="toasts">
{props.toasts.notices && props.toasts.notices.map((toast) => {
const { id } = toast;
return (
<CSSTransition
key={id}
timeout={500}
classNames="toast"
>
<Toast removeToast={props.removeToast} {...toast} />
</CSSTransition>
);
})}
</TransitionGroup>
);
Toasts.propTypes = {
toasts: PropTypes.object,
removeToast: PropTypes.func,
};
const mapStateToProps = (state) => {
const { toasts } = state;
const props = { toasts };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Toasts);

View File

@@ -26,7 +26,6 @@
display: flex;
align-items: center;
justify-content: center;
height: 400px;
}
.card-body--status {
@@ -40,11 +39,61 @@
background-size: 14px;
background-position: center;
background-repeat: no-repeat;
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiM0NjdmY2YiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==');
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiM0NjdmY2YiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
}
.card-refresh:hover,
.card-refresh:not(:disabled):not(.disabled):active,
.card-refresh:focus:active {
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==');
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
}
.card-title-stats {
font-size: 13px;
color: #9aa0ac;
}
.card-body-stats {
position: relative;
flex: 1 1 auto;
height: calc(100% - 3rem);
margin: 0;
padding: 1rem 1.5rem;
}
.card-value-stats {
display: block;
font-size: 2.1rem;
line-height: 2.7rem;
height: 2.7rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.card-value-percent {
position: absolute;
top: 15px;
right: 15px;
font-size: 0.9rem;
line-height: 1;
height: auto;
}
.card-value-percent:after {
content: "%";
}
.card--full {
height: calc(100% - 1.5rem);
}
.card-wrap {
height: 100%;
}
@media screen and (min-width: 1280px) {
.card-title-stats {
font-size: 14px;
}
}

View File

@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
import './Card.css';
const Card = props => (
<div className="card">
{ props.title &&
<div className={props.type ? `card ${props.type}` : 'card'}>
{props.title &&
<div className="card-header with-border">
<div className="card-inner">
<div className="card-title">
@@ -33,6 +33,7 @@ Card.propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
bodyType: PropTypes.string,
type: PropTypes.string,
refresh: PropTypes.node,
children: PropTypes.node.isRequired,
};

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
const Cell = props => (
<div className="stats__row">
<div className="stats__row-value mb-1">
<strong>{props.value}</strong>
<small className="ml-3 text-muted">
{props.percent}%
</small>
</div>
<div className="progress progress-xs">
<div
className="progress-bar"
style={{
width: `${props.percent}%`,
backgroundColor: props.color,
}}
/>
</div>
</div>
);
Cell.propTypes = {
value: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
};
export default Cell;

View File

@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import './Checkbox.css';
@@ -10,6 +11,7 @@ class Checkbox extends Component {
subtitle,
enabled,
handleChange,
t,
} = this.props;
return (
<div className="form__group">
@@ -18,8 +20,8 @@ class Checkbox extends Component {
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled}/>
<span className="checkbox__label">
<span className="checkbox__label-text">
<span className="checkbox__label-title">{title}</span>
<span className="checkbox__label-subtitle" dangerouslySetInnerHTML={{ __html: subtitle }}/>
<span className="checkbox__label-title">{ t(title) }</span>
<span className="checkbox__label-subtitle" dangerouslySetInnerHTML={{ __html: t(subtitle) }}/>
</span>
</span>
</label>
@@ -33,6 +35,7 @@ Checkbox.propTypes = {
subtitle: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
handleChange: PropTypes.func.isRequired,
t: PropTypes.func,
};
export default Checkbox;
export default withNamespaces()(Checkbox);

View File

@@ -0,0 +1,45 @@
.footer__row {
display: flex;
align-items: center;
flex-direction: column;
}
.footer__column {
margin-bottom: 15px;
}
.footer__column--language {
min-width: 220px;
margin-bottom: 0;
}
.footer__link {
display: inline-block;
vertical-align: middle;
margin-right: 15px;
}
.footer__link--report {
position: relative;
top: 1px;
margin-right: 0;
}
@media screen and (min-width: 768px) {
.footer__copyright {
margin-right: 25px;
}
.footer__row {
flex-direction: row;
}
.footer__column {
margin-bottom: 0;
}
.footer__column--language {
min-width: initial;
margin-left: auto;
}
}

View File

@@ -1,4 +1,10 @@
import React, { Component } from 'react';
import { Trans, withNamespaces } from 'react-i18next';
import { REPOSITORY, LANGUAGES } from '../../helpers/constants';
import i18n from '../../i18n';
import './Footer.css';
import './Select.css';
class Footer extends Component {
getYear = () => {
@@ -6,26 +12,35 @@ class Footer extends Component {
return today.getFullYear();
};
changeLanguage = (event) => {
i18n.changeLanguage(event.target.value);
}
render() {
return (
<footer className="footer">
<div className="container">
<div className="row align-items-center flex-row-reverse">
<div className="col-12 col-lg-auto ml-lg-auto">
<ul className="list-inline list-inline-dots text-center mb-0">
<li className="list-inline-item">
<a href="https://adguard.com/welcome.html" target="_blank" rel="noopener noreferrer">Homepage</a>
</li>
<li className="list-inline-item">
<a href="https://github.com/AdguardTeam/" target="_blank" rel="noopener noreferrer">Github</a>
</li>
<li className="list-inline-item">
<a href="https://adguard.com/privacy.html" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
</li>
</ul>
<div className="footer__row">
<div className="footer__column">
<div className="footer__copyright">
<Trans>copyright</Trans> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
</div>
</div>
<div className="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
© AdGuard {this.getYear()}
<div className="footer__column">
<a href={REPOSITORY.URL} className="footer__link" target="_blank" rel="noopener noreferrer">
<Trans>homepage</Trans>
</a>
<a href={`${REPOSITORY.URL}/issues/new`} className="btn btn-outline-primary btn-sm footer__link footer__link--report" target="_blank" rel="noopener noreferrer">
<Trans>report_an_issue</Trans>
</a>
</div>
<div className="footer__column footer__column--language">
<select className="form-control select select--language" value={i18n.language} onChange={this.changeLanguage}>
{LANGUAGES.map(language =>
<option key={language.key} value={language.key}>
{language.name}
</option>)}
</select>
</div>
</div>
</div>
@@ -34,4 +49,4 @@ class Footer extends Component {
}
}
export default Footer;
export default withNamespaces()(Footer);

View File

@@ -0,0 +1,9 @@
.line__tooltip {
padding: 2px 10px 7px;
line-height: 1.1;
color: #fff;
}
.line__tooltip-text {
font-size: 0.7rem;
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ResponsiveLine } from '@nivo/line';
import './Line.css';
const Line = props => (
props.data &&
<ResponsiveLine
data={props.data}
margin={{
top: 15,
right: 0,
bottom: 1,
left: 0,
}}
minY="auto"
stacked={false}
curve='linear'
axisBottom={{
tickSize: 0,
tickPadding: 10,
}}
axisLeft={{
tickSize: 0,
tickPadding: 10,
}}
enableGridX={false}
enableGridY={false}
enableDots={false}
enableArea={true}
animate={false}
colorBy={() => (props.color)}
tooltip={slice => (
<div>
{slice.data.map(d => (
<div key={d.serie.id} className="line__tooltip">
<span className="line__tooltip-text">
<strong>{d.data.y}</strong>
<br/>
<small>{d.data.x}</small>
</span>
</div>
))}
</div>
)}
theme={{
tooltip: {
container: {
padding: '0',
background: '#333',
borderRadius: '4px',
},
},
}}
/>
);
Line.propTypes = {
data: PropTypes.array.isRequired,
color: PropTypes.string.isRequired,
};
export default Line;

View File

@@ -2,16 +2,20 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import { R_URL_REQUIRES_PROTOCOL } from '../../helpers/constants';
import './Modal.css';
ReactModal.setAppElement('#root');
export default class Modal extends Component {
state = {
url: '',
isUrlValid: false,
};
const initialState = {
url: '',
name: '',
isUrlValid: false,
};
class Modal extends Component {
state = initialState;
// eslint-disable-next-line
isUrlValid = url => {
@@ -27,33 +31,48 @@ export default class Modal extends Component {
}
};
handleNameChange = (e) => {
const { value: name } = e.currentTarget;
this.setState({ ...this.state, name });
};
handleNext = () => {
this.props.addFilter(this.state.url);
this.props.addFilter(this.state.url, this.state.name);
setTimeout(() => {
if (this.props.isFilterAdded) {
this.props.toggleModal();
this.closeModal();
}
}, 2000);
};
closeModal = () => {
this.props.toggleModal();
this.setState({ ...this.state, ...initialState });
}
render() {
const {
isOpen,
toggleModal,
title,
inputDescription,
} = this.props;
const { isUrlValid, url } = this.state;
const inputClass = classnames({
const { isUrlValid, url, name } = this.state;
const inputUrlClass = classnames({
'form-control mb-2': true,
'is-invalid': url.length > 0 && !isUrlValid,
'is-valid': url.length > 0 && isUrlValid,
});
const inputNameClass = classnames({
'form-control mb-2': true,
'is-valid': name.length > 0,
});
const renderBody = () => {
if (!this.props.isFilterAdded) {
return (
<React.Fragment>
<input type="text" className={inputClass} placeholder="Enter URL or path" onChange={this.handleUrlChange}/>
<input type="text" className={inputNameClass} placeholder={ this.props.t('enter_name_hint') } onChange={this.handleNameChange} />
<input type="text" className={inputUrlClass} placeholder={ this.props.t('enter_url_hint') } onChange={this.handleUrlChange} />
{inputDescription &&
<div className="description">
{inputDescription}
@@ -63,26 +82,26 @@ export default class Modal extends Component {
}
return (
<div className="description">
Url added successfully
<Trans>Url added successfully</Trans>
</div>
);
};
const isValidForSubmit = !(url.length > 0 && isUrlValid);
const isValidForSubmit = !(url.length > 0 && isUrlValid && name.length > 0);
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
closeTimeoutMS={0}
isOpen={ isOpen }
onRequestClose={toggleModal}
onRequestClose={this.closeModal}
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{title}
</h4>
<button type="button" className="close" onClick={toggleModal}>
<button type="button" className="close" onClick={this.closeModal}>
<span className="sr-only">Close</span>
</button>
</div>
@@ -92,8 +111,8 @@ export default class Modal extends Component {
{
!this.props.isFilterAdded &&
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={toggleModal}>Cancel</button>
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}>Add filter</button>
<button type="button" className="btn btn-secondary" onClick={this.closeModal}><Trans>cancel_btn</Trans></button>
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}><Trans>add_filter_btn</Trans></button>
</div>
}
</div>
@@ -109,4 +128,7 @@ Modal.propTypes = {
inputDescription: PropTypes.string,
addFilter: PropTypes.func.isRequired,
isFilterAdded: PropTypes.bool,
t: PropTypes.func,
};
export default withNamespaces()(Modal);

View File

@@ -0,0 +1,102 @@
.popover-wrap {
position: relative;
top: 1px;
display: inline-block;
vertical-align: middle;
align-self: flex-start;
}
.popover__trigger {
position: relative;
top: 3px;
margin: 0 8px;
cursor: pointer;
}
.popover__trigger:after {
content: "";
position: absolute;
top: -6px;
left: -3px;
width: 26px;
height: 24px;
}
.popover__body {
content: "";
display: flex;
position: absolute;
bottom: calc(100% + 3px);
left: 50%;
min-width: 275px;
padding: 10px 15px;
font-size: 0.8rem;
white-space: normal;
color: #fff;
background-color: #585965;
border-radius: 3px;
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
}
.popover__body--filter {
min-width: 100%;
}
.popover__body:after {
content: "";
position: absolute;
bottom: -5px;
left: calc(50% - 6px);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #585965;
}
.popover__trigger:hover + .popover__body,
.popover__body:hover {
visibility: visible;
opacity: 1;
}
.popover__icon {
width: 20px;
height: 20px;
stroke: #9aa0ac;
}
.popover__icon--green {
stroke: #66b574;
}
.popover__list-title {
margin-bottom: 3px;
}
.popover__list-item {
margin-bottom: 2px;
}
.popover__list-item--nowrap {
max-width: 300px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.popover__list-item:last-child {
margin-bottom: 0;
}
.popover__link {
color: #66b586;
}
.popover__link:hover,
.popover__link:focus {
color: #66b586;
}

View File

@@ -0,0 +1,56 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import { getSourceData } from '../../helpers/trackers/trackers';
import { captitalizeWords } from '../../helpers/helpers';
import './Popover.css';
class Popover extends Component {
render() {
const { data } = this.props;
const sourceData = getSourceData(data);
const source = (
<div className="popover__list-item">
<Trans>source_label</Trans>: <a className="popover__link" target="_blank" rel="noopener noreferrer" href={sourceData.url}><strong>{sourceData.name}</strong></a>
</div>
);
const tracker = (
<div className="popover__list-item">
<Trans>name_table_header</Trans>: <a className="popover__link" target="_blank" rel="noopener noreferrer" href={data.url}><strong>{data.name}</strong></a>
</div>
);
const categoryName = captitalizeWords(data.category);
return (
<div className="popover-wrap">
<div className="popover__trigger">
<svg className="popover__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</div>
<div className="popover__body">
<div className="popover__list">
<div className="popover__list-title">
<Trans>found_in_known_domain_db</Trans>
</div>
{tracker}
<div className="popover__list-item">
<Trans>category_label</Trans>: <strong>
<Trans>{categoryName}</Trans></strong>
</div>
{source}
</div>
</div>
</div>
);
}
}
Popover.propTypes = {
data: PropTypes.object.isRequired,
};
export default withNamespaces()(Popover);

View File

@@ -0,0 +1,34 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import './Popover.css';
class PopoverFilter extends Component {
render() {
return (
<div className="popover-wrap">
<div className="popover__trigger popover__trigger--filter">
<svg className="popover__icon popover__icon--green" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
</div>
<div className="popover__body popover__body--filter">
<div className="popover__list">
<div className="popover__list-item popover__list-item--nowrap">
<Trans>rule_label</Trans>: <strong>{this.props.rule}</strong>
</div>
{this.props.filter && <div className="popover__list-item popover__list-item--nowrap">
<Trans>filter_label</Trans>: <strong>{this.props.filter}</strong>
</div>}
</div>
</div>
</div>
);
}
}
PopoverFilter.propTypes = {
rule: PropTypes.string.isRequired,
filter: PropTypes.string,
};
export default withNamespaces()(PopoverFilter);

View File

@@ -1,4 +1,17 @@
.ReactTable .rt-th,
.ReactTable .rt-td {
padding: 10px 15px;
overflow: visible;
}
.ReactTable .rt-tbody {
overflow: visible;
}
.rt-tr-group .red {
background-color: #fff4f2;
}
.rt-tr-group .green {
background-color: #f1faf3;
}

View File

@@ -0,0 +1,16 @@
.select.select--language {
height: 45px;
padding: 0 32px 2px 33px;
outline: 0;
border-color: rgba(0, 40, 100, 0.12);
background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg");
background-repeat: no-repeat, no-repeat;
background-position: left 11px center, right 9px center;
background-size: 14px, 17px 20px;
appearance: none;
cursor: pointer;
}
.select--language::-ms-expand {
opacity: 0;
}

View File

@@ -7,10 +7,10 @@ const Status = props => (
<div className="status">
<Card bodyType="card-body card-body--status">
<div className="h4 font-weight-light mb-4">
You are currently not using AdGuard DNS
You are currently not using AdGuard Home
</div>
<button className="btn btn-success" onClick={props.handleStatusChange}>
Enable AdGuard DNS
Enable AdGuard Home
</button>
</Card>
</div>

View File

@@ -10373,6 +10373,8 @@ body.fixed-header .header {
font-size: 0.875rem;
padding: 1.25rem 0;
color: #9aa0ac;
position: relative;
z-index: 102;
}
.footer a:not(.btn) {

View File

@@ -5,6 +5,7 @@
vertical-align: middle;
width: 18px;
height: 18px;
flex-shrink: 0;
margin-left: 5px;
background-image: url("./svg/help-circle.svg");
background-size: 100%;
@@ -15,15 +16,15 @@
content: attr(data-tooltip);
display: block;
position: absolute;
bottom: calc(100% + 12px);
left: calc(50% - 103px);
width: 206px;
bottom: calc(100% + 10px);
left: 50%;
padding: 10px 15px;
font-size: 0.85rem;
text-align: center;
color: #fff;
background-color: #585965;
border-radius: 3px;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
}
@@ -31,7 +32,7 @@
.tooltip-custom:after {
content: "";
position: relative;
top: -9px;
top: -7px;
left: calc(50% - 6px);
visibility: hidden;
opacity: 0;
@@ -47,3 +48,7 @@
visibility: visible;
opacity: 1;
}
.tooltip-custom--narrow:before {
width: 220px;
}

View File

@@ -4,11 +4,12 @@ import PropTypes from 'prop-types';
import './Tooltip.css';
const Tooltip = props => (
<div data-tooltip={props.text} className="tooltip-custom"></div>
<div data-tooltip={props.text} className={`tooltip-custom ${props.type || ''}`}></div>
);
Tooltip.propTypes = {
text: PropTypes.string.isRequired,
type: PropTypes.string,
};
export default Tooltip;

View File

@@ -0,0 +1,6 @@
.update {
position: relative;
z-index: 102;
margin-bottom: 0;
padding: 0.75rem 0;
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Update.css';
const Update = props => (
<div className="alert alert-info update">
<div className="container">
{props.announcement} <a href={props.announcementUrl} target="_blank" rel="noopener noreferrer">Click here</a> for more info.
</div>
</div>
);
Update.propTypes = {
announcement: PropTypes.string.isRequired,
announcementUrl: PropTypes.string.isRequired,
};
export default Update;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -1,10 +1,10 @@
import { connect } from 'react-redux';
import { getLogs, toggleLogStatus, downloadQueryLog } from '../actions';
import { getLogs, toggleLogStatus, downloadQueryLog, getFilteringStatus, setRules, addSuccessToast } from '../actions';
import Logs from '../components/Logs';
const mapStateToProps = (state) => {
const { queryLogs, dashboard } = state;
const props = { queryLogs, dashboard };
const { queryLogs, dashboard, filtering } = state;
const props = { queryLogs, dashboard, filtering };
return props;
};
@@ -12,6 +12,9 @@ const mapDispatchToProps = {
getLogs,
toggleLogStatus,
downloadQueryLog,
getFilteringStatus,
setRules,
addSuccessToast,
};
export default connect(

View File

@@ -1,10 +1,10 @@
import { connect } from 'react-redux';
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream } from '../actions';
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
const { settings } = state;
const props = { settings };
const { settings, dashboard } = state;
const props = { settings, dashboard };
return props;
};
@@ -13,6 +13,8 @@ const mapDispatchToProps = {
toggleSetting,
handleUpstreamChange,
setUpstream,
testUpstream,
addErrorToast,
};
export default connect(

View File

@@ -1 +1,57 @@
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
export const STATS_NAMES = {
avg_processing_time: 'average_processing_time',
blocked_filtering: 'Blocked by filters',
dns_queries: 'DNS queries',
replaced_parental: 'stats_adult',
replaced_safebrowsing: 'stats_malware_phishing',
replaced_safesearch: 'enforced_save_search',
};
export const STATUS_COLORS = {
blue: '#467fcf',
red: '#cd201f',
green: '#5eba00',
yellow: '#f1c40f',
};
export const REPOSITORY = {
URL: 'https://github.com/AdguardTeam/AdGuardHome',
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
};
export const LANGUAGES = [
{
key: 'en',
name: 'English',
},
{
key: 'es',
name: 'Español',
},
{
key: 'fr',
name: 'Français',
},
{
key: 'pt-br',
name: 'Português (BR)',
},
{
key: 'sv',
name: 'Svenska',
},
{
key: 'vi',
name: 'Tiếng Việt',
},
{
key: 'ru',
name: 'Русский',
},
{
key: 'ja',
name: '日本語',
},
];

View File

@@ -1,10 +1,12 @@
import dateParse from 'date-fns/parse';
import dateFormat from 'date-fns/format';
import startOfToday from 'date-fns/start_of_today';
import subHours from 'date-fns/sub_hours';
import addHours from 'date-fns/add_hours';
import round from 'lodash/round';
const formatTime = (time) => {
import { STATS_NAMES } from './constants';
export const formatTime = (time) => {
const parsedTime = dateParse(time);
return dateFormat(parsedTime, 'HH:mm:ss');
};
@@ -14,6 +16,10 @@ export const normalizeLogs = logs => logs.map((log) => {
time,
question,
answer: response,
reason,
client,
filterId,
rule,
} = log;
const { host: domain, type } = question;
const responsesArray = response ? response.map((response) => {
@@ -21,20 +27,27 @@ export const normalizeLogs = logs => logs.map((log) => {
return `${type}: ${value} (ttl=${ttl})`;
}) : [];
return {
time: formatTime(time),
time,
domain,
type,
response: responsesArray,
reason,
client,
filterId,
rule,
};
});
export const normalizeHistory = history => Object.keys(history).map((key) => {
const id = key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase());
let id = STATS_NAMES[key];
if (!id) {
id = key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase());
}
const today = startOfToday();
const dayAgo = subHours(Date.now(), 24);
const data = history[key].map((item, index) => {
const formatHour = dateFormat(addHours(today, index), 'HH:mm');
const formatHour = dateFormat(addHours(dayAgo, index), 'ddd HH:00');
const roundValue = round(item, 2);
return {
@@ -53,13 +66,22 @@ export const normalizeFilteringStatus = (filteringStatus) => {
const { enabled, filters, user_rules: userRules } = filteringStatus;
const newFilters = filters ? filters.map((filter) => {
const {
url, enabled, last_updated: lastUpdated = Date.now(), name = 'Default name', rules_count: rulesCount = 0,
id, url, enabled, lastUpdated: lastUpdated = Date.now(), name = 'Default name', rulesCount: rulesCount = 0,
} = filter;
return {
url, enabled, lastUpdated: formatTime(lastUpdated), name, rulesCount,
id, url, enabled, lastUpdated: formatTime(lastUpdated), name, rulesCount,
};
}) : [];
const newUserRules = Array.isArray(userRules) ? userRules.join('\n') : '';
return { enabled, userRules: newUserRules, filters: newFilters };
};
export const getPercent = (amount, number) => {
if (amount > 0 && number > 0) {
return round(100 / (amount / number), 2);
}
return 0;
};
export const captitalizeWords = text => text.split(/[ -_]/g).map(str => str.charAt(0).toUpperCase() + str.substr(1)).join(' ');

View File

@@ -0,0 +1,83 @@
{
"timeUpdated": "2018-10-14",
"categories": {
"0": "audio_video_player",
"1": "comments",
"2": "customer_interaction",
"3": "pornvertising",
"4": "advertising",
"5": "essential",
"6": "site_analytics",
"7": "social_media",
"8": "misc",
"9": "cdn",
"10": "hosting",
"11": "unknown",
"12": "extensions",
"101": "mobile_analytics"
},
"trackers": {
"facebook_audience": {
"name": "Facebook Audience Network",
"categoryId": 4,
"url": "https://www.facebook.com/business/products/audience-network"
},
"crashlytics": {
"name": "Crashlytics",
"categoryId": 101,
"url": "https://crashlytics.com/"
},
"flurry": {
"name": "Flurry",
"categoryId": 101,
"url": "http://www.flurry.com/"
},
"hockeyapp": {
"name": "HockeyApp",
"categoryId": 101,
"url": "https://hockeyapp.net/"
},
"firebase": {
"name": "Firebase",
"categoryId": 101,
"url": "https://firebase.google.com/"
},
"appsflyer": {
"name": "AppsFlyer",
"categoryId": 101,
"url": "https://www.appsflyer.com/"
},
"yandex_appmetrica": {
"name": "Yandex AppMetrica",
"categoryId": 101,
"url": "https://appmetrica.yandex.com/"
},
"adjust": {
"name": "Adjust",
"categoryId": 101,
"url": "https://www.adjust.com/"
},
"branch": {
"name": "Branch.io",
"categoryId": 101,
"url": "https://branch.io/"
},
"adocean": {
"name": "Gemius Adocean",
"categoryId": 4,
"url": "https://adocean-global.com/"
}
},
"trackerDomains": {
"graph.facebook.com": "facebook_audience",
"crashlytics.com": "crashlytics",
"flurry.com": "flurry",
"hockeyapp.net": "hockeyapp",
"app-measurement.com": "firebase",
"appsflyer.com": "appsflyer",
"appmetrica.yandex.com": "yandex_appmetrica",
"adjust.com": "adjust",
"mobileapptracking.com": "branch",
"adocean.cz": "adocean"
}
}

View File

@@ -0,0 +1,104 @@
import whotracksmeDb from './whotracksme.json';
import adguardDb from './adguard.json';
import { REPOSITORY } from '../constants';
/**
@typedef TrackerData
@type {object}
@property {string} id - tracker ID.
@property {string} name - tracker name.
@property {string} url - tracker website url.
@property {number} category - tracker category.
@property {source} source - tracker data source.
*/
/**
* Tracker data sources
*/
export const sources = {
WHOTRACKSME: 1,
ADGUARD: 2,
};
/**
* Gets tracker data in the specified database
*
* @param {String} domainName domain name to check
* @param {*} trackersDb trackers database
* @param {number} source source ID
* @returns {TrackerData} tracker data or null if no matching tracker found
*/
const getTrackerDataFromDb = (domainName, trackersDb, source) => {
if (!domainName) {
return null;
}
const parts = domainName.split(/\./g).reverse();
let hostToCheck = '';
// Check every subdomain
for (let i = 0; i < parts.length; i += 1) {
hostToCheck = parts[i] + (i > 0 ? '.' : '') + hostToCheck;
const trackerId = trackersDb.trackerDomains[hostToCheck];
if (trackerId) {
const trackerData = trackersDb.trackers[trackerId];
const categoryName = trackersDb.categories[trackerData.categoryId];
return {
id: trackerId,
name: trackerData.name,
url: trackerData.url,
category: categoryName,
source,
};
}
}
// No tracker found for the specified domain
return null;
};
/**
* Gets the source metadata for the specified tracker
* @param {TrackerData} trackerData tracker data
* @returns {source} source metadata or null if no matching tracker found
*/
export const getSourceData = (trackerData) => {
if (!trackerData || !trackerData.source) {
return null;
}
if (trackerData.source === sources.WHOTRACKSME) {
return {
name: 'Whotracks.me',
url: `https://whotracks.me/trackers/${trackerData.id}.html`,
};
} else if (trackerData.source === sources.ADGUARD) {
return {
name: 'AdGuard',
url: REPOSITORY.TRACKERS_DB,
};
}
return null;
};
/**
* Gets tracker data from the trackers database
*
* @param {String} domainName domain name to check
* @returns {TrackerData} tracker data or null if no matching tracker found
*/
export const getTrackerData = (domainName) => {
if (!domainName) {
return null;
}
let data = getTrackerDataFromDb(domainName, adguardDb, sources.ADGUARD);
if (!data) {
data = getTrackerDataFromDb(domainName, whotracksmeDb, sources.WHOTRACKSME);
}
return data;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
/*
* Project: tiny-version-compare https://github.com/bfred-it/tiny-version-compare
* License (MIT) https://github.com/bfred-it/tiny-version-compare/blob/master/LICENSE
*/
const split = v => String(v).replace(/^[vr]/, '') // Drop initial 'v' or 'r'
.replace(/([a-z]+)/gi, '.$1.') // Sort each word separately
.replace(/[-.]+/g, '.') // Consider dashes as separators (+ trim multiple separators)
.split('.');
// Development versions are considered "negative",
// but localeCompare doesn't handle negative numbers.
// This offset is applied to reset the lowest development version to 0
const offset = (part) => {
// Not numeric, return as is
if (Number.isNaN(part)) {
return part;
}
return 5 + Number(part);
};
const parsePart = (part) => {
// Missing, consider it zero
if (typeof part === 'undefined') {
return 0;
}
// Sort development versions
switch (part.toLowerCase()) {
case 'dev':
return -5;
case 'alpha':
return -4;
case 'beta':
return -3;
case 'rc':
return -2;
case 'pre':
return -1;
default:
}
// Return as is, its either a plain number or text that will be sorted alphabetically
return part;
};
const versionCompare = (prev, next) => {
const a = split(prev);
const b = split(next);
for (let i = 0; i < a.length || i < b.length; i += 1) {
const ai = offset(parsePart(a[i]));
const bi = offset(parsePart(b[i]));
const sort = String(ai).localeCompare(bi, 'en', {
numeric: true,
});
// Once the difference is found,
// stop comparing the rest of the parts
if (sort !== 0) {
return sort;
}
}
// No difference found
return 0;
};
export default versionCompare;

59
client/src/i18n.js Normal file
View File

@@ -0,0 +1,59 @@
import i18n from 'i18next';
import { reactI18nextModule } from 'react-i18next';
import { initReactI18n } from 'react-i18next/hooks';
import langDetect from 'i18next-browser-languagedetector';
import vi from './__locales/vi.json';
import en from './__locales/en.json';
import ru from './__locales/ru.json';
import es from './__locales/es.json';
import fr from './__locales/fr.json';
import ja from './__locales/ja.json';
import sv from './__locales/sv.json';
import ptBR from './__locales/pt-br.json';
const resources = {
en: {
translation: en,
},
vi: {
translation: vi,
},
ru: {
translation: ru,
},
es: {
translation: es,
},
fr: {
translation: fr,
},
ja: {
translation: ja,
},
sv: {
translation: sv,
},
'pt-BR': {
translation: ptBR,
},
};
i18n
.use(langDetect)
.use(initReactI18n)
.use(reactI18nextModule) // passes i18n down to react-i18next
.init({
resources,
fallbackLng: 'en',
keySeparator: false, // we use content as keys
nsSeparator: false, // Fix character in content
interpolation: {
escapeValue: false, // not needed for react!!
},
react: {
wait: true,
},
});
export default i18n;

View File

@@ -5,6 +5,7 @@ import './components/App/index.css';
import App from './containers/App';
import configureStore from './configureStore';
import reducers from './reducers';
import './i18n';
const store = configureStore(reducers, {}); // set initial state
ReactDOM.render(

View File

@@ -1,5 +1,8 @@
import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions';
import { loadingBarReducer } from 'react-redux-loading-bar';
import nanoid from 'nanoid';
import versionCompare from '../helpers/versionCompare';
import * as actions from '../actions';
@@ -24,14 +27,14 @@ const settings = handleActions({
[actions.setUpstreamRequest]: state => ({ ...state, processingUpstream: true }),
[actions.setUpstreamFailure]: state => ({ ...state, processingUpstream: false }),
[actions.setUpstreamSuccess]: state => ({ ...state, processingUpstream: false }),
[actions.handleUpstreamChange]: (state, { payload }) => {
const { upstream } = payload;
return { ...state, upstream };
},
[actions.testUpstreamRequest]: state => ({ ...state, processingTestUpstream: true }),
[actions.testUpstreamFailure]: state => ({ ...state, processingTestUpstream: false }),
[actions.testUpstreamSuccess]: state => ({ ...state, processingTestUpstream: false }),
}, {
processing: true,
processingUpstream: true,
upstream: '',
processingTestUpstream: false,
processingSetUpstream: false,
});
const dashboard = handleActions({
@@ -44,6 +47,9 @@ const dashboard = handleActions({
dns_port: dnsPort,
dns_address: dnsAddress,
querylog_enabled: queryLogEnabled,
upstream_dns: upstreamDns,
protection_enabled: protectionEnabled,
language,
} = payload;
const newState = {
...state,
@@ -53,6 +59,9 @@ const dashboard = handleActions({
dnsPort,
dnsAddress,
queryLogEnabled,
upstreamDns: upstreamDns.join('\n'),
protectionEnabled,
language,
};
return newState;
},
@@ -98,12 +107,61 @@ const dashboard = handleActions({
const { queryLogEnabled } = state;
return ({ ...state, queryLogEnabled: !queryLogEnabled, logStatusProcessing: false });
},
[actions.getVersionRequest]: state => ({ ...state, processingVersion: true }),
[actions.getVersionFailure]: state => ({ ...state, processingVersion: false }),
[actions.getVersionSuccess]: (state, { payload }) => {
const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion;
if (versionCompare(currentVersion, payload.version) === -1) {
const {
announcement,
announcement_url: announcementUrl,
} = payload;
const newState = {
...state,
announcement,
announcementUrl,
isUpdateAvailable: true,
};
return newState;
}
return state;
},
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
[actions.getFilteringSuccess]: (state, { payload }) => {
const newState = { ...state, isFilteringEnabled: payload, processingFiltering: false };
return newState;
},
[actions.toggleProtectionSuccess]: (state) => {
const newState = { ...state, protectionEnabled: !state.protectionEnabled };
return newState;
},
[actions.handleUpstreamChange]: (state, { payload }) => {
const { upstreamDns } = payload;
return { ...state, upstreamDns };
},
[actions.getLanguageSuccess]: (state, { payload }) => {
const newState = { ...state, language: payload };
return newState;
},
}, {
processing: true,
isCoreRunning: false,
processingTopStats: true,
processingStats: true,
logStatusProcessing: false,
processingVersion: true,
processingFiltering: true,
upstreamDns: [],
protectionEnabled: false,
});
const queryLogs = handleActions({
@@ -172,9 +230,39 @@ const filtering = handleActions({
userRules: '',
});
const toasts = handleActions({
[actions.addErrorToast]: (state, { payload }) => {
const errorToast = {
id: nanoid(),
message: payload.error.toString(),
type: 'error',
};
const newState = { ...state, notices: [...state.notices, errorToast] };
return newState;
},
[actions.addSuccessToast]: (state, { payload }) => {
const successToast = {
id: nanoid(),
message: payload,
type: 'success',
};
const newState = { ...state, notices: [...state.notices, successToast] };
return newState;
},
[actions.removeToast]: (state, { payload }) => {
const filtered = state.notices.filter(notice => notice.id !== payload);
const newState = { ...state, notices: filtered };
return newState;
},
}, { notices: [] });
export default combineReducers({
settings,
dashboard,
queryLogs,
filtering,
toasts,
loadingBar: loadingBarReducer,
});

View File

@@ -4,6 +4,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack');
const flexBugsFixes = require('postcss-flexbugs-fixes');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const RESOURCES_PATH = path.resolve(__dirname);
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
@@ -19,7 +20,7 @@ const config = {
},
output: {
path: PUBLIC_PATH,
filename: '[name].js',
filename: '[name].[chunkhash].js',
},
resolve: {
modules: ['node_modules'],
@@ -92,12 +93,18 @@ const config = {
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
new CleanWebpackPlugin(['*.*'], {
root: PUBLIC_PATH,
verbose: false,
dry: false,
}),
new HtmlWebpackPlugin({
inject: true,
cache: false,
template: HTML_PATH,
}),
new ExtractTextPlugin({
filename: '[name].css',
filename: '[name].[contenthash].css',
}),
],
};

9626
client/yarn.lock vendored

File diff suppressed because it is too large Load Diff

234
config.go
View File

@@ -14,79 +14,141 @@ import (
"gopkg.in/yaml.v2"
)
// Current schema version. We compare it with the value from
// the configuration file and perform necessary upgrade operations if needed
const SchemaVersion = 1
// Directory where we'll store all downloaded filters contents
const FiltersDir = "filters"
// User filter ID is always 0
const UserFilterId = 0
// Just a counter that we use for incrementing the filter ID
var NextFilterId = time.Now().Unix()
// configuration is loaded from YAML
type configuration struct {
// Config filename (can be overriden via the command line arguments)
ourConfigFilename string
ourBinaryDir string
// Basically, this is our working directory
ourBinaryDir string
// Directory to store data (i.e. filters contents)
ourDataDir string
BindHost string `yaml:"bind_host"`
BindPort int `yaml:"bind_port"`
CoreDNS coreDNSConfig `yaml:"coredns"`
Filters []filter `yaml:"filters"`
UserRules []string `yaml:"user_rules"`
// Schema version of the config file. This value is used when performing the app updates.
SchemaVersion int `yaml:"schema_version"`
BindHost string `yaml:"bind_host"`
BindPort int `yaml:"bind_port"`
AuthName string `yaml:"auth_name"`
AuthPass string `yaml:"auth_pass"`
CoreDNS coreDNSConfig `yaml:"coredns"`
Filters []filter `yaml:"filters"`
UserRules []string `yaml:"user_rules"`
Language string `yaml:"language"` // two-letter ISO 639-1 language code
sync.Mutex `yaml:"-"`
sync.RWMutex `yaml:"-"`
}
type coreDnsFilter struct {
ID int64 `yaml:"-"`
Path string `yaml:"-"`
}
type coreDNSConfig struct {
Port int `yaml:"port"`
binaryFile string
coreFile string
FilterFile string `yaml:"-"`
FilteringEnabled bool `yaml:"filtering_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"`
ParentalSensitivity int `yaml:"parental_sensitivity"`
QueryLogEnabled bool `yaml:"querylog_enabled"`
Pprof string `yaml:"pprof"`
UpstreamDNS []string `yaml:"upstream_dns"`
Cache string `yaml:"cache"`
Prometheus string `yaml:"prometheus"`
Filters []coreDnsFilter `yaml:"-"`
Port int `yaml:"port"`
ProtectionEnabled bool `yaml:"protection_enabled"`
FilteringEnabled bool `yaml:"filtering_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"`
ParentalSensitivity int `yaml:"parental_sensitivity"`
BlockedResponseTTL int `yaml:"blocked_response_ttl"`
QueryLogEnabled bool `yaml:"querylog_enabled"`
Ratelimit int `yaml:"ratelimit"`
RefuseAny bool `yaml:"refuse_any"`
Pprof string `yaml:"-"`
Cache string `yaml:"-"`
Prometheus string `yaml:"-"`
BootstrapDNS string `yaml:"bootstrap_dns"`
UpstreamDNS []string `yaml:"upstream_dns"`
}
type filter struct {
Enabled bool `json:"enabled"`
ID int64 `json:"id" yaml:"id"` // auto-assigned when filter is added (see NextFilterId)
URL string `json:"url"`
RulesCount int `json:"rules_count" yaml:"-"`
Name string `json:"name" yaml:"-"`
Name string `json:"name" yaml:"name"`
Enabled bool `json:"enabled"`
RulesCount int `json:"rulesCount" yaml:"-"`
contents []byte
LastUpdated time.Time `json:"last_updated" yaml:"-"`
LastUpdated time.Time `json:"lastUpdated" yaml:"last_updated"`
}
var defaultDNS = []string{"1.1.1.1", "1.0.0.1"}
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
// initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{
ourConfigFilename: "AdguardDNS.yaml",
ourConfigFilename: "AdGuardHome.yaml",
ourDataDir: "data",
BindPort: 3000,
BindHost: "127.0.0.1",
CoreDNS: coreDNSConfig{
Port: 53,
binaryFile: "coredns", // only filename, no path
coreFile: "Corefile", // only filename, no path
FilterFile: "dnsfilter.txt", // only filename, no path
binaryFile: "coredns", // only filename, no path
coreFile: "Corefile", // only filename, no path
ProtectionEnabled: true,
FilteringEnabled: true,
SafeBrowsingEnabled: true,
SafeBrowsingEnabled: false,
BlockedResponseTTL: 10, // in seconds
QueryLogEnabled: true,
Ratelimit: 20,
RefuseAny: true,
BootstrapDNS: "8.8.8.8:53",
UpstreamDNS: defaultDNS,
Cache: "cache",
Prometheus: "prometheus :9153",
},
Filters: []filter{
{Enabled: true, URL: "https://filters.adtidy.org/windows/filters/15.txt"},
{ID: 1, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{ID: 2, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{ID: 3, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{ID: 4, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
}
// Creates a helper object for working with the user rules
func getUserFilter() filter {
// TODO: This should be calculated when UserRules are set
var contents []byte
for _, rule := range config.UserRules {
contents = append(contents, []byte(rule)...)
contents = append(contents, '\n')
}
userFilter := filter{
// User filter always has constant ID=0
ID: UserFilterId,
contents: contents,
Enabled: true,
}
return userFilter
}
// Loads configuration from the YAML file
func parseConfig() error {
configfile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
log.Printf("Reading YAML file: %s", configfile)
if _, err := os.Stat(configfile); os.IsNotExist(err) {
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
log.Printf("Reading YAML file: %s", configFile)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
// do nothing, file doesn't exist
log.Printf("YAML file doesn't exist, skipping: %s", configfile)
log.Printf("YAML file doesn't exist, skipping: %s", configFile)
return nil
}
yamlFile, err := ioutil.ReadFile(configfile)
yamlFile, err := ioutil.ReadFile(configFile)
if err != nil {
log.Printf("Couldn't read config file: %s", err)
return err
@@ -97,22 +159,54 @@ func parseConfig() error {
return err
}
// Deduplicate filters
{
i := 0 // output index, used for deletion later
urls := map[string]bool{}
for _, filter := range config.Filters {
if _, ok := urls[filter.URL]; !ok {
// we didn't see it before, keep it
urls[filter.URL] = true // remember the URL
config.Filters[i] = filter
i++
}
}
// all entries we want to keep are at front, delete the rest
config.Filters = config.Filters[:i]
}
// Set the next filter ID to max(filter.ID) + 1
for i := range config.Filters {
if NextFilterId < config.Filters[i].ID {
NextFilterId = config.Filters[i].ID + 1
}
}
return nil
}
// Saves configuration to the YAML file and also saves the user filter contents to a file
func writeConfig() error {
configfile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
log.Printf("Writing YAML file: %s", configfile)
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
log.Printf("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config)
if err != nil {
log.Printf("Couldn't generate YAML file: %s", err)
return err
}
err = ioutil.WriteFile(configfile, yamlText, 0644)
err = writeFileSafe(configFile, yamlText)
if err != nil {
log.Printf("Couldn't write YAML config: %s", err)
log.Printf("Couldn't save YAML config: %s", err)
return err
}
userFilter := getUserFilter()
err = userFilter.save()
if err != nil {
log.Printf("Couldn't save the user filter: %s", err)
return err
}
return nil
}
@@ -120,18 +214,19 @@ func writeConfig() error {
// coredns config
// --------------
func writeCoreDNSConfig() error {
corefile := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
log.Printf("Writing DNS config: %s", corefile)
configtext, err := generateCoreDNSConfigText()
coreFile := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
log.Printf("Writing DNS config: %s", coreFile)
configText, err := generateCoreDNSConfigText()
if err != nil {
log.Printf("Couldn't generate DNS config: %s", err)
return err
}
err = ioutil.WriteFile(corefile, []byte(configtext), 0644)
err = writeFileSafe(coreFile, []byte(configText))
if err != nil {
log.Printf("Couldn't write DNS config: %s", err)
log.Printf("Couldn't save DNS config: %s", err)
return err
}
return err
return nil
}
func writeAllConfigs() error {
@@ -148,15 +243,26 @@ func writeAllConfigs() error {
return nil
}
const coreDNSConfigTemplate = `. {
{{if .FilteringEnabled}}dnsfilter {{.FilterFile}} {
const coreDNSConfigTemplate = `.:{{.Port}} {
{{if .ProtectionEnabled}}dnsfilter {
{{if .SafeBrowsingEnabled}}safebrowsing{{end}}
{{if .ParentalEnabled}}parental {{.ParentalSensitivity}}{{end}}
{{if .SafeSearchEnabled}}safesearch{{end}}
{{if .QueryLogEnabled}}querylog{{end}}
blocked_ttl {{.BlockedResponseTTL}}
{{if .FilteringEnabled}}
{{range .Filters}}
filter {{.ID}} "{{.Path}}"
{{end}}
{{end}}
}{{end}}
{{.Pprof}}
{{if .UpstreamDNS}}forward . {{range .UpstreamDNS}}{{.}} {{end}}{{end}}
{{if .RefuseAny}}refuseany{{end}}
{{if gt .Ratelimit 0}}ratelimit {{.Ratelimit}}{{end}}
hosts {
fallthrough
}
{{if .UpstreamDNS}}upstream {{range .UpstreamDNS}}{{.}} {{end}} { bootstrap {{.BootstrapDNS}} }{{end}}
{{.Cache}}
{{.Prometheus}}
}
@@ -164,7 +270,7 @@ const coreDNSConfigTemplate = `. {
var removeEmptyLines = regexp.MustCompile("([\t ]*\n)+")
// generate config text
// generate CoreDNS config text
func generateCoreDNSConfigText() (string, error) {
t, err := template.New("config").Parse(coreDNSConfigTemplate)
if err != nil {
@@ -173,15 +279,37 @@ func generateCoreDNSConfigText() (string, error) {
}
var configBytes bytes.Buffer
temporaryConfig := config.CoreDNS
// fill the list of filters
filters := make([]coreDnsFilter, 0)
// first of all, append the user filter
userFilter := getUserFilter()
if len(userFilter.contents) > 0 {
filters = append(filters, coreDnsFilter{ID: userFilter.ID, Path: userFilter.getFilterFilePath()})
}
// then go through other filters
for i := range config.Filters {
filter := &config.Filters[i]
if filter.Enabled && len(filter.contents) > 0 {
filters = append(filters, coreDnsFilter{ID: filter.ID, Path: filter.getFilterFilePath()})
}
}
temporaryConfig.Filters = filters
// run the template
err = t.Execute(&configBytes, config.CoreDNS)
err = t.Execute(&configBytes, &temporaryConfig)
if err != nil {
log.Printf("Couldn't generate DNS config: %s", err)
return "", err
}
configtext := configBytes.String()
configText := configBytes.String()
// remove empty lines from generated config
configtext = removeEmptyLines.ReplaceAllString(configtext, "\n")
return configtext, nil
configText = removeEmptyLines.ReplaceAllString(configText, "\n")
return configText, nil
}

1070
control.go

File diff suppressed because it is too large Load Diff

132
coredns.go Normal file
View File

@@ -0,0 +1,132 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"sync" // Include all plugins.
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin"
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/ratelimit"
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/refuseany"
_ "github.com/AdguardTeam/AdGuardHome/upstream"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/coremain"
_ "github.com/coredns/coredns/plugin/auto"
_ "github.com/coredns/coredns/plugin/autopath"
_ "github.com/coredns/coredns/plugin/bind"
_ "github.com/coredns/coredns/plugin/cache"
_ "github.com/coredns/coredns/plugin/chaos"
_ "github.com/coredns/coredns/plugin/debug"
_ "github.com/coredns/coredns/plugin/dnssec"
_ "github.com/coredns/coredns/plugin/dnstap"
_ "github.com/coredns/coredns/plugin/erratic"
_ "github.com/coredns/coredns/plugin/errors"
_ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/health"
_ "github.com/coredns/coredns/plugin/hosts"
_ "github.com/coredns/coredns/plugin/loadbalance"
_ "github.com/coredns/coredns/plugin/log"
_ "github.com/coredns/coredns/plugin/loop"
_ "github.com/coredns/coredns/plugin/metadata"
_ "github.com/coredns/coredns/plugin/metrics"
_ "github.com/coredns/coredns/plugin/nsid"
_ "github.com/coredns/coredns/plugin/pprof"
_ "github.com/coredns/coredns/plugin/proxy"
_ "github.com/coredns/coredns/plugin/reload"
_ "github.com/coredns/coredns/plugin/rewrite"
_ "github.com/coredns/coredns/plugin/root"
_ "github.com/coredns/coredns/plugin/secondary"
_ "github.com/coredns/coredns/plugin/template"
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/whoami"
_ "github.com/mholt/caddy/onevent"
)
// Directives are registered in the order they should be
// executed.
//
// Ordering is VERY important. Every plugin will
// feel the effects of all other plugin below
// (after) them during a request, but they must not
// care what plugin above them are doing.
var directives = []string{
"metadata",
"tls",
"reload",
"nsid",
"root",
"bind",
"debug",
"health",
"pprof",
"prometheus",
"errors",
"log",
"refuseany",
"ratelimit",
"dnsfilter",
"dnstap",
"chaos",
"loadbalance",
"cache",
"rewrite",
"dnssec",
"autopath",
"template",
"hosts",
"file",
"auto",
"secondary",
"loop",
"forward",
"proxy",
"upstream",
"erratic",
"whoami",
"on",
}
func init() {
dnsserver.Directives = directives
}
var (
isCoreDNSRunningLock sync.Mutex
isCoreDNSRunning = false
)
func isRunning() bool {
isCoreDNSRunningLock.Lock()
value := isCoreDNSRunning
isCoreDNSRunningLock.Unlock()
return value
}
func startDNSServer() error {
isCoreDNSRunningLock.Lock()
if isCoreDNSRunning {
isCoreDNSRunningLock.Unlock()
return fmt.Errorf("Unable to start coreDNS: Already running")
}
isCoreDNSRunning = true
isCoreDNSRunningLock.Unlock()
configpath := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
os.Args = os.Args[:1]
os.Args = append(os.Args, "-conf")
os.Args = append(os.Args, configpath)
err := writeCoreDNSConfig()
if err != nil {
errortext := fmt.Errorf("Unable to write coredns config: %s", err)
log.Println(errortext)
return errortext
}
go coremain.Run()
return nil
}

View File

@@ -12,20 +12,16 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"golang.org/x/net/context"
)
@@ -45,73 +41,63 @@ func init() {
})
}
type Plugin struct {
d *dnsfilter.Dnsfilter
Next plugin.Handler
upstream upstream.Upstream
hosts map[string]net.IP
type plugFilter struct {
ID int64
Path string
}
type plugSettings struct {
SafeBrowsingBlockHost string
ParentalBlockHost string
QueryLogEnabled bool
BlockedTTL uint32 // in seconds, default 3600
Filters []plugFilter
}
var defaultPlugin = Plugin{
type plug struct {
d *dnsfilter.Dnsfilter
Next plugin.Handler
upstream upstream.Upstream
settings plugSettings
sync.RWMutex
}
var defaultPluginSettings = plugSettings{
SafeBrowsingBlockHost: "safebrowsing.block.dns.adguard.com",
ParentalBlockHost: "family.block.dns.adguard.com",
BlockedTTL: 3600, // in seconds
Filters: make([]plugFilter, 0),
}
func newDnsCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
}
var (
requests = newDnsCounter("requests_total", "Count of requests seen by dnsfilter.")
filtered = newDnsCounter("filtered_total", "Count of requests filtered by dnsfilter.")
filteredLists = newDnsCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
filteredSafebrowsing = newDnsCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
filteredParental = newDnsCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
filteredInvalid = newDnsCounter("filtered_invalid_total", "Count of requests filtered by dnsfilter because they were invalid.")
whitelisted = newDnsCounter("whitelisted_total", "Count of requests not filtered by dnsfilter because they are whitelisted.")
safesearch = newDnsCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
errorsTotal = newDnsCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
)
//
// coredns handling functions
//
func setupPlugin(c *caddy.Controller) (*Plugin, error) {
func setupPlugin(c *caddy.Controller) (*plug, error) {
// create new Plugin and copy default values
var d = new(Plugin)
*d = defaultPlugin
d.d = dnsfilter.New()
d.hosts = make(map[string]net.IP)
p := &plug{
settings: defaultPluginSettings,
d: dnsfilter.New(),
}
log.Println("Initializing the CoreDNS plugin")
var filterFileName string
for c.Next() {
args := c.RemainingArgs()
if len(args) == 0 {
// must have at least one argument
return nil, c.ArgErr()
}
filterFileName = args[0]
for c.NextBlock() {
switch c.Val() {
blockValue := c.Val()
switch blockValue {
case "safebrowsing":
d.d.EnableSafeBrowsing()
log.Println("Browsing security service is enabled")
p.d.EnableSafeBrowsing()
if c.NextArg() {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
d.d.SetSafeBrowsingServer(c.Val())
p.d.SetSafeBrowsingServer(c.Val())
}
case "safesearch":
d.d.EnableSafeSearch()
log.Println("Safe search is enabled")
p.d.EnableSafeSearch()
case "parental":
if !c.NextArg() {
return nil, c.ArgErr()
@@ -120,7 +106,9 @@ func setupPlugin(c *caddy.Controller) (*Plugin, error) {
if err != nil {
return nil, c.ArgErr()
}
err = d.d.EnableParental(sensitivity)
log.Println("Parental control is enabled")
err = p.d.EnableParental(sensitivity)
if err != nil {
return nil, c.ArgErr()
}
@@ -128,118 +116,165 @@ func setupPlugin(c *caddy.Controller) (*Plugin, error) {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
d.ParentalBlockHost = c.Val()
p.settings.ParentalBlockHost = c.Val()
}
case "blocked_ttl":
if !c.NextArg() {
return nil, c.ArgErr()
}
blockedTtl, err := strconv.ParseUint(c.Val(), 10, 32)
if err != nil {
return nil, c.ArgErr()
}
log.Printf("Blocked request TTL is %d", blockedTtl)
p.settings.BlockedTTL = uint32(blockedTtl)
case "querylog":
d.QueryLogEnabled = true
once.Do(func() {
go startQueryLogServer() // TODO: how to handle errors?
log.Println("Query log is enabled")
p.settings.QueryLogEnabled = true
case "filter":
if !c.NextArg() {
return nil, c.ArgErr()
}
filterId, err := strconv.ParseInt(c.Val(), 10, 64)
if err != nil {
return nil, c.ArgErr()
}
if !c.NextArg() {
return nil, c.ArgErr()
}
filterPath := c.Val()
// Initialize filter and add it to the list
p.settings.Filters = append(p.settings.Filters, plugFilter{
ID: filterId,
Path: filterPath,
})
}
}
}
file, err := os.Open(filterFileName)
if err != nil {
return nil, err
}
defer file.Close()
for _, filter := range p.settings.Filters {
log.Printf("Loading rules from %s", filter.Path)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if d.parseEtcHosts(text) {
continue
}
err = d.d.AddRule(text, 0)
if err == dnsfilter.ErrInvalidSyntax {
continue
}
file, err := os.Open(filter.Path)
if err != nil {
return nil, err
}
//noinspection GoDeferInLoop
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
err = p.d.AddRule(text, filter.ID)
if err == dnsfilter.ErrAlreadyExists || err == dnsfilter.ErrInvalidSyntax {
continue
}
if err != nil {
log.Printf("Cannot add rule %s: %s", text, err)
// Just ignore invalid rules
continue
}
count++
}
log.Printf("Added %d rules from filter ID=%d", count, filter.ID)
if err = scanner.Err(); err != nil {
return nil, err
}
}
if err = scanner.Err(); err != nil {
log.Printf("Loading stats from querylog")
err := fillStatsFromQueryLog()
if err != nil {
log.Printf("Failed to load stats from querylog: %s", err)
return nil, err
}
d.upstream, err = upstream.New(nil)
if p.settings.QueryLogEnabled {
onceQueryLog.Do(func() {
go periodicQueryLogRotate()
go periodicHourlyTopRotate()
go statsRotator()
})
}
onceHook.Do(func() {
caddy.RegisterEventHook("dnsfilter-reload", hook)
})
p.upstream, err = upstream.New(nil)
if err != nil {
return nil, err
}
return d, nil
return p, nil
}
func setup(c *caddy.Controller) error {
d, err := setupPlugin(c)
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
d.Next = next
return d
p.Next = next
return p
})
c.OnStartup(func() error {
once.Do(func() {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(requests)
x.MustRegister(filtered)
x.MustRegister(filteredLists)
x.MustRegister(filteredSafebrowsing)
x.MustRegister(filteredParental)
x.MustRegister(whitelisted)
x.MustRegister(safesearch)
x.MustRegister(errorsTotal)
x.MustRegister(d)
}
})
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(requests)
x.MustRegister(filtered)
x.MustRegister(filteredLists)
x.MustRegister(filteredSafebrowsing)
x.MustRegister(filteredParental)
x.MustRegister(whitelisted)
x.MustRegister(safesearch)
x.MustRegister(errorsTotal)
x.MustRegister(elapsedTime)
x.MustRegister(p)
}
return nil
})
c.OnShutdown(d.OnShutdown)
c.OnShutdown(p.onShutdown)
c.OnFinalShutdown(p.onFinalShutdown)
return nil
}
func (d *Plugin) parseEtcHosts(text string) bool {
if pos := strings.IndexByte(text, '#'); pos != -1 {
text = text[0:pos]
}
fields := strings.Fields(text)
if len(fields) < 2 {
return false
}
addr := net.ParseIP(fields[0])
if addr == nil {
return false
}
for _, host := range fields[1:] {
if val, ok := d.hosts[host]; ok {
log.Printf("warning: host %s already has value %s, will overwrite it with %s", host, val, addr)
}
d.hosts[host] = addr
}
return true
func (p *plug) onShutdown() error {
p.Lock()
p.d.Destroy()
p.d = nil
p.Unlock()
return nil
}
func (d *Plugin) OnShutdown() error {
d.d.Destroy()
d.d = nil
func (p *plug) onFinalShutdown() error {
logBufferLock.Lock()
err := flushToFile(logBuffer)
if err != nil {
log.Printf("failed to flush to file: %s", err)
return err
}
logBufferLock.Unlock()
return nil
}
type statsFunc func(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType)
//noinspection GoUnusedParameter
func doDesc(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- *prometheus.Desc)
if ok == false {
if !ok {
log.Printf("Couldn't convert ch to chan<- *prometheus.Desc\n")
return
}
@@ -248,7 +283,7 @@ func doDesc(ch interface{}, name string, text string, value float64, valueType p
func doMetric(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- prometheus.Metric)
if ok == false {
if !ok {
log.Printf("Couldn't convert ch to chan<- prometheus.Metric\n")
return
}
@@ -260,34 +295,39 @@ func gen(ch interface{}, doFunc statsFunc, name string, text string, value float
doFunc(ch, name, text, value, valueType)
}
func (d *Plugin) doStats(ch interface{}, doFunc statsFunc) {
stats := d.d.GetStats()
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_requests", "Number of safebrowsing HTTP requests that were sent", float64(stats.Safebrowsing.Requests), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_cachehits", "Number of safebrowsing lookups that didn't need HTTP requests", float64(stats.Safebrowsing.CacheHits), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending", "Number of currently pending safebrowsing HTTP requests", float64(stats.Safebrowsing.Pending), prometheus.GaugeValue)
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending_max", "Maximum number of pending safebrowsing HTTP requests", float64(stats.Safebrowsing.PendingMax), prometheus.GaugeValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_requests", "Number of parental HTTP requests that were sent", float64(stats.Parental.Requests), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_cachehits", "Number of parental lookups that didn't need HTTP requests", float64(stats.Parental.CacheHits), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_pending", "Number of currently pending parental HTTP requests", float64(stats.Parental.Pending), prometheus.GaugeValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_pending_max", "Maximum number of pending parental HTTP requests", float64(stats.Parental.PendingMax), prometheus.GaugeValue)
func doStatsLookup(ch interface{}, doFunc statsFunc, name string, lookupstats *dnsfilter.LookupStats) {
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_requests", name), fmt.Sprintf("Number of %s HTTP requests that were sent", name), float64(lookupstats.Requests), prometheus.CounterValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_cachehits", name), fmt.Sprintf("Number of %s lookups that didn't need HTTP requests", name), float64(lookupstats.CacheHits), prometheus.CounterValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending", name), fmt.Sprintf("Number of currently pending %s HTTP requests", name), float64(lookupstats.Pending), prometheus.GaugeValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending_max", name), fmt.Sprintf("Maximum number of pending %s HTTP requests", name), float64(lookupstats.PendingMax), prometheus.GaugeValue)
}
func (d *Plugin) Describe(ch chan<- *prometheus.Desc) {
d.doStats(ch, doDesc)
func (p *plug) doStats(ch interface{}, doFunc statsFunc) {
p.RLock()
stats := p.d.GetStats()
doStatsLookup(ch, doFunc, "safebrowsing", &stats.Safebrowsing)
doStatsLookup(ch, doFunc, "parental", &stats.Parental)
p.RUnlock()
}
func (d *Plugin) Collect(ch chan<- prometheus.Metric) {
d.doStats(ch, doMetric)
// Describe is called by prometheus handler to know stat types
func (p *plug) Describe(ch chan<- *prometheus.Desc) {
p.doStats(ch, doDesc)
}
func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
// Collect is called by prometheus handler to collect stats
func (p *plug) Collect(ch chan<- prometheus.Metric) {
p.doStats(ch, doMetric)
}
func (p *plug) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
// check if it's a domain name or IP address
addr := net.ParseIP(val)
var records []dns.RR
log.Println("Will give", val, "instead of", host)
// log.Println("Will give", val, "instead of", host) // debug logging
if addr != nil {
// this is an IP address, return it
result, err := dns.NewRR(host + " A " + val)
result, err := dns.NewRR(fmt.Sprintf("%s %d A %s", host, p.settings.BlockedTTL, val))
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
@@ -299,7 +339,7 @@ func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseW
req.SetQuestion(dns.Fqdn(val), question.Qtype)
req.RecursionDesired = true
reqstate := request.Request{W: w, Req: req, Context: ctx}
result, err := d.upstream.Lookup(reqstate, dns.Fqdn(val), reqstate.QType())
result, err := p.upstream.Lookup(reqstate, dns.Fqdn(val), reqstate.QType())
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
@@ -327,9 +367,9 @@ func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseW
// generate SOA record that makes DNS clients cache NXdomain results
// the only value that is important is TTL in header, other values like refresh, retry, expire and minttl are irrelevant
func genSOA(r *dns.Msg) []dns.RR {
func (p *plug) genSOA(r *dns.Msg) []dns.RR {
zone := r.Question[0].Name
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: 3600, Class: dns.ClassINET}
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: p.settings.BlockedTTL, Class: dns.ClassINET}
Mbox := "hostmaster."
if zone[0] != '.' {
@@ -337,20 +377,20 @@ func genSOA(r *dns.Msg) []dns.RR {
}
Ns := "fake-for-negative-caching.adguard.com."
soa := defaultSOA
soa := *defaultSOA
soa.Hdr = header
soa.Mbox = Mbox
soa.Ns = Ns
soa.Serial = uint32(time.Now().Unix())
return []dns.RR{soa}
soa.Serial = 100500 // faster than uint32(time.Now().Unix())
return []dns.RR{&soa}
}
func writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
func (p *plug) writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r, Context: ctx}
m := new(dns.Msg)
m.SetRcode(state.Req, dns.RcodeNameError)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Ns = genSOA(r)
m.Ns = p.genSOA(r)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
@@ -361,108 +401,116 @@ func writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int,
return dns.RcodeNameError, nil
}
func (d *Plugin) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error, dnsfilter.Result) {
func (p *plug) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, dnsfilter.Result, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions"), dnsfilter.Result{}
return dns.RcodeFormatError, dnsfilter.Result{}, fmt.Errorf("got a DNS request with more than one Question")
}
for _, question := range r.Question {
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
// if input is empty host, filter it out right away
if index := strings.IndexByte(host, byte('.')); index == -1 {
rcode, err := writeNXdomain(ctx, w, r)
if err != nil {
return rcode, err, dnsfilter.Result{}
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
}
// is it a safesearch domain?
if val, ok := d.d.SafeSearchDomain(host); ok {
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
p.RLock()
if val, ok := p.d.SafeSearchDomain(host); ok {
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, err, dnsfilter.Result{}
p.RUnlock()
return rcode, dnsfilter.Result{}, err
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
}
// is it in hosts?
if val, ok := d.hosts[host]; ok {
// it is, if it's a loopback host, reply with NXDOMAIN
if val.IsLoopback() {
rcode, err := writeNXdomain(ctx, w, r)
if err != nil {
return rcode, err, dnsfilter.Result{}
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
}
// it's not a loopback host, replace it with value specified
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val.String(), question)
if err != nil {
return rcode, err, dnsfilter.Result{}
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
p.RUnlock()
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}, err
}
p.RUnlock()
// needs to be filtered instead
result, err := d.d.CheckHost(host)
p.RLock()
result, err := p.d.CheckHost(host)
if err != nil {
log.Printf("plugin/dnsfilter: %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err), dnsfilter.Result{}
p.RUnlock()
return dns.RcodeServerFailure, dnsfilter.Result{}, fmt.Errorf("plugin/dnsfilter: %s", err)
}
p.RUnlock()
// safebrowsing
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredSafeBrowsing {
// return cname safebrowsing.block.dns.adguard.com
val := d.SafeBrowsingBlockHost
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, err, dnsfilter.Result{}
}
return rcode, err, result
}
if result.IsFiltered {
switch result.Reason {
case dnsfilter.FilteredSafeBrowsing:
// return cname safebrowsing.block.dns.adguard.com
val := p.settings.SafeBrowsingBlockHost
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredParental:
// return cname family.block.dns.adguard.com
val := p.settings.ParentalBlockHost
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredBlackList:
// parental
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredParental {
// return cname
val := d.ParentalBlockHost
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, err, dnsfilter.Result{}
if result.Ip == nil {
// return NXDomain
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
} else {
// This is a hosts-syntax rule
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, result.Ip.String(), question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
}
case dnsfilter.FilteredInvalid:
// return NXdomain
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
default:
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for filtering host \"%s\": %v, %+v", host, result.Reason, result)
}
return rcode, err, result
}
// blacklist
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredBlackList {
rcode, err := writeNXdomain(ctx, w, r)
if err != nil {
return rcode, err, dnsfilter.Result{}
} else {
switch result.Reason {
case dnsfilter.NotFilteredWhiteList:
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
return rcode, result, err
case dnsfilter.NotFilteredNotFound:
// do nothing, pass through to lower code
default:
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for not filtering host \"%s\": %v, %+v", host, result.Reason, result)
}
return rcode, err, result
}
if result.IsFiltered == false && result.Reason == dnsfilter.NotFilteredWhiteList {
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
return rcode, err, result
}
}
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
return rcode, err, dnsfilter.Result{}
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
return rcode, dnsfilter.Result{}, err
}
func (d *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ServeDNS handles the DNS request and refuses if it's in filterlists
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
start := time.Now()
requests.Inc()
state := request.Request{W: w, Req: r}
ip := state.IP()
// capture the written answer
rrw := dnstest.NewRecorder(w)
rcode, err, result := d.serveDNSInternal(ctx, rrw, r)
rcode, result, err := p.serveDNSInternal(ctx, rrw, r)
if rcode > 0 {
// actually send the answer if we have one
state := request.Request{W: w, Req: r}
answer := new(dns.Msg)
answer.SetRcode(r, rcode)
state.SizeAndDo(answer)
w.WriteMsg(answer)
err = w.WriteMsg(answer)
if err != nil {
return dns.RcodeServerFailure, err
}
}
// increment counters
@@ -496,12 +544,16 @@ func (d *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
}
// log
if d.QueryLogEnabled {
logRequest(rrw.Msg, result, time.Since(start))
elapsed := time.Since(start)
elapsedTime.Observe(elapsed.Seconds())
if p.settings.QueryLogEnabled {
logRequest(r, rrw.Msg, result, time.Since(start), ip)
}
return rcode, err
}
func (d *Plugin) Name() string { return "dnsfilter" }
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "dnsfilter" }
var once sync.Once
var onceHook sync.Once
var onceQueryLog sync.Once

View File

@@ -20,10 +20,21 @@ func TestSetup(t *testing.T) {
config string
failing bool
}{
{`dnsfilter`, true},
{`dnsfilter ../tests/dns.txt`, false},
{`dnsfilter ../tests/dns.txt { safebrowsing }`, false},
{`dnsfilter ../tests/dns.txt { parental }`, true},
{`dnsfilter`, false},
{`dnsfilter {
filter 0 /dev/nonexistent/abcdef
}`, true},
{`dnsfilter {
filter 0 ../tests/dns.txt
}`, false},
{`dnsfilter {
safebrowsing
filter 0 ../tests/dns.txt
}`, false},
{`dnsfilter {
parental
filter 0 ../tests/dns.txt
}`, true},
} {
c := caddy.NewTestController("dns", testcase.config)
err := setup(c)
@@ -39,57 +50,23 @@ func TestSetup(t *testing.T) {
}
}
func TestEtcHostsParse(t *testing.T) {
addr := "216.239.38.120"
text := []byte(fmt.Sprintf(" %s google.com www.google.com # enforce google's safesearch ", addr))
tmpfile, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write(text); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
c := caddy.NewTestController("dns", fmt.Sprintf("dnsfilter %s", tmpfile.Name()))
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
if len(p.hosts) != 2 {
t.Fatal("Expected p.hosts to have two keys")
}
val, ok := p.hosts["google.com"]
if !ok {
t.Fatal("Expected google.com to be set in p.hosts")
}
if !val.Equal(net.ParseIP(addr)) {
t.Fatalf("Expected google.com's value %s to match %s", val, addr)
}
}
func TestEtcHostsFilter(t *testing.T) {
text := []byte("127.0.0.1 doubleclick.net\n" + "127.0.0.1 example.org example.net www.example.org www.example.net")
tmpfile, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write(text); err != nil {
if _, err = tmpfile.Write(text); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
if err = tmpfile.Close(); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
c := caddy.NewTestController("dns", fmt.Sprintf("dnsfilter %s", tmpfile.Name()))
configText := fmt.Sprintf("dnsfilter {\nfilter 0 %s\n}", tmpfile.Name())
c := caddy.NewTestController("dns", configText)
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
@@ -126,11 +103,16 @@ func TestEtcHostsFilter(t *testing.T) {
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value for host %s has rcode %d that does not match captured rcode %d", testcase.host, rcode, rrw.Rcode)
}
filtered := rcode == dns.RcodeNameError
if testcase.filtered == true && testcase.filtered != filtered {
A, ok := rrw.Msg.Answer[0].(*dns.A)
if !ok {
t.Fatalf("Host %s expected to have result A", testcase.host)
}
ip := net.IPv4(127, 0, 0, 1)
filtered := ip.Equal(A.A)
if testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be filtered, instead it is not filtered", testcase.host)
}
if testcase.filtered == false && testcase.filtered != filtered {
if !testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
}
}

View File

@@ -0,0 +1,410 @@
package dnsfilter
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
var (
requests = newDNSCounter("requests_total", "Count of requests seen by dnsfilter.")
filtered = newDNSCounter("filtered_total", "Count of requests filtered by dnsfilter.")
filteredLists = newDNSCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
filteredSafebrowsing = newDNSCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
filteredParental = newDNSCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
filteredInvalid = newDNSCounter("filtered_invalid_total", "Count of requests filtered by dnsfilter because they were invalid.")
whitelisted = newDNSCounter("whitelisted_total", "Count of requests not filtered by dnsfilter because they are whitelisted.")
safesearch = newDNSCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
errorsTotal = newDNSCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
elapsedTime = newDNSHistogram("request_duration", "Histogram of the time (in seconds) each request took.")
)
// entries for single time period (for example all per-second entries)
type statsEntries map[string][statsHistoryElements]float64
// how far back to keep the stats
const statsHistoryElements = 60 + 1 // +1 for calculating delta
// each periodic stat is a map of arrays
type periodicStats struct {
Entries statsEntries
period time.Duration // how long one entry lasts
LastRotate time.Time // last time this data was rotated
sync.RWMutex
}
type stats struct {
PerSecond periodicStats
PerMinute periodicStats
PerHour periodicStats
PerDay periodicStats
}
// per-second/per-minute/per-hour/per-day stats
var statistics stats
func initPeriodicStats(periodic *periodicStats, period time.Duration) {
periodic.Entries = statsEntries{}
periodic.LastRotate = time.Now()
periodic.period = period
}
func init() {
purgeStats()
}
func purgeStats() {
initPeriodicStats(&statistics.PerSecond, time.Second)
initPeriodicStats(&statistics.PerMinute, time.Minute)
initPeriodicStats(&statistics.PerHour, time.Hour)
initPeriodicStats(&statistics.PerDay, time.Hour*24)
}
func (p *periodicStats) Inc(name string, when time.Time) {
// calculate how many periods ago this happened
elapsed := int64(time.Since(when) / p.period)
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
p.Lock()
currentValues := p.Entries[name]
currentValues[elapsed]++
p.Entries[name] = currentValues
p.Unlock()
}
func (p *periodicStats) Observe(name string, when time.Time, value float64) {
// calculate how many periods ago this happened
elapsed := int64(time.Since(when) / p.period)
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
p.Lock()
{
countname := name + "_count"
currentValues := p.Entries[countname]
value := currentValues[elapsed]
// trace("Will change p.Entries[%s][%d] from %v to %v", countname, elapsed, value, value+1)
value += 1
currentValues[elapsed] = value
p.Entries[countname] = currentValues
}
{
totalname := name + "_sum"
currentValues := p.Entries[totalname]
currentValues[elapsed] += value
p.Entries[totalname] = currentValues
}
p.Unlock()
}
func (p *periodicStats) statsRotate(now time.Time) {
p.Lock()
rotations := int64(now.Sub(p.LastRotate) / p.period)
if rotations > statsHistoryElements {
rotations = statsHistoryElements
}
// calculate how many times we should rotate
for r := int64(0); r < rotations; r++ {
for key, values := range p.Entries {
newValues := [statsHistoryElements]float64{}
for i := 1; i < len(values); i++ {
newValues[i] = values[i-1]
}
p.Entries[key] = newValues
}
}
if rotations > 0 {
p.LastRotate = now
}
p.Unlock()
}
func statsRotator() {
for range time.Tick(time.Second) {
now := time.Now()
statistics.PerSecond.statsRotate(now)
statistics.PerMinute.statsRotate(now)
statistics.PerHour.statsRotate(now)
statistics.PerDay.statsRotate(now)
}
}
// counter that wraps around prometheus Counter but also adds to periodic stats
type counter struct {
name string // used as key in periodic stats
value int64
prom prometheus.Counter
}
func newDNSCounter(name string, help string) *counter {
// trace("called")
c := &counter{}
c.prom = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
c.name = name
return c
}
func (c *counter) IncWithTime(when time.Time) {
statistics.PerSecond.Inc(c.name, when)
statistics.PerMinute.Inc(c.name, when)
statistics.PerHour.Inc(c.name, when)
statistics.PerDay.Inc(c.name, when)
c.value++
c.prom.Inc()
}
func (c *counter) Inc() {
c.IncWithTime(time.Now())
}
func (c *counter) Describe(ch chan<- *prometheus.Desc) {
c.prom.Describe(ch)
}
func (c *counter) Collect(ch chan<- prometheus.Metric) {
c.prom.Collect(ch)
}
type histogram struct {
name string // used as key in periodic stats
count int64
total float64
prom prometheus.Histogram
}
func newDNSHistogram(name string, help string) *histogram {
// trace("called")
h := &histogram{}
h.prom = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
h.name = name
return h
}
func (h *histogram) ObserveWithTime(value float64, when time.Time) {
statistics.PerSecond.Observe(h.name, when, value)
statistics.PerMinute.Observe(h.name, when, value)
statistics.PerHour.Observe(h.name, when, value)
statistics.PerDay.Observe(h.name, when, value)
h.count++
h.total += value
h.prom.Observe(value)
}
func (h *histogram) Observe(value float64) {
h.ObserveWithTime(value, time.Now())
}
func (h *histogram) Describe(ch chan<- *prometheus.Desc) {
h.prom.Describe(ch)
}
func (h *histogram) Collect(ch chan<- prometheus.Metric) {
h.prom.Collect(ch)
}
// -----
// stats
// -----
func HandleStats(w http.ResponseWriter, r *http.Request) {
const numHours = 24
histrical := generateMapFromStats(&statistics.PerHour, 0, numHours)
// sum them up
summed := map[string]interface{}{}
for key, values := range histrical {
summedValue := 0.0
floats, ok := values.([]float64)
if !ok {
continue
}
for _, v := range floats {
summedValue += v
}
summed[key] = summedValue
}
// don't forget to divide by number of elements in returned slice
if val, ok := summed["avg_processing_time"]; ok {
if flval, flok := val.(float64); flok {
flval /= numHours
summed["avg_processing_time"] = flval
}
}
summed["stats_period"] = "24 hours"
json, err := json.Marshal(summed)
if err != nil {
errortext := fmt.Sprintf("Unable to marshal status json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(json)
if err != nil {
errortext := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
}
func generateMapFromStats(stats *periodicStats, start int, end int) map[string]interface{} {
// clamp
start = clamp(start, 0, statsHistoryElements)
end = clamp(end, 0, statsHistoryElements)
avgProcessingTime := make([]float64, 0)
count := getReversedSlice(stats.Entries[elapsedTime.name+"_count"], start, end)
sum := getReversedSlice(stats.Entries[elapsedTime.name+"_sum"], start, end)
for i := 0; i < len(count); i++ {
var avg float64
if count[i] != 0 {
avg = sum[i] / count[i]
avg *= 1000
}
avgProcessingTime = append(avgProcessingTime, avg)
}
result := map[string]interface{}{
"dns_queries": getReversedSlice(stats.Entries[requests.name], start, end),
"blocked_filtering": getReversedSlice(stats.Entries[filtered.name], start, end),
"replaced_safebrowsing": getReversedSlice(stats.Entries[filteredSafebrowsing.name], start, end),
"replaced_safesearch": getReversedSlice(stats.Entries[safesearch.name], start, end),
"replaced_parental": getReversedSlice(stats.Entries[filteredParental.name], start, end),
"avg_processing_time": avgProcessingTime,
}
return result
}
func HandleStatsHistory(w http.ResponseWriter, r *http.Request) {
// handle time unit and prepare our time window size
now := time.Now()
timeUnitString := r.URL.Query().Get("time_unit")
var stats *periodicStats
var timeUnit time.Duration
switch timeUnitString {
case "seconds":
timeUnit = time.Second
stats = &statistics.PerSecond
case "minutes":
timeUnit = time.Minute
stats = &statistics.PerMinute
case "hours":
timeUnit = time.Hour
stats = &statistics.PerHour
case "days":
timeUnit = time.Hour * 24
stats = &statistics.PerDay
default:
http.Error(w, "Must specify valid time_unit parameter", 400)
return
}
// parse start and end time
startTime, err := time.Parse(time.RFC3339, r.URL.Query().Get("start_time"))
if err != nil {
errortext := fmt.Sprintf("Must specify valid start_time parameter: %s", err)
log.Println(errortext)
http.Error(w, errortext, 400)
return
}
endTime, err := time.Parse(time.RFC3339, r.URL.Query().Get("end_time"))
if err != nil {
errortext := fmt.Sprintf("Must specify valid end_time parameter: %s", err)
log.Println(errortext)
http.Error(w, errortext, 400)
return
}
// check if start and time times are within supported time range
timeRange := timeUnit * statsHistoryElements
if startTime.Add(timeRange).Before(now) {
http.Error(w, "start_time parameter is outside of supported range", 501)
return
}
if endTime.Add(timeRange).Before(now) {
http.Error(w, "end_time parameter is outside of supported range", 501)
return
}
// calculate start and end of our array
// basically it's how many hours/minutes/etc have passed since now
start := int(now.Sub(endTime) / timeUnit)
end := int(now.Sub(startTime) / timeUnit)
// swap them around if they're inverted
if start > end {
start, end = end, start
}
data := generateMapFromStats(stats, start, end)
json, err := json.Marshal(data)
if err != nil {
errortext := fmt.Sprintf("Unable to marshal status json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(json)
if err != nil {
errortext := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
}
func HandleStatsReset(w http.ResponseWriter, r *http.Request) {
purgeStats()
_, err := fmt.Fprintf(w, "OK\n")
if err != nil {
errortext := fmt.Sprintf("Couldn't write body: %s", err)
log.Println(errortext)
http.Error(w, errortext, http.StatusInternalServerError)
}
}
func clamp(value, low, high int) int {
if value < low {
return low
}
if value > high {
return high
}
return value
}
// --------------------------
// helper functions for stats
// --------------------------
func getReversedSlice(input [statsHistoryElements]float64, start int, end int) []float64 {
output := make([]float64, 0)
for i := start; i <= end; i++ {
output = append([]float64{input[i]}, output...)
}
return output
}

View File

@@ -5,70 +5,166 @@ import (
"fmt"
"log"
"net/http"
"os"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/coredns/coredns/plugin/pkg/response"
"github.com/miekg/dns"
"github.com/zfjagann/golang-ring"
)
var logBuffer = ring.Ring{}
const (
logBufferCap = 5000 // maximum capacity of logBuffer before it's flushed to disk
queryLogTimeLimit = time.Hour * 24 // how far in the past we care about querylogs
queryLogRotationPeriod = time.Hour * 24 // rotate the log every 24 hours
queryLogFileName = "querylog.json" // .gz added during compression
queryLogSize = 5000 // maximum API response for /querylog
queryLogTopSize = 500 // Keep in memory only top N values
)
var (
logBufferLock sync.RWMutex
logBuffer []*logEntry
queryLogCache []*logEntry
queryLogLock sync.RWMutex
)
type logEntry struct {
R *dns.Msg
Result dnsfilter.Result
Time time.Time
Elapsed time.Duration
Question []byte
Answer []byte `json:",omitempty"` // sometimes empty answers happen like binerdunt.top or rev2.globalrootservers.net
Result dnsfilter.Result
Time time.Time
Elapsed time.Duration
IP string
}
func init() {
logBuffer.SetCapacity(1000)
}
func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, elapsed time.Duration, ip string) {
var q []byte
var a []byte
var err error
func logRequest(r *dns.Msg, result dnsfilter.Result, elapsed time.Duration) {
entry := logEntry{
R: r,
Result: result,
Time: time.Now(),
Elapsed: elapsed,
if question != nil {
q, err = question.Pack()
if err != nil {
log.Printf("failed to pack question for querylog: %s", err)
return
}
}
if answer != nil {
a, err = answer.Pack()
if err != nil {
log.Printf("failed to pack answer for querylog: %s", err)
return
}
}
now := time.Now()
entry := logEntry{
Question: q,
Answer: a,
Result: result,
Time: now,
Elapsed: elapsed,
IP: ip,
}
var flushBuffer []*logEntry
logBufferLock.Lock()
logBuffer = append(logBuffer, &entry)
if len(logBuffer) >= logBufferCap {
flushBuffer = logBuffer
logBuffer = nil
}
logBufferLock.Unlock()
queryLogLock.Lock()
queryLogCache = append(queryLogCache, &entry)
if len(queryLogCache) > queryLogSize {
toremove := len(queryLogCache) - queryLogSize
queryLogCache = queryLogCache[toremove:]
}
queryLogLock.Unlock()
// add it to running top
err = runningTop.addEntry(&entry, question, now)
if err != nil {
log.Printf("Failed to add entry to running top: %s", err)
// don't do failure, just log
}
// if buffer needs to be flushed to disk, do it now
if len(flushBuffer) > 0 {
// write to file
// do it in separate goroutine -- we are stalling DNS response this whole time
go flushToFile(flushBuffer)
}
logBuffer.Enqueue(entry)
}
func handler(w http.ResponseWriter, r *http.Request) {
values := logBuffer.Values()
//noinspection GoUnusedParameter
func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
queryLogLock.RLock()
values := make([]*logEntry, len(queryLogCache))
copy(values, queryLogCache)
queryLogLock.RUnlock()
// reverse it so that newest is first
for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
values[left], values[right] = values[right], values[left]
}
var data = []map[string]interface{}{}
for _, value := range values {
entry, ok := value.(logEntry)
if !ok {
continue
for _, entry := range values {
var q *dns.Msg
var a *dns.Msg
if len(entry.Question) > 0 {
q = new(dns.Msg)
if err := q.Unpack(entry.Question); err != nil {
// ignore, log and move on
log.Printf("Failed to unpack dns message question: %s", err)
q = nil
}
}
if len(entry.Answer) > 0 {
a = new(dns.Msg)
if err := a.Unpack(entry.Answer); err != nil {
// ignore, log and move on
log.Printf("Failed to unpack dns message question: %s", err)
a = nil
}
}
jsonentry := map[string]interface{}{
jsonEntry := map[string]interface{}{
"reason": entry.Result.Reason.String(),
"elapsed_ms": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
"elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
"time": entry.Time.Format(time.RFC3339),
"client": entry.IP,
}
question := map[string]interface{}{
"host": strings.ToLower(strings.TrimSuffix(entry.R.Question[0].Name, ".")),
"type": dns.Type(entry.R.Question[0].Qtype).String(),
"class": dns.Class(entry.R.Question[0].Qclass).String(),
if q != nil {
jsonEntry["question"] = map[string]interface{}{
"host": strings.ToLower(strings.TrimSuffix(q.Question[0].Name, ".")),
"type": dns.Type(q.Question[0].Qtype).String(),
"class": dns.Class(q.Question[0].Qclass).String(),
}
}
jsonentry["question"] = question
status, _ := response.Typify(entry.R, time.Now().UTC())
jsonentry["status"] = status.String()
if a != nil {
status, _ := response.Typify(a, time.Now().UTC())
jsonEntry["status"] = status.String()
}
if len(entry.Result.Rule) > 0 {
jsonentry["rule"] = entry.Result.Rule
jsonEntry["rule"] = entry.Result.Rule
jsonEntry["filterId"] = entry.Result.FilterID
}
if len(entry.R.Answer) > 0 {
if a != nil && len(a.Answer) > 0 {
var answers = []map[string]interface{}{}
for _, k := range entry.R.Answer {
for _, k := range a.Answer {
header := k.Header()
answer := map[string]interface{}{
"type": dns.TypeToString[header.Rrtype],
@@ -106,41 +202,39 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
answers = append(answers, answer)
}
jsonentry["answer"] = answers
jsonEntry["answer"] = answers
}
data = append(data, jsonentry)
data = append(data, jsonEntry)
}
json, err := json.Marshal(data)
jsonVal, err := json.Marshal(data)
if err != nil {
errortext := fmt.Sprintf("Couldn't marshal data into json: %s", err)
log.Println(errortext)
http.Error(w, errortext, http.StatusInternalServerError)
errorText := fmt.Sprintf("Couldn't marshal data into json: %s", err)
log.Println(errorText)
http.Error(w, errorText, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(json)
_, err = w.Write(jsonVal)
if err != nil {
errortext := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
errorText := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(errorText)
http.Error(w, errorText, http.StatusInternalServerError)
}
}
func startQueryLogServer() {
listenAddr := "127.0.0.1:8618" // sha512sum of "querylog" then each byte summed
http.HandleFunc("/querylog", handler)
if err := http.ListenAndServe(listenAddr, nil); err != nil {
log.Fatalf("error in ListenAndServe: %s", err)
}
}
func trace(text string) {
func trace(format string, args ...interface{}) {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
log.Printf("%s(): %s\n", f.Name(), text)
var buf strings.Builder
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
text := fmt.Sprintf(format, args...)
buf.WriteString(text)
if len(text) == 0 || text[len(text)-1] != '\n' {
buf.WriteRune('\n')
}
fmt.Fprint(os.Stderr, buf.String())
}

View File

@@ -0,0 +1,291 @@
package dnsfilter
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"log"
"os"
"sync"
"time"
"github.com/go-test/deep"
)
var (
fileWriteLock sync.Mutex
)
const enableGzip = false
func flushToFile(buffer []*logEntry) error {
if len(buffer) == 0 {
return nil
}
start := time.Now()
var b bytes.Buffer
e := json.NewEncoder(&b)
for _, entry := range buffer {
err := e.Encode(entry)
if err != nil {
log.Printf("Failed to marshal entry: %s", err)
return err
}
}
elapsed := time.Since(start)
log.Printf("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer)))
err := checkBuffer(buffer, b)
if err != nil {
log.Printf("failed to check buffer: %s", err)
return err
}
var zb bytes.Buffer
filename := queryLogFileName
// gzip enabled?
if enableGzip {
filename += ".gz"
zw := gzip.NewWriter(&zb)
zw.Name = queryLogFileName
zw.ModTime = time.Now()
_, err = zw.Write(b.Bytes())
if err != nil {
log.Printf("Couldn't compress to gzip: %s", err)
zw.Close()
return err
}
if err = zw.Close(); err != nil {
log.Printf("Couldn't close gzip writer: %s", err)
return err
}
} else {
zb = b
}
fileWriteLock.Lock()
defer fileWriteLock.Unlock()
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Printf("failed to create file \"%s\": %s", filename, err)
return err
}
defer f.Close()
n, err := f.Write(zb.Bytes())
if err != nil {
log.Printf("Couldn't write to file: %s", err)
return err
}
log.Printf("ok \"%s\": %v bytes written", filename, n)
return nil
}
func checkBuffer(buffer []*logEntry, b bytes.Buffer) error {
l := len(buffer)
d := json.NewDecoder(&b)
i := 0
for d.More() {
entry := &logEntry{}
err := d.Decode(entry)
if err != nil {
log.Printf("Failed to decode: %s", err)
return err
}
if diff := deep.Equal(entry, buffer[i]); diff != nil {
log.Printf("decoded buffer differs: %s", diff)
return fmt.Errorf("decoded buffer differs: %s", diff)
}
i++
}
if i != l {
err := fmt.Errorf("check fail: %d vs %d entries", l, i)
log.Print(err)
return err
}
log.Printf("check ok: %d entries", i)
return nil
}
func rotateQueryLog() error {
from := queryLogFileName
to := queryLogFileName + ".1"
if enableGzip {
from = queryLogFileName + ".gz"
to = queryLogFileName + ".gz.1"
}
if _, err := os.Stat(from); os.IsNotExist(err) {
// do nothing, file doesn't exist
return nil
}
err := os.Rename(from, to)
if err != nil {
log.Printf("Failed to rename querylog: %s", err)
return err
}
log.Printf("Rotated from %s to %s successfully", from, to)
return nil
}
func periodicQueryLogRotate() {
for range time.Tick(queryLogRotationPeriod) {
err := rotateQueryLog()
if err != nil {
log.Printf("Failed to rotate querylog: %s", err)
// do nothing, continue rotating
}
}
}
func genericLoader(onEntry func(entry *logEntry) error, needMore func() bool, timeWindow time.Duration) error {
now := time.Now()
// read from querylog files, try newest file first
files := []string{}
if enableGzip {
files = []string{
queryLogFileName + ".gz",
queryLogFileName + ".gz.1",
}
} else {
files = []string{
queryLogFileName,
queryLogFileName + ".1",
}
}
// read from all files
for _, file := range files {
if !needMore() {
break
}
if _, err := os.Stat(file); os.IsNotExist(err) {
// do nothing, file doesn't exist
continue
}
f, err := os.Open(file)
if err != nil {
log.Printf("Failed to open file \"%s\": %s", file, err)
// try next file
continue
}
defer f.Close()
var d *json.Decoder
if enableGzip {
trace("Creating gzip reader")
zr, err := gzip.NewReader(f)
if err != nil {
log.Printf("Failed to create gzip reader: %s", err)
continue
}
defer zr.Close()
trace("Creating json decoder")
d = json.NewDecoder(zr)
} else {
d = json.NewDecoder(f)
}
i := 0
over := 0
max := 10000 * time.Second
var sum time.Duration
// entries on file are in oldest->newest order
// we want maxLen newest
for d.More() {
if !needMore() {
break
}
var entry logEntry
err := d.Decode(&entry)
if err != nil {
log.Printf("Failed to decode: %s", err)
// next entry can be fine, try more
continue
}
if now.Sub(entry.Time) > timeWindow {
// trace("skipping entry") // debug logging
continue
}
if entry.Elapsed > max {
over++
} else {
sum += entry.Elapsed
}
i++
err = onEntry(&entry)
if err != nil {
return err
}
}
elapsed := time.Since(now)
var perunit time.Duration
var avg time.Duration
if i > 0 {
perunit = elapsed / time.Duration(i)
avg = sum / time.Duration(i)
}
log.Printf("file \"%s\": read %d entries in %v, %v/entry, %v over %v, %v avg", file, i, elapsed, perunit, over, max, avg)
}
return nil
}
func appendFromLogFile(values []*logEntry, maxLen int, timeWindow time.Duration) []*logEntry {
a := []*logEntry{}
onEntry := func(entry *logEntry) error {
a = append(a, entry)
if len(a) > maxLen {
toskip := len(a) - maxLen
a = a[toskip:]
}
return nil
}
needMore := func() bool {
return true
}
err := genericLoader(onEntry, needMore, timeWindow)
if err != nil {
log.Printf("Failed to load entries from querylog: %s", err)
return values
}
// now that we've read all eligible entries, reverse the slice to make it go from newest->oldest
for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {
a[left], a[right] = a[right], a[left]
}
// append it to values
values = append(values, a...)
// then cut off of it is bigger than maxLen
if len(values) > maxLen {
values = values[:maxLen]
}
return values
}

View File

@@ -0,0 +1,386 @@
package dnsfilter
import (
"bytes"
"fmt"
"log"
"net/http"
"os"
"path"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/bluele/gcache"
"github.com/miekg/dns"
)
type hourTop struct {
domains gcache.Cache
blocked gcache.Cache
clients gcache.Cache
mutex sync.RWMutex
}
func (top *hourTop) init() {
top.domains = gcache.New(queryLogTopSize).LRU().Build()
top.blocked = gcache.New(queryLogTopSize).LRU().Build()
top.clients = gcache.New(queryLogTopSize).LRU().Build()
}
type dayTop struct {
hours []*hourTop
hoursLock sync.RWMutex // writelock this lock ONLY WHEN rotating or intializing hours!
loaded bool
loadedLock sync.Mutex
}
var runningTop dayTop
func init() {
runningTop.hoursWriteLock()
for i := 0; i < 24; i++ {
hour := hourTop{}
hour.init()
runningTop.hours = append(runningTop.hours, &hour)
}
runningTop.hoursWriteUnlock()
}
func rotateHourlyTop() {
log.Printf("Rotating hourly top")
hour := &hourTop{}
hour.init()
runningTop.hoursWriteLock()
runningTop.hours = append([]*hourTop{hour}, runningTop.hours...)
runningTop.hours = runningTop.hours[:24]
runningTop.hoursWriteUnlock()
}
func periodicHourlyTopRotate() {
t := time.Hour
for range time.Tick(t) {
rotateHourlyTop()
}
}
func (top *hourTop) incrementValue(key string, cache gcache.Cache) error {
top.Lock()
defer top.Unlock()
ivalue, err := cache.Get(key)
if err == gcache.KeyNotFoundError {
// we just set it and we're done
err = cache.Set(key, 1)
if err != nil {
log.Printf("Failed to set hourly top value: %s", err)
return err
}
return nil
}
if err != nil {
log.Printf("gcache encountered an error during get: %s", err)
return err
}
cachedValue, ok := ivalue.(int)
if !ok {
err = fmt.Errorf("SHOULD NOT HAPPEN: gcache has non-int as value: %v", ivalue)
log.Println(err)
return err
}
err = cache.Set(key, cachedValue+1)
if err != nil {
log.Printf("Failed to set hourly top value: %s", err)
return err
}
return nil
}
func (top *hourTop) incrementDomains(key string) error {
return top.incrementValue(key, top.domains)
}
func (top *hourTop) incrementBlocked(key string) error {
return top.incrementValue(key, top.blocked)
}
func (top *hourTop) incrementClients(key string) error {
return top.incrementValue(key, top.clients)
}
// if does not exist -- return 0
func (top *hourTop) lockedGetValue(key string, cache gcache.Cache) (int, error) {
ivalue, err := cache.Get(key)
if err == gcache.KeyNotFoundError {
return 0, nil
}
if err != nil {
log.Printf("gcache encountered an error during get: %s", err)
return 0, err
}
value, ok := ivalue.(int)
if !ok {
err := fmt.Errorf("SHOULD NOT HAPPEN: gcache has non-int as value: %v", ivalue)
log.Println(err)
return 0, err
}
return value, nil
}
func (top *hourTop) lockedGetDomains(key string) (int, error) {
return top.lockedGetValue(key, top.domains)
}
func (top *hourTop) lockedGetBlocked(key string) (int, error) {
return top.lockedGetValue(key, top.blocked)
}
func (top *hourTop) lockedGetClients(key string) (int, error) {
return top.lockedGetValue(key, top.clients)
}
func (r *dayTop) addEntry(entry *logEntry, q *dns.Msg, now time.Time) error {
// figure out which hour bucket it belongs to
hour := int(now.Sub(entry.Time).Hours())
if hour >= 24 {
log.Printf("t %v is >24 hours ago, ignoring", entry.Time)
return nil
}
hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, "."))
// get value, if not set, crate one
runningTop.hoursReadLock()
defer runningTop.hoursReadUnlock()
err := runningTop.hours[hour].incrementDomains(hostname)
if err != nil {
log.Printf("Failed to increment value: %s", err)
return err
}
if entry.Result.IsFiltered {
err := runningTop.hours[hour].incrementBlocked(hostname)
if err != nil {
log.Printf("Failed to increment value: %s", err)
return err
}
}
if len(entry.IP) > 0 {
err := runningTop.hours[hour].incrementClients(entry.IP)
if err != nil {
log.Printf("Failed to increment value: %s", err)
return err
}
}
return nil
}
func fillStatsFromQueryLog() error {
now := time.Now()
runningTop.loadedWriteLock()
defer runningTop.loadedWriteUnlock()
if runningTop.loaded {
return nil
}
onEntry := func(entry *logEntry) error {
if len(entry.Question) == 0 {
log.Printf("entry question is absent, skipping")
return nil
}
if entry.Time.After(now) {
log.Printf("t %v vs %v is in the future, ignoring", entry.Time, now)
return nil
}
q := new(dns.Msg)
if err := q.Unpack(entry.Question); err != nil {
log.Printf("failed to unpack dns message question: %s", err)
return err
}
if len(q.Question) != 1 {
log.Printf("malformed dns message, has no questions, skipping")
return nil
}
err := runningTop.addEntry(entry, q, now)
if err != nil {
log.Printf("Failed to add entry to running top: %s", err)
return err
}
queryLogLock.Lock()
queryLogCache = append(queryLogCache, entry)
if len(queryLogCache) > queryLogSize {
toremove := len(queryLogCache) - queryLogSize
queryLogCache = queryLogCache[toremove:]
}
queryLogLock.Unlock()
requests.IncWithTime(entry.Time)
if entry.Result.IsFiltered {
filtered.IncWithTime(entry.Time)
}
switch entry.Result.Reason {
case dnsfilter.NotFilteredWhiteList:
whitelisted.IncWithTime(entry.Time)
case dnsfilter.NotFilteredError:
errorsTotal.IncWithTime(entry.Time)
case dnsfilter.FilteredBlackList:
filteredLists.IncWithTime(entry.Time)
case dnsfilter.FilteredSafeBrowsing:
filteredSafebrowsing.IncWithTime(entry.Time)
case dnsfilter.FilteredParental:
filteredParental.IncWithTime(entry.Time)
case dnsfilter.FilteredInvalid:
// do nothing
case dnsfilter.FilteredSafeSearch:
safesearch.IncWithTime(entry.Time)
}
elapsedTime.ObserveWithTime(entry.Elapsed.Seconds(), entry.Time)
return nil
}
needMore := func() bool { return true }
err := genericLoader(onEntry, needMore, queryLogTimeLimit)
if err != nil {
log.Printf("Failed to load entries from querylog: %s", err)
return err
}
runningTop.loaded = true
return nil
}
func HandleStatsTop(w http.ResponseWriter, r *http.Request) {
domains := map[string]int{}
blocked := map[string]int{}
clients := map[string]int{}
do := func(keys []interface{}, getter func(key string) (int, error), result map[string]int) {
for _, ikey := range keys {
key, ok := ikey.(string)
if !ok {
continue
}
value, err := getter(key)
if err != nil {
log.Printf("Failed to get top domains value for %v: %s", key, err)
return
}
result[key] += value
}
}
runningTop.hoursReadLock()
for hour := 0; hour < 24; hour++ {
runningTop.hours[hour].RLock()
do(runningTop.hours[hour].domains.Keys(), runningTop.hours[hour].lockedGetDomains, domains)
do(runningTop.hours[hour].blocked.Keys(), runningTop.hours[hour].lockedGetBlocked, blocked)
do(runningTop.hours[hour].clients.Keys(), runningTop.hours[hour].lockedGetClients, clients)
runningTop.hours[hour].RUnlock()
}
runningTop.hoursReadUnlock()
// use manual json marshalling because we want maps to be sorted by value
json := bytes.Buffer{}
json.WriteString("{\n")
gen := func(json *bytes.Buffer, name string, top map[string]int, addComma bool) {
json.WriteString(" ")
json.WriteString(fmt.Sprintf("%q", name))
json.WriteString(": {\n")
sorted := sortByValue(top)
// no more than 50 entries
if len(sorted) > 50 {
sorted = sorted[:50]
}
for i, key := range sorted {
json.WriteString(" ")
json.WriteString(fmt.Sprintf("%q", key))
json.WriteString(": ")
json.WriteString(strconv.Itoa(top[key]))
if i+1 != len(sorted) {
json.WriteByte(',')
}
json.WriteByte('\n')
}
json.WriteString(" }")
if addComma {
json.WriteByte(',')
}
json.WriteByte('\n')
}
gen(&json, "top_queried_domains", domains, true)
gen(&json, "top_blocked_domains", blocked, true)
gen(&json, "top_clients", clients, true)
json.WriteString(" \"stats_period\": \"24 hours\"\n")
json.WriteString("}\n")
w.Header().Set("Content-Type", "application/json")
_, err := w.Write(json.Bytes())
if err != nil {
errortext := fmt.Sprintf("Couldn't write body: %s", err)
log.Println(errortext)
http.Error(w, errortext, http.StatusInternalServerError)
}
}
// helper function for querylog API
func sortByValue(m map[string]int) []string {
type kv struct {
k string
v int
}
var ss []kv
for k, v := range m {
ss = append(ss, kv{k, v})
}
sort.Slice(ss, func(l, r int) bool {
return ss[l].v > ss[r].v
})
sorted := []string{}
for _, v := range ss {
sorted = append(sorted, v.k)
}
return sorted
}
func (d *dayTop) hoursWriteLock() { tracelock(); d.hoursLock.Lock() }
func (d *dayTop) hoursWriteUnlock() { tracelock(); d.hoursLock.Unlock() }
func (d *dayTop) hoursReadLock() { tracelock(); d.hoursLock.RLock() }
func (d *dayTop) hoursReadUnlock() { tracelock(); d.hoursLock.RUnlock() }
func (d *dayTop) loadedWriteLock() { tracelock(); d.loadedLock.Lock() }
func (d *dayTop) loadedWriteUnlock() { tracelock(); d.loadedLock.Unlock() }
func (h *hourTop) Lock() { tracelock(); h.mutex.Lock() }
func (h *hourTop) RLock() { tracelock(); h.mutex.RLock() }
func (h *hourTop) RUnlock() { tracelock(); h.mutex.RUnlock() }
func (h *hourTop) Unlock() { tracelock(); h.mutex.Unlock() }
func tracelock() {
if false { // not commented out to make code checked during compilation
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := path.Base(runtime.FuncForPC(pc[1]).Name())
lockf := path.Base(runtime.FuncForPC(pc[0]).Name())
fmt.Fprintf(os.Stderr, "%s(): %s\n", f, lockf)
}
}

View File

@@ -3,8 +3,8 @@ package ratelimit
import (
"errors"
"log"
"sort"
"strconv"
"sync"
"time"
// ratelimiting and per-ip buckets
@@ -15,6 +15,7 @@ import (
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
@@ -22,15 +23,15 @@ import (
"golang.org/x/net/context"
)
const defaultRatelimit = 100
const defaultMaxRateLimitedIPs = 1024 * 1024
const defaultRatelimit = 30
const defaultResponseSize = 1000
var (
tokenBuckets = cache.New(time.Hour, time.Hour)
)
// main function
func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ServeDNS handles the DNS request and refuses if it's an beyind specified ratelimit
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
ip := state.IP()
allow, err := p.allowRequest(ip)
@@ -41,10 +42,34 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
ratelimited.Inc()
return 0, nil
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
// Record response to get status code and size of the reply.
rw := dnstest.NewRecorder(w)
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
size := rw.Len
if size > defaultResponseSize && state.Proto() == "udp" {
// For large UDP responses we call allowRequest more times
// The exact number of times depends on the response size
for i := 0; i < size/defaultResponseSize; i++ {
p.allowRequest(ip)
}
}
return status, err
}
func (p *Plugin) allowRequest(ip string) (bool, error) {
func (p *plug) allowRequest(ip string) (bool, error) {
if len(p.whitelist) > 0 {
i := sort.SearchStrings(p.whitelist, ip)
if i < len(p.whitelist) && p.whitelist[i] == ip {
return true, nil
}
}
if _, found := tokenBuckets.Get(ip); !found {
tokenBuckets.Set(ip, rate.New(p.ratelimit, time.Second), time.Hour)
}
@@ -59,7 +84,7 @@ func (p *Plugin) allowRequest(ip string) (bool, error) {
}
rl, ok := value.(*rate.RateLimiter)
if ok == false {
if !ok {
text := "SHOULD NOT HAPPEN: non-bool entry found in safebrowsing lookup cache"
log.Println(text)
err := errors.New(text)
@@ -80,51 +105,69 @@ func init() {
})
}
type Plugin struct {
type plug struct {
Next plugin.Handler
// configuration for creating above
ratelimit int // in requests per second per IP
ratelimit int // in requests per second per IP
whitelist []string // a list of whitelisted IP addresses
}
func setup(c *caddy.Controller) error {
p := &Plugin{ratelimit: defaultRatelimit}
config := dnsserver.GetConfig(c)
func setupPlugin(c *caddy.Controller) (*plug, error) {
p := &plug{ratelimit: defaultRatelimit}
for c.Next() {
args := c.RemainingArgs()
if len(args) <= 0 {
continue
if len(args) > 0 {
ratelimit, err := strconv.Atoi(args[0])
if err != nil {
return nil, c.ArgErr()
}
p.ratelimit = ratelimit
}
ratelimit, err := strconv.Atoi(args[0])
if err != nil {
return c.ArgErr()
for c.NextBlock() {
switch c.Val() {
case "whitelist":
p.whitelist = c.RemainingArgs()
if len(p.whitelist) > 0 {
sort.Strings(p.whitelist)
}
}
}
p.ratelimit = ratelimit
}
return p, nil
}
func setup(c *caddy.Controller) error {
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
once.Do(func() {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
})
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
return nil
})
return nil
}
func newDnsCounter(name string, help string) prometheus.Counter {
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
@@ -134,9 +177,8 @@ func newDnsCounter(name string, help string) prometheus.Counter {
}
var (
ratelimited = newDnsCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
ratelimited = newDNSCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
)
func (d *Plugin) Name() string { return "ratelimit" }
var once sync.Once
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "ratelimit" }

View File

@@ -0,0 +1,82 @@
package ratelimit
import (
"testing"
"github.com/mholt/caddy"
)
func TestSetup(t *testing.T) {
for i, testcase := range []struct {
config string
failing bool
}{
{`ratelimit`, false},
{`ratelimit 100`, false},
{`ratelimit {
whitelist 127.0.0.1
}`, false},
{`ratelimit 50 {
whitelist 127.0.0.1 176.103.130.130
}`, false},
{`ratelimit test`, true},
} {
c := caddy.NewTestController("dns", testcase.config)
err := setup(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}
func TestRatelimiting(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1`)
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, err = p.allowRequest("127.0.0.1")
if err != nil || allowed {
t.Fatal("Second request must have been ratelimited")
}
}
func TestWhitelist(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1 { whitelist 127.0.0.2 127.0.0.1 127.0.0.125 }`)
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, err = p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("Second request must have been allowed due to whitelist")
}
}

View File

@@ -3,7 +3,6 @@ package refuseany
import (
"fmt"
"log"
"sync"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
@@ -15,11 +14,12 @@ import (
"golang.org/x/net/context"
)
type Plugin struct {
type plug struct {
Next plugin.Handler
}
func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ServeDNS handles the DNS request and refuses if it's an ANY request
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions")
@@ -27,8 +27,6 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
q := r.Question[0]
if q.Qtype == dns.TypeANY {
log.Printf("Got request with type ANY, will respond with NOTIMP\n")
state := request.Request{W: w, Req: r, Context: ctx}
rcode := dns.RcodeNotImplemented
@@ -41,9 +39,9 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
return dns.RcodeServerFailure, err
}
return rcode, nil
} else {
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
func init() {
@@ -54,7 +52,7 @@ func init() {
}
func setup(c *caddy.Controller) error {
p := &Plugin{}
p := &plug{}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
@@ -63,22 +61,20 @@ func setup(c *caddy.Controller) error {
})
c.OnStartup(func() error {
once.Do(func() {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
})
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
return nil
})
return nil
}
func newDnsCounter(name string, help string) prometheus.Counter {
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "refuseany",
@@ -88,9 +84,8 @@ func newDnsCounter(name string, help string) prometheus.Counter {
}
var (
ratelimited = newDnsCounter("refusedany_total", "Count of ANY requests that have been dropped")
ratelimited = newDNSCounter("refusedany_total", "Count of ANY requests that have been dropped")
)
func (d *Plugin) Name() string { return "refuseany" }
var once sync.Once
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "refuseany" }

36
coredns_plugin/reload.go Normal file
View File

@@ -0,0 +1,36 @@
package dnsfilter
import (
"log"
"github.com/mholt/caddy"
)
var Reload = make(chan bool)
func hook(event caddy.EventName, info interface{}) error {
if event != caddy.InstanceStartupEvent {
return nil
}
// this should be an instance. ok to panic if not
instance := info.(*caddy.Instance)
go func() {
for range Reload {
corefile, err := caddy.LoadCaddyfile(instance.Caddyfile().ServerType())
if err != nil {
continue
}
_, err = instance.Restart(corefile)
if err != nil {
log.Printf("Corefile changed but reload failed: %s", err)
continue
}
// hook will be called again from new instance
return
}
}()
return nil
}

View File

@@ -1,9 +1,9 @@
# AdGuard DNS Go library
# AdGuard Home's DNS filtering go library
Example use:
```bash
[ -z "$GOPATH" ] && export GOPATH=$HOME/go
go get -d github.com/AdguardTeam/AdguardDNS/dnsfilter
go get -d github.com/AdguardTeam/AdGuardHome/dnsfilter
```
Create file filter.go
@@ -11,7 +11,7 @@ Create file filter.go
package main
import (
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"log"
)
@@ -48,7 +48,7 @@ You can also enable checking against AdGuard's SafeBrowsing:
package main
import (
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"log"
)

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"regexp"
"strings"
@@ -21,16 +22,23 @@ import (
)
const defaultCacheSize = 64 * 1024 // in number of elements
const defaultCacheTime time.Duration = 30 * time.Minute
const defaultCacheTime = 30 * time.Minute
const defaultHTTPTimeout time.Duration = 5 * time.Minute
const defaultHTTPTimeout = 5 * time.Minute
const defaultHTTPMaxIdleConnections = 100
const defaultSafebrowsingServer = "sb.adtidy.org"
const defaultSafebrowsingURL = "http://%s/safebrowsing-lookup-hash.html?prefixes=%s"
const defaultParentalURL = "http://pctrl.adguard.com/check-parental-control-hash?prefixes=%s&sensitivity=%d"
const defaultParentalServer = "pctrl.adguard.com"
const defaultParentalURL = "http://%s/check-parental-control-hash?prefixes=%s&sensitivity=%d"
// ErrInvalidSyntax is returned by AddRule when the rule is invalid
var ErrInvalidSyntax = errors.New("dnsfilter: invalid rule syntax")
// ErrInvalidSyntax is returned by AddRule when the rule was already added to the filter
var ErrAlreadyExists = errors.New("dnsfilter: rule was already added")
// ErrInvalidParental is returned by EnableParental when sensitivity is not a valid value
var ErrInvalidParental = errors.New("dnsfilter: invalid parental sensitivity, must be either 3, 10, 13 or 17")
const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet spot
@@ -38,29 +46,35 @@ const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet
const enableFastLookup = true // flag for debugging, must be true in production for faster performance
const enableDelayedCompilation = true // flag for debugging, must be true in production for faster performance
type Config struct {
type config struct {
parentalServer string
parentalSensitivity int // must be either 3, 10, 13 or 17
parentalEnabled bool
safeSearchEnabled bool
safeBrowsingEnabled bool
safeBrowsingServer string
parentalEnabled bool
parentalSensitivity int // must be either 3, 10, 13 or 17
}
type Rule struct {
type rule struct {
text string // text without @@ decorators or $ options
shortcut string // for speeding up lookup
originalText string // original text for reporting back to applications
ip net.IP // IP address (for the case when we're matching a hosts file)
// options
options []string // optional options after $
// parsed options
apps []string
isWhitelist bool
isImportant bool
apps []string
// user-supplied data
listID uint32
listID int64
// suffix matching
isSuffix bool
suffix string
// compiled regexp
compiled *regexp.Regexp
@@ -68,6 +82,7 @@ type Rule struct {
sync.RWMutex
}
// LookupStats store stats collected during safebrowsing or parental checks
type LookupStats struct {
Requests uint64 // number of HTTP requests that were sent
CacheHits uint64 // number of lookups that didn't need HTTP requests
@@ -75,6 +90,7 @@ type LookupStats struct {
PendingMax int64 // maximum number of pending HTTP requests
}
// Stats store LookupStats for both safebrowsing and parental
type Stats struct {
Safebrowsing LookupStats
Parental LookupStats
@@ -82,7 +98,7 @@ type Stats struct {
// Dnsfilter holds added rules and performs hostname matches against the rules
type Dnsfilter struct {
storage map[string]*Rule // rule storage, not used for matching, needs to be key->value
storage map[string]bool // rule storage, not used for matching, just for filtering out duplicates
storageMutex sync.RWMutex
// rules are checked against these lists in the order defined here
@@ -94,12 +110,12 @@ type Dnsfilter struct {
client http.Client // handle for http client -- single instance as recommended by docs
transport *http.Transport // handle for http transport used by http client
config Config
config config
}
//go:generate stringer -type=Reason
// filtered/notfiltered reason
// Reason holds an enum detailing why it was filtered or not filtered
type Reason int
const (
@@ -119,27 +135,31 @@ const (
// these variables need to survive coredns reload
var (
stats Stats
safebrowsingCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
parentalCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
safebrowsingCache gcache.Cache
parentalCache gcache.Cache
)
// search result
// Result holds state of hostname check
type Result struct {
IsFiltered bool
Reason Reason
Rule string
IsFiltered bool `json:",omitempty"` // True if the host name is filtered
Reason Reason `json:",omitempty"` // Reason for blocking / unblocking
Rule string `json:",omitempty"` // Original rule text
Ip net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax
FilterID int64 `json:",omitempty"` // Filter ID the rule belongs to
}
// Matched can be used to see if any match at all was found, no matter filtered or not
func (r Reason) Matched() bool {
return r != NotFilteredNotFound
}
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
func (d *Dnsfilter) CheckHost(host string) (Result, error) {
// sometimes DNS clients will try to resolve ".", which in turns transforms into "" when it reaches here
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
if host == "" {
return Result{Reason: FilteredInvalid}, nil
return Result{Reason: NotFilteredNotFound}, nil
}
host = strings.ToLower(host)
// try filter lists first
result, err := d.matchHost(host)
@@ -185,30 +205,43 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
//
type rulesTable struct {
rulesByShortcut map[string][]*Rule
rulesLeftovers []*Rule
rulesByHost map[string]*rule
rulesByShortcut map[string][]*rule
rulesLeftovers []*rule
sync.RWMutex
}
func newRulesTable() *rulesTable {
return &rulesTable{
rulesByShortcut: make(map[string][]*Rule),
rulesLeftovers: make([]*Rule, 0),
rulesByHost: make(map[string]*rule),
rulesByShortcut: make(map[string][]*rule),
rulesLeftovers: make([]*rule, 0),
}
}
func (r *rulesTable) Add(rule *Rule) {
func (r *rulesTable) Add(rule *rule) {
r.Lock()
if len(rule.shortcut) == shortcutLength && enableFastLookup {
if rule.ip != nil {
// Hosts syntax
r.rulesByHost[rule.text] = rule
} else if //noinspection GoBoolExpressions
len(rule.shortcut) == shortcutLength && enableFastLookup {
// Adblock syntax with a shortcut
r.rulesByShortcut[rule.shortcut] = append(r.rulesByShortcut[rule.shortcut], rule)
} else {
// Adblock syntax -- too short to have a shortcut
r.rulesLeftovers = append(r.rulesLeftovers, rule)
}
r.Unlock()
}
func (r *rulesTable) matchByHost(host string) (Result, error) {
res, err := r.searchShortcuts(host)
// First: examine the hosts-syntax rules
res, err := r.searchByHost(host)
if err != nil {
return res, err
}
@@ -216,6 +249,16 @@ func (r *rulesTable) matchByHost(host string) (Result, error) {
return res, nil
}
// Second: examine the adblock-syntax rules with shortcuts
res, err = r.searchShortcuts(host)
if err != nil {
return res, err
}
if res.Reason.Matched() {
return res, nil
}
// Third: examine the others
res, err = r.searchLeftovers(host)
if err != nil {
return res, err
@@ -227,6 +270,17 @@ func (r *rulesTable) matchByHost(host string) (Result, error) {
return Result{}, nil
}
func (r *rulesTable) searchByHost(host string) (Result, error) {
rule, ok := r.rulesByHost[host]
if ok {
return rule.match(host)
}
return Result{}, nil
}
func (r *rulesTable) searchShortcuts(host string) (Result, error) {
// check in shortcuts first
for i := 0; i < len(host); i++ {
@@ -292,7 +346,7 @@ func findOptionIndex(text string) int {
return -1
}
func (rule *Rule) extractOptions() error {
func (rule *rule) extractOptions() error {
optIndex := findOptionIndex(rule.text)
if optIndex == 0 { // starts with $
return ErrInvalidSyntax
@@ -330,7 +384,7 @@ func (rule *Rule) extractOptions() error {
return nil
}
func (rule *Rule) parseOptions() error {
func (rule *rule) parseOptions() error {
err := rule.extractOptions()
if err != nil {
return err
@@ -351,7 +405,7 @@ func (rule *Rule) parseOptions() error {
return nil
}
func (rule *Rule) extractShortcut() {
func (rule *rule) extractShortcut() {
// regex rules have no shortcuts
if rule.text[0] == '/' && rule.text[len(rule.text)-1] == '/' {
return
@@ -376,14 +430,23 @@ func (rule *Rule) extractShortcut() {
rule.shortcut = strings.ToLower(longestField)
}
func (rule *Rule) compile() error {
func (rule *rule) compile() error {
rule.RLock()
isCompiled := rule.compiled != nil
isCompiled := rule.isSuffix || rule.compiled != nil
rule.RUnlock()
if isCompiled {
return nil
}
isSuffix, suffix := getSuffix(rule.text)
if isSuffix {
rule.Lock()
rule.isSuffix = isSuffix
rule.suffix = suffix
rule.Unlock()
return nil
}
expr, err := ruleToRegexp(rule.text)
if err != nil {
return err
@@ -401,23 +464,46 @@ func (rule *Rule) compile() error {
return nil
}
func (rule *Rule) match(host string) (Result, error) {
// Checks if the rule matches the specified host and returns a corresponding Result object
func (rule *rule) match(host string) (Result, error) {
res := Result{}
if rule.ip != nil && rule.text == host {
// This is a hosts-syntax rule -- just check that the hostname matches and return the result
return Result{
IsFiltered: true,
Reason: FilteredBlackList,
Rule: rule.originalText,
Ip: rule.ip,
FilterID: rule.listID,
}, nil
}
err := rule.compile()
if err != nil {
return res, err
}
rule.RLock()
matched := rule.compiled.MatchString(host)
matched := false
if rule.isSuffix {
if host == rule.suffix {
matched = true
} else if strings.HasSuffix(host, "."+rule.suffix) {
matched = true
}
} else {
matched = rule.compiled.MatchString(host)
}
rule.RUnlock()
if matched {
res.Reason = FilteredBlackList
res.IsFiltered = true
res.FilterID = rule.listID
res.Rule = rule.originalText
if rule.isWhitelist {
res.Reason = NotFilteredWhiteList
res.IsFiltered = false
}
res.Rule = rule.text
}
return res, nil
}
@@ -439,7 +525,7 @@ func getCachedReason(cache gcache.Cache, host string) (result Result, isFound bo
// since it can be something else, validate that it belongs to proper type
cachedValue, ok := rawValue.(Result)
if ok == false {
if !ok {
// this is not our type -- error
text := "SHOULD NOT HAPPEN: entry with invalid type was found in lookup cache"
log.Println(text)
@@ -455,7 +541,7 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
var hashparam bytes.Buffer
hashes := map[string]bool{}
tld, icann := publicsuffix.PublicSuffix(host)
if icann == false {
if !icann {
// private suffixes like cloudfront.net
tld = ""
}
@@ -487,6 +573,10 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
}
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
// prevent recursion -- checking the host of safebrowsing server makes no sense
if host == d.config.safeBrowsingServer {
return Result{}, nil
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultSafebrowsingURL, d.config.safeBrowsingServer, hashparam)
return url
@@ -516,21 +606,29 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
}
return result, nil
}
if safebrowsingCache == nil {
safebrowsingCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
}
result, err := d.lookupCommon(host, &stats.Safebrowsing, safebrowsingCache, true, format, handleBody)
return result, err
}
func (d *Dnsfilter) checkParental(host string) (Result, error) {
format2 := func(hashparam string) string {
url := fmt.Sprintf(defaultParentalURL, hashparam, d.config.parentalSensitivity)
// prevent recursion -- checking the host of parental safety server makes no sense
if host == d.config.parentalServer {
return Result{}, nil
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultParentalURL, d.config.parentalServer, hashparam, d.config.parentalSensitivity)
return url
}
handleBody2 := func(body []byte, hashes map[string]bool) (Result, error) {
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
// parse json
var m []struct {
Blocked bool `json:"blocked"`
ClientTTL int `json:"clientTtl"`
Reason string `json:"reason"`
Hash string `json:"hash"`
}
err := json.Unmarshal(body, &m)
if err != nil {
@@ -542,6 +640,9 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
result := Result{}
for i := range m {
if !hashes[m[i].Hash] {
continue
}
if m[i].Blocked {
result.IsFiltered = true
result.Reason = FilteredParental
@@ -551,7 +652,10 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
}
return result, nil
}
result, err := d.lookupCommon(host, &stats.Parental, parentalCache, false, format2, handleBody2)
if parentalCache == nil {
parentalCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
}
result, err := d.lookupCommon(host, &stats.Parental, parentalCache, false, format, handleBody)
return result, err
}
@@ -563,7 +667,7 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
// check cache
cachedValue, isFound, err := getCachedReason(cache, host)
if isFound {
atomic.AddUint64(&stats.Safebrowsing.CacheHits, 1)
atomic.AddUint64(&lookupstats.CacheHits, 1)
return cachedValue, nil
}
if err != nil {
@@ -601,7 +705,10 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
switch {
case resp.StatusCode == 204:
// empty result, save cache
cache.Set(host, Result{})
err = cache.Set(host, Result{})
if err != nil {
return Result{}, err
}
return Result{}, nil
case resp.StatusCode != 200:
// error, don't save cache
@@ -614,7 +721,10 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
return Result{}, err
}
cache.Set(host, result)
err = cache.Set(host, result)
if err != nil {
return Result{}, err
}
return result, nil
}
@@ -623,27 +733,34 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
//
// AddRule adds a rule, checking if it is a valid rule first and if it wasn't added already
func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
func (d *Dnsfilter) AddRule(input string, filterListID int64) error {
input = strings.TrimSpace(input)
d.storageMutex.RLock()
_, exists := d.storage[input]
d.storageMutex.RUnlock()
if exists {
// already added
return ErrInvalidSyntax
return ErrAlreadyExists
}
if !isValidRule(input) {
return ErrInvalidSyntax
}
rule := Rule{
// First, check if this is a hosts-syntax rule
if d.parseEtcHosts(input, filterListID) {
// This is a valid hosts-syntax rule, no need for further parsing
return nil
}
// Start parsing the rule
rule := rule{
text: input, // will be modified
originalText: input,
listID: filterListID,
}
// mark rule as whitelist if it starts with @@
// Mark rule as whitelist if it starts with @@
if strings.HasPrefix(rule.text, "@@") {
rule.isWhitelist = true
rule.text = rule.text[2:]
@@ -656,6 +773,7 @@ func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
rule.extractShortcut()
//noinspection GoBoolExpressions
if !enableDelayedCompilation {
err := rule.compile()
if err != nil {
@@ -671,12 +789,44 @@ func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
}
d.storageMutex.Lock()
d.storage[input] = &rule
d.storage[input] = true
d.storageMutex.Unlock()
destination.Add(&rule)
return nil
}
// Parses the hosts-syntax rules. Returns false if the input string is not of hosts-syntax.
func (d *Dnsfilter) parseEtcHosts(input string, filterListID int64) bool {
// Strip the trailing comment
ruleText := input
if pos := strings.IndexByte(ruleText, '#'); pos != -1 {
ruleText = ruleText[0:pos]
}
fields := strings.Fields(ruleText)
if len(fields) < 2 {
return false
}
addr := net.ParseIP(fields[0])
if addr == nil {
return false
}
d.storageMutex.Lock()
d.storage[input] = true
d.storageMutex.Unlock()
for _, host := range fields[1:] {
rule := rule{
text: host,
originalText: input,
listID: filterListID,
ip: addr,
}
d.blackList.Add(&rule)
}
return true
}
// matchHost is a low-level way to check only if hostname is filtered by rules, skipping expensive safebrowsing and parental lookups
func (d *Dnsfilter) matchHost(host string) (Result, error) {
lists := []*rulesTable{
@@ -701,10 +851,11 @@ func (d *Dnsfilter) matchHost(host string) (Result, error) {
// lifecycle helper functions
//
// New creates properly initialized DNS Filter that is ready to be used
func New() *Dnsfilter {
d := new(Dnsfilter)
d.storage = make(map[string]*Rule)
d.storage = make(map[string]bool)
d.important = newRulesTable()
d.whiteList = newRulesTable()
d.blackList = newRulesTable()
@@ -723,21 +874,29 @@ func New() *Dnsfilter {
Timeout: defaultHTTPTimeout,
}
d.config.safeBrowsingServer = defaultSafebrowsingServer
d.config.parentalServer = defaultParentalServer
return d
}
// Destroy is optional if you want to tidy up goroutines without waiting for them to die off
// right now it closes idle HTTP connections if there are any
func (d *Dnsfilter) Destroy() {
d.transport.CloseIdleConnections()
if d != nil && d.transport != nil {
d.transport.CloseIdleConnections()
}
}
//
// config manipulation helpers
//
// EnableSafeBrowsing turns on checking hostnames in malware/phishing database
func (d *Dnsfilter) EnableSafeBrowsing() {
d.config.safeBrowsingEnabled = true
}
// EnableParental turns on checking hostnames for containing adult content
func (d *Dnsfilter) EnableParental(sensitivity int) error {
switch sensitivity {
case 3, 10, 13, 17:
@@ -749,10 +908,13 @@ func (d *Dnsfilter) EnableParental(sensitivity int) error {
}
}
// EnableSafeSearch turns on enforcing safesearch in search engines
// only used in coredns plugin and requires caller to use SafeSearchDomain()
func (d *Dnsfilter) EnableSafeSearch() {
d.config.safeSearchEnabled = true
}
// SetSafeBrowsingServer lets you optionally change hostname of safesearch lookup
func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
if len(host) == 0 {
d.config.safeBrowsingServer = defaultSafebrowsingServer
@@ -761,38 +923,35 @@ func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
}
}
// SetHTTPTimeout lets you optionally change timeout during lookups
func (d *Dnsfilter) SetHTTPTimeout(t time.Duration) {
d.client.Timeout = t
}
// ResetHTTPTimeout resets lookup timeouts
func (d *Dnsfilter) ResetHTTPTimeout() {
d.client.Timeout = defaultHTTPTimeout
}
// SafeSearchDomain returns replacement address for search engine
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
if d.config.safeSearchEnabled == false {
return "", false
if d.config.safeSearchEnabled {
val, ok := safeSearchDomains[host]
return val, ok
}
val, ok := safeSearchDomains[host]
return val, ok
return "", false
}
//
// stats
//
// GetStats return dns filtering stats since startup
func (d *Dnsfilter) GetStats() Stats {
return stats
}
// Count returns number of rules added to filter
func (d *Dnsfilter) Count() int {
return len(d.storage)
}
//
// cache control, right now needed only for tests
//
func purgeCaches() {
safebrowsingCache.Purge()
parentalCache.Purge()
}

View File

@@ -1,8 +1,14 @@
package dnsfilter
import (
"archive/zip"
"bytes"
"io/ioutil"
"net/http"
"net/http/httptest"
"path"
"runtime/pprof"
"strings"
"testing"
"time"
@@ -11,9 +17,175 @@ import (
"os"
"runtime"
"github.com/shirou/gopsutil/process"
"go.uber.org/goleak"
)
// first in file because it must be run first
func TestLotsOfRulesMemoryUsage(t *testing.T) {
start := getRSS()
trace("RSS before loading rules - %d kB\n", start/1024)
dumpMemProfile(_Func() + "1.pprof")
d := NewForTest()
defer d.Destroy()
err := loadTestRules(d)
if err != nil {
t.Error(err)
}
afterLoad := getRSS()
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
tests := []struct {
host string
match bool
}{
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.thisistesthost.com", false},
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.ad.doubleclick.net", true},
}
for _, testcase := range tests {
ret, err := d.CheckHost(testcase.host)
if err != nil {
t.Errorf("Error while matching host %s: %s", testcase.host, err)
}
if !ret.IsFiltered && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to not match", testcase.host)
}
if ret.IsFiltered && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to match", testcase.host)
}
}
afterMatch := getRSS()
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
}
func getRSS() uint64 {
proc, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
panic(err)
}
minfo, err := proc.MemoryInfo()
return minfo.RSS
}
func dumpMemProfile(name string) {
runtime.GC()
f, err := os.Create(name)
if err != nil {
panic(err)
}
defer f.Close()
runtime.GC() // update the stats before writing them
err = pprof.WriteHeapProfile(f)
if err != nil {
panic(err)
}
}
const topHostsFilename = "../tests/top-1m.csv"
func fetchTopHostsFromNet() {
trace("Fetching top hosts from network")
resp, err := http.Get("http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip")
if err != nil {
panic(err)
}
defer resp.Body.Close()
trace("Reading zipfile body")
zipfile, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
trace("Opening zipfile")
r, err := zip.NewReader(bytes.NewReader(zipfile), int64(len(zipfile)))
if err != nil {
panic(err)
}
if len(r.File) != 1 {
panic(fmt.Errorf("zipfile must have only one entry: %+v", r))
}
f := r.File[0]
trace("Unpacking file %s from zipfile", f.Name)
rc, err := f.Open()
if err != nil {
panic(err)
}
trace("Reading file %s contents", f.Name)
body, err := ioutil.ReadAll(rc)
if err != nil {
panic(err)
}
rc.Close()
trace("Writing file %s contents to disk", f.Name)
err = ioutil.WriteFile(topHostsFilename+".tmp", body, 0644)
if err != nil {
panic(err)
}
err = os.Rename(topHostsFilename+".tmp", topHostsFilename)
if err != nil {
panic(err)
}
}
func getTopHosts() {
// if file doesn't exist, fetch it
if _, err := os.Stat(topHostsFilename); os.IsNotExist(err) {
// file does not exist, fetch it
fetchTopHostsFromNet()
}
}
func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
start := getRSS()
trace("RSS before loading rules - %d kB\n", start/1024)
dumpMemProfile(_Func() + "1.pprof")
d := NewForTest()
defer d.Destroy()
mustLoadTestRules(d)
trace("Have %d rules", d.Count())
afterLoad := getRSS()
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
getTopHosts()
hostnames, err := os.Open(topHostsFilename)
if err != nil {
t.Fatal(err)
}
defer hostnames.Close()
afterHosts := getRSS()
trace("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024)
dumpMemProfile(_Func() + "2.pprof")
{
scanner := bufio.NewScanner(hostnames)
for scanner.Scan() {
line := scanner.Text()
records := strings.Split(line, ",")
ret, err := d.CheckHost(records[1] + "." + records[1])
if err != nil {
t.Error(err)
}
if ret.Reason.Matched() {
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
}
}
}
afterMatch := getRSS()
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
}
func TestRuleToRegexp(t *testing.T) {
tests := []struct {
rule string
@@ -23,7 +195,7 @@ func TestRuleToRegexp(t *testing.T) {
{"/doubleclick/", "doubleclick", nil},
{"/", "", ErrInvalidSyntax},
{`|double*?.+[]|(){}#$\|`, `^double.*\?\.\+\[\]\|\(\)\{\}\#\$\\$`, nil},
{`||doubleclick.net^`, `^([a-z0-9-_.]+\.)?doubleclick\.net([^ a-zA-Z0-9.%]|$)`, nil},
{`||doubleclick.net^`, `(?:^|\.)doubleclick\.net$`, nil},
}
for _, testcase := range tests {
converted, err := ruleToRegexp(testcase.rule)
@@ -36,6 +208,38 @@ func TestRuleToRegexp(t *testing.T) {
}
}
func TestSuffixRule(t *testing.T) {
for _, testcase := range []struct {
rule string
isSuffix bool
suffix string
}{
{`||doubleclick.net^`, true, `doubleclick.net`}, // entire string or subdomain match
{`||doubleclick.net|`, true, `doubleclick.net`}, // entire string or subdomain match
{`|doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`|*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`||*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`||*doubleclick.net|`, false, ``}, // TODO: ends with doubleclick.net
{`||*doublec*lick.net^`, false, ``}, // has a wildcard inside, has to be regexp
{`||*doublec|lick.net^`, false, ``}, // has a special symbol inside, has to be regexp
{`/abracadabra/`, false, ``}, // regexp, not anchored
{`/abracadabra$/`, false, ``}, // TODO: simplify simple suffix regexes
} {
isSuffix, suffix := getSuffix(testcase.rule)
if testcase.isSuffix != isSuffix {
t.Errorf("Results do not match for \"%s\": got %v expected %v", testcase.rule, isSuffix, testcase.isSuffix)
continue
}
if testcase.isSuffix && testcase.suffix != suffix {
t.Errorf("Result suffix does not match for \"%s\": got \"%s\" expected \"%s\"", testcase.rule, suffix, testcase.suffix)
continue
}
// trace("\"%s\": %v: %s", testcase.rule, isSuffix, suffix)
}
}
//
// helper functions
//
@@ -57,7 +261,7 @@ func (d *Dnsfilter) checkAddRule(t *testing.T, rule string) {
func (d *Dnsfilter) checkAddRuleFail(t *testing.T, rule string) {
t.Helper()
err := d.AddRule(rule, 0)
if err == ErrInvalidSyntax {
if err == ErrInvalidSyntax || err == ErrAlreadyExists {
return
}
if err != nil {
@@ -77,6 +281,20 @@ func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
}
}
func (d *Dnsfilter) checkMatchIp(t *testing.T, hostname string, ip string) {
t.Helper()
ret, err := d.CheckHost(hostname)
if err != nil {
t.Errorf("Error while matching host %s: %s", hostname, err)
}
if !ret.IsFiltered {
t.Errorf("Expected hostname %s to match", hostname)
}
if ret.Ip == nil || ret.Ip.String() != ip {
t.Errorf("Expected ip %s to match, actual: %v", ip, ret.Ip)
}
}
func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) {
t.Helper()
ret, err := d.CheckHost(hostname)
@@ -100,7 +318,7 @@ func loadTestRules(d *Dnsfilter) error {
for scanner.Scan() {
rule := scanner.Text()
err = d.AddRule(rule, 0)
if err == ErrInvalidSyntax {
if err == ErrInvalidSyntax || err == ErrAlreadyExists {
continue
}
if err != nil {
@@ -112,6 +330,13 @@ func loadTestRules(d *Dnsfilter) error {
return err
}
func mustLoadTestRules(d *Dnsfilter) {
err := loadTestRules(d)
if err != nil {
panic(err)
}
}
func NewForTest() *Dnsfilter {
d := New()
purgeCaches()
@@ -124,7 +349,9 @@ func NewForTest() *Dnsfilter {
func TestSanityCheck(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "||doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatchEmpty(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
@@ -132,6 +359,86 @@ func TestSanityCheck(t *testing.T) {
d.checkAddRuleFail(t, "lkfaojewhoawehfwacoefawr$@#$@3413841384")
}
func TestEtcHostsMatching(t *testing.T) {
d := NewForTest()
defer d.Destroy()
addr := "216.239.38.120"
text := fmt.Sprintf(" %s google.com www.google.com # enforce google's safesearch ", addr)
d.checkAddRule(t, text)
d.checkMatchIp(t, "google.com", addr)
d.checkMatchIp(t, "www.google.com", addr)
d.checkMatchEmpty(t, "subdomain.google.com")
d.checkMatchEmpty(t, "example.org")
}
func TestSuffixMatching1(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "||doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatchEmpty(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching2(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "|doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatchEmpty(t, "www.doubleclick.net")
d.checkMatchEmpty(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching3(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching4(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "*doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching5(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "|*doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching6(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "||*doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestCount(t *testing.T) {
d := NewForTest()
defer d.Destroy()
@@ -167,6 +474,15 @@ func TestDnsFilterWhitelist(t *testing.T) {
d.checkMatch(t, "example.org")
d.checkMatchEmpty(t, "test.example.org")
d.checkMatchEmpty(t, "test.test.example.org")
d.checkAddRule(t, "||googleadapis.l.google.com^|")
d.checkMatch(t, "googleadapis.l.google.com")
d.checkMatch(t, "test.googleadapis.l.google.com")
d.checkAddRule(t, "@@||googleadapis.l.google.com|")
d.checkMatchEmpty(t, "googleadapis.l.google.com")
d.checkMatchEmpty(t, "test.googleadapis.l.google.com")
}
func TestDnsFilterImportant(t *testing.T) {
@@ -217,52 +533,6 @@ func TestAddRuleFail(t *testing.T) {
d.checkAddRuleFail(t, "lkfaojewhoawehfwacoefawr$@#$@3413841384")
}
func printMemStats(r runtime.MemStats) {
fmt.Printf("Alloc: %.2f, HeapAlloc: %.2f Mb, Sys: %.2f Mb, HeapSys: %.2f Mb\n",
float64(r.Alloc)/1024.0/1024.0, float64(r.HeapAlloc)/1024.0/1024.0,
float64(r.Sys)/1024.0/1024.0, float64(r.HeapSys)/1024.0/1024.0)
}
func TestLotsOfRulesMemoryUsage(t *testing.T) {
var start, afterLoad, end runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&start)
fmt.Printf("Memory usage before loading rules - %d kB alloc, %d kB sys\n", start.Alloc/1024, start.Sys/1024)
d := NewForTest()
defer d.Destroy()
err := loadTestRules(d)
if err != nil {
t.Error(err)
}
runtime.GC()
runtime.ReadMemStats(&afterLoad)
fmt.Printf("Memory usage after loading rules - %d kB alloc, %d kB sys\n", afterLoad.Alloc/1024, afterLoad.Sys/1024)
tests := []struct {
host string
match bool
}{
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.thisistesthost.com", false},
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.ad.doubleclick.net", true},
}
for _, testcase := range tests {
ret, err := d.CheckHost(testcase.host)
if err != nil {
t.Errorf("Error while matching host %s: %s", testcase.host, err)
}
if ret.IsFiltered == false && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to not match", testcase.host)
}
if ret.IsFiltered == true && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to match", testcase.host)
}
}
runtime.GC()
runtime.ReadMemStats(&end)
fmt.Printf("Memory usage after matching - %d kB alloc, %d kB sys\n", afterLoad.Alloc/1024, afterLoad.Sys/1024)
}
func TestSafeBrowsing(t *testing.T) {
testCases := []string{
"",
@@ -356,6 +626,8 @@ func TestParentalControl(t *testing.T) {
if stats.Parental.Requests != l {
t.Errorf("Parental lookup negative cache is not working")
}
d.checkMatchEmpty(t, "api.jquery.com")
}
func TestSafeSearch(t *testing.T) {
@@ -385,43 +657,44 @@ var regexRules = []string{"/example\\.org/", "@@||test.example.org^"}
var maskRules = []string{"test*.example.org^", "exam*.com"}
var tests = []struct {
testname string
rules []string
hostname string
result bool
testname string
rules []string
hostname string
isFiltered bool
reason Reason
}{
{"sanity", []string{"||doubleclick.net^"}, "www.doubleclick.net", true},
{"sanity", []string{"||doubleclick.net^"}, "nodoubleclick.net", false},
{"sanity", []string{"||doubleclick.net^"}, "doubleclick.net.ru", false},
{"sanity", []string{"||doubleclick.net^"}, "wmconvirus.narod.ru", false},
{"blocking", blockingRules, "example.org", true},
{"blocking", blockingRules, "test.example.org", true},
{"blocking", blockingRules, "test.test.example.org", true},
{"blocking", blockingRules, "testexample.org", false},
{"blocking", blockingRules, "onemoreexample.org", false},
{"whitelist", whitelistRules, "example.org", true},
{"whitelist", whitelistRules, "test.example.org", false},
{"whitelist", whitelistRules, "test.test.example.org", false},
{"whitelist", whitelistRules, "testexample.org", false},
{"whitelist", whitelistRules, "onemoreexample.org", false},
{"important", importantRules, "example.org", false},
{"important", importantRules, "test.example.org", true},
{"important", importantRules, "test.test.example.org", true},
{"important", importantRules, "testexample.org", false},
{"important", importantRules, "onemoreexample.org", false},
{"regex", regexRules, "example.org", true},
{"regex", regexRules, "test.example.org", false},
{"regex", regexRules, "test.test.example.org", false},
{"regex", regexRules, "testexample.org", true},
{"regex", regexRules, "onemoreexample.org", true},
{"mask", maskRules, "test.example.org", true},
{"mask", maskRules, "test2.example.org", true},
{"mask", maskRules, "example.com", true},
{"mask", maskRules, "exampleeee.com", true},
{"mask", maskRules, "onemoreexamsite.com", true},
{"mask", maskRules, "example.org", false},
{"mask", maskRules, "testexample.org", false},
{"mask", maskRules, "example.co.uk", false},
{"sanity", []string{"||doubleclick.net^"}, "www.doubleclick.net", true, FilteredBlackList},
{"sanity", []string{"||doubleclick.net^"}, "nodoubleclick.net", false, NotFilteredNotFound},
{"sanity", []string{"||doubleclick.net^"}, "doubleclick.net.ru", false, NotFilteredNotFound},
{"sanity", []string{"||doubleclick.net^"}, "wmconvirus.narod.ru", false, NotFilteredNotFound},
{"blocking", blockingRules, "example.org", true, FilteredBlackList},
{"blocking", blockingRules, "test.example.org", true, FilteredBlackList},
{"blocking", blockingRules, "test.test.example.org", true, FilteredBlackList},
{"blocking", blockingRules, "testexample.org", false, NotFilteredNotFound},
{"blocking", blockingRules, "onemoreexample.org", false, NotFilteredNotFound},
{"whitelist", whitelistRules, "example.org", true, FilteredBlackList},
{"whitelist", whitelistRules, "test.example.org", false, NotFilteredWhiteList},
{"whitelist", whitelistRules, "test.test.example.org", false, NotFilteredWhiteList},
{"whitelist", whitelistRules, "testexample.org", false, NotFilteredNotFound},
{"whitelist", whitelistRules, "onemoreexample.org", false, NotFilteredNotFound},
{"important", importantRules, "example.org", false, NotFilteredWhiteList},
{"important", importantRules, "test.example.org", true, FilteredBlackList},
{"important", importantRules, "test.test.example.org", true, FilteredBlackList},
{"important", importantRules, "testexample.org", false, NotFilteredNotFound},
{"important", importantRules, "onemoreexample.org", false, NotFilteredNotFound},
{"regex", regexRules, "example.org", true, FilteredBlackList},
{"regex", regexRules, "test.example.org", false, NotFilteredWhiteList},
{"regex", regexRules, "test.test.example.org", false, NotFilteredWhiteList},
{"regex", regexRules, "testexample.org", true, FilteredBlackList},
{"regex", regexRules, "onemoreexample.org", true, FilteredBlackList},
{"mask", maskRules, "test.example.org", true, FilteredBlackList},
{"mask", maskRules, "test2.example.org", true, FilteredBlackList},
{"mask", maskRules, "example.com", true, FilteredBlackList},
{"mask", maskRules, "exampleeee.com", true, FilteredBlackList},
{"mask", maskRules, "onemoreexamsite.com", true, FilteredBlackList},
{"mask", maskRules, "example.org", false, NotFilteredNotFound},
{"mask", maskRules, "testexample.org", false, NotFilteredNotFound},
{"mask", maskRules, "example.co.uk", false, NotFilteredNotFound},
}
func TestMatching(t *testing.T) {
@@ -439,8 +712,11 @@ func TestMatching(t *testing.T) {
if err != nil {
t.Errorf("Error while matching host %s: %s", test.hostname, err)
}
if ret.IsFiltered != test.result {
t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret, test.result)
if ret.IsFiltered != test.isFiltered {
t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret.IsFiltered, test.isFiltered)
}
if ret.Reason != test.reason {
t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, ret.Reason.String(), test.reason.String())
}
})
}
@@ -457,6 +733,7 @@ func BenchmarkAddRule(b *testing.B) {
err := d.AddRule(rule, 0)
switch err {
case nil:
case ErrAlreadyExists: // ignore rules which were already added
case ErrInvalidSyntax: // ignore invalid syntax
default:
b.Fatalf("Error while adding rule %s: %s", rule, err)
@@ -476,6 +753,7 @@ func BenchmarkAddRuleParallel(b *testing.B) {
}
switch err {
case nil:
case ErrAlreadyExists: // ignore rules which were already added
case ErrInvalidSyntax: // ignore invalid syntax
default:
b.Fatalf("Error while adding rule %s: %s", rule, err)
@@ -569,6 +847,80 @@ func BenchmarkLotsOfRulesMatchParallel(b *testing.B) {
})
}
func BenchmarkLotsOfRulesLotsOfHosts(b *testing.B) {
d := NewForTest()
defer d.Destroy()
mustLoadTestRules(d)
getTopHosts()
hostnames, err := os.Open(topHostsFilename)
if err != nil {
b.Fatal(err)
}
defer hostnames.Close()
scanner := bufio.NewScanner(hostnames)
b.ResetTimer()
for n := 0; n < b.N; n++ {
havedata := scanner.Scan()
if !havedata {
hostnames.Seek(0, 0)
scanner = bufio.NewScanner(hostnames)
havedata = scanner.Scan()
}
if !havedata {
b.Fatal(scanner.Err())
}
line := scanner.Text()
records := strings.Split(line, ",")
ret, err := d.CheckHost(records[1] + "." + records[1])
if err != nil {
b.Error(err)
}
if ret.Reason.Matched() {
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
}
}
}
func BenchmarkLotsOfRulesLotsOfHostsParallel(b *testing.B) {
d := NewForTest()
defer d.Destroy()
mustLoadTestRules(d)
getTopHosts()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
hostnames, err := os.Open(topHostsFilename)
if err != nil {
b.Fatal(err)
}
defer hostnames.Close()
scanner := bufio.NewScanner(hostnames)
for pb.Next() {
havedata := scanner.Scan()
if !havedata {
hostnames.Seek(0, 0)
scanner = bufio.NewScanner(hostnames)
havedata = scanner.Scan()
}
if !havedata {
b.Fatal(scanner.Err())
}
line := scanner.Text()
records := strings.Split(line, ",")
ret, err := d.CheckHost(records[1] + "." + records[1])
if err != nil {
b.Error(err)
}
if ret.Reason.Matched() {
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
}
}
})
}
func BenchmarkSafeBrowsing(b *testing.B) {
d := NewForTest()
defer d.Destroy()
@@ -638,3 +990,36 @@ func BenchmarkSafeSearchParallel(b *testing.B) {
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
//
// helper functions for debugging and testing
//
func purgeCaches() {
if safebrowsingCache != nil {
safebrowsingCache.Purge()
}
if parentalCache != nil {
parentalCache.Purge()
}
}
func _Func() string {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
func trace(format string, args ...interface{}) {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
var buf strings.Builder
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
text := fmt.Sprintf(format, args...)
buf.WriteString(text)
if len(text) == 0 || text[len(text)-1] != '\n' {
buf.WriteRune('\n')
}
fmt.Print(buf.String())
}

View File

@@ -1,9 +1,6 @@
package dnsfilter
import (
"fmt"
"path"
"runtime"
"strings"
"sync/atomic"
)
@@ -22,11 +19,17 @@ func isValidRule(rule string) bool {
return false
}
// Filter out all sorts of cosmetic rules:
// https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#cosmetic-rules
masks := []string{
"##",
"#@#",
"#?#",
"#@?#",
"#$#",
"#@$#",
"#?$#",
"#@?$#",
"$$",
"$@$",
"#%#",
@@ -49,33 +52,9 @@ func updateMax(valuePtr *int64, maxPtr *int64) {
break
}
swapped := atomic.CompareAndSwapInt64(maxPtr, max, current)
if swapped == true {
if swapped {
break
}
// swapping failed because value has changed after reading, try again
}
}
//
// helper functions for debugging and testing
//
func _Func() string {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
func trace(format string, args ...interface{}) {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
var buf strings.Builder
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
text := fmt.Sprintf(format, args...)
buf.WriteString(text)
if len(text) == 0 || text[len(text)-1] != '\n' {
buf.WriteRune('\n')
}
fmt.Print(buf.String())
}

View File

@@ -5,8 +5,8 @@ import (
)
func ruleToRegexp(rule string) (string, error) {
const hostStart = "^([a-z0-9-_.]+\\.)?"
const hostEnd = "([^ a-zA-Z0-9.%]|$)"
const hostStart = `(?:^|\.)`
const hostEnd = `$`
// empty or short rule -- do nothing
if !isValidRule(rule) {
@@ -49,3 +49,43 @@ func ruleToRegexp(rule string) (string, error) {
return sb.String(), nil
}
// handle suffix rule ||example.com^ -- either entire string is example.com or *.example.com
func getSuffix(rule string) (bool, string) {
// if starts with / and ends with /, it's already a regexp
// TODO: if a regexp is simple `/abracadabra$/`, then simplify it maybe?
if rule[0] == '/' && rule[len(rule)-1] == '/' {
return false, ""
}
// must start with ||
if rule[0] != '|' || rule[1] != '|' {
return false, ""
}
rule = rule[2:]
// suffix rule must end with ^ or |
lastChar := rule[len(rule)-1]
if lastChar != '^' && lastChar != '|' {
return false, ""
}
// last char was checked, eat it
rule = rule[:len(rule)-1]
// it might also end with ^|
if rule[len(rule)-1] == '^' {
rule = rule[:len(rule)-1]
}
// check that it doesn't have any special characters inside
for _, r := range rule {
switch r {
case '|':
return false, ""
case '*':
return false, ""
}
}
return true, rule
}

Some files were not shown because too many files have changed in this diff Show More