Compare commits

...

24 Commits

Author SHA1 Message Date
Ainar Garipov
f1dd33346a Pull request: upd-chlog
Merge in DNS/adguard-home from upd-chlog to master

Squashed commit of the following:

commit 8885f3f2291947d76203873dce0ccfd5c270fa7f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 16:56:38 2022 +0300

    all: upd chlog
2022-10-07 17:05:01 +03:00
Ainar Garipov
960a7a75ed Pull request: upd-go
Merge in DNS/adguard-home from upd-go to master

Squashed commit of the following:

commit 3dffc8b5d8951216c1e695472199fc9e9d85e1c7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 14:30:23 2022 +0300

    all: fix chlog

commit cca70bd6cd27d04cd7cebe14a4e4bef112ce2bcb
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 14:07:39 2022 +0300

    all: upd go
2022-10-07 15:48:51 +03:00
Ainar Garipov
330ac30324 Pull request: 3418-clientid-doh
Closes #3418.

Squashed commit of the following:

commit 8a1180f8ef03d30ea3ae6a3e3121ddcac513f45b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 5 17:26:22 2022 +0300

    all: imp docs, tests

commit 9629c69b39540db119044f2f79c1c4ed39de911f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 5 15:34:33 2022 +0300

    dnsforward: accept clientids from doh client srvname
2022-10-05 18:11:09 +03:00
Ainar Garipov
2e0f6e5468 Pull request: refactor-opts
Updates #2893.

Squashed commit of the following:

commit c7027abd1088e27569367f3450e9225ff605b43d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 5 16:54:23 2022 +0300

    home: imp docs

commit 86a5b0aca916a7db608eba8263ecdc6ca79c8043
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 5 16:50:44 2022 +0300

    home: refactor opts more

commit 74c5989d1edf8d007dec847f4aaa0d7a0d24dc38
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 5 15:17:26 2022 +0300

    home: refactor option parsing
2022-10-05 17:07:08 +03:00
Ainar Garipov
f557339ca0 Pull request: imp-cache-label
Merge in DNS/adguard-home from imp-cache-label to master

Squashed commit of the following:

commit 10f62aa078b5306525578e22476835ee2e7bac66
Merge: 08c2de0e fe8be370
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 4 16:30:43 2022 +0300

    Merge branch 'master' into imp-cache-label

commit 08c2de0edbb1138b47d1a02d6630aa99b7ddcec9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 4 16:19:36 2022 +0300

    client: imp label

commit e66fbbe3cc6f929ff26fe3d7b8e14acc95f5c0ff
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 4 16:17:15 2022 +0300

    client: imp upstream example

commit d073f71cc5df4ba5f7de7ed08ad1215f7a198539
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 4 15:44:58 2022 +0300

    client: imp upstreams

commit b78d06db645a9f496bed699f4d4bf8c7396148f3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 4 14:59:30 2022 +0300

    client: imp cache size label
2022-10-04 16:36:38 +03:00
Ainar Garipov
fe8be3701f Pull request: websvc-config-manager
Merge in DNS/adguard-home from websvc-config-manager to master

Squashed commit of the following:

commit 2143b47c6528030dfe059172888fddf9061e42da
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 4 14:50:47 2022 +0300

    next: add config manager
2022-10-04 16:02:55 +03:00
Ainar Garipov
c26ab190e7 Pull request: imp-json-resp
Merge in DNS/adguard-home from imp-json-resp to master

Squashed commit of the following:

commit 44532b6fa551815e5ea876e7320ce0a73c32b6fb
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Sep 30 15:59:58 2022 +0300

    all: imp json resp
2022-10-04 14:35:10 +03:00
Ainar Garipov
bf792b83f6 Pull request: upd-chlog
Merge in DNS/adguard-home from upd-chlog to master

Squashed commit of the following:

commit 368025b27cb74fc5b6acbfd9382adeebc4ca6cbd
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Oct 3 19:23:32 2022 +0300

    all: upd chlog
2022-10-03 19:32:55 +03:00
Ainar Garipov
0cce420261 Pull request: 3955-doh3
Updates #3955.

Squashed commit of the following:

commit acfd5ccc29ff03dfae1e51e52649acdf05042d9f
Merge: caeac6e5 61bd217e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Oct 3 18:00:37 2022 +0300

    Merge branch 'master' into 3955-doh3

commit caeac6e5401bcaa95bba8d2b84a943b6c9a5898a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Oct 3 17:54:16 2022 +0300

    all: fix server closing; imp docs

commit 87396141ff49d48ae54b4184559070e7885bccc7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Oct 3 17:33:39 2022 +0300

    all: add doh3 support
2022-10-03 18:08:05 +03:00
Eugene Burkov
61bd217eb3 Pull request: 4983 fix custom svcs
Merge in DNS/adguard-home from 4983-fix-custom-svcs to master

Updates #4945.
Updates #4982.

* commit '739e0098ec127045197ea697bae78acb2bd2c729':
  all: imp code, wording
  home: fix empty svcs
  bugfix: Fixed bug which was causing clients to continue following global service blocks even when user opted to not do that
2022-10-03 17:57:13 +03:00
Eugene Burkov
739e0098ec all: imp code, wording 2022-10-03 17:48:11 +03:00
Eugene Burkov
27032ef79e home: fix empty svcs 2022-10-03 17:30:03 +03:00
Ishan Jain
5e626306d1 bugfix: Fixed bug which was causing clients to continue following global service blocks even when user opted to not do that 2022-10-03 00:02:35 +05:30
Eugene Burkov
2ffea605cf Pull request: home: fix client blocked svcs
Merge in DNS/adguard-home from fix-blocked-svcs to master

Squashed commit of the following:

commit 7e4ccbeb8866501c8f488efa76f7ce32369416ec
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Sep 30 16:48:27 2022 +0300

    all: imp log of changes

commit 171836fb8033ac1b7fc3ed3f4bd18669795fe1dd
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Sep 30 16:22:07 2022 +0300

    all: log changes

commit f5cde0a75a84b8efe59154ad019846ea06ec7c08
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Sep 30 16:04:14 2022 +0300

    home: fix client blocked svcs
2022-09-30 16:57:33 +03:00
Ainar Garipov
4d404b887f Pull request: 4970-error-415
Updates #4970.

Squashed commit of the following:

commit 10365d9c8474e9d9735f581fb32b2892b2153cc4
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Sep 30 14:23:06 2022 +0300

    all: imp docs, names

commit cff1103a0618a6430dc91e7e018febbf313c12ba
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Sep 30 14:02:38 2022 +0300

    home: imp content-type check
2022-09-30 14:41:25 +03:00
Ainar Garipov
7b48863041 Pull request: upd-chlog
Merge in DNS/adguard-home from upd-chlog to master

Squashed commit of the following:

commit b53de96bc5d1bc0ff81ceb6c716614fd094913e7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Sep 29 19:46:36 2022 +0300

    all: upd chlog
2022-09-29 19:51:33 +03:00
Ainar Garipov
756b14a61d Pull request: HOFTIX-csrf
Merge in DNS/adguard-home from HOFTIX-csrf to master

Squashed commit of the following:

commit 75ab27bf6c52b80ab4e7347d7c254fa659eac244
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Sep 29 18:45:54 2022 +0300

    all: imp cookie security; rm plain-text apis
2022-09-29 19:04:26 +03:00
Eugene Burkov
b71a5d86de Pull request: 4945 fix user rules
Merge in DNS/adguard-home from 4945-fix-user-rules to master

Updates #4945.
Updates #4871.

Squashed commit of the following:

commit 415a262e5af0821b658ed2a1b365d471f1452a6a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 29 18:05:48 2022 +0300

    home: fix user rules
2022-09-29 18:30:35 +03:00
Ainar Garipov
d45fa5801e Pull request: upd-i18n
Merge in DNS/adguard-home from upd-i18n to master

Squashed commit of the following:

commit c8ad18e03d1d6206c3220751c5c720a5eef3e3a9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Sep 29 17:12:22 2022 +0300

    client: upd i18n
2022-09-29 17:17:27 +03:00
Eugene Burkov
47c9c946a3 Pull request: 4871 imp filtering
Merge in DNS/adguard-home from 4871-imp-filtering to master

Closes #4871.

Squashed commit of the following:

commit 618e7c558447703c114332708c94ef1b34362cf9
Merge: 41ff8ab7 11e4f091
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 22 19:27:08 2022 +0300

    Merge branch 'master' into 4871-imp-filtering

commit 41ff8ab755a87170e7334dedcae00f01dcca238a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 22 19:26:11 2022 +0300

    filtering: imp code, log

commit e4ae1d1788406ffd7ef0fcc6df896a22b0c2db37
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 22 14:11:07 2022 +0300

    filtering: move handlers into single func

commit f7a340b4c10980f512ae935a156f02b0133a1627
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Sep 21 19:21:09 2022 +0300

    all: imp code

commit e064bf4d3de0283e4bda2aaf5b9822bb8a08f4a6
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Sep 20 20:12:16 2022 +0300

    all: imp name

commit e7eda3905762f0821e1be1ac3cf77e0ecbedeff4
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Sep 20 17:51:23 2022 +0300

    all: finally get rid of filtering

commit 188550d873e625cc2951583bb3a2eaad036745f5
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Sep 20 17:36:03 2022 +0300

    filtering: merge refresh

commit e54ed9c7952b17e66b790c835269b28fbc26f9ca
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Sep 20 17:16:23 2022 +0300

    filtering: merge filters

commit 32da31b754a319487d5f9d5e81e607d349b90180
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Sep 20 14:48:13 2022 +0300

    filtering: imp docs

commit 43b0cafa7a27bb9b620c2ba50ccdddcf32cfcecc
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Sep 20 14:38:04 2022 +0300

    all: imp code

commit 253a2ea6c92815d364546e34d631e406dd604644
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Sep 19 20:43:15 2022 +0300

    filtering: rm important flag

commit 1b87f08f946389d410f13412c7e486290d5e752d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Sep 19 17:05:40 2022 +0300

    all: move filtering to the package

commit daa13499f1dd4fe475c4b75769e34f1eb0915bdf
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Sep 19 15:13:55 2022 +0300

    all: finish merging

commit d6db75eb2e1f23528e9200ea51507eb793eefa3c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Sep 16 18:18:14 2022 +0300

    all: continue merging

commit 45b4c484deb7198a469aa18d719bb9dbe81e5d22
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Sep 14 15:44:22 2022 +0300

    all: merge filtering types
2022-09-23 13:23:35 +03:00
Ainar Garipov
11e4f09165 Pull request: imp-scripts
Merge in DNS/adguard-home from imp-scripts to master

Squashed commit of the following:

commit ab63a8a2dd1b64287e00a2a6f747fd48b530709e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Sep 21 19:15:06 2022 +0300

    all: imp scripts; upd tools; doc
2022-09-21 19:21:13 +03:00
Ainar Garipov
c45c02de29 Pull request: imp-stalebot
Merge in DNS/adguard-home from imp-stalebot to master

Squashed commit of the following:

commit d1fb5c6da25eeb168c53abfc7af714827a5242cd
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Sep 21 14:31:50 2022 +0300

    all: imp stalebot
2022-09-21 15:02:35 +03:00
Ainar Garipov
4fc045de11 Pull request: 4927-ddr-template
Updates #4927.

Squashed commit of the following:

commit 8cf080d5355261ced7e8b10de607cbf37e1d663d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Sep 20 15:18:48 2022 +0300

    dnsforward: fix doh template
2022-09-20 15:26:10 +03:00
Ildar Kamalov
cc2388e0c8 Pull request: 4815 fix query log modal on tablet
Updates #4815

Squashed commit of the following:

commit 148c39ac40963a593885b86a0c851b4010b68ab0
Merge: 3447611d ab6da05b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Sep 20 13:21:06 2022 +0300

    Merge branch 'master' into 4815-tablet-view

commit 3447611dc0b1c7d2cc1f8235d1c469dd92736166
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Sep 16 17:01:05 2022 +0300

    client: fix query log modal on tablet
2022-09-20 13:48:57 +03:00
138 changed files with 3774 additions and 2228 deletions

22
.github/stale.yml vendored
View File

@@ -4,15 +4,17 @@
'daysUntilClose': 15
# Issues with these labels will never be considered stale.
'exemptLabels':
- 'bug'
- 'documentation'
- 'enhancement'
- 'feature request'
- 'help wanted'
- 'localization'
- 'needs investigation'
- 'recurrent'
- 'research'
- 'bug'
- 'documentation'
- 'enhancement'
- 'feature request'
- 'help wanted'
- 'localization'
- 'needs investigation'
- 'recurrent'
- 'research'
# Set to true to ignore issues in a milestone.
'exemptMilestones': true
# Label to use when marking an issue as stale.
'staleLabel': 'wontfix'
# Comment to post when marking an issue as stale. Set to `false` to disable.
@@ -22,3 +24,5 @@
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable.
'closeComment': false
# Limit the number of actions per hour.
'limitPerRun': 1

View File

@@ -1,7 +1,7 @@
'name': 'build'
'env':
'GO_VERSION': '1.18.6'
'GO_VERSION': '1.18.7'
'NODE_VERSION': '14'
'on':

View File

@@ -1,7 +1,7 @@
'name': 'lint'
'env':
'GO_VERSION': '1.18.6'
'GO_VERSION': '1.18.7'
'on':
'push':

View File

@@ -12,11 +12,129 @@ and this project adheres to
## [Unreleased]
<!--
## [v0.108.0] - 2022-12-01 (APPROX.)
## [v0.108.0] - TBA (APPROX.)
-->
## Added
- The ability to put [ClientIDs][clientid] into DNS-over-HTTPS hostnames as
opposed to URL paths ([#3418]). Note that AdGuard Home checks the server name
only if the URL does not contain a ClientID.
[#3418]: https://github.com/AdguardTeam/AdGuardHome/issues/3418
[clientid]: https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#clientid
<!--
## [v0.107.17] - 2022-11-02 (APPROX.)
See also the [v0.107.17 GitHub milestone][ms-v0.107.17].
[ms-v0.107.17]: https://github.com/AdguardTeam/AdGuardHome/milestone/52?closed=1
-->
## [v0.107.16] - 2022-10-07
This is a security update. There is no GitHub milestone, since no GitHub issues
were resolved.
## Security
- Go version has been updated to prevent the possibility of exploiting the
CVE-2022-2879, CVE-2022-2880, and CVE-2022-41715 Go vulnerabilities fixed in
[Go 1.18.7][go-1.18.7].
[go-1.18.7]: https://groups.google.com/g/golang-announce/c/xtuG5faxtaU
## [v0.107.15] - 2022-10-03
See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
### Security
- As an additional CSRF protection measure, AdGuard Home now ensures that
requests that change its state but have no body (such as `POST
/control/stats_reset` requests) do not have a `Content-Type` header set on
them ([#4970]).
### Added
#### Experimental HTTP/3 Support
See [#3955] and the related issues for more details. These features are still
experimental and may break or change in the future.
- DNS-over-HTTP/3 DNS and web UI client request support. This feature must be
explicitly enabled by setting the new property `dns.serve_http3` in the
configuration file to `true`.
- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration
file property `dns.use_http3_upstreams` is set to `true`.
- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions
using the `h3://` scheme.
### Fixed
- User-specific blocked services not applying correctly ([#4945], [#4982],
[#4983]).
- `only application/json is allowed` errors in various APIs ([#4970]).
[#3955]: https://github.com/AdguardTeam/AdGuardHome/issues/3955
[#4945]: https://github.com/AdguardTeam/AdGuardHome/issues/4945
[#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970
[#4982]: https://github.com/AdguardTeam/AdGuardHome/issues/4982
[#4983]: https://github.com/AdguardTeam/AdGuardHome/issues/4983
[ms-v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/milestone/51?closed=1
## [v0.107.14] - 2022-09-29
See also the [v0.107.14 GitHub milestone][ms-v0.107.14].
### Security
A Cross-Site Request Forgery (CSRF) vulnerability has been discovered. The CVE
number is to be assigned. We thank Daniel Elkabes from Mend.io for reporting
this vulnerability to us.
#### `SameSite` Policy
The `SameSite` policy on the AdGuard Home session cookies is now set to `Lax`.
Which means that the only cross-site HTTP request for which the browser is
allowed to send the session cookie is navigating to the AdGuard Home domain.
**Users are strongly advised to log out, clear browser cache, and log in again
after updating.**
#### Removal Of Plain-Text APIs (BREAKING API CHANGE)
We have implemented several measures to prevent such vulnerabilities in the
future, but some of these measures break backwards compatibility for the sake of
better protection.
The following APIs, which previously accepted or returned `text/plain` data,
now accept or return data as JSON. All new formats for the request and response
bodies are documented in `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
- `GET /control/i18n/current_language`;
- `POST /control/dhcp/find_active_dhcp`;
- `POST /control/filtering/set_rules`;
- `POST /control/i18n/change_language`.
#### Stricter Content-Type Checks (BREAKING API CHANGE)
All JSON APIs that expect a body now check if the request actually has
`Content-Type` set to `application/json`.
#### Other Security Changes
- Weaker cipher suites that use the CBC (cipher block chaining) mode of
operation have been disabled ([#2993]).
@@ -25,19 +143,15 @@ and this project adheres to
- Support for plain (unencrypted) HTTP/2 ([#4930]). This is useful for AdGuard
Home installations behind a reverse proxy.
### Fixed
- Incorrect path template in DDR responses ([#4927]).
[#2993]: https://github.com/AdguardTeam/AdGuardHome/issues/2993
[#4927]: https://github.com/AdguardTeam/AdGuardHome/issues/4927
[#4930]: https://github.com/AdguardTeam/AdGuardHome/issues/4930
<!--
## [v0.107.14] - 2022-10-05 (APPROX.)
See also the [v0.107.14 GitHub milestone][ms-v0.107.14].
[ms-v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/milestone/50?closed=1
-->
@@ -74,7 +188,7 @@ See also the [v0.107.12 GitHub milestone][ms-v0.107.12].
### Security
- Go version was updated to prevent the possibility of exploiting the
- Go version has been updated to prevent the possibility of exploiting the
CVE-2022-27664 and CVE-2022-32190 Go vulnerabilities fixed in
[Go 1.18.6][go-1.18.6].
@@ -195,7 +309,7 @@ See also the [v0.107.9 GitHub milestone][ms-v0.107.9].
### Security
- Go version was updated to prevent the possibility of exploiting the
- Go version has been updated to prevent the possibility of exploiting the
CVE-2022-32189 Go vulnerability fixed in [Go 1.18.5][go-1.18.5]. Go 1.17
support has also been removed, as it has reached end of life and will not
receive security updates.
@@ -238,7 +352,7 @@ See also the [v0.107.8 GitHub milestone][ms-v0.107.8].
### Security
- Go version was updated to prevent the possibility of exploiting the
- Go version has been updated to prevent the possibility of exploiting the
CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, and other Go vulnerabilities
fixed in [Go 1.17.12][go-1.17.12].
@@ -274,7 +388,7 @@ See also the [v0.107.7 GitHub milestone][ms-v0.107.7].
### Security
- Go version was updated to prevent the possibility of exploiting the
- Go version has been updated to prevent the possibility of exploiting the
[CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and
[CVE-2022-29804] Go vulnerabilities.
- Enforced password strength policy ([#3503]).
@@ -431,7 +545,7 @@ See also the [v0.107.6 GitHub milestone][ms-v0.107.6].
### Security
- `User-Agent` HTTP header removed from outgoing DNS-over-HTTPS requests.
- Go version was updated to prevent the possibility of exploiting the
- Go version has been updated to prevent the possibility of exploiting the
[CVE-2022-24675], [CVE-2022-27536], and [CVE-2022-28327] Go vulnerabilities.
### Added
@@ -486,7 +600,7 @@ were resolved.
### Security
- Go version was updated to prevent the possibility of exploiting the
- Go version has been updated to prevent the possibility of exploiting the
[CVE-2022-24921] Go vulnerability.
[CVE-2022-24921]: https://www.cvedetails.com/cve/CVE-2022-24921
@@ -499,7 +613,7 @@ See also the [v0.107.4 GitHub milestone][ms-v0.107.4].
### Security
- Go version was updated to prevent the possibility of exploiting the
- Go version has been updated to prevent the possibility of exploiting the
[CVE-2022-23806], [CVE-2022-23772], and [CVE-2022-23773] Go vulnerabilities.
### Fixed
@@ -1236,11 +1350,14 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...HEAD
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...HEAD
[v0.107.17]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...v0.107.17
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...HEAD
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...HEAD
[v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...v0.107.16
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
[v0.107.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12
[v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11

View File

@@ -34,7 +34,7 @@ YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
--ignore-engines --ignore-optional --ignore-platform\
--ignore-scripts
V1API = 0
NEXTAPI = 0
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
@@ -63,7 +63,7 @@ ENV = env\
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
RACE='$(RACE)'\
SIGN='$(SIGN)'\
V1API='$(V1API)'\
NEXTAPI='$(NEXTAPI)'\
VERBOSE='$(VERBOSE)'\
VERSION='$(VERSION)'\

18
SECURITY.md Normal file
View File

@@ -0,0 +1,18 @@
# Security Policy
## Reporting a Vulnerability
Please send your vulnerability reports to <security@adguard.com>. To make sure
that your report reaches us, please:
1. Include the words “AdGuard Home” and “vulnerability” to the subject line as
well as a short description of the vulnerability. For example:
> AdGuard Home API vulnerability: possible XSS attack
2. Make sure that the message body contains a clear description of the
vulnerability.
If you have not received a reply to your email within 7 days, please make sure
to follow up with us again at <security@adguard.com>. Once again, make sure
that the word “vulnerability” is in the subject line.

View File

@@ -7,7 +7,7 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:5.1'
'dockerGo': 'adguard/golang-ubuntu:5.2'
'stages':
- 'Build frontend':
@@ -322,7 +322,7 @@
# need to build a few of these.
'variables':
'channel': 'beta'
'dockerGo': 'adguard/golang-ubuntu:5.1'
'dockerGo': 'adguard/golang-ubuntu:5.2'
# release-vX.Y.Z branches are the branches from which the actual final release
# is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
@@ -337,4 +337,4 @@
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerGo': 'adguard/golang-ubuntu:5.1'
'dockerGo': 'adguard/golang-ubuntu:5.2'

View File

@@ -5,7 +5,7 @@
'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests'
'variables':
'dockerGo': 'adguard/golang-ubuntu:5.1'
'dockerGo': 'adguard/golang-ubuntu:5.2'
'stages':
- 'Tests':

View File

@@ -635,5 +635,6 @@
"parental_control": "Бацькоўскі кантроль",
"safe_browsing": "Бяспечны інтэрнэт",
"served_from_cache": "{{value}} <i>(атрымана з кэша)</i>",
"form_error_password_length": "Пароль павінен быць не менш за {{value}} сімвалаў"
"form_error_password_length": "Пароль павінен быць не менш за {{value}} сімвалаў",
"anonymizer_notification": "<0>Заўвага:</0> Ананімізацыя IP уключана. Вы можаце адключыць яго ў <1>Агульных наладах</1> ."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Rodičovská ochrana",
"safe_browsing": "Bezpečné prohlížení",
"served_from_cache": "{{value}} <i>(převzato z mezipaměti)</i>",
"form_error_password_length": "Heslo musí být alespoň {{value}} znaků dlouhé"
"form_error_password_length": "Heslo musí být alespoň {{value}} znaků dlouhé",
"anonymizer_notification": "<0>Poznámka:</0> Anonymizace IP je zapnuta. Můžete ji vypnout v <1>Obecných nastaveních</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Forældrekontrol",
"safe_browsing": "Sikker Browsing",
"served_from_cache": "{{value}} <i>(leveret fra cache)</i>",
"form_error_password_length": "Adgangskoden skal udgøre mindst {{value}} tegn."
"form_error_password_length": "Adgangskoden skal udgøre mindst {{value}} tegn.",
"anonymizer_notification": "<0>Bemærk:</0> IP-anonymisering er aktiveret. Det kan deaktiveres via <1>Generelle indstillinger</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Kindersicherung",
"safe_browsing": "Internetsicherheit",
"served_from_cache": "{{value}} <i>(aus dem Cache abgerufen)</i>",
"form_error_password_length": "Das Passwort muss mindestens {{value}} Zeichen enthalten"
"form_error_password_length": "Das Passwort muss mindestens {{value}} Zeichen enthalten",
"anonymizer_notification": "<0>Hinweis:</0> Die IP-Anonymisierung ist aktiviert. Sie können sie in den <1>Allgemeinen Einstellungen</1> deaktivieren."
}

View File

@@ -215,6 +215,7 @@
"example_upstream_udp": "regular DNS (over UDP, hostname);",
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>;",
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>;",
"example_upstream_doh3": "encrypted DNS-over-HTTPS with forced <0>HTTP/3</0> and no fallback to HTTP/2 or below;",
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>;",
"example_upstream_sdns": "<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;",
"example_upstream_tcp": "regular DNS (over TCP);",
@@ -605,7 +606,7 @@
"blocklist": "Blocklist",
"milliseconds_abbreviation": "ms",
"cache_size": "Cache size",
"cache_size_desc": "DNS cache size (in bytes).",
"cache_size_desc": "DNS cache size (in bytes). To disable caching, leave empty.",
"cache_ttl_min_override": "Override minimum TTL",
"cache_ttl_max_override": "Override maximum TTL",
"enter_cache_size": "Enter cache size (bytes)",

View File

@@ -635,5 +635,6 @@
"parental_control": "Control parental",
"safe_browsing": "Navegación segura",
"served_from_cache": "{{value}} <i>(servido desde la caché)</i>",
"form_error_password_length": "La contraseña debe tener al menos {{value}} caracteres"
"form_error_password_length": "La contraseña debe tener al menos {{value}} caracteres",
"anonymizer_notification": "<0>Nota:</0> La anonimización de IP está habilitada. Puedes deshabilitarla en <1>Configuración general</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Lapsilukko",
"safe_browsing": "Turvallinen selaus",
"served_from_cache": "{{value}} <i>(jaettu välimuistista)</i>",
"form_error_password_length": "Salasanan on oltava ainakin {{value}} merkkiä"
"form_error_password_length": "Salasanan on oltava ainakin {{value}} merkkiä",
"anonymizer_notification": "<0>Huomioi:</0> IP-osoitteen anonymisointi on käytössä. Voit poistaa sen käytöstä <1>Yleisistä asetuksista</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Contrôle parental",
"safe_browsing": "Navigation sécurisée",
"served_from_cache": "{{value}} <i>(depuis le cache)</i>",
"form_error_password_length": "Le mot de passe doit comporter au moins {{value}} caractères"
"form_error_password_length": "Le mot de passe doit comporter au moins {{value}} caractères",
"anonymizer_notification": "<0>Note :</0> L'anonymisation IP est activée. Vous pouvez la désactiver dans les <1>paramètres généraux</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Roditeljska zaštita",
"safe_browsing": "Sigurno surfanje",
"served_from_cache": "{{value}} <i>(dohvaćeno iz predmemorije)</i>",
"form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova"
"form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova",
"anonymizer_notification": "<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Szülői felügyelet",
"safe_browsing": "Biztonságos böngészés",
"served_from_cache": "{{value}} <i>(gyorsítótárból kiszolgálva)</i>",
"form_error_password_length": "A jelszó legalább {{value}} karakter hosszú kell, hogy legyen"
"form_error_password_length": "A jelszó legalább {{value}} karakter hosszú kell, hogy legyen",
"anonymizer_notification": "<0>Megjegyzés:</0> Az IP anonimizálás engedélyezve van. Az <1>Általános beállításoknál letilthatja</1> ."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Kontrol Orang Tua",
"safe_browsing": "Penjelajahan Aman",
"served_from_cache": "{{value}} <i>(disajikan dari cache)</i>",
"form_error_password_length": "Kata sandi harus minimal {{value}} karakter"
"form_error_password_length": "Kata sandi harus minimal {{value}} karakter",
"anonymizer_notification": "<0>Catatan:</0> Anonimisasi IP diaktifkan. Anda dapat menonaktifkannya di <1>Pengaturan umum</1> ."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Controllo Parentale",
"safe_browsing": "Navigazione Sicura",
"served_from_cache": "{{value}} <i>(fornito dalla cache)</i>",
"form_error_password_length": "La password deve contenere almeno {{value}} caratteri"
"form_error_password_length": "La password deve contenere almeno {{value}} caratteri",
"anonymizer_notification": "<0>Attenzione:</0> L'anonimizzazione dell'IP è abilitata. Puoi disabilitarla in <1>Impostazioni generali</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "ペアレンタルコントロール",
"safe_browsing": "セーフブラウジング",
"served_from_cache": "{{value}} <i>(キャッシュから応答)</i>",
"form_error_password_length": "パスワードは{{value}}文字以上にしてください"
"form_error_password_length": "パスワードは{{value}}文字以上にしてください",
"anonymizer_notification": "【<0>注意</0>】IPの匿名化が有効になっています。 <1>一般設定</1>で無効にできます。"
}

View File

@@ -635,5 +635,6 @@
"parental_control": "자녀 보호",
"safe_browsing": "세이프 브라우징",
"served_from_cache": "{{value}} <i>(캐시에서 제공)</i>",
"form_error_password_length": "비밀번호는 {{value}}자 이상이어야 합니다"
"form_error_password_length": "비밀번호는 {{value}}자 이상이어야 합니다",
"anonymizer_notification": "<0>참고:</0> IP 익명화가 활성화되었습니다. <1>일반 설정</1>에서 비활성화할 수 있습니다."
}

View File

@@ -557,7 +557,7 @@
"fastest_addr_desc": "Alle DNS-servers bevragen en het snelste IP adres terugkoppelen. Dit zal de DNS verzoeken vertragen omdat AdGuard Home moet wachten op de antwoorden van alles DNS-servers, maar verbetert wel de connectiviteit.",
"autofix_warning_text": "Als je op \"Repareren\" klikt, configureert AdGuard Home jouw systeem om de AdGuard Home DNS-server te gebruiken.",
"autofix_warning_list": "De volgende taken worden uitgevoerd: <0> Deactiveren van Systeem DNSStubListener</0> <0> DNS-serveradres instellen op 127.0.0.1 </0> <0> Symbolisch koppelingsdoel van /etc/resolv.conf vervangen door /run/systemd/resolve/resolv.conf </0> <0> Stop DNSStubListener (herlaad systemd-resolved service) </0>",
"autofix_warning_result": "Als gevolg hiervan worden alle DNS-verzoeken van je systeem standaard door AdGuard Home verwerkt.",
"autofix_warning_result": "Als gevolg hiervan worden alle DNS-aanvragen van je systeem standaard door AdGuard Home verwerkt.",
"tags_title": "Labels",
"tags_desc": "Je kunt labels selecteren die overeenkomen met de client. Labels kunnen worden opgenomen in de filterregels om ze \n nauwkeuriger toe te passen. <0>Meer informatie</0>.",
"form_select_tags": "Client tags selecteren",
@@ -628,12 +628,13 @@
"original_response": "Oorspronkelijke reactie",
"click_to_view_queries": "Klik om queries te bekijken",
"port_53_faq_link": "Poort 53 wordt vaak gebruikt door services als DNSStubListener- of de systeem DNS-resolver. Lees a.u.b. <0>deze instructie</0> hoe dit is op te lossen.",
"adg_will_drop_dns_queries": "AdGuard Home zal alle DNS-verzoeken van deze cliënt laten vervallen.",
"adg_will_drop_dns_queries": "AdGuard Home zal alle DNS-aanvragen van deze cliënt laten vervallen.",
"filter_allowlist": "WAARSCHUWING: Deze actie zal ook de regel \"{{disallowed_rule}}\" uitsluiten van de lijst met toegestane clients.",
"last_rule_in_allowlist": "Kan deze client niet weigeren omdat het uitsluiten van de regel \"{{disallowed_rule}}\" de lijst \"Toegestane clients\" zal UITSCHAKELEN.",
"use_saved_key": "De eerder opgeslagen sleutel gebruiken",
"parental_control": "Ouderlijk toezicht",
"safe_browsing": "Veilig browsen",
"served_from_cache": "{{value}} <i>(geleverd vanuit cache)</i>",
"form_error_password_length": "Wachtwoord moet minimaal {{value}} tekens lang zijn"
"form_error_password_length": "Wachtwoord moet minimaal {{value}} tekens lang zijn",
"anonymizer_notification": "<0>Opmerking:</0> IP-anonimisering is ingeschakeld. Je kunt het uitschakelen in <1>Algemene instellingen</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Kontrola rodzicielska",
"safe_browsing": "Bezpieczne przeglądanie",
"served_from_cache": "{{value}} <i>(podawane z pamięci podręcznej)</i>",
"form_error_password_length": "Hasło musi mieć co najmniej {{value}} znaków"
"form_error_password_length": "Hasło musi mieć co najmniej {{value}} znaków",
"anonymizer_notification": "<0>Uwaga:</0> Anonimizacja IP jest włączona. Możesz ją wyłączyć w <1>Ustawieniach ogólnych</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Controle parental",
"safe_browsing": "Navegação segura",
"served_from_cache": "{{value}} <i>(servido do cache)</i>",
"form_error_password_length": "A senha deve ter pelo menos {{value}} caracteres"
"form_error_password_length": "A senha deve ter pelo menos {{value}} caracteres",
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-lo em <1>Configurações gerais</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Controlo parental",
"safe_browsing": "Navegação segura",
"served_from_cache": "{{value}} <i>(servido do cache)</i>",
"form_error_password_length": "A palavra-passe deve ter pelo menos {{value}} caracteres"
"form_error_password_length": "A palavra-passe deve ter pelo menos {{value}} caracteres",
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-la em <1>Definições gerais</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Control Parental",
"safe_browsing": "Navigare în siguranță",
"served_from_cache": "{{value}} <i>(furnizat din cache)</i>",
"form_error_password_length": "Parola trebuie să aibă cel puțin {{value}} caractere"
"form_error_password_length": "Parola trebuie să aibă cel puțin {{value}} caractere",
"anonymizer_notification": "<0>Nota:</0> Anonimizarea IP este activată. Puteți să o dezactivați în <1>Setări generale</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Родительский контроль",
"safe_browsing": "Безопасный интернет",
"served_from_cache": "{{value}} <i>(получено из кеша)</i>",
"form_error_password_length": "Пароль должен быть длиной не меньше {{value}} символов"
"form_error_password_length": "Пароль должен быть длиной не меньше {{value}} символов",
"anonymizer_notification": "<0>Внимание:</0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Rodičovská kontrola",
"safe_browsing": "Bezpečné prehliadanie",
"served_from_cache": "{{value}} <i>(prevzatá z cache pamäte)</i>",
"form_error_password_length": "Heslo musí mať dĺžku aspoň {{value}} znakov"
"form_error_password_length": "Heslo musí mať dĺžku aspoň {{value}} znakov",
"anonymizer_notification": "<0>Poznámka:</0> Anonymizácia IP je zapnutá. Môžete ju vypnúť vo <1>Všeobecných nastaveniach</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Starševski nadzor",
"safe_browsing": "Varno brskanje",
"served_from_cache": "{{value}} <i>(postreženo iz predpomnilnika)</i>",
"form_error_password_length": "Geslo mora vsebovati najmanj {{value}} znakov"
"form_error_password_length": "Geslo mora vsebovati najmanj {{value}} znakov",
"anonymizer_notification": "<0>Opomba:</0> Anonimizacija IP je omogočena. Onemogočite ga lahko v <1>Splošnih nastavitvah</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Roditeljska kontrola",
"safe_browsing": "Sigurno pregledanje",
"served_from_cache": "{{value}} <i>(posluženo iz predmemorije)</i>",
"form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova"
"form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova",
"anonymizer_notification": "<0>Nota:</0> IP prepoznavanje je omogućeno. Možete ga onemogućiti u opštim <1>postavkama</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Föräldrakontroll",
"safe_browsing": "Säker surfning",
"served_from_cache": "{{value}} <i>(levereras från cache)</i>",
"form_error_password_length": "Lösenordet måste vara minst {{value}} tecken långt"
"form_error_password_length": "Lösenordet måste vara minst {{value}} tecken långt",
"anonymizer_notification": "<0>Observera:</0> IP-anonymisering är aktiverad. Du kan inaktivera den i <1>Allmänna inställningar</1>."
}

View File

@@ -368,7 +368,7 @@
"encryption_server_enter": "Alan adınızı girin",
"encryption_server_desc": "Ayarlanırsa, AdGuard Home ClientID'leri algılar, DDR sorgularına yanıt verir ve ek bağlantı doğrulamaları gerçekleştirir. Ayarlanmazsa, bu özellikler devre dışı bırakılır. Sertifikadaki DNS Adlarından biriyle eşleşmelidir.",
"encryption_redirect": "Otomatik olarak HTTPS'e yönlendir",
"encryption_redirect_desc": "Etkinleştirirseniz, AdGuard Home sizi HTTP adresi yerine HTTPS adresine yönlendirir.",
"encryption_redirect_desc": "İşaretlenirse, AdGuard Home sizi otomatik olarak HTTP adresinden HTTPS adreslerine yönlendirecektir.",
"encryption_https": "HTTPS bağlantı noktası",
"encryption_https_desc": "HTTPS bağlantı noktası yapılandırılırsa, AdGuard Home yönetici arayüzüne HTTPS aracılığıyla erişilebilir olacak ve ayrıca '/dns-query' üzerinden DNS-over-HTTPS bağlantısı sağlayacaktır.",
"encryption_dot": "DNS-over-TLS bağlantı noktası",
@@ -408,7 +408,7 @@
"fix": "Düzelt",
"dns_providers": "Aralarından seçim yapabileceğiniz, bilinen <0>DNS sağlayıcıların listesi</0>.",
"update_now": "Şimdi güncelle",
"update_failed": "Otomatik güncelleme başarısız oldu. Elle güncellemek için lütfen <a>bu adımları uygulayın</a>.",
"update_failed": "Otomatik güncelleme başarısız oldu. Elle güncellemek için lütfen <a>bu adımları izleyin</a>.",
"manual_update": "Elle güncellemek için lütfen <a>bu adımları uygulayın</a>.",
"processing_update": "Lütfen bekleyin, AdGuard Home güncelleniyor",
"clients_title": "Kalıcı istemciler",
@@ -635,5 +635,6 @@
"parental_control": "Ebeveyn Denetimi",
"safe_browsing": "Güvenli Gezinti",
"served_from_cache": "{{value}} <i>(önbellekten kullanıldı)</i>",
"form_error_password_length": "Parola en az {{value}} karakter uzunluğunda olmalıdır"
"form_error_password_length": "Parola en az {{value}} karakter uzunluğunda olmalıdır",
"anonymizer_notification": "<0>Not:</0> IP anonimleştirme etkinleştirildi. Bunu <1>Genel ayarlardan</1> devre dışı bırakabilirsiniz."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Батьківський контроль",
"safe_browsing": "Безпечний перегляд",
"served_from_cache": "{{value}} <i>(отримано з кешу)</i>",
"form_error_password_length": "Пароль мусить мати принаймні {{value}} символів"
"form_error_password_length": "Пароль мусить мати принаймні {{value}} символів",
"anonymizer_notification": "<0>Примітка:</0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування</1> ."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "Quản lý của phụ huynh",
"safe_browsing": "Duyệt web an toàn",
"served_from_cache": "{{value}} <i>(được phục vụ từ bộ nhớ cache)</i>",
"form_error_password_length": "Mật khẩu phải có ít nhất {{value}} ký tự"
"form_error_password_length": "Mật khẩu phải có ít nhất {{value}} ký tự",
"anonymizer_notification": "<0> Lưu ý:</0> Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung</1>."
}

View File

@@ -635,5 +635,6 @@
"parental_control": "家长控制",
"safe_browsing": "安全浏览",
"served_from_cache": "{{value}}<i>(由缓存提供)</i>",
"form_error_password_length": "密码必须至少有 {{value}} 个字符"
"form_error_password_length": "密码必须至少有 {{value}} 个字符",
"anonymizer_notification": "<0>注意:</0> IP 匿名化已启用。您可以在<1>常规设置</1>中禁用它。"
}

View File

@@ -635,5 +635,6 @@
"parental_control": "家長控制",
"safe_browsing": "安全瀏覽",
"served_from_cache": "{{value}} <i>(由快取提供)</i>",
"form_error_password_length": "密碼必須為至少長 {{value}} 個字元"
"form_error_password_length": "密碼必須為至少長 {{value}} 個字元",
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。"
}

View File

@@ -31,7 +31,9 @@ export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
export const setRules = (rules) => async (dispatch) => {
dispatch(setRulesRequest());
try {
const normalizedRules = normalizeRulesTextarea(rules);
const normalizedRules = {
rules: normalizeRulesTextarea(rules)?.split('\n'),
};
await apiClient.setRules(normalizedRules);
dispatch(addSuccessToast('updated_custom_filtering_toast'));
dispatch(setRulesSuccess());

View File

@@ -355,7 +355,7 @@ export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');
export const changeLanguage = (lang) => async (dispatch) => {
dispatch(changeLanguageRequest());
try {
await apiClient.changeLanguage(lang);
await apiClient.changeLanguage({ language: lang });
dispatch(changeLanguageSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
@@ -370,8 +370,8 @@ export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS');
export const getLanguage = () => async (dispatch) => {
dispatch(getLanguageRequest());
try {
const language = await apiClient.getCurrentLanguage();
dispatch(getLanguageSuccess(language));
const langSettings = await apiClient.getCurrentLanguage();
dispatch(getLanguageSuccess(langSettings.language));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getLanguageFailure());
@@ -421,7 +421,10 @@ export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
export const findActiveDhcp = (name) => async (dispatch, getState) => {
dispatch(findActiveDhcpRequest());
try {
const activeDhcp = await apiClient.findActiveDhcp(name);
const req = {
interface: name,
};
const activeDhcp = await apiClient.findActiveDhcp(req);
dispatch(findActiveDhcpSuccess(activeDhcp));
const { check, interface_name, interfaces } = getState().dhcp;
const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name;

View File

@@ -10,11 +10,17 @@ class Api {
async makeRequest(path, method = 'POST', config) {
const url = `${this.baseUrl}/${path}`;
const axiosConfig = config || {};
if (method !== 'GET' && axiosConfig.data) {
axiosConfig.headers = axiosConfig.headers || {};
axiosConfig.headers['Content-Type'] = axiosConfig.headers['Content-Type'] || 'application/json';
}
try {
const response = await axios({
url,
method,
...config,
...axiosConfig,
});
return response.data;
} catch (error) {
@@ -55,7 +61,6 @@ class Api {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = {
data: servers,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@@ -64,7 +69,6 @@ class Api {
const { path, method } = this.GLOBAL_VERSION;
const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@@ -100,7 +104,6 @@ class Api {
const { path, method } = this.FILTERING_REFRESH;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
@@ -110,7 +113,6 @@ class Api {
const { path, method } = this.FILTERING_ADD_FILTER;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
@@ -120,7 +122,6 @@ class Api {
const { path, method } = this.FILTERING_REMOVE_FILTER;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
@@ -130,7 +131,6 @@ class Api {
const { path, method } = this.FILTERING_SET_RULES;
const parameters = {
data: rules,
headers: { 'Content-Type': 'text/plain' },
};
return this.makeRequest(path, method, parameters);
}
@@ -139,7 +139,6 @@ class Api {
const { path, method } = this.FILTERING_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -148,7 +147,6 @@ class Api {
const { path, method } = this.FILTERING_SET_URL;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -173,12 +171,7 @@ class Api {
enableParentalControl() {
const { path, method } = this.PARENTAL_ENABLE;
const parameter = 'sensitivity=TEEN'; // this parameter TEEN is hardcoded
const config = {
data: parameter,
headers: { 'Content-Type': 'text/plain' },
};
return this.makeRequest(path, method, config);
return this.makeRequest(path, method);
}
disableParentalControl() {
@@ -240,11 +233,10 @@ class Api {
return this.makeRequest(path, method);
}
changeLanguage(lang) {
changeLanguage(config) {
const { path, method } = this.CHANGE_LANGUAGE;
const parameters = {
data: lang,
headers: { 'Content-Type': 'text/plain' },
data: config,
};
return this.makeRequest(path, method, parameters);
}
@@ -280,16 +272,14 @@ class Api {
const { path, method } = this.DHCP_SET_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
findActiveDhcp(name) {
findActiveDhcp(req) {
const { path, method } = this.DHCP_FIND_ACTIVE;
const parameters = {
data: name,
headers: { 'Content-Type': 'text/plain' },
data: req,
};
return this.makeRequest(path, method, parameters);
}
@@ -298,7 +288,6 @@ class Api {
const { path, method } = this.DHCP_ADD_STATIC_LEASE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -307,7 +296,6 @@ class Api {
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -338,7 +326,6 @@ class Api {
const { path, method } = this.INSTALL_CONFIGURE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -347,7 +334,6 @@ class Api {
const { path, method } = this.INSTALL_CHECK_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -368,7 +354,6 @@ class Api {
const { path, method } = this.TLS_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -377,7 +362,6 @@ class Api {
const { path, method } = this.TLS_VALIDATE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -402,7 +386,6 @@ class Api {
const { path, method } = this.ADD_CLIENT;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -411,7 +394,6 @@ class Api {
const { path, method } = this.DELETE_CLIENT;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -420,7 +402,6 @@ class Api {
const { path, method } = this.UPDATE_CLIENT;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -445,7 +426,6 @@ class Api {
const { path, method } = this.ACCESS_SET;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -466,7 +446,6 @@ class Api {
const { path, method } = this.REWRITE_ADD;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -475,7 +454,6 @@ class Api {
const { path, method } = this.REWRITE_DELETE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -501,7 +479,6 @@ class Api {
const { path, method } = this.BLOCKED_SERVICES_SET;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
@@ -529,7 +506,6 @@ class Api {
const { path, method } = this.STATS_CONFIG;
const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@@ -565,7 +541,6 @@ class Api {
const { path, method } = this.QUERY_LOG_CONFIG;
const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@@ -582,7 +557,6 @@ class Api {
const { path, method } = this.LOGIN;
const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@@ -609,7 +583,6 @@ class Api {
const { path, method } = this.SET_DNS_CONFIG;
const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}

View File

@@ -62,7 +62,7 @@ const ClientCell = ({
'white-space--nowrap': isDetailed,
});
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
const hintClass = classNames('icons mr-4 icon--24 logs__question icon--lightgray', {
'my-3': isDetailed,
});

View File

@@ -34,7 +34,7 @@ const DomainCell = ({
'my-3': isDetailed,
});
const privacyIconClass = classNames('icons mx-2 icon--24 d-none d-sm-block', {
const privacyIconClass = classNames('icons mx-2 icon--24 d-none d-sm-block logs__question', {
'icon--green': hasTracker,
'icon--disabled': !hasTracker,
'my-3': isDetailed,

View File

@@ -49,6 +49,12 @@
padding-top: 1rem;
}
@media (max-width: 1024px) {
.grid .key-colon, .grid .title--border {
font-weight: 600;
}
}
@media (max-width: 767.98px) {
.grid {
grid-template-columns: 35% 55%;
@@ -70,10 +76,6 @@
grid-column: 2 / span 1;
margin: 0 !important;
}
.grid .key-colon, .grid .title--border {
font-weight: 600;
}
}
.grid .key-colon:nth-child(odd)::after {

View File

@@ -97,7 +97,7 @@ const ResponseCell = ({
return (
<div className="logs__cell logs__cell--response" role="gridcell">
<IconTooltip
className={classNames('icons mr-4 icon--24 icon--lightgray', { 'my-3': isDetailed })}
className={classNames('icons mr-4 icon--24 icon--lightgray logs__question', { 'my-3': isDetailed })}
columnClass='grid grid--limited'
tooltipClass='px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details'
contentItemClass='text-truncate key-colon o-hidden'

View File

@@ -485,3 +485,13 @@
.bg--green {
color: var(--green79);
}
@media (max-width: 1024px) {
.logs__question {
display: none;
}
}
.logs__modal {
max-width: 720px;
}

View File

@@ -184,27 +184,34 @@ const Logs = () => {
setButtonType={setButtonType}
setModalOpened={setModalOpened}
/>
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
onRequestClose={closeModal}
style={{
content: {
width: '100%',
height: 'fit-content',
left: 0,
top: 47,
padding: '1rem 1.5rem 1rem',
},
overlay: {
backgroundColor: 'rgba(0,0,0,0.5)',
},
}}
<Modal
portalClassName='grid'
isOpen={isSmallScreen && isModalOpened}
onRequestClose={closeModal}
style={{
content: {
width: '100%',
height: 'fit-content',
left: '50%',
top: 47,
padding: '1rem 1.5rem 1rem',
maxWidth: '720px',
transform: 'translateX(-50%)',
},
overlay: {
backgroundColor: 'rgba(0,0,0,0.5)',
},
}}
>
<svg
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
onClick={closeModal}>
<use xlinkHref="#cross" />
</svg>
{processContent(detailedDataCurrent, buttonType)}
<div className="logs__modal-wrap">
<svg
className="icon icon--24 icon-cross d-block cursor--pointer"
onClick={closeModal}
>
<use xlinkHref="#cross" />
</svg>
{processContent(detailedDataCurrent, buttonType)}
</div>
</Modal>
</>;

View File

@@ -57,6 +57,22 @@ const Examples = (props) => (
example_upstream_doh
</Trans>
</li>
<li>
<code>h3://unfiltered.adguard-dns.com/dns-query</code>: <Trans
components={[
<a
href="https://en.wikipedia.org/wiki/HTTP/3"
target="_blank"
rel="noopener noreferrer"
key="0"
>
HTTP/3
</a>,
]}
>
example_upstream_doh3
</Trans>
</li>
<li>
<code>quic://unfiltered.adguard-dns.com</code>: <Trans
components={[

16
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.18
require (
github.com/AdguardTeam/dnsproxy v0.44.0
github.com/AdguardTeam/dnsproxy v0.45.2
github.com/AdguardTeam/golibs v0.10.9
github.com/AdguardTeam/urlfilter v0.16.0
github.com/NYTimes/gziphandler v1.1.1
@@ -18,7 +18,7 @@ require (
github.com/google/uuid v1.3.0
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
github.com/kardianos/service v1.2.1
github.com/lucas-clemente/quic-go v0.29.0
github.com/lucas-clemente/quic-go v0.29.1
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
github.com/mdlayher/netlink v1.6.0
// TODO(a.garipov): This package is deprecated; find a new one or use
@@ -28,10 +28,10 @@ require (
github.com/stretchr/testify v1.8.0
github.com/ti-mo/netfilter v0.4.0
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9
golang.org/x/net v0.0.0-20220927171203-f486391704dc
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0
@@ -43,10 +43,12 @@ require (
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
github.com/mdlayher/packet v1.0.0 // indirect
@@ -57,7 +59,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 // indirect
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e // indirect
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect

35
go.sum
View File

@@ -1,5 +1,5 @@
github.com/AdguardTeam/dnsproxy v0.44.0 h1:JzIxEXF4OyJq4wZVHeZkM1af4VfuwcgrUzjgdBGljsE=
github.com/AdguardTeam/dnsproxy v0.44.0/go.mod h1:HsxYYW/bC8uo+4eX9pRW21hFD6gWZdrvcfBb1R6/AzU=
github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko=
github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
@@ -23,6 +23,8 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -88,8 +90,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE=
github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
@@ -125,6 +129,7 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@@ -168,16 +173,16 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI=
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0=
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e h1:WhB000cGjOfbJiedMGvJkMTclI18VD69w27k+sceql8=
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -189,6 +194,7 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -200,8 +206,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -224,6 +230,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -247,8 +254,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 h1:C1tElbkWrsSkn3IRl1GCW/gETw1TywWIPgwZtXTZbYg=
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -0,0 +1,33 @@
// Package aghchan contains channel utilities.
package aghchan
import (
"fmt"
"time"
)
// Receive returns an error if it cannot receive a value form c before timeout
// runs out.
func Receive[T any](c <-chan T, timeout time.Duration) (v T, ok bool, err error) {
var zero T
timeoutCh := time.After(timeout)
select {
case <-timeoutCh:
// TODO(a.garipov): Consider implementing [errors.Aser] for
// os.ErrTimeout.
return zero, false, fmt.Errorf("did not receive after %s", timeout)
case v, ok = <-c:
return v, ok, nil
}
}
// MustReceive panics if it cannot receive a value form c before timeout runs
// out.
func MustReceive[T any](c <-chan T, timeout time.Duration) (v T, ok bool) {
v, ok, err := Receive(c, timeout)
if err != nil {
panic(err)
}
return v, ok
}

View File

@@ -2,13 +2,21 @@
package aghhttp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/log"
)
// HTTP scheme constants.
const (
SchemeHTTP = "http"
SchemeHTTPS = "https"
)
// RegisterFunc is the function that sets the handler to handle the URL for the
// method.
//
@@ -25,6 +33,50 @@ func OK(w http.ResponseWriter) {
// Error writes formatted message to w and also logs it.
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
text := fmt.Sprintf(format, args...)
log.Error("%s %s: %s", r.Method, r.URL, text)
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text)
http.Error(w, text, code)
}
// UserAgent returns the ID of the service as a User-Agent string. It can also
// be used as the value of the Server HTTP header.
func UserAgent() (ua string) {
return fmt.Sprintf("AdGuardHome/%s", version.Version())
}
// textPlainDeprMsg is the message returned to API users when they try to use
// an API that used to accept "text/plain" but doesn't anymore.
const textPlainDeprMsg = `using this api with the text/plain content-type is deprecated; ` +
`use application/json`
// WriteTextPlainDeprecated responds to the request with a message about
// deprecation and removal of a plain-text API if the request is made with the
// "text/plain" content-type.
func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainText bool) {
if r.Header.Get(HdrNameContentType) != HdrValTextPlain {
return false
}
Error(r, w, http.StatusUnsupportedMediaType, textPlainDeprMsg)
return true
}
// WriteJSONResponse sets the content-type header in w.Header() to
// "application/json", writes a header with a "200 OK" status, encodes resp to
// w, calls [Error] on any returned error, and returns it as well.
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
return WriteJSONResponseCode(w, r, http.StatusOK, resp)
}
// WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to
// redefine the status code.
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
w.WriteHeader(code)
w.Header().Set(HdrNameContentType, HdrValApplicationJSON)
err = json.NewEncoder(w).Encode(resp)
if err != nil {
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
}
return err
}

View File

@@ -0,0 +1,22 @@
package aghhttp
// HTTP Headers
// HTTP header name constants.
//
// TODO(a.garipov): Remove unused.
const (
HdrNameAcceptEncoding = "Accept-Encoding"
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HdrNameContentEncoding = "Content-Encoding"
HdrNameContentType = "Content-Type"
HdrNameServer = "Server"
HdrNameTrailer = "Trailer"
HdrNameUserAgent = "User-Agent"
)
// HTTP header value constants.
const (
HdrValApplicationJSON = "application/json"
HdrValTextPlain = "text/plain"
)

View File

@@ -10,9 +10,9 @@ import (
"testing/fstest"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter"
@@ -163,15 +163,9 @@ func TestHostsContainer_refresh(t *testing.T) {
checkRefresh := func(t *testing.T, want *HostsRecord) {
t.Helper()
var ok bool
var upd *netutil.IPMap
select {
case upd, ok = <-hc.Upd():
require.True(t, ok)
require.NotNil(t, upd)
case <-time.After(1 * time.Second):
t.Fatal("did not receive after 1s")
}
upd, ok := aghchan.MustReceive(hc.Upd(), 1*time.Second)
require.True(t, ok)
require.NotNil(t, upd)
assert.Equal(t, 1, upd.Len())

View File

@@ -18,27 +18,18 @@ import (
// How to test on a real Linux machine:
//
// 1. Run:
// 1. Run "sudo ipset create example_set hash:ip family ipv4".
//
// sudo ipset create example_set hash:ip family ipv4
// 2. Run "sudo ipset list example_set". The Members field should be empty.
//
// 2. Run:
// 3. Add the line "example.com/example_set" to your AdGuardHome.yaml.
//
// sudo ipset list example_set
// 4. Start AdGuardHome.
//
// The Members field should be empty.
// 5. Make requests to example.com and its subdomains.
//
// 3. Add the line "example.com/example_set" to your AdGuardHome.yaml.
//
// 4. Start AdGuardHome.
//
// 5. Make requests to example.com and its subdomains.
//
// 6. Run:
//
// sudo ipset list example_set
//
// The Members field should contain the resolved IP addresses.
// 6. Run "sudo ipset list example_set". The Members field should contain the
// resolved IP addresses.
// newIpsetMgr returns a new Linux ipset manager.
func newIpsetMgr(ipsetConf []string) (set IpsetManager, err error) {

View File

@@ -1,6 +1,7 @@
package aghtest
import (
"context"
"io/fs"
"net"
@@ -15,6 +16,8 @@ import (
// Standard Library
// Package fs
// type check
var _ fs.FS = &FS{}
@@ -58,6 +61,8 @@ func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
return fsys.OnStat(name)
}
// Package net
// type check
var _ net.Listener = (*Listener)(nil)
@@ -83,32 +88,10 @@ func (l *Listener) Close() (err error) {
return l.OnClose()
}
// Module dnsproxy
// type check
var _ upstream.Upstream = (*UpstreamMock)(nil)
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
//
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
// rename it to just Upstream.
type UpstreamMock struct {
OnAddress func() (addr string)
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
}
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
func (u *UpstreamMock) Address() (addr string) {
return u.OnAddress()
}
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
return u.OnExchange(req)
}
// Module AdGuardHome
// Package aghos
// type check
var _ aghos.FSWatcher = (*FSWatcher)(nil)
@@ -133,3 +116,57 @@ func (w *FSWatcher) Add(name string) (err error) {
func (w *FSWatcher) Close() (err error) {
return w.OnClose()
}
// Package websvc
// ServiceWithConfig is a mock [websvc.ServiceWithConfig] implementation for
// tests.
type ServiceWithConfig[ConfigType any] struct {
OnStart func() (err error)
OnShutdown func(ctx context.Context) (err error)
OnConfig func() (c ConfigType)
}
// Start implements the [websvc.ServiceWithConfig] interface for
// *ServiceWithConfig.
func (s *ServiceWithConfig[_]) Start() (err error) {
return s.OnStart()
}
// Shutdown implements the [websvc.ServiceWithConfig] interface for
// *ServiceWithConfig.
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
return s.OnShutdown(ctx)
}
// Config implements the [websvc.ServiceWithConfig] interface for
// *ServiceWithConfig.
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
return s.OnConfig()
}
// Module dnsproxy
// Package upstream
// type check
var _ upstream.Upstream = (*UpstreamMock)(nil)
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
//
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
// rename it to just Upstream.
type UpstreamMock struct {
OnAddress func() (addr string)
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
}
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
func (u *UpstreamMock) Address() (addr string) {
return u.OnAddress()
}
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
return u.OnExchange(req)
}

View File

@@ -1,9 +1,9 @@
package aghtest_test
import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
)
// type check
var _ aghos.FSWatcher = (*aghtest.FSWatcher)(nil)
var _ websvc.ServiceWithConfig[struct{}] = (*aghtest.ServiceWithConfig[struct{}])(nil)

View File

@@ -5,11 +5,9 @@ package dhcpd
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -80,18 +78,7 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
status.Leases = s.Leases(LeasesDynamic)
status.StaticLeases = s.Leases(LeasesStatic)
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(status)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to marshal DHCP status json: %s",
err,
)
}
_ = aghhttp.WriteJSONResponse(w, r, status)
}
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
@@ -248,22 +235,7 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return
}
if conf.Enabled != aghalg.NBNull {
s.conf.Enabled = conf.Enabled == aghalg.NBTrue
}
if conf.InterfaceName != "" {
s.conf.InterfaceName = conf.InterfaceName
}
if srv4 != nil {
s.srv4 = srv4
}
if srv6 != nil {
s.srv6 = srv6
}
s.setConfFromJSON(conf, srv4, srv6)
s.conf.ConfigModified()
err = s.dbLoad()
@@ -282,6 +254,26 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
}
}
// setConfFromJSON sets configuration parameters in s from the new configuration
// decoded from JSON.
func (s *server) setConfFromJSON(conf *dhcpServerConfigJSON, srv4, srv6 DHCPServer) {
if conf.Enabled != aghalg.NBNull {
s.conf.Enabled = conf.Enabled == aghalg.NBTrue
}
if conf.InterfaceName != "" {
s.conf.InterfaceName = conf.InterfaceName
}
if srv4 != nil {
s.srv4 = srv4
}
if srv6 != nil {
s.srv6 = srv6
}
}
type netInterfaceJSON struct {
Name string `json:"name"`
HardwareAddr string `json:"hardware_address"`
@@ -410,31 +402,37 @@ type dhcpSearchResult struct {
V6 dhcpSearchV6Result `json:"v6"`
}
// Perform the following tasks:
// . Search for another DHCP server running
// . Check if a static IP is configured for the network interface
// Respond with results
// findActiveServerReq is the JSON structure for the request to find active DHCP
// servers.
type findActiveServerReq struct {
Interface string `json:"interface"`
}
// handleDHCPFindActiveServer performs the following tasks:
// 1. searches for another DHCP server in the network;
// 2. check if a static IP is configured for the network interface;
// 3. responds with the results.
func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
// This use of ReadAll is safe, because request's body is now limited.
body, err := io.ReadAll(r.Body)
if aghhttp.WriteTextPlainDeprecated(w, r) {
return
}
req := &findActiveServerReq{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
msg := fmt.Sprintf("failed to read request body: %s", err)
log.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
return
}
ifaceName := strings.TrimSpace(string(body))
ifaceName := req.Interface
if ifaceName == "" {
msg := "empty interface name specified"
log.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
aghhttp.Error(r, w, http.StatusBadRequest, "empty interface name")
return
}
result := dhcpSearchResult{
result := &dhcpSearchResult{
V4: dhcpSearchV4Result{
OtherServer: dhcpSearchOtherResult{
Found: "no",
@@ -459,6 +457,14 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
result.V4.StaticIP.IP = aghnet.GetSubnet(ifaceName).String()
}
setOtherDHCPResult(ifaceName, result)
_ = aghhttp.WriteJSONResponse(w, r, result)
}
// setOtherDHCPResult sets the results of the check for another DHCP server in
// result.
func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
found4, found6, err4, err6 := aghnet.CheckOtherDHCP(ifaceName)
if err4 != nil {
result.V4.OtherServer.Found = "error"
@@ -466,24 +472,13 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
} else if found4 {
result.V4.OtherServer.Found = "yes"
}
if err6 != nil {
result.V6.OtherServer.Found = "error"
result.V6.OtherServer.Error = err6.Error()
} else if found6 {
result.V6.OtherServer.Found = "yes"
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Failed to marshal DHCP found json: %s",
err,
)
}
}
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {

View File

@@ -3,11 +3,10 @@
package dhcpd
import (
"encoding/json"
"net/http"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log"
)
// jsonError is a generic JSON error response.
@@ -25,15 +24,9 @@ type jsonError struct {
// TODO(a.garipov): Either take the logger from the server after we've
// refactored logging or make this not a method of *Server.
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotImplemented)
err := json.NewEncoder(w).Encode(&jsonError{
_ = aghhttp.WriteJSONResponseCode(w, r, http.StatusNotImplemented, &jsonError{
Message: aghos.Unsupported("dhcp").Error(),
})
if err != nil {
log.Debug("writing 501 json response: %s", err)
}
}
// registerHandlers sets the handlers for DHCP HTTP API that always respond with

View File

@@ -183,15 +183,7 @@ func (s *Server) accessListJSON() (j accessListJSON) {
}
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
j := s.accessListJSON()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(j)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON())
}
// validateAccessSet checks the internal accessListJSON lists. To search for

View File

@@ -123,7 +123,14 @@ type quicConnection interface {
func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
proto := pctx.Proto
if proto == proxy.ProtoHTTPS {
return clientIDFromDNSContextHTTPS(pctx)
clientID, err = clientIDFromDNSContextHTTPS(pctx)
if err != nil {
return "", fmt.Errorf("checking url: %w", err)
} else if clientID != "" {
return clientID, nil
}
// Go on and check the domain name as well.
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
return "", nil
}
@@ -133,31 +140,9 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
return "", nil
}
cliSrvName := ""
switch proto {
case proxy.ProtoTLS:
conn := pctx.Conn
tc, ok := conn.(tlsConn)
if !ok {
return "", fmt.Errorf(
"proxy ctx conn of proto %s is %T, want *tls.Conn",
proto,
conn,
)
}
cliSrvName = tc.ConnectionState().ServerName
case proxy.ProtoQUIC:
conn, ok := pctx.QUICConnection.(quicConnection)
if !ok {
return "", fmt.Errorf(
"proxy ctx quic conn of proto %s is %T, want quic.Connection",
proto,
pctx.QUICConnection,
)
}
cliSrvName = conn.ConnectionState().TLS.ServerName
cliSrvName, err := clientServerName(pctx, proto)
if err != nil {
return "", err
}
clientID, err = clientIDFromClientServerName(
@@ -171,3 +156,35 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
return clientID, nil
}
// clientServerName returns the TLS server name based on the protocol.
func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string, err error) {
switch proto {
case proxy.ProtoHTTPS:
if connState := pctx.HTTPRequest.TLS; connState != nil {
srvName = pctx.HTTPRequest.TLS.ServerName
}
case proxy.ProtoQUIC:
qConn := pctx.QUICConnection
conn, ok := qConn.(quicConnection)
if !ok {
return "", fmt.Errorf(
"proxy ctx quic conn of proto %s is %T, want quic.Connection",
proto,
qConn,
)
}
srvName = conn.ConnectionState().TLS.ServerName
case proxy.ProtoTLS:
conn := pctx.Conn
tc, ok := conn.(tlsConn)
if !ok {
return "", fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn)
}
srvName = tc.ConnectionState().ServerName
}
return srvName, nil
}

View File

@@ -160,6 +160,22 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
wantClientID: "insensitive",
wantErrMsg: ``,
strictSNI: true,
}, {
name: "https_no_clientid",
proto: proxy.ProtoHTTPS,
hostSrvName: "example.com",
cliSrvName: "example.com",
wantClientID: "",
wantErrMsg: "",
strictSNI: true,
}, {
name: "https_clientid",
proto: proxy.ProtoHTTPS,
hostSrvName: "example.com",
cliSrvName: "cli.example.com",
wantClientID: "cli",
wantErrMsg: "",
strictSNI: true,
}}
for _, tc := range testCases {
@@ -173,16 +189,32 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
conf: ServerConfig{TLSConfig: tlsConf},
}
var conn net.Conn
if tc.proto == proxy.ProtoTLS {
conn = testTLSConn{
var (
conn net.Conn
qconn quic.Connection
httpReq *http.Request
)
switch tc.proto {
case proxy.ProtoHTTPS:
u := &url.URL{
Path: "/dns-query",
}
connState := &tls.ConnectionState{
ServerName: tc.cliSrvName,
}
httpReq = &http.Request{
URL: u,
TLS: connState,
}
case proxy.ProtoQUIC:
qconn = testQUICConnection{
serverName: tc.cliSrvName,
}
}
var qconn quic.Connection
if tc.proto == proxy.ProtoQUIC {
qconn = testQUICConnection{
case proxy.ProtoTLS:
conn = testTLSConn{
serverName: tc.cliSrvName,
}
}
@@ -190,6 +222,7 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
pctx := &proxy.DNSContext{
Proto: tc.proto,
Conn: conn,
HTTPRequest: httpReq,
QUICConnection: qconn,
}
@@ -205,56 +238,76 @@ func TestClientIDFromDNSContextHTTPS(t *testing.T) {
testCases := []struct {
name string
path string
cliSrvName string
wantClientID string
wantErrMsg string
}{{
name: "no_clientid",
path: "/dns-query",
cliSrvName: "example.com",
wantClientID: "",
wantErrMsg: "",
}, {
name: "no_clientid_slash",
path: "/dns-query/",
cliSrvName: "example.com",
wantClientID: "",
wantErrMsg: "",
}, {
name: "clientid",
path: "/dns-query/cli",
cliSrvName: "example.com",
wantClientID: "cli",
wantErrMsg: "",
}, {
name: "clientid_slash",
path: "/dns-query/cli/",
cliSrvName: "example.com",
wantClientID: "cli",
wantErrMsg: "",
}, {
name: "clientid_case",
path: "/dns-query/InSeNsItIvE",
cliSrvName: "example.com",
wantClientID: "insensitive",
wantErrMsg: ``,
}, {
name: "bad_url",
path: "/foo",
cliSrvName: "example.com",
wantClientID: "",
wantErrMsg: `clientid check: invalid path "/foo"`,
}, {
name: "extra",
path: "/dns-query/cli/foo",
cliSrvName: "example.com",
wantClientID: "",
wantErrMsg: `clientid check: invalid path "/dns-query/cli/foo": extra parts`,
}, {
name: "invalid_clientid",
path: "/dns-query/!!!",
cliSrvName: "example.com",
wantClientID: "",
wantErrMsg: `clientid check: invalid clientid "!!!": bad domain name label rune '!'`,
}, {
name: "both_ids",
path: "/dns-query/right",
cliSrvName: "wrong.example.com",
wantClientID: "right",
wantErrMsg: "",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
connState := &tls.ConnectionState{
ServerName: tc.cliSrvName,
}
r := &http.Request{
URL: &url.URL{
Path: tc.path,
},
TLS: connState,
}
pctx := &proxy.DNSContext{

View File

@@ -201,6 +201,10 @@ type ServerConfig struct {
// Register an HTTP handler
HTTPRegister aghhttp.RegisterFunc
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
// resolving PTR queries for local addresses.
LocalPTRResolvers []string
// ResolveClients signals if the RDNS should resolve clients' addresses.
ResolveClients bool
@@ -208,9 +212,12 @@ type ServerConfig struct {
// locally-served networks should be resolved via private PTR resolvers.
UsePrivateRDNS bool
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
// resolving PTR queries for local addresses.
LocalPTRResolvers []string
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
ServeHTTP3 bool
// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
// upstreams.
UseHTTP3Upstreams bool
}
// if any of ServerConfig values are zero, then default values from below are used
@@ -226,6 +233,7 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
conf = proxy.Config{
UDPListenAddr: srvConf.UDPListenAddrs,
TCPListenAddr: srvConf.TCPListenAddrs,
HTTP3: srvConf.ServeHTTP3,
Ratelimit: int(srvConf.Ratelimit),
RatelimitWhitelist: srvConf.RatelimitWhitelist,
RefuseAny: srvConf.RefuseAny,
@@ -324,6 +332,20 @@ func (s *Server) initDefaultSettings() {
}
}
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
// depending on configuration.
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
if !http3 {
return upstream.DefaultHTTPVersions
}
return []upstream.HTTPVersion{
upstream.HTTPVersion3,
upstream.HTTPVersion2,
upstream.HTTPVersion11,
}
}
// prepareUpstreamSettings - prepares upstream DNS server settings
func (s *Server) prepareUpstreamSettings() error {
// We're setting a customized set of RootCAs
@@ -353,12 +375,14 @@ func (s *Server) prepareUpstreamSettings() error {
upstreams = s.conf.UpstreamDNS
}
httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams)
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
upstreamConfig, err := proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
},
)
if err != nil {
@@ -371,8 +395,9 @@ func (s *Server) prepareUpstreamSettings() error {
uc, err = proxy.ParseUpstreamsConfig(
defaultDNS,
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
},
)
if err != nil {

View File

@@ -296,7 +296,7 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
values := []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"h2"}},
&dns.SVCBPort{Port: uint16(addr.Port)},
&dns.SVCBDoHPath{Template: "/dns-query?dns"},
&dns.SVCBDoHPath{Template: "/dns-query{?dns}"},
}
ans := &dns.SVCB{

View File

@@ -26,7 +26,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
Value: []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"h2"}},
&dns.SVCBPort{Port: 8044},
&dns.SVCBDoHPath{Template: "/dns-query?dns"},
&dns.SVCBDoHPath{Template: "/dns-query{?dns}"},
},
}

View File

@@ -67,10 +67,11 @@ func createTestServer(
ID: 0, Data: []byte(rules),
}}
f := filtering.New(filterConf, filters)
f, err := filtering.New(filterConf, filters)
require.NoError(t, err)
f.SetEnabled(true)
var err error
s, err = NewServer(DNSCreateParams{
DHCPServer: testDHCP,
DNSFilter: f,
@@ -774,7 +775,9 @@ func TestBlockedCustomIP(t *testing.T) {
Data: []byte(rules),
}}
f := filtering.New(&filtering.Config{}, filters)
f, err := filtering.New(&filtering.Config{}, filters)
require.NoError(t, err)
s, err := NewServer(DNSCreateParams{
DHCPServer: testDHCP,
DNSFilter: f,
@@ -906,7 +909,9 @@ func TestRewrite(t *testing.T) {
Type: dns.TypeCNAME,
}},
}
f := filtering.New(c, nil)
f, err := filtering.New(c, nil)
require.NoError(t, err)
f.SetEnabled(true)
s, err := NewServer(DNSCreateParams{
@@ -1021,19 +1026,14 @@ var testDHCP = &dhcpd.MockInterface{
OnWriteDiskConfig: func(c *dhcpd.ServerConfig) { panic("not implemented") },
}
// func (*testDHCP) Leases(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
// return []*dhcpd.Lease{{
// IP: net.IP{192, 168, 12, 34},
// HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
// Hostname: "myhost",
// }}
// }
func TestPTRResponseFromDHCPLeases(t *testing.T) {
const localDomain = "lan"
flt, err := filtering.New(&filtering.Config{}, nil)
require.NoError(t, err)
s, err := NewServer(DNSCreateParams{
DNSFilter: filtering.New(&filtering.Config{}, nil),
DNSFilter: flt,
DHCPServer: testDHCP,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
LocalDomain: localDomain,
@@ -1100,9 +1100,11 @@ func TestPTRResponseFromHosts(t *testing.T) {
assert.Equal(t, uint32(1), atomic.LoadUint32(&eventsCalledCounter))
})
flt := filtering.New(&filtering.Config{
flt, err := filtering.New(&filtering.Config{
EtcHosts: hc,
}, nil)
require.NoError(t, err)
flt.SetEnabled(true)
var s *Server

View File

@@ -151,7 +151,7 @@ func (s *Server) checkHostRules(host string, rrtype uint16, setts *filtering.Set
}
// filterDNSResponse checks each resource record of the response's answer
// section from pctx and returns a non-nil res if at least one of canonnical
// section from pctx and returns a non-nil res if at least one of canonical
// names or IP addresses in it matches the filtering rules.
func (s *Server) filterDNSResponse(
pctx *proxy.DNSContext,

View File

@@ -35,7 +35,8 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
ID: 0, Data: []byte(rules),
}}
f := filtering.New(&filtering.Config{}, filters)
f, err := filtering.New(&filtering.Config{}, filters)
require.NoError(t, err)
f.SetEnabled(true)
s, err := NewServer(DNSCreateParams{

View File

@@ -112,13 +112,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
DefautLocalPTRUpstreams: defLocalPTRUps,
}
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(resp); err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encoder: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
@@ -349,7 +343,10 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
conf, err = proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
&upstream.Options{
Bootstrap: []string{},
Timeout: DefaultTimeout,
},
)
if err != nil {
return nil, err
@@ -412,7 +409,15 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
return nil
}
var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"}
var protocols = []string{
"h3://",
"https://",
"quic://",
"sdns://",
"tcp://",
"tls://",
"udp://",
}
// validateUpstream returns an error if u alongside with domains is not a valid
// upstream configuration. useDefault is true if the upstream is
@@ -659,24 +664,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
result[host] = "OK"
}
jsonVal, err := json.Marshal(result)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to marshal status json: %s",
err,
)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
}
_ = aghhttp.WriteJSONResponse(w, r, result)
}
// handleDoH is the DNS-over-HTTPs handler.
@@ -692,11 +680,13 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
return
}
if !s.IsRunning() {
aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
return
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/netutil"
@@ -116,7 +117,8 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
s.conf = tc.conf()
s.handleGetConfig(w, nil)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
cType := w.Header().Get(aghhttp.HdrNameContentType)
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
assert.JSONEq(t, string(caseWant), w.Body.String())
})
}

View File

@@ -421,42 +421,39 @@ func initBlockedServices() {
}
// BlockedSvcKnown - return TRUE if a blocked service name is known
func BlockedSvcKnown(s string) bool {
_, ok := serviceRules[s]
func BlockedSvcKnown(s string) (ok bool) {
_, ok = serviceRules[s]
return ok
}
// ApplyBlockedServices - set blocked services settings for this DNS request
func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string, global bool) {
func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
setts.ServicesRules = []ServiceEntry{}
if global {
if list == nil {
d.confLock.RLock()
defer d.confLock.RUnlock()
list = d.Config.BlockedServices
}
for _, name := range list {
rules, ok := serviceRules[name]
if !ok {
log.Error("unknown service name: %s", name)
continue
}
s := ServiceEntry{}
s.Name = name
s.Rules = rules
setts.ServicesRules = append(setts.ServicesRules, s)
setts.ServicesRules = append(setts.ServicesRules, ServiceEntry{
Name: name,
Rules: rules,
})
}
}
func (d *DNSFilter) handleBlockedServicesAvailableServices(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(serviceIDs)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding available services: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, serviceIDs)
}
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
@@ -464,13 +461,7 @@ func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Req
list := d.Config.BlockedServices
d.confLock.RUnlock()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(list)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding services: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, list)
}
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
@@ -490,10 +481,3 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
d.ConfigModified()
}
// registerBlockedServicesHandlers - register HTTP handlers
func (d *DNSFilter) registerBlockedServicesHandlers() {
d.Config.HTTPRegister(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesAvailableServices)
d.Config.HTTPRegister(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
d.Config.HTTPRegister(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
}

View File

@@ -49,7 +49,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) {
|1.2.3.5.in-addr.arpa^$dnsrewrite=NOERROR;PTR;new-ptr-with-dot.
`
f := newForTest(t, nil, []Filter{{ID: 0, Data: []byte(text)}})
f, _ := newForTest(t, nil, []Filter{{ID: 0, Data: []byte(text)}})
setts := &Settings{
FilteringEnabled: true,
}

View File

@@ -1,4 +1,4 @@
package home
package filtering
import (
"bufio"
@@ -8,63 +8,29 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
"golang.org/x/exp/slices"
)
var nextFilterID = time.Now().Unix() // semi-stable way to generate an unique ID
// filterDir is the subdirectory of a data directory to store downloaded
// filters.
const filterDir = "filters"
// Filtering - module object
type Filtering struct {
// conf FilteringConf
refreshStatus uint32 // 0:none; 1:in progress
refreshLock sync.Mutex
filterTitleRegexp *regexp.Regexp
}
// nextFilterID is a way to seed a unique ID generation.
//
// TODO(e.burkov): Use more deterministic approach.
var nextFilterID = time.Now().Unix()
// Init - initialize the module
func (f *Filtering) Init() {
f.filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
_ = os.MkdirAll(filepath.Join(Context.getDataDir(), filterDir), 0o755)
f.loadFilters(config.Filters)
f.loadFilters(config.WhitelistFilters)
deduplicateFilters()
updateUniqueFilterID(config.Filters)
updateUniqueFilterID(config.WhitelistFilters)
}
// Start - start the module
func (f *Filtering) Start() {
f.RegisterFilteringHandlers()
// Here we should start updating filters,
// but currently we can't wake up the periodic task to do so.
// So for now we just start this periodic task from here.
go f.periodicallyRefreshFilters()
}
// Close - close the module
func (f *Filtering) Close() {
}
func defaultFilters() []filter {
return []filter{
{Filter: filtering.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard DNS filter"},
{Filter: filtering.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway Default Blocklist"},
}
}
// field ordering is important -- yaml fields will mirror ordering from here
type filter struct {
// FilterYAML respresents a filter list in the configuration file.
//
// TODO(e.burkov): Investigate if the field oredering is important.
type FilterYAML struct {
Enabled bool
URL string // URL or a file path
Name string `yaml:"name"`
@@ -73,91 +39,108 @@ type filter struct {
checksum uint32 // checksum of the file data
white bool
filtering.Filter `yaml:",inline"`
Filter `yaml:",inline"`
}
// Clear filter rules
func (filter *FilterYAML) unload() {
filter.RulesCount = 0
filter.checksum = 0
}
// Path to the filter contents
func (filter *FilterYAML) Path(dataDir string) string {
return filepath.Join(dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
}
const (
statusFound = 1
statusEnabledChanged = 2
statusURLChanged = 4
statusURLExists = 8
statusUpdateRequired = 0x10
statusFound = 1 << iota
statusEnabledChanged
statusURLChanged
statusURLExists
statusUpdateRequired
)
// Update properties for a filter specified by its URL
// Return status* flags.
func (f *Filtering) filterSetProperties(url string, newf filter, whitelist bool) int {
func (d *DNSFilter) filterSetProperties(url string, newf FilterYAML, whitelist bool) int {
r := 0
config.Lock()
defer config.Unlock()
d.filtersMu.Lock()
defer d.filtersMu.Unlock()
filters := &config.Filters
filters := d.Filters
if whitelist {
filters = &config.WhitelistFilters
filters = d.WhitelistFilters
}
for i := range *filters {
filt := &(*filters)[i]
if filt.URL != url {
continue
i := slices.IndexFunc(filters, func(filt FilterYAML) bool {
return filt.URL == url
})
if i == -1 {
return 0
}
filt := &filters[i]
log.Debug("filter: set properties: %s: {%s %s %v}", filt.URL, newf.Name, newf.URL, newf.Enabled)
filt.Name = newf.Name
if filt.URL != newf.URL {
r |= statusURLChanged | statusUpdateRequired
if d.filterExistsNoLock(newf.URL) {
return statusURLExists
}
log.Debug("filter: set properties: %s: {%s %s %v}",
filt.URL, newf.Name, newf.URL, newf.Enabled)
filt.Name = newf.Name
filt.URL = newf.URL
filt.unload()
filt.LastUpdated = time.Time{}
filt.checksum = 0
filt.RulesCount = 0
}
if filt.URL != newf.URL {
r |= statusURLChanged | statusUpdateRequired
if filterExistsNoLock(newf.URL) {
return statusURLExists
}
filt.URL = newf.URL
filt.unload()
filt.LastUpdated = time.Time{}
filt.checksum = 0
filt.RulesCount = 0
}
if filt.Enabled != newf.Enabled {
r |= statusEnabledChanged
filt.Enabled = newf.Enabled
if filt.Enabled {
if (r & statusURLChanged) == 0 {
err := d.load(filt)
if err != nil {
// TODO(e.burkov): It seems the error is only returned when
// the file exists and couldn't be open. Investigate and
// improve.
log.Error("loading filter %d: %s", filt.ID, err)
if filt.Enabled != newf.Enabled {
r |= statusEnabledChanged
filt.Enabled = newf.Enabled
if filt.Enabled {
if (r & statusURLChanged) == 0 {
e := f.load(filt)
if e != nil {
// This isn't a fatal error,
// because it may occur when someone removes the file from disk.
filt.LastUpdated = time.Time{}
filt.checksum = 0
filt.RulesCount = 0
r |= statusUpdateRequired
}
filt.LastUpdated = time.Time{}
filt.checksum = 0
filt.RulesCount = 0
r |= statusUpdateRequired
}
} else {
filt.unload()
}
} else {
filt.unload()
}
return r | statusFound
}
return 0
return r | statusFound
}
// Return TRUE if a filter with this URL exists
func filterExists(url string) bool {
config.RLock()
r := filterExistsNoLock(url)
config.RUnlock()
func (d *DNSFilter) filterExists(url string) bool {
d.filtersMu.RLock()
defer d.filtersMu.RUnlock()
r := d.filterExistsNoLock(url)
return r
}
func filterExistsNoLock(url string) bool {
for _, f := range config.Filters {
func (d *DNSFilter) filterExistsNoLock(url string) bool {
for _, f := range d.Filters {
if f.URL == url {
return true
}
}
for _, f := range config.WhitelistFilters {
for _, f := range d.WhitelistFilters {
if f.URL == url {
return true
}
@@ -167,26 +150,26 @@ func filterExistsNoLock(url string) bool {
// Add a filter
// Return FALSE if a filter with this URL exists
func filterAdd(f filter) bool {
config.Lock()
defer config.Unlock()
func (d *DNSFilter) filterAdd(flt FilterYAML) bool {
d.filtersMu.Lock()
defer d.filtersMu.Unlock()
// Check for duplicates
if filterExistsNoLock(f.URL) {
if d.filterExistsNoLock(flt.URL) {
return false
}
if f.white {
config.WhitelistFilters = append(config.WhitelistFilters, f)
if flt.white {
d.WhitelistFilters = append(d.WhitelistFilters, flt)
} else {
config.Filters = append(config.Filters, f)
d.Filters = append(d.Filters, flt)
}
return true
}
// Load filters from the disk
// And if any filter has zero ID, assign a new one
func (f *Filtering) loadFilters(array []filter) {
func (d *DNSFilter) loadFilters(array []FilterYAML) {
for i := range array {
filter := &array[i] // otherwise we're operating on a copy
if filter.ID == 0 {
@@ -198,32 +181,30 @@ func (f *Filtering) loadFilters(array []filter) {
continue
}
err := f.load(filter)
err := d.load(filter)
if err != nil {
log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
}
}
}
func deduplicateFilters() {
// 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++
func deduplicateFilters(filters []FilterYAML) (deduplicated []FilterYAML) {
urls := stringutil.NewSet()
lastIdx := 0
for _, filter := range filters {
if !urls.Has(filter.URL) {
urls.Add(filter.URL)
filters[lastIdx] = filter
lastIdx++
}
}
// all entries we want to keep are at front, delete the rest
config.Filters = config.Filters[:i]
return filters[:lastIdx]
}
// Set the next filter ID to max(filter.ID) + 1
func updateUniqueFilterID(filters []filter) {
func updateUniqueFilterID(filters []FilterYAML) {
for _, filter := range filters {
if nextFilterID < filter.ID {
nextFilterID = filter.ID + 1
@@ -238,22 +219,19 @@ func assignUniqueFilterID() int64 {
}
// Sets up a timer that will be checking for filters updates periodically
func (f *Filtering) periodicallyRefreshFilters() {
func (d *DNSFilter) periodicallyRefreshFilters() {
const maxInterval = 1 * 60 * 60
intval := 5 // use a dynamically increasing time interval
for {
isNetworkErr := false
if config.DNS.FiltersUpdateIntervalHours != 0 && atomic.CompareAndSwapUint32(&f.refreshStatus, 0, 1) {
f.refreshLock.Lock()
_, isNetworkErr = f.refreshFiltersIfNecessary(filterRefreshBlocklists | filterRefreshAllowlists)
f.refreshLock.Unlock()
f.refreshStatus = 0
if !isNetworkErr {
isNetErr, ok := false, false
if d.FiltersUpdateIntervalHours != 0 {
_, isNetErr, ok = d.tryRefreshFilters(true, true, false)
if ok && !isNetErr {
intval = maxInterval
}
}
if isNetworkErr {
if isNetErr {
intval *= 2
if intval > maxInterval {
intval = maxInterval
@@ -264,51 +242,73 @@ func (f *Filtering) periodicallyRefreshFilters() {
}
}
// Refresh filters
// flags: filterRefresh*
// important:
// tryRefreshFilters is like [refreshFilters], but backs down if the update is
// already going on.
//
// TRUE: ignore the fact that we're currently updating the filters
func (f *Filtering) refreshFilters(flags int, important bool) (int, error) {
set := atomic.CompareAndSwapUint32(&f.refreshStatus, 0, 1)
if !important && !set {
return 0, fmt.Errorf("filters update procedure is already running")
// TODO(e.burkov): Get rid of the concurrency pattern which requires the
// sync.Mutex.TryLock.
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
if ok = d.refreshLock.TryLock(); !ok {
return 0, false, ok
}
defer d.refreshLock.Unlock()
f.refreshLock.Lock()
nUpdated, _ := f.refreshFiltersIfNecessary(flags)
f.refreshLock.Unlock()
f.refreshStatus = 0
return nUpdated, nil
updated, isNetworkErr = d.refreshFiltersIntl(block, allow, force)
return updated, isNetworkErr, ok
}
func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []filter, []bool, bool) {
var updateFilters []filter
// refreshFilters updates the lists and returns the number of updated ones.
// It's safe for concurrent use, but blocks at least until the previous
// refreshing is finished.
func (d *DNSFilter) refreshFilters(block, allow, force bool) (updated int) {
d.refreshLock.Lock()
defer d.refreshLock.Unlock()
updated, _ = d.refreshFiltersIntl(block, allow, force)
return updated
}
// listsToUpdate returns the slice of filter lists that could be updated.
func (d *DNSFilter) listsToUpdate(filters *[]FilterYAML, force bool) (toUpd []FilterYAML) {
now := time.Now()
d.filtersMu.RLock()
defer d.filtersMu.RUnlock()
for i := range *filters {
flt := &(*filters)[i] // otherwise we will be operating on a copy
log.Debug("checking list at index %d: %v", i, flt)
if !flt.Enabled {
continue
}
if !force {
exp := flt.LastUpdated.Add(time.Duration(d.FiltersUpdateIntervalHours) * time.Hour)
if now.Before(exp) {
continue
}
}
toUpd = append(toUpd, FilterYAML{
Filter: Filter{
ID: flt.ID,
},
URL: flt.URL,
Name: flt.Name,
checksum: flt.checksum,
})
}
return toUpd
}
func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int, []FilterYAML, []bool, bool) {
var updateFlags []bool // 'true' if filter data has changed
now := time.Now()
config.RLock()
for i := range *filters {
f := &(*filters)[i] // otherwise we will be operating on a copy
if !f.Enabled {
continue
}
expireTime := f.LastUpdated.Unix() + int64(config.DNS.FiltersUpdateIntervalHours)*60*60
if !force && expireTime > now.Unix() {
continue
}
var uf filter
uf.ID = f.ID
uf.URL = f.URL
uf.Name = f.Name
uf.checksum = f.checksum
updateFilters = append(updateFilters, uf)
}
config.RUnlock()
updateFilters := d.listsToUpdate(filters, force)
if len(updateFilters) == 0 {
return 0, nil, nil, false
}
@@ -316,7 +316,7 @@ func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []f
nfail := 0
for i := range updateFilters {
uf := &updateFilters[i]
updated, err := f.update(uf)
updated, err := d.update(uf)
updateFlags = append(updateFlags, updated)
if err != nil {
nfail++
@@ -334,7 +334,7 @@ func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []f
uf := &updateFilters[i]
updated := updateFlags[i]
config.Lock()
d.filtersMu.Lock()
for k := range *filters {
f := &(*filters)[k]
if f.ID != uf.ID || f.URL != uf.URL {
@@ -352,20 +352,14 @@ func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []f
f.checksum = uf.checksum
updateCount++
}
config.Unlock()
d.filtersMu.Unlock()
}
return updateCount, updateFilters, updateFlags, false
}
const (
filterRefreshForce = 1 // ignore last file modification date
filterRefreshAllowlists = 2 // update allow-lists
filterRefreshBlocklists = 4 // update block-lists
)
// refreshFiltersIfNecessary checks filters and updates them if necessary. If
// force is true, it ignores the filter.LastUpdated field value.
// refreshFiltersIntl checks filters and updates them if necessary. If force is
// true, it ignores the filter.LastUpdated field value.
//
// Algorithm:
//
@@ -378,53 +372,49 @@ const (
// that this method works only on Unix systems. On Windows, don't pass
// files to filtering, pass the whole data.
//
// refreshFiltersIfNecessary returns the number of updated filters. It also
// returns true if there was a network error and nothing could be updated.
// refreshFiltersIntl returns the number of updated filters. It also returns
// true if there was a network error and nothing could be updated.
//
// TODO(a.garipov, e.burkov): What the hell?
func (f *Filtering) refreshFiltersIfNecessary(flags int) (int, bool) {
log.Debug("Filters: updating...")
func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
log.Debug("filtering: updating...")
updateCount := 0
var updateFilters []filter
var updateFlags []bool
netError := false
netErrorW := false
force := false
if (flags & filterRefreshForce) != 0 {
force = true
updNum := 0
var lists []FilterYAML
var toUpd []bool
isNetErr := false
if block {
updNum, lists, toUpd, isNetErr = d.refreshFiltersArray(&d.Filters, force)
}
if (flags & filterRefreshBlocklists) != 0 {
updateCount, updateFilters, updateFlags, netError = f.refreshFiltersArray(&config.Filters, force)
if allow {
updNumAl, listsAl, toUpdAl, isNetErrAl := d.refreshFiltersArray(&d.WhitelistFilters, force)
updNum += updNumAl
lists = append(lists, listsAl...)
toUpd = append(toUpd, toUpdAl...)
isNetErr = isNetErr || isNetErrAl
}
if (flags & filterRefreshAllowlists) != 0 {
updateCountW := 0
var updateFiltersW []filter
var updateFlagsW []bool
updateCountW, updateFiltersW, updateFlagsW, netErrorW = f.refreshFiltersArray(&config.WhitelistFilters, force)
updateCount += updateCountW
updateFilters = append(updateFilters, updateFiltersW...)
updateFlags = append(updateFlags, updateFlagsW...)
}
if netError && netErrorW {
if isNetErr {
return 0, true
}
if updateCount != 0 {
enableFilters(false)
if updNum != 0 {
d.EnableFilters(false)
for i := range updateFilters {
uf := &updateFilters[i]
updated := updateFlags[i]
for i := range lists {
uf := &lists[i]
updated := toUpd[i]
if !updated {
continue
}
_ = os.Remove(uf.Path() + ".old")
_ = os.Remove(uf.Path(d.DataDir) + ".old")
}
}
log.Debug("Filters: update finished")
return updateCount, false
log.Debug("filtering: update finished")
return updNum, false
}
// Allows printable UTF-8 text with CR, LF, TAB characters
@@ -440,7 +430,7 @@ func isPrintableText(data []byte, len int) bool {
}
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
func (f *Filtering) parseFilterContents(file io.Reader) (int, uint32, string) {
func (d *DNSFilter) parseFilterContents(file io.Reader) (int, uint32, string) {
rulesCount := 0
name := ""
seenTitle := false
@@ -455,7 +445,7 @@ func (f *Filtering) parseFilterContents(file io.Reader) (int, uint32, string) {
if len(line) == 0 {
//
} else if line[0] == '!' {
m := f.filterTitleRegexp.FindAllStringSubmatch(line, -1)
m := d.filterTitleRegexp.FindAllStringSubmatch(line, -1)
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
name = m[0][1]
seenTitle = true
@@ -476,11 +466,11 @@ func (f *Filtering) parseFilterContents(file io.Reader) (int, uint32, string) {
}
// Perform upgrade on a filter and update LastUpdated value
func (f *Filtering) update(filter *filter) (bool, error) {
b, err := f.updateIntl(filter)
func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
b, err := d.updateIntl(filter)
filter.LastUpdated = time.Now()
if !b {
e := os.Chtimes(filter.Path(), filter.LastUpdated, filter.LastUpdated)
e := os.Chtimes(filter.Path(d.DataDir), filter.LastUpdated, filter.LastUpdated)
if e != nil {
log.Error("os.Chtimes(): %v", e)
}
@@ -488,7 +478,7 @@ func (f *Filtering) update(filter *filter) (bool, error) {
return b, err
}
func (f *Filtering) read(reader io.Reader, tmpFile *os.File, filter *filter) (int, error) {
func (d *DNSFilter) read(reader io.Reader, tmpFile *os.File, filter *FilterYAML) (int, error) {
htmlTest := true
firstChunk := make([]byte, 4*1024)
firstChunkLen := 0
@@ -539,20 +529,20 @@ func (f *Filtering) read(reader io.Reader, tmpFile *os.File, filter *filter) (in
// finalizeUpdate closes and gets rid of temporary file f with filter's content
// according to updated. It also saves new values of flt's name, rules number
// and checksum if sucсeeded.
func finalizeUpdate(
f *os.File,
flt *filter,
func (d *DNSFilter) finalizeUpdate(
file *os.File,
flt *FilterYAML,
updated bool,
name string,
rnum int,
cs uint32,
) (err error) {
tmpFileName := f.Name()
tmpFileName := file.Name()
// Close the file before renaming it because it's required on Windows.
//
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
if err = f.Close(); err != nil {
if err = file.Close(); err != nil {
return fmt.Errorf("closing temporary file: %w", err)
}
@@ -562,9 +552,9 @@ func finalizeUpdate(
return os.Remove(tmpFileName)
}
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path())
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
if err = os.Rename(tmpFileName, flt.Path()); err != nil {
if err = os.Rename(tmpFileName, flt.Path(d.DataDir)); err != nil {
return errors.WithDeferred(err, os.Remove(tmpFileName))
}
@@ -578,12 +568,12 @@ func finalizeUpdate(
// processUpdate copies filter's content from src to dst and returns the name,
// rules number, and checksum for it. It also returns the number of bytes read
// from src.
func (f *Filtering) processUpdate(
func (d *DNSFilter) processUpdate(
src io.Reader,
dst *os.File,
flt *filter,
flt *FilterYAML,
) (name string, rnum int, cs uint32, n int, err error) {
if n, err = f.read(src, dst, flt); err != nil {
if n, err = d.read(src, dst, flt); err != nil {
return "", 0, 0, 0, err
}
@@ -591,14 +581,14 @@ func (f *Filtering) processUpdate(
return "", 0, 0, 0, err
}
rnum, cs, name = f.parseFilterContents(dst)
rnum, cs, name = d.parseFilterContents(dst)
return name, rnum, cs, n, nil
}
// updateIntl updates the flt rewriting it's actual file. It returns true if
// the actual update has been performed.
func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
log.Tracef("downloading update for filter %d from %s", flt.ID, flt.URL)
var name string
@@ -606,12 +596,12 @@ func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
var cs uint32
var tmpFile *os.File
tmpFile, err = os.CreateTemp(filepath.Join(Context.getDataDir(), filterDir), "")
tmpFile, err = os.CreateTemp(filepath.Join(d.DataDir, filterDir), "")
if err != nil {
return false, err
}
defer func() {
err = errors.WithDeferred(err, finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
ok = ok && err == nil
if ok {
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
@@ -638,7 +628,7 @@ func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
r = file
} else {
var resp *http.Response
resp, err = Context.client.Get(flt.URL)
resp, err = d.HTTPClient.Get(flt.URL)
if err != nil {
log.Printf("requesting filter from %s, skip: %s", flt.URL, err)
@@ -655,16 +645,16 @@ func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
r = resp.Body
}
name, rnum, cs, n, err = f.processUpdate(r, tmpFile, flt)
name, rnum, cs, n, err = d.processUpdate(r, tmpFile, flt)
return cs != flt.checksum, err
}
// loads filter contents from the file in dataDir
func (f *Filtering) load(filter *filter) (err error) {
filterFilePath := filter.Path()
func (d *DNSFilter) load(filter *FilterYAML) (err error) {
filterFilePath := filter.Path(d.DataDir)
log.Tracef("filtering: loading filter %d contents to: %s", filter.ID, filterFilePath)
log.Tracef("filtering: loading filter %d from %s", filter.ID, filterFilePath)
file, err := os.Open(filterFilePath)
if errors.Is(err, os.ErrNotExist) {
@@ -682,7 +672,7 @@ func (f *Filtering) load(filter *filter) (err error) {
log.Tracef("filtering: File %s, id %d, length %d", filterFilePath, filter.ID, st.Size())
rulesCount, checksum, _ := f.parseFilterContents(file)
rulesCount, checksum, _ := d.parseFilterContents(file)
filter.RulesCount = rulesCount
filter.checksum = checksum
@@ -691,56 +681,45 @@ func (f *Filtering) load(filter *filter) (err error) {
return nil
}
// Clear filter rules
func (filter *filter) unload() {
filter.RulesCount = 0
filter.checksum = 0
func (d *DNSFilter) EnableFilters(async bool) {
d.filtersMu.RLock()
defer d.filtersMu.RUnlock()
d.enableFiltersLocked(async)
}
// Path to the filter contents
func (filter *filter) Path() string {
return filepath.Join(Context.getDataDir(), filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
}
func enableFilters(async bool) {
config.RLock()
defer config.RUnlock()
enableFiltersLocked(async)
}
func enableFiltersLocked(async bool) {
filters := []filtering.Filter{{
ID: filtering.CustomListID,
Data: []byte(strings.Join(config.UserRules, "\n")),
func (d *DNSFilter) enableFiltersLocked(async bool) {
filters := []Filter{{
ID: CustomListID,
Data: []byte(strings.Join(d.UserRules, "\n")),
}}
for _, filter := range config.Filters {
for _, filter := range d.Filters {
if !filter.Enabled {
continue
}
filters = append(filters, filtering.Filter{
filters = append(filters, Filter{
ID: filter.ID,
FilePath: filter.Path(),
FilePath: filter.Path(d.DataDir),
})
}
var allowFilters []filtering.Filter
for _, filter := range config.WhitelistFilters {
var allowFilters []Filter
for _, filter := range d.WhitelistFilters {
if !filter.Enabled {
continue
}
allowFilters = append(allowFilters, filtering.Filter{
allowFilters = append(allowFilters, Filter{
ID: filter.ID,
FilePath: filter.Path(),
FilePath: filter.Path(d.DataDir),
})
}
if err := Context.dnsFilter.SetFilters(filters, allowFilters, async); err != nil {
if err := d.SetFilters(filters, allowFilters, async); err != nil {
log.Debug("enabling filters: %s", err)
}
Context.dnsFilter.SetEnabled(config.DNS.FilteringEnabled)
d.SetEnabled(d.FilteringEnabled)
}

View File

@@ -1,4 +1,4 @@
package home
package filtering
import (
"io/fs"
@@ -51,15 +51,17 @@ func TestFilters(t *testing.T) {
l := testStartFilterListener(t, &fltContent)
Context = homeContext{
workDir: t.TempDir(),
client: &http.Client{
tempDir := t.TempDir()
filters, err := New(&Config{
DataDir: tempDir,
HTTPClient: &http.Client{
Timeout: 5 * time.Second,
},
}
Context.filters.Init()
}, nil)
require.NoError(t, err)
f := &filter{
f := &FilterYAML{
URL: (&url.URL{
Scheme: "http",
Host: (&netutil.IPPort{
@@ -71,21 +73,22 @@ func TestFilters(t *testing.T) {
}
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
ok, err := Context.filters.update(f)
var ok bool
ok, err = filters.update(f)
require.NoError(t, err)
want(t, ok)
assert.Equal(t, wantRulesCount, f.RulesCount)
var dir []fs.DirEntry
dir, err = os.ReadDir(filepath.Join(Context.getDataDir(), filterDir))
dir, err = os.ReadDir(filepath.Join(tempDir, filterDir))
require.NoError(t, err)
assert.Len(t, dir, 1)
require.FileExists(t, f.Path())
require.FileExists(t, f.Path(tempDir))
err = Context.filters.load(f)
err = filters.load(f)
require.NoError(t, err)
}
@@ -105,11 +108,9 @@ func TestFilters(t *testing.T) {
})
t.Run("load_unload", func(t *testing.T) {
err := Context.filters.load(f)
err = filters.load(f)
require.NoError(t, err)
f.unload()
})
require.NoError(t, os.Remove(f.Path()))
}

View File

@@ -6,7 +6,10 @@ import (
"fmt"
"io/fs"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"strings"
@@ -24,6 +27,7 @@ import (
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"golang.org/x/exp/slices"
)
// The IDs of built-in filter lists.
@@ -69,8 +73,13 @@ type Config struct {
// enabled is used to be returned within Settings.
//
// It is of type uint32 to be accessed by atomic.
//
// TODO(e.burkov): Use atomic.Bool in Go 1.19.
enabled uint32
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
ParentalEnabled bool `yaml:"parental_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
@@ -98,6 +107,24 @@ type Config struct {
// CustomResolver is the resolver used by DNSFilter.
CustomResolver Resolver `yaml:"-"`
// HTTPClient is the client to use for updating the remote filters.
HTTPClient *http.Client `yaml:"-"`
// DataDir is used to store filters' contents.
DataDir string `yaml:"-"`
// filtersMu protects filter lists.
filtersMu *sync.RWMutex
// Filters are the blocking filter lists.
Filters []FilterYAML `yaml:"-"`
// WhitelistFilters are the allowing filter lists.
WhitelistFilters []FilterYAML `yaml:"-"`
// UserRules is the global list of custom rules.
UserRules []string `yaml:"-"`
}
// LookupStats store stats collected during safebrowsing or parental checks
@@ -128,11 +155,13 @@ type hostChecker struct {
// DNSFilter matches hostnames and DNS requests against filtering rules.
type DNSFilter struct {
rulesStorage *filterlist.RuleStorage
filteringEngine *urlfilter.DNSEngine
rulesStorage *filterlist.RuleStorage
filteringEngine *urlfilter.DNSEngine
rulesStorageAllow *filterlist.RuleStorage
filteringEngineAllow *urlfilter.DNSEngine
engineLock sync.RWMutex
engineLock sync.RWMutex
parentalServer string // access via methods
safeBrowsingServer string // access via methods
@@ -156,6 +185,12 @@ type DNSFilter struct {
// TODO(e.burkov): Use upstream that configured in dnsforward instead.
resolver Resolver
refreshLock *sync.Mutex
// filterTitleRegexp is the regular expression to retrieve a name of a
// filter list.
filterTitleRegexp *regexp.Regexp
hostCheckers []hostChecker
}
@@ -168,7 +203,7 @@ type Filter struct {
Data []byte `yaml:"-"`
// ID is automatically assigned when filter is added using nextFilterID.
ID int64
ID int64 `yaml:"id"`
}
// Reason holds an enum detailing why it was filtered or not filtered
@@ -245,15 +280,7 @@ func (r Reason) String() string {
}
// In returns true if reasons include r.
func (r Reason) In(reasons ...Reason) (ok bool) {
for _, reason := range reasons {
if r == reason {
return true
}
}
return false
}
func (r Reason) In(reasons ...Reason) (ok bool) { return slices.Contains(reasons, r) }
// SetEnabled sets the status of the *DNSFilter.
func (d *DNSFilter) SetEnabled(enabled bool) {
@@ -261,6 +288,7 @@ func (d *DNSFilter) SetEnabled(enabled bool) {
if enabled {
i = 1
}
atomic.StoreUint32(&d.enabled, uint32(i))
}
@@ -279,11 +307,20 @@ func (d *DNSFilter) GetConfig() (s Settings) {
// WriteDiskConfig - write configuration
func (d *DNSFilter) WriteDiskConfig(c *Config) {
d.confLock.Lock()
defer d.confLock.Unlock()
func() {
d.confLock.Lock()
defer d.confLock.Unlock()
*c = d.Config
c.Rewrites = cloneRewrites(c.Rewrites)
*c = d.Config
c.Rewrites = cloneRewrites(c.Rewrites)
}()
d.filtersMu.RLock()
defer d.filtersMu.RUnlock()
c.Filters = slices.Clone(d.Filters)
c.WhitelistFilters = slices.Clone(d.WhitelistFilters)
c.UserRules = slices.Clone(d.UserRules)
}
// cloneRewrites returns a deep copy of entries.
@@ -309,6 +346,8 @@ func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool)
}
d.filtersInitializerLock.Lock() // prevent multiple writers from adding more than 1 task
defer d.filtersInitializerLock.Unlock()
// remove all pending tasks
stop := false
for !stop {
@@ -321,7 +360,6 @@ func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool)
}
d.filtersInitializerChan <- params
d.filtersInitializerLock.Unlock()
return nil
}
@@ -350,22 +388,19 @@ func (d *DNSFilter) filtersInitializer() {
func (d *DNSFilter) Close() {
d.engineLock.Lock()
defer d.engineLock.Unlock()
d.reset()
}
func (d *DNSFilter) reset() {
var err error
if d.rulesStorage != nil {
err = d.rulesStorage.Close()
if err != nil {
if err := d.rulesStorage.Close(); err != nil {
log.Error("filtering: rulesStorage.Close: %s", err)
}
}
if d.rulesStorageAllow != nil {
err = d.rulesStorageAllow.Close()
if err != nil {
if err := d.rulesStorageAllow.Close(); err != nil {
log.Error("filtering: rulesStorageAllow.Close: %s", err)
}
}
@@ -885,29 +920,30 @@ func InitModule() {
initBlockedServices()
}
// New creates properly initialized DNS Filter that is ready to be used.
func New(c *Config, blockFilters []Filter) (d *DNSFilter) {
// New creates properly initialized DNS Filter that is ready to be used. c must
// be non-nil.
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d = &DNSFilter{
resolver: net.DefaultResolver,
resolver: net.DefaultResolver,
refreshLock: &sync.Mutex{},
filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`),
}
if c != nil {
d.safebrowsingCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.SafeBrowsingCacheSize,
})
d.safeSearchCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.SafeSearchCacheSize,
})
d.parentalCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.ParentalCacheSize,
})
d.safebrowsingCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.SafeBrowsingCacheSize,
})
d.safeSearchCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.SafeSearchCacheSize,
})
d.parentalCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.ParentalCacheSize,
})
if c.CustomResolver != nil {
d.resolver = c.CustomResolver
}
if r := c.CustomResolver; r != nil {
d.resolver = r
}
d.hostCheckers = []hostChecker{{
@@ -930,27 +966,26 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter) {
name: "safe search",
}}
err := d.initSecurityServices()
if err != nil {
log.Error("filtering: initialize services: %s", err)
defer func() { err = errors.Annotate(err, "filtering: %w") }()
return nil
err = d.initSecurityServices()
if err != nil {
return nil, fmt.Errorf("initializing services: %s", err)
}
if c != nil {
d.Config = *c
err = d.prepareRewrites()
if err != nil {
log.Error("rewrites: preparing: %s", err)
d.Config = *c
d.filtersMu = &sync.RWMutex{}
return nil
}
err = d.prepareRewrites()
if err != nil {
return nil, fmt.Errorf("rewrites: preparing: %s", err)
}
bsvcs := []string{}
for _, s := range d.BlockedServices {
if !BlockedSvcKnown(s) {
log.Debug("skipping unknown blocked-service %q", s)
continue
}
bsvcs = append(bsvcs, s)
@@ -960,13 +995,24 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter) {
if blockFilters != nil {
err = d.initFiltering(nil, blockFilters)
if err != nil {
log.Error("Can't initialize filtering subsystem: %s", err)
d.Close()
return nil
return nil, fmt.Errorf("initializing filtering subsystem: %s", err)
}
}
return d
_ = os.MkdirAll(filepath.Join(d.DataDir, filterDir), 0o755)
d.loadFilters(d.Filters)
d.loadFilters(d.WhitelistFilters)
d.Filters = deduplicateFilters(d.Filters)
d.WhitelistFilters = deduplicateFilters(d.WhitelistFilters)
updateUniqueFilterID(d.Filters)
updateUniqueFilterID(d.WhitelistFilters)
return d, nil
}
// Start - start the module:
@@ -976,9 +1022,10 @@ func (d *DNSFilter) Start() {
d.filtersInitializerChan = make(chan filtersInitializerParams, 1)
go d.filtersInitializer()
if d.Config.HTTPRegister != nil { // for tests
d.registerSecurityHandlers()
d.registerRewritesHandlers()
d.registerBlockedServicesHandlers()
}
d.RegisterFilteringHandlers()
// Here we should start updating filters,
// but currently we can't wake up the periodic task to do so.
// So for now we just start this periodic task from here.
go d.periodicallyRefreshFilters()
}

View File

@@ -26,10 +26,6 @@ const (
pcBlocked = "pornhub.com"
)
var setts = Settings{
ProtectionEnabled: true,
}
// Helpers.
func purgeCaches(d *DNSFilter) {
@@ -44,8 +40,8 @@ func purgeCaches(d *DNSFilter) {
}
}
func newForTest(t testing.TB, c *Config, filters []Filter) *DNSFilter {
setts = Settings{
func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts *Settings) {
setts = &Settings{
ProtectionEnabled: true,
FilteringEnabled: true,
}
@@ -57,26 +53,31 @@ func newForTest(t testing.TB, c *Config, filters []Filter) *DNSFilter {
setts.SafeSearchEnabled = c.SafeSearchEnabled
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
setts.ParentalEnabled = c.ParentalEnabled
} else {
// It must not be nil.
c = &Config{}
}
d := New(c, filters)
purgeCaches(d)
f, err := New(c, filters)
require.NoError(t, err)
return d
purgeCaches(f)
return f, setts
}
func (d *DNSFilter) checkMatch(t *testing.T, hostname string) {
func (d *DNSFilter) checkMatch(t *testing.T, hostname string, setts *Settings) {
t.Helper()
res, err := d.CheckHost(hostname, dns.TypeA, &setts)
res, err := d.CheckHost(hostname, dns.TypeA, setts)
require.NoErrorf(t, err, "host %q", hostname)
assert.Truef(t, res.IsFiltered, "host %q", hostname)
}
func (d *DNSFilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16) {
func (d *DNSFilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16, setts *Settings) {
t.Helper()
res, err := d.CheckHost(hostname, qtype, &setts)
res, err := d.CheckHost(hostname, qtype, setts)
require.NoErrorf(t, err, "host %q", hostname, err)
require.NotEmpty(t, res.Rules, "host %q", hostname)
@@ -88,10 +89,10 @@ func (d *DNSFilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16
assert.Equalf(t, ip, r.IP.String(), "host %q", hostname)
}
func (d *DNSFilter) checkMatchEmpty(t *testing.T, hostname string) {
func (d *DNSFilter) checkMatchEmpty(t *testing.T, hostname string, setts *Settings) {
t.Helper()
res, err := d.CheckHost(hostname, dns.TypeA, &setts)
res, err := d.CheckHost(hostname, dns.TypeA, setts)
require.NoErrorf(t, err, "host %q", hostname)
assert.Falsef(t, res.IsFiltered, "host %q", hostname)
@@ -111,19 +112,19 @@ func TestEtcHostsMatching(t *testing.T) {
filters := []Filter{{
ID: 0, Data: []byte(text),
}}
d := newForTest(t, nil, filters)
d, setts := newForTest(t, nil, filters)
t.Cleanup(d.Close)
d.checkMatchIP(t, "google.com", addr, dns.TypeA)
d.checkMatchIP(t, "www.google.com", addr, dns.TypeA)
d.checkMatchEmpty(t, "subdomain.google.com")
d.checkMatchEmpty(t, "example.org")
d.checkMatchIP(t, "google.com", addr, dns.TypeA, setts)
d.checkMatchIP(t, "www.google.com", addr, dns.TypeA, setts)
d.checkMatchEmpty(t, "subdomain.google.com", setts)
d.checkMatchEmpty(t, "example.org", setts)
// IPv4 match.
d.checkMatchIP(t, "block.com", "0.0.0.0", dns.TypeA)
d.checkMatchIP(t, "block.com", "0.0.0.0", dns.TypeA, setts)
// Empty IPv6.
res, err := d.CheckHost("block.com", dns.TypeAAAA, &setts)
res, err := d.CheckHost("block.com", dns.TypeAAAA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
@@ -134,10 +135,10 @@ func TestEtcHostsMatching(t *testing.T) {
assert.Empty(t, res.Rules[0].IP)
// IPv6 match.
d.checkMatchIP(t, "ipv6.com", addr6, dns.TypeAAAA)
d.checkMatchIP(t, "ipv6.com", addr6, dns.TypeAAAA, setts)
// Empty IPv4.
res, err = d.CheckHost("ipv6.com", dns.TypeA, &setts)
res, err = d.CheckHost("ipv6.com", dns.TypeA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
@@ -148,7 +149,7 @@ func TestEtcHostsMatching(t *testing.T) {
assert.Empty(t, res.Rules[0].IP)
// Two IPv4, both must be returned.
res, err = d.CheckHost("host2", dns.TypeA, &setts)
res, err = d.CheckHost("host2", dns.TypeA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
@@ -159,7 +160,7 @@ func TestEtcHostsMatching(t *testing.T) {
assert.Equal(t, res.Rules[1].IP, net.IP{0, 0, 0, 2})
// One IPv6 address.
res, err = d.CheckHost("host2", dns.TypeAAAA, &setts)
res, err = d.CheckHost("host2", dns.TypeAAAA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
@@ -176,27 +177,27 @@ func TestSafeBrowsing(t *testing.T) {
aghtest.ReplaceLogWriter(t, logOutput)
aghtest.ReplaceLogLevel(t, log.DEBUG)
d := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
d, setts := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
d.checkMatch(t, sbBlocked)
d.checkMatch(t, sbBlocked, setts)
require.Contains(t, logOutput.String(), fmt.Sprintf("safebrowsing lookup for %q", sbBlocked))
d.checkMatch(t, "test."+sbBlocked)
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, pcBlocked)
d.checkMatch(t, "test."+sbBlocked, setts)
d.checkMatchEmpty(t, "yandex.ru", setts)
d.checkMatchEmpty(t, pcBlocked, setts)
// Cached result.
d.safeBrowsingServer = "127.0.0.1"
d.checkMatch(t, sbBlocked)
d.checkMatchEmpty(t, pcBlocked)
d.checkMatch(t, sbBlocked, setts)
d.checkMatchEmpty(t, pcBlocked, setts)
d.safeBrowsingServer = defaultSafebrowsingServer
}
func TestParallelSB(t *testing.T) {
d := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
d, setts := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
@@ -205,10 +206,10 @@ func TestParallelSB(t *testing.T) {
for i := 0; i < 100; i++ {
t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) {
t.Parallel()
d.checkMatch(t, sbBlocked)
d.checkMatch(t, "test."+sbBlocked)
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, pcBlocked)
d.checkMatch(t, sbBlocked, setts)
d.checkMatch(t, "test."+sbBlocked, setts)
d.checkMatchEmpty(t, "yandex.ru", setts)
d.checkMatchEmpty(t, pcBlocked, setts)
})
}
})
@@ -217,7 +218,7 @@ func TestParallelSB(t *testing.T) {
// Safe Search.
func TestSafeSearch(t *testing.T) {
d := newForTest(t, &Config{SafeSearchEnabled: true}, nil)
d, _ := newForTest(t, &Config{SafeSearchEnabled: true}, nil)
t.Cleanup(d.Close)
val, ok := d.SafeSearchDomain("www.google.com")
require.True(t, ok)
@@ -226,7 +227,7 @@ func TestSafeSearch(t *testing.T) {
}
func TestCheckHostSafeSearchYandex(t *testing.T) {
d := newForTest(t, &Config{
d, setts := newForTest(t, &Config{
SafeSearchEnabled: true,
}, nil)
t.Cleanup(d.Close)
@@ -243,7 +244,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
"www.yandex.com",
} {
t.Run(strings.ToLower(host), func(t *testing.T) {
res, err := d.CheckHost(host, dns.TypeA, &setts)
res, err := d.CheckHost(host, dns.TypeA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
@@ -258,7 +259,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
func TestCheckHostSafeSearchGoogle(t *testing.T) {
resolver := &aghtest.TestResolver{}
d := newForTest(t, &Config{
d, setts := newForTest(t, &Config{
SafeSearchEnabled: true,
CustomResolver: resolver,
}, nil)
@@ -277,7 +278,7 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
"www.google.je",
} {
t.Run(host, func(t *testing.T) {
res, err := d.CheckHost(host, dns.TypeA, &setts)
res, err := d.CheckHost(host, dns.TypeA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
@@ -291,12 +292,12 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
}
func TestSafeSearchCacheYandex(t *testing.T) {
d := newForTest(t, nil, nil)
d, setts := newForTest(t, nil, nil)
t.Cleanup(d.Close)
const domain = "yandex.ru"
// Check host with disabled safesearch.
res, err := d.CheckHost(domain, dns.TypeA, &setts)
res, err := d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
@@ -305,10 +306,10 @@ func TestSafeSearchCacheYandex(t *testing.T) {
yandexIP := net.IPv4(213, 180, 193, 56)
d = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
d, setts = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
t.Cleanup(d.Close)
res, err = d.CheckHost(domain, dns.TypeA, &setts)
res, err = d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
// For yandex we already know valid IP.
@@ -325,20 +326,20 @@ func TestSafeSearchCacheYandex(t *testing.T) {
func TestSafeSearchCacheGoogle(t *testing.T) {
resolver := &aghtest.TestResolver{}
d := newForTest(t, &Config{
d, setts := newForTest(t, &Config{
CustomResolver: resolver,
}, nil)
t.Cleanup(d.Close)
const domain = "www.google.ru"
res, err := d.CheckHost(domain, dns.TypeA, &setts)
res, err := d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
require.Empty(t, res.Rules)
d = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
d, setts = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
t.Cleanup(d.Close)
d.resolver = resolver
@@ -358,7 +359,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
}
}
res, err = d.CheckHost(domain, dns.TypeA, &setts)
res, err = d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
require.Len(t, res.Rules, 1)
@@ -379,22 +380,22 @@ func TestParentalControl(t *testing.T) {
aghtest.ReplaceLogWriter(t, logOutput)
aghtest.ReplaceLogLevel(t, log.DEBUG)
d := newForTest(t, &Config{ParentalEnabled: true}, nil)
d, setts := newForTest(t, &Config{ParentalEnabled: true}, nil)
t.Cleanup(d.Close)
d.SetParentalUpstream(aghtest.NewBlockUpstream(pcBlocked, true))
d.checkMatch(t, pcBlocked)
d.checkMatch(t, pcBlocked, setts)
require.Contains(t, logOutput.String(), fmt.Sprintf("parental lookup for %q", pcBlocked))
d.checkMatch(t, "www."+pcBlocked)
d.checkMatchEmpty(t, "www.yandex.ru")
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, "api.jquery.com")
d.checkMatch(t, "www."+pcBlocked, setts)
d.checkMatchEmpty(t, "www.yandex.ru", setts)
d.checkMatchEmpty(t, "yandex.ru", setts)
d.checkMatchEmpty(t, "api.jquery.com", setts)
// Test cached result.
d.parentalServer = "127.0.0.1"
d.checkMatch(t, pcBlocked)
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatch(t, pcBlocked, setts)
d.checkMatchEmpty(t, "yandex.ru", setts)
}
// Filtering.
@@ -679,10 +680,10 @@ func TestMatching(t *testing.T) {
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s-%s", tc.name, tc.host), func(t *testing.T) {
filters := []Filter{{ID: 0, Data: []byte(tc.rules)}}
d := newForTest(t, nil, filters)
d, setts := newForTest(t, nil, filters)
t.Cleanup(d.Close)
res, err := d.CheckHost(tc.host, tc.wantDNSType, &setts)
res, err := d.CheckHost(tc.host, tc.wantDNSType, setts)
require.NoError(t, err)
assert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, "Hostname %s has wrong result (%v must be %v)", tc.host, res.IsFiltered, tc.wantIsFiltered)
@@ -705,7 +706,7 @@ func TestWhitelist(t *testing.T) {
whiteFilters := []Filter{{
ID: 0, Data: []byte(whiteRules),
}}
d := newForTest(t, nil, filters)
d, setts := newForTest(t, nil, filters)
err := d.SetFilters(filters, whiteFilters, false)
require.NoError(t, err)
@@ -713,7 +714,7 @@ func TestWhitelist(t *testing.T) {
t.Cleanup(d.Close)
// Matched by white filter.
res, err := d.CheckHost("host1", dns.TypeA, &setts)
res, err := d.CheckHost("host1", dns.TypeA, setts)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
@@ -724,7 +725,7 @@ func TestWhitelist(t *testing.T) {
assert.Equal(t, "||host1^", res.Rules[0].Text)
// Not matched by white filter, but matched by block filter.
res, err = d.CheckHost("host2", dns.TypeA, &setts)
res, err = d.CheckHost("host2", dns.TypeA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
@@ -750,7 +751,7 @@ func applyClientSettings(setts *Settings) {
}
func TestClientSettings(t *testing.T) {
d := newForTest(t,
d, setts := newForTest(t,
&Config{
ParentalEnabled: true,
SafeBrowsingEnabled: false,
@@ -796,7 +797,7 @@ func TestClientSettings(t *testing.T) {
return func(t *testing.T) {
t.Helper()
r, err := d.CheckHost(tc.host, dns.TypeA, &setts)
r, err := d.CheckHost(tc.host, dns.TypeA, setts)
require.NoError(t, err)
if before {
@@ -814,7 +815,7 @@ func TestClientSettings(t *testing.T) {
t.Run(tc.name, makeTester(tc, tc.before))
}
applyClientSettings(&setts)
applyClientSettings(setts)
for _, tc := range testCases {
t.Run(tc.name, makeTester(tc, !tc.before))
@@ -824,13 +825,13 @@ func TestClientSettings(t *testing.T) {
// Benchmarks.
func BenchmarkSafeBrowsing(b *testing.B) {
d := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil)
d, setts := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil)
b.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
for n := 0; n < b.N; n++ {
res, err := d.CheckHost(sbBlocked, dns.TypeA, &setts)
res, err := d.CheckHost(sbBlocked, dns.TypeA, setts)
require.NoError(b, err)
assert.Truef(b, res.IsFiltered, "expected hostname %q to match", sbBlocked)
@@ -838,14 +839,14 @@ func BenchmarkSafeBrowsing(b *testing.B) {
}
func BenchmarkSafeBrowsingParallel(b *testing.B) {
d := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil)
d, setts := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil)
b.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
res, err := d.CheckHost(sbBlocked, dns.TypeA, &setts)
res, err := d.CheckHost(sbBlocked, dns.TypeA, setts)
require.NoError(b, err)
assert.Truef(b, res.IsFiltered, "expected hostname %q to match", sbBlocked)
@@ -854,7 +855,7 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
}
func BenchmarkSafeSearch(b *testing.B) {
d := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
d, _ := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
b.Cleanup(d.Close)
for n := 0; n < b.N; n++ {
val, ok := d.SafeSearchDomain("www.google.com")
@@ -865,7 +866,7 @@ func BenchmarkSafeSearch(b *testing.B) {
}
func BenchmarkSafeSearchParallel(b *testing.B) {
d := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
d, _ := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
b.Cleanup(d.Close)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {

View File

@@ -1,15 +1,13 @@
package home
package filtering
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -34,7 +32,7 @@ func validateFilterURL(urlStr string) (err error) {
return fmt.Errorf("checking filter url: %w", err)
}
if s := url.Scheme; s != schemeHTTP && s != schemeHTTPS {
if s := url.Scheme; s != aghhttp.SchemeHTTP && s != aghhttp.SchemeHTTPS {
return fmt.Errorf("checking filter url: invalid scheme %q", s)
}
@@ -47,7 +45,7 @@ type filterAddJSON struct {
Whitelist bool `json:"whitelist"`
}
func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
fj := filterAddJSON{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil {
@@ -65,14 +63,14 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
}
// Check for duplicates
if filterExists(fj.URL) {
if d.filterExists(fj.URL) {
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
return
}
// Set necessary properties
filt := filter{
filt := FilterYAML{
Enabled: true,
URL: fj.URL,
Name: fj.Name,
@@ -81,7 +79,7 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
filt.ID = assignUniqueFilterID()
// Download the filter contents
ok, err := f.update(&filt)
ok, err := d.update(&filt)
if err != nil {
aghhttp.Error(
r,
@@ -109,14 +107,14 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
// URL is assumed valid so append it to filters, update config, write new
// file and reload it to engines.
if !filterAdd(filt) {
if !d.filterAdd(filt) {
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", filt.URL)
return
}
onConfigModified()
enableFilters(true)
d.ConfigModified()
d.EnableFilters(true)
_, err = fmt.Fprintf(w, "OK %d rules\n", filt.RulesCount)
if err != nil {
@@ -124,7 +122,7 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
}
}
func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
func (d *DNSFilter) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
type request struct {
URL string `json:"url"`
Whitelist bool `json:"whitelist"`
@@ -138,23 +136,23 @@ func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Requ
return
}
config.Lock()
filters := &config.Filters
d.filtersMu.Lock()
filters := &d.Filters
if req.Whitelist {
filters = &config.WhitelistFilters
filters = &d.WhitelistFilters
}
var deleted filter
var newFilters []filter
for _, f := range *filters {
if f.URL != req.URL {
newFilters = append(newFilters, f)
var deleted FilterYAML
var newFilters []FilterYAML
for _, flt := range *filters {
if flt.URL != req.URL {
newFilters = append(newFilters, flt)
continue
}
deleted = f
path := f.Path()
deleted = flt
path := flt.Path(d.DataDir)
err = os.Rename(path, path+".old")
if err != nil {
log.Error("deleting filter %q: %s", path, err)
@@ -162,10 +160,10 @@ func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Requ
}
*filters = newFilters
config.Unlock()
d.filtersMu.Unlock()
onConfigModified()
enableFilters(true)
d.ConfigModified()
d.EnableFilters(true)
// NOTE: The old files "filter.txt.old" aren't deleted. It's not really
// necessary, but will require the additional complicated code to run
@@ -191,55 +189,51 @@ type filterURLReq struct {
Whitelist bool `json:"whitelist"`
}
func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
func (d *DNSFilter) handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
fj := filterURLReq{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
aghhttp.Error(r, w, http.StatusBadRequest, "decoding request: %s", err)
return
}
if fj.Data == nil {
err = errors.Error("data cannot be null")
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
aghhttp.Error(r, w, http.StatusBadRequest, "%s", errors.Error("data is absent"))
return
}
err = validateFilterURL(fj.Data.URL)
if err != nil {
err = fmt.Errorf("invalid url: %s", err)
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
aghhttp.Error(r, w, http.StatusBadRequest, "invalid url: %s", err)
return
}
filt := filter{
filt := FilterYAML{
Enabled: fj.Data.Enabled,
Name: fj.Data.Name,
URL: fj.Data.URL,
}
status := f.filterSetProperties(fj.URL, filt, fj.Whitelist)
status := d.filterSetProperties(fj.URL, filt, fj.Whitelist)
if (status & statusFound) == 0 {
http.Error(w, "URL doesn't exist", http.StatusBadRequest)
aghhttp.Error(r, w, http.StatusBadRequest, "URL doesn't exist")
return
}
if (status & statusURLExists) != 0 {
http.Error(w, "URL already exists", http.StatusBadRequest)
aghhttp.Error(r, w, http.StatusBadRequest, "URL already exists")
return
}
onConfigModified()
d.ConfigModified()
restart := (status & statusEnabledChanged) != 0
if (status&statusUpdateRequired) != 0 && fj.Data.Enabled {
// download new filter and apply its rules
flags := filterRefreshBlocklists
if fj.Whitelist {
flags = filterRefreshAllowlists
}
nUpdated, _ := f.refreshFilters(flags, true)
// download new filter and apply its rules.
nUpdated := d.refreshFilters(!fj.Whitelist, fj.Whitelist, false)
// if at least 1 filter has been updated, refreshFilters() restarts the filtering automatically
// if not - we restart the filtering ourselves
restart = false
@@ -249,25 +243,34 @@ func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request
}
if restart {
enableFilters(true)
d.EnableFilters(true)
}
}
func (f *Filtering) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
// This use of ReadAll is safe, because request's body is now limited.
body, err := io.ReadAll(r.Body)
// filteringRulesReq is the JSON structure for settings custom filtering rules.
type filteringRulesReq struct {
Rules []string `json:"rules"`
}
func (d *DNSFilter) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
if aghhttp.WriteTextPlainDeprecated(w, r) {
return
}
req := &filteringRulesReq{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
return
}
config.UserRules = strings.Split(string(body), "\n")
onConfigModified()
enableFilters(true)
d.UserRules = req.Rules
d.ConfigModified()
d.EnableFilters(true)
}
func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
type Req struct {
White bool `json:"whitelist"`
}
@@ -285,35 +288,20 @@ func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
return
}
flags := filterRefreshBlocklists
if req.White {
flags = filterRefreshAllowlists
}
func() {
// Temporarily unlock the Context.controlLock because the
// f.refreshFilters waits for it to be unlocked but it's
// actually locked in ensure wrapper.
//
// TODO(e.burkov): Reconsider this messy syncing process.
Context.controlLock.Unlock()
defer Context.controlLock.Lock()
resp.Updated, err = f.refreshFilters(flags|filterRefreshForce, false)
}()
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
var ok bool
resp.Updated, _, ok = d.tryRefreshFilters(!req.White, req.White, true)
if !ok {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"filters update procedure is already running",
)
return
}
js, err := json.Marshal(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
type filterJSON struct {
@@ -333,7 +321,7 @@ type filteringConfig struct {
Enabled bool `json:"enabled"`
}
func filterToJSON(f filter) filterJSON {
func filterToJSON(f FilterYAML) filterJSON {
fj := filterJSON{
ID: f.ID,
Enabled: f.Enabled,
@@ -350,37 +338,27 @@ func filterToJSON(f filter) filterJSON {
}
// Get filtering configuration
func (f *Filtering) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
resp := filteringConfig{}
config.RLock()
resp.Enabled = config.DNS.FilteringEnabled
resp.Interval = config.DNS.FiltersUpdateIntervalHours
for _, f := range config.Filters {
d.filtersMu.RLock()
resp.Enabled = d.FilteringEnabled
resp.Interval = d.FiltersUpdateIntervalHours
for _, f := range d.Filters {
fj := filterToJSON(f)
resp.Filters = append(resp.Filters, fj)
}
for _, f := range config.WhitelistFilters {
for _, f := range d.WhitelistFilters {
fj := filterToJSON(f)
resp.WhitelistFilters = append(resp.WhitelistFilters, fj)
}
resp.UserRules = config.UserRules
config.RUnlock()
resp.UserRules = d.UserRules
d.filtersMu.RUnlock()
jsonVal, err := json.Marshal(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// Set filtering configuration
func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
func (d *DNSFilter) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
req := filteringConfig{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -389,22 +367,22 @@ func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request
return
}
if !checkFiltersUpdateIntervalHours(req.Interval) {
if !ValidateUpdateIvl(req.Interval) {
aghhttp.Error(r, w, http.StatusBadRequest, "Unsupported interval")
return
}
func() {
config.Lock()
defer config.Unlock()
d.filtersMu.Lock()
defer d.filtersMu.Unlock()
config.DNS.FilteringEnabled = req.Enabled
config.DNS.FiltersUpdateIntervalHours = req.Interval
d.FilteringEnabled = req.Enabled
d.FiltersUpdateIntervalHours = req.Interval
}()
onConfigModified()
enableFilters(true)
d.ConfigModified()
d.EnableFilters(true)
}
type checkHostRespRule struct {
@@ -435,15 +413,15 @@ type checkHostResp struct {
FilterID int64 `json:"filter_id"`
}
func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
host := q.Get("name")
func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
host := r.URL.Query().Get("name")
setts := Context.dnsFilter.GetConfig()
setts := d.GetConfig()
setts.FilteringEnabled = true
setts.ProtectionEnabled = true
Context.dnsFilter.ApplyBlockedServices(&setts, nil, true)
result, err := Context.dnsFilter.CheckHost(host, dns.TypeA, &setts)
d.ApplyBlockedServices(&setts, nil)
result, err := d.CheckHost(host, dns.TypeA, &setts)
if err != nil {
aghhttp.Error(
r,
@@ -457,18 +435,20 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
return
}
resp := checkHostResp{}
resp.Reason = result.Reason.String()
resp.SvcName = result.ServiceName
resp.CanonName = result.CanonName
resp.IPList = result.IPList
rulesLen := len(result.Rules)
resp := checkHostResp{
Reason: result.Reason.String(),
SvcName: result.ServiceName,
CanonName: result.CanonName,
IPList: result.IPList,
Rules: make([]*checkHostRespRule, len(result.Rules)),
}
if len(result.Rules) > 0 {
if rulesLen > 0 {
resp.FilterID = result.Rules[0].FilterListID
resp.Rule = result.Rules[0].Text
}
resp.Rules = make([]*checkHostRespRule, len(result.Rules))
for i, r := range result.Rules {
resp.Rules[i] = &checkHostRespRule{
FilterListID: r.FilterListID,
@@ -476,28 +456,47 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
}
}
js, err := json.Marshal(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// RegisterFilteringHandlers - register handlers
func (f *Filtering) RegisterFilteringHandlers() {
httpRegister(http.MethodGet, "/control/filtering/status", f.handleFilteringStatus)
httpRegister(http.MethodPost, "/control/filtering/config", f.handleFilteringConfig)
httpRegister(http.MethodPost, "/control/filtering/add_url", f.handleFilteringAddURL)
httpRegister(http.MethodPost, "/control/filtering/remove_url", f.handleFilteringRemoveURL)
httpRegister(http.MethodPost, "/control/filtering/set_url", f.handleFilteringSetURL)
httpRegister(http.MethodPost, "/control/filtering/refresh", f.handleFilteringRefresh)
httpRegister(http.MethodPost, "/control/filtering/set_rules", f.handleFilteringSetRules)
httpRegister(http.MethodGet, "/control/filtering/check_host", f.handleCheckHost)
func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP := d.HTTPRegister
if registerHTTP == nil {
return
}
registerHTTP(http.MethodPost, "/control/safebrowsing/enable", d.handleSafeBrowsingEnable)
registerHTTP(http.MethodPost, "/control/safebrowsing/disable", d.handleSafeBrowsingDisable)
registerHTTP(http.MethodGet, "/control/safebrowsing/status", d.handleSafeBrowsingStatus)
registerHTTP(http.MethodPost, "/control/parental/enable", d.handleParentalEnable)
registerHTTP(http.MethodPost, "/control/parental/disable", d.handleParentalDisable)
registerHTTP(http.MethodGet, "/control/parental/status", d.handleParentalStatus)
registerHTTP(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable)
registerHTTP(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable)
registerHTTP(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus)
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
registerHTTP(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesAvailableServices)
registerHTTP(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
registerHTTP(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
registerHTTP(http.MethodGet, "/control/filtering/status", d.handleFilteringStatus)
registerHTTP(http.MethodPost, "/control/filtering/config", d.handleFilteringConfig)
registerHTTP(http.MethodPost, "/control/filtering/add_url", d.handleFilteringAddURL)
registerHTTP(http.MethodPost, "/control/filtering/remove_url", d.handleFilteringRemoveURL)
registerHTTP(http.MethodPost, "/control/filtering/set_url", d.handleFilteringSetURL)
registerHTTP(http.MethodPost, "/control/filtering/refresh", d.handleFilteringRefresh)
registerHTTP(http.MethodPost, "/control/filtering/set_rules", d.handleFilteringSetRules)
registerHTTP(http.MethodGet, "/control/filtering/check_host", d.handleCheckHost)
}
func checkFiltersUpdateIntervalHours(i uint32) bool {
// ValidateUpdateIvl returns false if i is not a valid filters update interval.
func ValidateUpdateIvl(i uint32) bool {
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
}

View File

@@ -133,34 +133,31 @@ func matchDomainWildcard(host, wildcard string) (ok bool) {
// 1. A and AAAA > CNAME
// 2. wildcard > exact
// 3. lower level wildcard > higher level wildcard
//
// TODO(a.garipov): Replace with slices.Sort.
type rewritesSorted []*LegacyRewrite
// Len implements the sort.Interface interface for legacyRewritesSorted.
// Len implements the sort.Interface interface for rewritesSorted.
func (a rewritesSorted) Len() (l int) { return len(a) }
// Swap implements the sort.Interface interface for legacyRewritesSorted.
// Swap implements the sort.Interface interface for rewritesSorted.
func (a rewritesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// Less implements the sort.Interface interface for legacyRewritesSorted.
// Less implements the sort.Interface interface for rewritesSorted.
func (a rewritesSorted) Less(i, j int) (less bool) {
if a[i].Type == dns.TypeCNAME && a[j].Type != dns.TypeCNAME {
ith, jth := a[i], a[j]
if ith.Type == dns.TypeCNAME && jth.Type != dns.TypeCNAME {
return true
} else if a[i].Type != dns.TypeCNAME && a[j].Type == dns.TypeCNAME {
} else if ith.Type != dns.TypeCNAME && jth.Type == dns.TypeCNAME {
return false
}
if isWildcard(a[i].Domain) {
if !isWildcard(a[j].Domain) {
return false
}
} else {
if isWildcard(a[j].Domain) {
return true
}
if iw, jw := isWildcard(ith.Domain), isWildcard(jth.Domain); iw != jw {
return jw
}
// Both are wildcards.
return len(a[i].Domain) > len(a[j].Domain)
// Both are either wildcards or not.
return len(ith.Domain) > len(jth.Domain)
}
// prepareRewrites normalizes and validates all legacy DNS rewrites.
@@ -243,13 +240,7 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
}
d.confLock.Unlock()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(arr)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encode: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, arr)
}
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
@@ -313,9 +304,3 @@ func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request)
d.Config.ConfigModified()
}
func (d *DNSFilter) registerRewritesHandlers() {
d.Config.HTTPRegister(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
d.Config.HTTPRegister(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
d.Config.HTTPRegister(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
}

View File

@@ -12,7 +12,7 @@ import (
// TODO(e.burkov): All the tests in this file may and should me merged together.
func TestRewrites(t *testing.T) {
d := newForTest(t, nil, nil)
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
d.Rewrites = []*LegacyRewrite{{
@@ -188,7 +188,7 @@ func TestRewrites(t *testing.T) {
}
func TestRewritesLevels(t *testing.T) {
d := newForTest(t, nil, nil)
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
// Exact host, wildcard L2, wildcard L3.
d.Rewrites = []*LegacyRewrite{{
@@ -235,7 +235,7 @@ func TestRewritesLevels(t *testing.T) {
}
func TestRewritesExceptionCNAME(t *testing.T) {
d := newForTest(t, nil, nil)
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
// Wildcard and exception for a sub-domain.
d.Rewrites = []*LegacyRewrite{{
@@ -286,7 +286,7 @@ func TestRewritesExceptionCNAME(t *testing.T) {
}
func TestRewritesExceptionIP(t *testing.T) {
d := newForTest(t, nil, nil)
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
// Exception for AAAA record.
d.Rewrites = []*LegacyRewrite{{

View File

@@ -5,7 +5,6 @@ import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
@@ -381,17 +380,13 @@ func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Req
}
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(&struct {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: d.Config.SafeBrowsingEnabled,
})
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
@@ -405,27 +400,11 @@ func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request
}
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(&struct {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: d.Config.ParentalEnabled,
})
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
}
}
func (d *DNSFilter) registerSecurityHandlers() {
d.Config.HTTPRegister(http.MethodPost, "/control/safebrowsing/enable", d.handleSafeBrowsingEnable)
d.Config.HTTPRegister(http.MethodPost, "/control/safebrowsing/disable", d.handleSafeBrowsingDisable)
d.Config.HTTPRegister(http.MethodGet, "/control/safebrowsing/status", d.handleSafeBrowsingStatus)
d.Config.HTTPRegister(http.MethodPost, "/control/parental/enable", d.handleParentalEnable)
d.Config.HTTPRegister(http.MethodPost, "/control/parental/disable", d.handleParentalDisable)
d.Config.HTTPRegister(http.MethodGet, "/control/parental/status", d.handleParentalStatus)
d.Config.HTTPRegister(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable)
d.Config.HTTPRegister(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable)
d.Config.HTTPRegister(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus)
_ = aghhttp.WriteJSONResponse(w, r, resp)
}

View File

@@ -107,7 +107,7 @@ func TestSafeBrowsingCache(t *testing.T) {
}
func TestSBPC_checkErrorUpstream(t *testing.T) {
d := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
d, _ := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
ups := aghtest.NewErrorUpstream()
@@ -128,7 +128,7 @@ func TestSBPC_checkErrorUpstream(t *testing.T) {
}
func TestSBPC(t *testing.T) {
d := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
d, _ := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
const hostname = "example.org"

View File

@@ -5,7 +5,6 @@ import (
"context"
"encoding/binary"
"encoding/gob"
"encoding/json"
"fmt"
"net"
"net/http"
@@ -146,21 +145,13 @@ func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Reque
}
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(&struct {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: d.Config.SafeSearchEnabled,
})
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to write response json: %s",
err,
)
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
var safeSearchDomains = map[string]string{

View File

@@ -8,12 +8,14 @@ import (
"fmt"
"net"
"net/http"
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -32,7 +34,8 @@ const sessionTokenSize = 16
type session struct {
userName string
expire uint32 // expiration time (in seconds)
// expire is the expiration time, in seconds.
expire uint32
}
func (s *session) serialize() []byte {
@@ -64,29 +67,29 @@ func (s *session) deserialize(data []byte) bool {
// Auth - global object
type Auth struct {
db *bbolt.DB
blocker *authRateLimiter
sessions map[string]*session
users []User
lock sync.Mutex
sessionTTL uint32
db *bbolt.DB
raleLimiter *authRateLimiter
sessions map[string]*session
users []webUser
lock sync.Mutex
sessionTTL uint32
}
// User object
type User struct {
// webUser represents a user of the Web UI.
type webUser struct {
Name string `yaml:"name"`
PasswordHash string `yaml:"password"` // bcrypt hash
PasswordHash string `yaml:"password"`
}
// InitAuth - create a global object
func InitAuth(dbFilename string, users []User, sessionTTL uint32, blocker *authRateLimiter) *Auth {
func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter *authRateLimiter) *Auth {
log.Info("Initializing auth module: %s", dbFilename)
a := &Auth{
sessionTTL: sessionTTL,
blocker: blocker,
sessions: make(map[string]*session),
users: users,
sessionTTL: sessionTTL,
raleLimiter: rateLimiter,
sessions: make(map[string]*session),
users: users,
}
var err error
a.db, err = bbolt.Open(dbFilename, 0o644, nil)
@@ -326,35 +329,25 @@ func newSessionToken() (data []byte, err error) {
return randData, nil
}
// cookieTimeFormat is the format to be used in (time.Time).Format for cookie's
// expiry field.
const cookieTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
// cookieExpiryFormat returns the formatted exp to be used in cookie string.
// It's quite simple for now, but probably will be expanded in the future.
func cookieExpiryFormat(exp time.Time) (formatted string) {
return exp.Format(cookieTimeFormat)
}
func (a *Auth) httpCookie(req loginJSON, addr string) (cookie string, err error) {
blocker := a.blocker
u := a.UserFind(req.Name, req.Password)
if len(u.Name) == 0 {
if blocker != nil {
blocker.inc(addr)
// newCookie creates a new authentication cookie.
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) {
rateLimiter := a.raleLimiter
u, ok := a.findUser(req.Name, req.Password)
if !ok {
if rateLimiter != nil {
rateLimiter.inc(addr)
}
return "", err
return nil, errors.Error("invalid username or password")
}
if blocker != nil {
blocker.remove(addr)
if rateLimiter != nil {
rateLimiter.remove(addr)
}
var sess []byte
sess, err = newSessionToken()
sess, err := newSessionToken()
if err != nil {
return "", err
return nil, fmt.Errorf("generating token: %w", err)
}
now := time.Now().UTC()
@@ -364,11 +357,15 @@ func (a *Auth) httpCookie(req loginJSON, addr string) (cookie string, err error)
expire: uint32(now.Unix()) + a.sessionTTL,
})
return fmt.Sprintf(
"%s=%s; Path=/; HttpOnly; Expires=%s",
sessionCookieName, hex.EncodeToString(sess),
cookieExpiryFormat(now.Add(cookieTTL)),
), nil
return &http.Cookie{
Name: sessionCookieName,
Value: hex.EncodeToString(sess),
Path: "/",
Expires: now.Add(cookieTTL),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
// realIP extracts the real IP address of the client from an HTTP request using
@@ -436,8 +433,8 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return
}
if blocker := Context.auth.blocker; blocker != nil {
if left := blocker.check(remoteAddr); left > 0 {
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
if left := rateLimiter.check(remoteAddr); left > 0 {
w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds())))
aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left)
@@ -445,10 +442,9 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
}
}
var cookie string
cookie, err = Context.auth.httpCookie(req, remoteAddr)
cookie, err := Context.auth.newCookie(req, remoteAddr)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "crypto rand reader: %s", err)
aghhttp.Error(r, w, http.StatusForbidden, "%s", err)
return
}
@@ -462,20 +458,11 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
log.Error("auth: unknown ip")
}
if len(cookie) == 0 {
log.Info("auth: failed to login user %q from ip %v", req.Name, ip)
time.Sleep(1 * time.Second)
http.Error(w, "invalid username or password", http.StatusBadRequest)
return
}
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
http.SetCookie(w, cookie)
h := w.Header()
h.Set("Set-Cookie", cookie)
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
h.Set("Pragma", "no-cache")
h.Set("Expires", "0")
@@ -484,17 +471,31 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie := r.Header.Get("Cookie")
sess := parseCookie(cookie)
respHdr := w.Header()
c, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// The user is already logged out.
respHdr.Set("Location", "/login.html")
w.WriteHeader(http.StatusFound)
Context.auth.RemoveSession(sess)
return
}
w.Header().Set("Location", "/login.html")
Context.auth.RemoveSession(c.Value)
s := fmt.Sprintf("%s=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT",
sessionCookieName)
w.Header().Set("Set-Cookie", s)
c = &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
respHdr.Set("Location", "/login.html")
respHdr.Set("Set-Cookie", c.String())
w.WriteHeader(http.StatusFound)
}
@@ -504,101 +505,108 @@ func RegisterAuthHandlers() {
httpRegister(http.MethodGet, "/control/logout", handleLogout)
}
func parseCookie(cookie string) string {
pairs := strings.Split(cookie, ";")
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
continue
}
if kv[0] == sessionCookieName {
return kv[1]
}
}
return ""
}
// optionalAuthThird return true if user should authenticate first.
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (authFirst bool) {
authFirst = false
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
if glProcessCookie(r) {
log.Debug("auth: authentication is handled by GL-Inet submodule")
return false
}
// redirect to login page if not authenticated
ok := false
isAuthenticated := false
cookie, err := r.Cookie(sessionCookieName)
if glProcessCookie(r) {
log.Debug("auth: authentication was handled by GL-Inet submodule")
ok = true
} else if err == nil {
r := Context.auth.checkSession(cookie.Value)
if r == checkSessionOK {
ok = true
} else if r < 0 {
log.Debug("auth: invalid cookie value: %s", cookie)
}
} else {
// there's no Cookie, check Basic authentication
user, pass, ok2 := r.BasicAuth()
if ok2 {
u := Context.auth.UserFind(user, pass)
if len(u.Name) != 0 {
ok = true
} else {
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// Check Basic authentication.
user, pass, hasBasic := r.BasicAuth()
if hasBasic {
_, isAuthenticated = Context.auth.findUser(user, pass)
if !isAuthenticated {
log.Info("auth: invalid Basic Authorization value")
}
}
}
if !ok {
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
if glProcessRedirect(w, r) {
log.Debug("auth: redirected to login page by GL-Inet submodule")
} else {
w.Header().Set("Location", "/login.html")
w.WriteHeader(http.StatusFound)
}
} else {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("Forbidden"))
} else {
res := Context.auth.checkSession(cookie.Value)
isAuthenticated = res == checkSessionOK
if !isAuthenticated {
log.Debug("auth: invalid cookie value: %s", cookie)
}
authFirst = true
}
return authFirst
if isAuthenticated {
return false
}
if p := r.URL.Path; p == "/" || p == "/index.html" {
if glProcessRedirect(w, r) {
log.Debug("auth: redirected to login page by GL-Inet submodule")
} else {
log.Debug("auth: redirected to login page")
w.Header().Set("Location", "/login.html")
w.WriteHeader(http.StatusFound)
}
} else {
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("Forbidden"))
}
return true
}
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the
// project.
func optionalAuth(
h func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/login.html" {
// redirect to dashboard if already authenticated
authRequired := Context.auth != nil && Context.auth.AuthRequired()
p := r.URL.Path
authRequired := Context.auth != nil && Context.auth.AuthRequired()
if p == "/login.html" {
cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil {
r := Context.auth.checkSession(cookie.Value)
if r == checkSessionOK {
// Redirect to the dashboard if already authenticated.
res := Context.auth.checkSession(cookie.Value)
if res == checkSessionOK {
w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound)
return
} else if r == checkSessionNotFound {
log.Debug("auth: invalid cookie value: %s", cookie)
}
}
} else if strings.HasPrefix(r.URL.Path, "/assets/") ||
strings.HasPrefix(r.URL.Path, "/login.") {
// process as usual
// no additional auth requirements
} else if Context.auth != nil && Context.auth.AuthRequired() {
log.Debug("auth: invalid cookie value: %s", cookie)
}
} else if isPublicResource(p) {
// Process as usual, no additional auth requirements.
} else if authRequired {
if optionalAuthThird(w, r) {
return
}
}
handler(w, r)
h(w, r)
}
}
// isPublicResource returns true if p is a path to a public resource.
func isPublicResource(p string) (ok bool) {
isAsset, err := path.Match("/assets/*", p)
if err != nil {
// The only error that is returned from path.Match is
// [path.ErrBadPattern]. This is a programmer error.
panic(fmt.Errorf("bad asset pattern: %w", err))
}
isLogin, err := path.Match("/login.*", p)
if err != nil {
// Same as above.
panic(fmt.Errorf("bad login pattern: %w", err))
}
return isAsset || isLogin
}
type authHandler struct {
handler http.Handler
}
@@ -612,7 +620,7 @@ func optionalAuthHandler(handler http.Handler) http.Handler {
}
// UserAdd - add new user
func (a *Auth) UserAdd(u *User, password string) {
func (a *Auth) UserAdd(u *webUser, password string) {
if len(password) == 0 {
return
}
@@ -631,31 +639,35 @@ func (a *Auth) UserAdd(u *User, password string) {
log.Debug("auth: added user: %s", u.Name)
}
// UserFind - find a user
func (a *Auth) UserFind(login, password string) User {
// findUser returns a user if there is one.
func (a *Auth) findUser(login, password string) (u webUser, ok bool) {
a.lock.Lock()
defer a.lock.Unlock()
for _, u := range a.users {
for _, u = range a.users {
if u.Name == login &&
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
return u
return u, true
}
}
return User{}
return webUser{}, false
}
// getCurrentUser returns the current user. It returns an empty User if the
// user is not found.
func (a *Auth) getCurrentUser(r *http.Request) User {
func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
// There's no Cookie, check Basic authentication.
user, pass, ok := r.BasicAuth()
if ok {
return Context.auth.UserFind(user, pass)
u, _ = Context.auth.findUser(user, pass)
return u
}
return User{}
return webUser{}
}
a.lock.Lock()
@@ -663,20 +675,20 @@ func (a *Auth) getCurrentUser(r *http.Request) User {
s, ok := a.sessions[cookie.Value]
if !ok {
return User{}
return webUser{}
}
for _, u := range a.users {
for _, u = range a.users {
if u.Name == s.userName {
return u
}
}
return User{}
return webUser{}
}
// GetUsers - get users
func (a *Auth) GetUsers() []User {
func (a *Auth) GetUsers() []webUser {
a.lock.Lock()
users := a.users
a.lock.Unlock()

View File

@@ -12,16 +12,11 @@ import (
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
aghtest.DiscardLogOutput(m)
}
func TestNewSessionToken(t *testing.T) {
// Successful case.
token, err := newSessionToken()
@@ -43,14 +38,14 @@ func TestAuth(t *testing.T) {
dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db")
users := []User{{
users := []webUser{{
Name: "name",
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}}
a := InitAuth(fn, nil, 60, nil)
s := session{}
user := User{Name: "name"}
user := webUser{Name: "name"}
a.UserAdd(&user, "password")
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
@@ -84,7 +79,8 @@ func TestAuth(t *testing.T) {
a.storeSession(sess, &s)
a.Close()
u := a.UserFind("name", "password")
u, ok := a.findUser("name", "password")
assert.True(t, ok)
assert.NotEmpty(t, u.Name)
time.Sleep(3 * time.Second)
@@ -118,7 +114,7 @@ func TestAuthHTTP(t *testing.T) {
dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db")
users := []User{
users := []webUser{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
}
Context.auth = InitAuth(fn, users, 60, nil)
@@ -150,18 +146,19 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled)
// perform login
cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}, "")
cookie, err := Context.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "")
require.NoError(t, err)
assert.NotEmpty(t, cookie)
require.NotNil(t, cookie)
// get /
handler2 = optionalAuth(handler)
w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie)
r.Header.Set("Cookie", cookie.String())
r.URL = &url.URL{Path: "/"}
handlerCalled = false
handler2(&w, &r)
assert.True(t, handlerCalled)
r.Header.Del("Cookie")
// get / with basic auth
@@ -177,7 +174,7 @@ func TestAuthHTTP(t *testing.T) {
// get login page with a valid cookie - we're redirected to /
handler2 = optionalAuth(handler)
w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie)
r.Header.Set("Cookie", cookie.String())
r.URL = &url.URL{Path: loginURL}
handlerCalled = false
handler2(&w, &r)

View File

@@ -456,8 +456,9 @@ func (clients *clientsContainer) findUpstreams(
conf, err = proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: config.DNS.BootstrapDNS,
Timeout: config.DNS.UpstreamTimeout.Duration,
Bootstrap: config.DNS.BootstrapDNS,
Timeout: config.DNS.UpstreamTimeout.Duration,
HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams),
},
)
if err != nil {

View File

@@ -93,13 +93,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
data.Tags = clientTags
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data)
if e != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "failed to encode to json: %v", e)
return
}
_ = aghhttp.WriteJSONResponse(w, r, data)
}
// Convert JSON object to Client object
@@ -249,11 +243,7 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
})
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write response: %s", err)
}
_ = aghhttp.WriteJSONResponse(w, r, data)
}
// findRuntime looks up the IP in runtime and temporary storages, like

View File

@@ -14,7 +14,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/dnsproxy/fastip"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
@@ -23,10 +22,9 @@ import (
yaml "gopkg.in/yaml.v3"
)
const (
dataDir = "data" // data storage
filterDir = "filters" // cache location for downloaded filters, it's under DataDir
)
// dataDir is the name of a directory under the working one to store some
// persistent data.
const dataDir = "data"
// logSettings are the logging settings part of the configuration file.
//
@@ -87,10 +85,10 @@ type configuration struct {
// It's reset after config is parsed
fileData []byte
BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
Users []User `yaml:"users"` // Users that can access HTTP server
BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
Users []webUser `yaml:"users"` // Users that can access HTTP server
// AuthAttempts is the maximum number of failed login attempts a user
// can do before being blocked.
AuthAttempts uint `yaml:"auth_attempts"`
@@ -108,9 +106,16 @@ type configuration struct {
DNS dnsConfig `yaml:"dns"`
TLS tlsConfigSettings `yaml:"tls"`
Filters []filter `yaml:"filters"`
WhitelistFilters []filter `yaml:"whitelist_filters"`
UserRules []string `yaml:"user_rules"`
// Filters reflects the filters from [filtering.Config]. It's cloned to the
// config used in the filtering module at the startup. Afterwards it's
// cloned from the filtering module back here.
//
// TODO(e.burkov): Move all the filtering configuration fields into the
// only configuration subsection covering the changes with a single
// migration.
Filters []filtering.FilterYAML `yaml:"filters"`
WhitelistFilters []filtering.FilterYAML `yaml:"whitelist_filters"`
UserRules []string `yaml:"user_rules"`
DHCP *dhcpd.ServerConfig `yaml:"dhcp"`
@@ -145,9 +150,7 @@ type dnsConfig struct {
dnsforward.FilteringConfig `yaml:",inline"`
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
DnsfilterConf filtering.Config `yaml:",inline"`
DnsfilterConf *filtering.Config `yaml:",inline"`
// UpstreamTimeout is the timeout for querying upstream servers.
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
@@ -163,6 +166,19 @@ type dnsConfig struct {
// LocalPTRResolvers is the slice of addresses to be used as upstreams
// for PTR queries for locally-served networks.
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
//
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
// experimental.
ServeHTTP3 bool `yaml:"serve_http3"`
// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
// upstreams.
//
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
// experimental.
UseHTTP3Upstreams bool `yaml:"use_http3_upstreams"`
}
type tlsConfigSettings struct {
@@ -193,15 +209,20 @@ type tlsConfigSettings struct {
//
// TODO(a.garipov, e.burkov): This global is awful and must be removed.
var config = &configuration{
BindPort: 3000,
BetaBindPort: 0,
BindHost: net.IP{0, 0, 0, 0},
AuthAttempts: 5,
AuthBlockMin: 15,
BindPort: 3000,
BetaBindPort: 0,
BindHost: net.IP{0, 0, 0, 0},
AuthAttempts: 5,
AuthBlockMin: 15,
WebSessionTTLHours: 30 * 24,
DNS: dnsConfig{
BindHosts: []net.IP{{0, 0, 0, 0}},
Port: defaultPortDNS,
StatsInterval: 1,
BindHosts: []net.IP{{0, 0, 0, 0}},
Port: defaultPortDNS,
StatsInterval: 1,
QueryLogEnabled: true,
QueryLogFileEnabled: true,
QueryLogInterval: timeutil.Duration{Duration: 90 * timeutil.Day},
QueryLogMemSize: 1000,
FilteringConfig: dnsforward.FilteringConfig{
ProtectionEnabled: true, // whether or not use any of filtering features
BlockingMode: dnsforward.BlockingModeDefault,
@@ -222,18 +243,42 @@ var config = &configuration{
// was later increased to 300 due to https://github.com/AdguardTeam/AdGuardHome/issues/2257
MaxGoroutines: 300,
},
FilteringEnabled: true, // whether or not use filter lists
FiltersUpdateIntervalHours: 24,
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
UsePrivateRDNS: true,
DnsfilterConf: &filtering.Config{
SafeBrowsingCacheSize: 1 * 1024 * 1024,
SafeSearchCacheSize: 1 * 1024 * 1024,
ParentalCacheSize: 1 * 1024 * 1024,
CacheTime: 30,
FilteringEnabled: true,
FiltersUpdateIntervalHours: 24,
},
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
UsePrivateRDNS: true,
},
TLS: tlsConfigSettings{
PortHTTPS: defaultPortHTTPS,
PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy
PortDNSOverQUIC: defaultPortQUIC,
},
Filters: []filtering.FilterYAML{{
Filter: filtering.Filter{ID: 1},
Enabled: true,
URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt",
Name: "AdGuard DNS filter",
}, {
Filter: filtering.Filter{ID: 2},
Enabled: false,
URL: "https://adaway.org/hosts.txt",
Name: "AdAway Default Blocklist",
}},
DHCP: &dhcpd.ServerConfig{
LocalDomainName: "lan",
Conf4: dhcpd.V4ServerConf{
LeaseDuration: dhcpd.DefaultDHCPLeaseTTL,
ICMPTimeout: dhcpd.DefaultDHCPTimeoutICMP,
},
Conf6: dhcpd.V6ServerConf{
LeaseDuration: dhcpd.DefaultDHCPLeaseTTL,
},
},
Clients: &clientsConfig{
Sources: &clientSourcesConf{
@@ -255,31 +300,6 @@ var config = &configuration{
SchemaVersion: currentSchemaVersion,
}
// initConfig initializes default configuration for the current OS&ARCH
func initConfig() {
config.WebSessionTTLHours = 30 * 24
config.DNS.QueryLogEnabled = true
config.DNS.QueryLogFileEnabled = true
config.DNS.QueryLogInterval = timeutil.Duration{Duration: 90 * timeutil.Day}
config.DNS.QueryLogMemSize = 1000
config.DNS.CacheSize = 4 * 1024 * 1024
config.DNS.DnsfilterConf.SafeBrowsingCacheSize = 1 * 1024 * 1024
config.DNS.DnsfilterConf.SafeSearchCacheSize = 1 * 1024 * 1024
config.DNS.DnsfilterConf.ParentalCacheSize = 1 * 1024 * 1024
config.DNS.DnsfilterConf.CacheTime = 30
config.Filters = defaultFilters()
config.DHCP.Conf4.LeaseDuration = dhcpd.DefaultDHCPLeaseTTL
config.DHCP.Conf4.ICMPTimeout = dhcpd.DefaultDHCPTimeoutICMP
config.DHCP.Conf6.LeaseDuration = dhcpd.DefaultDHCPLeaseTTL
if ch := version.Channel(); ch == version.ChannelEdge || ch == version.ChannelDevelopment {
config.BetaBindPort = 3001
}
}
// getConfigFilename returns path to the current config file
func (c *configuration) getConfigFilename() string {
configFile, err := filepath.EvalSymlinks(Context.configFilename)
@@ -348,8 +368,8 @@ func parseConfig() (err error) {
return fmt.Errorf("validating udp ports: %w", err)
}
if !checkFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) {
config.DNS.FiltersUpdateIntervalHours = 24
if !filtering.ValidateUpdateIvl(config.DNS.DnsfilterConf.FiltersUpdateIntervalHours) {
config.DNS.DnsfilterConf.FiltersUpdateIntervalHours = 24
}
if config.DNS.UpstreamTimeout.Duration == 0 {
@@ -418,10 +438,11 @@ func (c *configuration) write() (err error) {
config.DNS.AnonymizeClientIP = dc.AnonymizeClientIP
}
if Context.dnsFilter != nil {
c := filtering.Config{}
Context.dnsFilter.WriteDiskConfig(&c)
config.DNS.DnsfilterConf = c
if Context.filters != nil {
Context.filters.WriteDiskConfig(config.DNS.DnsfilterConf)
config.Filters = config.DNS.DnsfilterConf.Filters
config.WhitelistFilters = config.DNS.DnsfilterConf.WhitelistFilters
config.UserRules = config.DNS.DnsfilterConf.UserRules
}
if s := Context.dnsServer; s != nil {

View File

@@ -1,13 +1,13 @@
package home
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"runtime"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
@@ -97,16 +97,16 @@ func collectDNSAddresses() (addrs []string, err error) {
// statusResponse is a response for /control/status endpoint.
type statusResponse struct {
Version string `json:"version"`
Language string `json:"language"`
DNSAddrs []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
IsProtectionEnabled bool `json:"protection_enabled"`
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
// openapi.yaml declares.
IsDHCPAvailable bool `json:"dhcp_available"`
IsRunning bool `json:"running"`
Version string `json:"version"`
Language string `json:"language"`
IsDHCPAvailable bool `json:"dhcp_available"`
IsRunning bool `json:"running"`
}
func handleStatus(w http.ResponseWriter, r *http.Request) {
@@ -125,12 +125,12 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
defer config.RUnlock()
resp = statusResponse{
Version: version.Version(),
DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port,
HTTPPort: config.BindPort,
IsRunning: isRunning(),
Version: version.Version(),
Language: config.Language,
IsRunning: isRunning(),
}
}()
@@ -146,13 +146,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
resp.IsDHCPAvailable = Context.dhcpServer != nil
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
type profileJSON struct {
@@ -160,16 +154,12 @@ type profileJSON struct {
}
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
pj := profileJSON{}
u := Context.auth.getCurrentUser(r)
pj.Name = u.Name
data, err := json.Marshal(pj)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
return
resp := &profileJSON{
Name: u.Name,
}
_, _ = w.Write(data)
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// ------------------------
@@ -199,19 +189,29 @@ func httpRegister(method, url string, handler http.HandlerFunc) {
Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
}
// ----------------------------------
// helper functions for HTTP handlers
// ----------------------------------
func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
// ensure returns a wrapped handler that makes sure that the request has the
// correct method as well as additional method and header checks.
func ensure(
method string,
handler func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) {
log.Debug("%s %v", r.Method, r.URL)
start := time.Now()
m, u := r.Method, r.URL
log.Debug("started %s %s %s", m, r.Host, u)
defer func() { log.Debug("finished %s %s %s in %s", m, r.Host, u, time.Since(start)) }()
if m != method {
aghhttp.Error(r, w, http.StatusMethodNotAllowed, "only method %s is allowed", method)
if r.Method != method {
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
return
}
if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
if modifiesData(m) {
if !ensureContentType(w, r) {
return
}
Context.controlLock.Lock()
defer Context.controlLock.Unlock()
}
@@ -220,6 +220,42 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun
}
}
// modifiesData returns true if m is an HTTP method that can modify data.
func modifiesData(m string) (ok bool) {
return m == http.MethodPost || m == http.MethodPut || m == http.MethodDelete
}
// ensureContentType makes sure that the content type of a data-modifying
// request is set correctly. If it is not, ensureContentType writes a response
// to w, and ok is false.
func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {
const statusUnsup = http.StatusUnsupportedMediaType
cType := r.Header.Get(aghhttp.HdrNameContentType)
if r.ContentLength == 0 {
if cType == "" {
return true
}
// Assume that browsers always send a content type when submitting HTML
// forms and require no content type for requests with no body to make
// sure that the request comes from JavaScript.
aghhttp.Error(r, w, statusUnsup, "empty body with content-type %q not allowed", cType)
return false
}
const wantCType = aghhttp.HdrValApplicationJSON
if cType == wantCType {
return true
}
aghhttp.Error(r, w, statusUnsup, "only content-type %s is allowed", wantCType)
return false
}
func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure(http.MethodPost, handler)
}
@@ -291,7 +327,7 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
}
httpsURL := &url.URL{
Scheme: schemeHTTPS,
Scheme: aghhttp.SchemeHTTPS,
Host: hostPort,
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
@@ -307,7 +343,7 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
//
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin.
originURL := &url.URL{
Scheme: schemeHTTP,
Scheme: aghhttp.SchemeHTTP,
Host: r.Host,
}
w.Header().Set("Access-Control-Allow-Origin", originURL.String())

View File

@@ -21,6 +21,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/lucas-clemente/quic-go/http3"
)
// getAddrsResponse is the response for /install/get_addresses endpoint.
@@ -59,19 +60,7 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
data.Interfaces[iface.Name] = iface
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to marshal default addresses to json: %s",
err,
)
return
}
_ = aghhttp.WriteJSONResponse(w, r, data)
}
type checkConfReqEnt struct {
@@ -201,13 +190,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding the response: %s", err)
return
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// handleStaticIP - handles static IP request
@@ -346,6 +329,7 @@ func copyInstallSettings(dst, src *configuration) {
// shutdownTimeout is the timeout for shutting HTTP server down operation.
const shutdownTimeout = 5 * time.Second
// shutdownSrv shuts srv down and prints error messages to the log.
func shutdownSrv(ctx context.Context, srv *http.Server) {
defer log.OnPanic("")
@@ -354,13 +338,38 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
}
err := srv.Shutdown(ctx)
if err != nil {
const msgFmt = "shutting down http server %q: %s"
if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err)
} else {
log.Error(msgFmt, srv.Addr, err)
}
if err == nil {
return
}
const msgFmt = "shutting down http server %q: %s"
if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err)
} else {
log.Error(msgFmt, srv.Addr, err)
}
}
// shutdownSrv3 shuts srv down and prints error messages to the log.
//
// TODO(a.garipov): Think of a good way to merge with [shutdownSrv].
func shutdownSrv3(srv *http3.Server) {
defer log.OnPanic("")
if srv == nil {
return
}
err := srv.Close()
if err == nil {
return
}
const msgFmt = "shutting down http/3 server %q: %s"
if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err)
} else {
log.Error(msgFmt, srv.Addr, err)
}
}
@@ -424,7 +433,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return
}
u := &User{
u := &webUser{
Name: req.Username,
}
Context.auth.UserAdd(u, req.Password)
@@ -563,16 +572,11 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Failed to encode 'check_config' JSON data: %s",
err,
)
aghhttp.Error(r, w, http.StatusBadRequest, "encoding check_config: %s", err)
return
}
body := nonBetaReqBody.String()
r.Body = io.NopCloser(strings.NewReader(body))
r.ContentLength = int64(len(body))
@@ -640,13 +644,7 @@ func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Reques
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Failed to encode 'check_config' JSON data: %s",
err,
)
aghhttp.Error(r, w, http.StatusBadRequest, "encoding configure: %s", err)
return
}
@@ -688,19 +686,7 @@ func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Req
data.Interfaces = ifaces
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to marshal default addresses to json: %s",
err,
)
return
}
_ = aghhttp.WriteJSONResponse(w, r, data)
}
// registerBetaInstallHandlers registers the install handlers for new client

View File

@@ -28,8 +28,6 @@ type temporaryError interface {
// Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := &versionResponse{}
if Context.disableUpdate {
resp.Disabled = true
@@ -71,10 +69,7 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
return
}
err = json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "writing body: %s", err)
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// requestVersionInfo sets the VersionInfo field of resp if it can reach the

View File

@@ -31,7 +31,10 @@ const (
// Called by other modules when configuration is changed
func onConfigModified() {
_ = config.write()
err := config.write()
if err != nil {
log.Error("writing config: %s", err)
}
}
// initDNSServer creates an instance of the dnsforward.Server
@@ -71,11 +74,11 @@ func initDNSServer() (err error) {
}
Context.queryLog = querylog.New(conf)
filterConf := config.DNS.DnsfilterConf
filterConf.EtcHosts = Context.etcHosts
filterConf.ConfigModified = onConfigModified
filterConf.HTTPRegister = httpRegister
Context.dnsFilter = filtering.New(&filterConf, nil)
Context.filters, err = filtering.New(config.DNS.DnsfilterConf, nil)
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return err
}
var privateNets netutil.SubnetSet
switch len(config.DNS.PrivateNets) {
@@ -83,13 +86,10 @@ func initDNSServer() (err error) {
// Use an optimized locally-served matcher.
privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
case 1:
var n *net.IPNet
n, err = netutil.ParseSubnet(config.DNS.PrivateNets[0])
privateNets, err = netutil.ParseSubnet(config.DNS.PrivateNets[0])
if err != nil {
return fmt.Errorf("preparing the set of private subnets: %w", err)
}
privateNets = n
default:
var nets []*net.IPNet
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
@@ -101,15 +101,13 @@ func initDNSServer() (err error) {
}
p := dnsforward.DNSCreateParams{
DNSFilter: Context.dnsFilter,
DNSFilter: Context.filters,
Stats: Context.stats,
QueryLog: Context.queryLog,
PrivateNets: privateNets,
Anonymizer: anonymizer,
LocalDomain: config.DHCP.LocalDomainName,
}
if Context.dhcpServer != nil {
p.DHCPServer = Context.dhcpServer
DHCPServer: Context.dhcpServer,
}
Context.dnsServer, err = dnsforward.NewServer(p)
@@ -143,7 +141,6 @@ func initDNSServer() (err error) {
Context.whois = initWHOIS(&Context.clients)
}
Context.filters.Init()
return nil
}
@@ -249,11 +246,14 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
newConf.FilterHandler = applyAdditionalFiltering
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
newConf.ResolveClients = config.Clients.Sources.RDNS
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
newConf.ResolveClients = config.Clients.Sources.RDNS
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
newConf.ServeHTTP3 = dnsConf.ServeHTTP3
newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams
return newConf, nil
}
@@ -335,9 +335,12 @@ func getDNSEncryption() (de dnsEncryption) {
// applyAdditionalFiltering adds additional client information and settings if
// the client has them.
func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering.Settings) {
Context.dnsFilter.ApplyBlockedServices(setts, nil, true)
// pref is a prefix for logging messages around the scope.
const pref = "applying filters"
log.Debug("looking up settings for client with ip %s and clientid %q", clientIP, clientID)
Context.filters.ApplyBlockedServices(setts, nil)
log.Debug("%s: looking for client with ip %s and clientid %q", pref, clientIP, clientID)
if clientIP == nil {
return
@@ -349,16 +352,22 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
if !ok {
c, ok = Context.clients.Find(clientIP.String())
if !ok {
log.Debug("client with ip %s and clientid %q not found", clientIP, clientID)
log.Debug("%s: no clients with ip %s and clientid %q", pref, clientIP, clientID)
return
}
}
log.Debug("using settings for client %q with ip %s and clientid %q", c.Name, clientIP, clientID)
log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID)
if c.UseOwnBlockedServices {
Context.dnsFilter.ApplyBlockedServices(setts, c.BlockedServices, false)
// TODO(e.burkov): Get rid of this crutch.
svcs := c.BlockedServices
if svcs == nil {
svcs = []string{}
}
Context.filters.ApplyBlockedServices(setts, svcs)
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
}
setts.ClientName = c.Name
@@ -381,7 +390,7 @@ func startDNSServer() error {
return fmt.Errorf("unable to start forwarding DNS server: Already running")
}
enableFiltersLocked(false)
Context.filters.EnableFilters(false)
Context.clients.Start()
@@ -390,7 +399,6 @@ func startDNSServer() error {
return fmt.Errorf("couldn't start forwarding DNS server: %w", err)
}
Context.dnsFilter.Start()
Context.filters.Start()
Context.stats.Start()
Context.queryLog.Start()
@@ -449,10 +457,7 @@ func closeDNSServer() {
Context.dnsServer = nil
}
if Context.dnsFilter != nil {
Context.dnsFilter.Close()
Context.dnsFilter = nil
}
Context.filters.Close()
if Context.stats != nil {
err := Context.stats.Close()
@@ -469,7 +474,5 @@ func closeDNSServer() {
Context.queryLog = nil
}
Context.filters.Close()
log.Debug("Closed all DNS modules")
log.Debug("all dns modules are closed")
}

View File

@@ -20,6 +20,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
@@ -33,6 +34,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"golang.org/x/exp/slices"
"gopkg.in/natefinch/lumberjack.v2"
)
@@ -52,10 +54,9 @@ type homeContext struct {
dnsServer *dnsforward.Server // DNS module
rdns *RDNS // rDNS module
whois *WHOIS // WHOIS module
dnsFilter *filtering.DNSFilter // DNS filtering module
dhcpServer dhcpd.Interface // DHCP module
auth *Auth // HTTP authentication module
filters Filtering // DNS filtering module
filters *filtering.DNSFilter // DNS filtering module
web *Web // Web (HTTP, HTTPS) module
tls *TLSMod // TLS module
// etcHosts is an IP-hostname pairs set taken from system configuration
@@ -96,9 +97,15 @@ var Context homeContext
// Main is the entry point
func Main(clientBuildFS fs.FS) {
// config can be specified, which reads options from there, but other command line flags have to override config values
// therefore, we must do it manually instead of using a lib
args := loadOptions()
initCmdLineOpts()
// The configuration file path can be overridden, but other command-line
// options have to override config values. Therefore, do it manually
// instead of using package flag.
//
// TODO(a.garipov): The comment above is most likely false. Replace with
// package flag.
opts := loadCmdLineOpts()
Context.appSignalChannel = make(chan os.Signal)
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
@@ -119,29 +126,26 @@ func Main(clientBuildFS fs.FS) {
}
}()
if args.serviceControlAction != "" {
handleServiceControlAction(args, clientBuildFS)
if opts.serviceControlAction != "" {
handleServiceControlAction(opts, clientBuildFS)
return
}
// run the protection
run(args, clientBuildFS)
run(opts, clientBuildFS)
}
func setupContext(args options) {
Context.runningAsService = args.runningAsService
Context.disableUpdate = args.disableUpdate ||
version.Channel() == version.ChannelDevelopment
func setupContext(opts options) {
setupContextFlags(opts)
Context.firstRun = detectFirstRun()
if Context.firstRun {
log.Info("This is the first time AdGuard Home is launched")
checkPermissions()
switch version.Channel() {
case version.ChannelEdge, version.ChannelDevelopment:
config.BetaBindPort = 3001
default:
// Go on.
}
initConfig()
Context.tlsRoots = LoadSystemRootCAs()
Context.transport = &http.Transport{
DialContext: customDialContext,
@@ -168,13 +172,13 @@ func setupContext(args options) {
os.Exit(1)
}
if args.checkConfig {
if opts.checkConfig {
log.Info("configuration file is ok")
os.Exit(0)
}
if !args.noEtcHosts && config.Clients.Sources.HostsFile {
if !opts.noEtcHosts && config.Clients.Sources.HostsFile {
err = setupHostsContainer()
fatalOnError(err)
}
@@ -183,6 +187,24 @@ func setupContext(args options) {
Context.mux = http.NewServeMux()
}
// setupContextFlags sets global flags and prints their status to the log.
func setupContextFlags(opts options) {
Context.firstRun = detectFirstRun()
if Context.firstRun {
log.Info("This is the first time AdGuard Home is launched")
checkPermissions()
}
Context.runningAsService = opts.runningAsService
// Don't print the runningAsService flag, since that has already been done
// in [run].
Context.disableUpdate = opts.disableUpdate || version.Channel() == version.ChannelDevelopment
if Context.disableUpdate {
log.Info("AdGuard Home updates are disabled")
}
}
// logIfUnsupported logs a formatted warning if the error is one of the
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
// nil. Otherwise, it returns err.
@@ -264,7 +286,16 @@ func setupHostsContainer() (err error) {
return nil
}
func setupConfig(args options) (err error) {
func setupConfig(opts options) (err error) {
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts
config.DNS.DnsfilterConf.ConfigModified = onConfigModified
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
config.DNS.DnsfilterConf.DataDir = Context.getDataDir()
config.DNS.DnsfilterConf.Filters = slices.Clone(config.Filters)
config.DNS.DnsfilterConf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules)
config.DNS.DnsfilterConf.HTTPClient = Context.client
config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified
@@ -297,9 +328,9 @@ func setupConfig(args options) (err error) {
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
if args.bindPort != 0 {
if opts.bindPort != 0 {
tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(tcpPorts, tcpPort(args.bindPort), tcpPort(config.BetaBindPort))
addPorts(tcpPorts, tcpPort(opts.bindPort), tcpPort(config.BetaBindPort))
udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(config.DNS.Port))
@@ -321,23 +352,23 @@ func setupConfig(args options) (err error) {
return fmt.Errorf("validating udp ports: %w", err)
}
config.BindPort = args.bindPort
config.BindPort = opts.bindPort
}
// override bind host/port from the console
if args.bindHost != nil {
config.BindHost = args.bindHost
if opts.bindHost != nil {
config.BindHost = opts.bindHost
}
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
Context.pidFileName = args.pidFile
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
Context.pidFileName = opts.pidFile
}
return nil
}
func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
var clientFS, clientBetaFS fs.FS
if args.localFrontend {
if opts.localFrontend {
log.Info("warning: using local frontend files")
clientFS = os.DirFS("build/static")
@@ -366,9 +397,11 @@ func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
clientFS: clientFS,
clientBetaFS: clientBetaFS,
serveHTTP3: config.DNS.ServeHTTP3,
}
web = CreateWeb(&webConf)
web = newWeb(&webConf)
if web == nil {
return nil, fmt.Errorf("initializing web: %w", err)
}
@@ -383,28 +416,26 @@ func fatalOnError(err error) {
}
// run configures and starts AdGuard Home.
func run(args options, clientBuildFS fs.FS) {
var err error
func run(opts options, clientBuildFS fs.FS) {
// configure config filename
initConfigFilename(args)
initConfigFilename(opts)
// configure working dir and config path
initWorkingDir(args)
initWorkingDir(opts)
// configure log level and output
configureLogger(args)
configureLogger(opts)
// Print the first message after logger is configured.
log.Println(version.Full())
log.Info(version.Full())
log.Debug("current working directory is %s", Context.workDir)
if args.runningAsService {
if opts.runningAsService {
log.Info("AdGuard Home is running as a service")
}
setupContext(args)
setupContext(opts)
err = configureOS(config)
err := configureOS(config)
fatalOnError(err)
// clients package uses filtering package's static data (filtering.BlockedSvcKnown()),
@@ -412,7 +443,7 @@ func run(args options, clientBuildFS fs.FS) {
// but also avoid relying on automatic Go init() function
filtering.InitModule()
err = setupConfig(args)
err = setupConfig(opts)
fatalOnError(err)
if !Context.firstRun {
@@ -441,10 +472,10 @@ func run(args options, clientBuildFS fs.FS) {
}
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
GLMode = args.glinetMode
var arl *authRateLimiter
GLMode = opts.glinetMode
var rateLimiter *authRateLimiter
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
arl = newAuthRateLimiter(
rateLimiter = newAuthRateLimiter(
time.Duration(config.AuthBlockMin)*time.Minute,
config.AuthAttempts,
)
@@ -456,7 +487,7 @@ func run(args options, clientBuildFS fs.FS) {
sessFilename,
config.Users,
config.WebSessionTTLHours*60*60,
arl,
rateLimiter,
)
if Context.auth == nil {
log.Fatalf("Couldn't initialize Auth module")
@@ -468,7 +499,7 @@ func run(args options, clientBuildFS fs.FS) {
log.Fatalf("Can't initialize TLS module")
}
Context.web, err = initWeb(args, clientBuildFS)
Context.web, err = initWeb(opts, clientBuildFS)
fatalOnError(err)
if !Context.firstRun {
@@ -560,10 +591,10 @@ func writePIDFile(fn string) bool {
return true
}
func initConfigFilename(args options) {
func initConfigFilename(opts options) {
// config file path can be overridden by command-line arguments:
if args.configFilename != "" {
Context.configFilename = args.configFilename
if opts.confFilename != "" {
Context.configFilename = opts.confFilename
} else {
// Default config file name
Context.configFilename = "AdGuardHome.yaml"
@@ -572,15 +603,15 @@ func initConfigFilename(args options) {
// initWorkingDir initializes the workDir
// if no command-line arguments specified, we use the directory where our binary file is located
func initWorkingDir(args options) {
func initWorkingDir(opts options) {
execPath, err := os.Executable()
if err != nil {
panic(err)
}
if args.workDir != "" {
if opts.workDir != "" {
// If there is a custom config file, use it's directory as our working dir
Context.workDir = args.workDir
Context.workDir = opts.workDir
} else {
Context.workDir = filepath.Dir(execPath)
}
@@ -594,15 +625,15 @@ func initWorkingDir(args options) {
}
// configureLogger configures logger level and output
func configureLogger(args options) {
func configureLogger(opts options) {
ls := getLogSettings()
// command-line arguments can override config settings
if args.verbose || config.Verbose {
if opts.verbose || config.Verbose {
ls.Verbose = true
}
if args.logFile != "" {
ls.File = args.logFile
if opts.logFile != "" {
ls.File = opts.logFile
} else if config.File != "" {
ls.File = config.File
}
@@ -623,7 +654,7 @@ func configureLogger(args options) {
// happen pretty quickly.
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
if args.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if nothing
// else is configured. Otherwise, we'll simply lose the log output.
ls.File = configSyslog
@@ -713,25 +744,29 @@ func exitWithError() {
os.Exit(64)
}
// loadOptions reads command line arguments and initializes configuration
func loadOptions() options {
o, f, err := parse(os.Args[0], os.Args[1:])
// loadCmdLineOpts reads command line arguments and initializes configuration
// from them. If there is an error or an effect, loadCmdLineOpts processes them
// and exits.
func loadCmdLineOpts() (opts options) {
opts, eff, err := parseCmdOpts(os.Args[0], os.Args[1:])
if err != nil {
log.Error(err.Error())
_ = printHelp(os.Args[0])
printHelp(os.Args[0])
exitWithError()
} else if f != nil {
err = f()
}
if eff != nil {
err = eff()
if err != nil {
log.Error(err.Error())
exitWithError()
} else {
os.Exit(0)
}
os.Exit(0)
}
return o
return opts
}
// printWebAddrs prints addresses built from proto, addr, and an appropriate
@@ -763,12 +798,12 @@ func printHTTPAddresses(proto string) {
}
port := config.BindPort
if proto == schemeHTTPS {
if proto == aghhttp.SchemeHTTPS {
port = tlsConf.PortHTTPS
}
// TODO(e.burkov): Inspect and perhaps merge with the previous condition.
if proto == schemeHTTPS && tlsConf.ServerName != "" {
if proto == aghhttp.SchemeHTTPS && tlsConf.ServerName != "" {
printWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS, 0)
return

View File

@@ -0,0 +1,12 @@
package home
import (
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
)
func TestMain(m *testing.M) {
aghtest.DiscardLogOutput(m)
initCmdLineOpts()
}

View File

@@ -1,10 +1,8 @@
package home
import (
"fmt"
"io"
"encoding/json"
"net/http"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/log"
@@ -51,43 +49,35 @@ var allowedLanguages = stringutil.NewSet(
"zh-tw",
)
func handleI18nCurrentLanguage(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
log.Printf("config.Language is %s", config.Language)
_, err := fmt.Fprintf(w, "%s\n", config.Language)
if err != nil {
msg := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(msg)
http.Error(w, msg, http.StatusInternalServerError)
// languageJSON is the JSON structure for language requests and responses.
type languageJSON struct {
Language string `json:"language"`
}
return
}
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
log.Printf("home: language is %s", config.Language)
_ = aghhttp.WriteJSONResponse(w, r, &languageJSON{
Language: config.Language,
})
}
func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
// This use of ReadAll is safe, because request's body is now limited.
body, err := io.ReadAll(r.Body)
if aghhttp.WriteTextPlainDeprecated(w, r) {
return
}
langReq := &languageJSON{}
err := json.NewDecoder(r.Body).Decode(langReq)
if err != nil {
msg := fmt.Sprintf("failed to read request body: %s", err)
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
aghhttp.Error(r, w, http.StatusInternalServerError, "reading req: %s", err)
return
}
language := strings.TrimSpace(string(body))
if language == "" {
msg := "empty language specified"
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
if !allowedLanguages.Has(language) {
msg := fmt.Sprintf("unknown language specified: %s", language)
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
lang := langReq.Language
if !allowedLanguages.Has(lang) {
aghhttp.Error(r, w, http.StatusBadRequest, "unknown language: %q", lang)
return
}
@@ -96,7 +86,8 @@ func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
config.Lock()
defer config.Unlock()
config.Language = language
config.Language = lang
log.Printf("home: language is set to %s", lang)
}()
onConfigModified()

View File

@@ -8,6 +8,7 @@ import (
"net/url"
"path"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
@@ -82,7 +83,7 @@ func encodeMobileConfig(d *dnsSettings, clientID string) ([]byte, error) {
case dnsProtoHTTPS:
dspName = fmt.Sprintf("%s DoH", d.ServerName)
u := &url.URL{
Scheme: schemeHTTPS,
Scheme: aghhttp.SchemeHTTPS,
Host: d.ServerName,
Path: path.Join("/dns-query", clientID),
}

View File

@@ -5,30 +5,60 @@ import (
"net"
"os"
"strconv"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
)
// options passed from command-line arguments
type options struct {
verbose bool // is verbose logging enabled
configFilename string // path to the config file
workDir string // path to the working directory where we will store the filters data and the querylog
bindHost net.IP // host address to bind HTTP server on
bindPort int // port to serve HTTP pages on
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
pidFile string // File name to save PID to
checkConfig bool // Check configuration and exit
disableUpdate bool // If set, don't check for updates
// TODO(a.garipov): Replace with package flag.
// service control action (see service.ControlAction array + "status" command)
// options represents the command-line options.
type options struct {
// confFilename is the path to the configuration file.
confFilename string
// workDir is the path to the working directory where AdGuard Home stores
// filter data, the query log, and other data.
workDir string
// logFile is the path to the log file. If empty, AdGuard Home writes to
// stdout; if "syslog", to syslog.
logFile string
// pidFile is the file name for the file to which the PID is saved.
pidFile string
// serviceControlAction is the service action to perform. See
// [service.ControlAction] and [handleServiceControlAction].
serviceControlAction string
// runningAsService flag is set to true when options are passed from the service runner
// bindHost is the address on which to serve the HTTP UI.
bindHost net.IP
// bindPort is the port on which to serve the HTTP UI.
bindPort int
// checkConfig is true if the current invocation is only required to check
// the configuration file and exit.
checkConfig bool
// disableUpdate, if set, makes AdGuard Home not check for updates.
disableUpdate bool
// verbose shows if verbose logging is enabled.
verbose bool
// runningAsService flag is set to true when options are passed from the
// service runner
//
// TODO(a.garipov): Perhaps this could be determined by a non-empty
// serviceControlAction?
runningAsService bool
glinetMode bool // Activate GL-Inet compatibility mode
// glinetMode shows if the GL-Inet compatibility mode is enabled.
glinetMode bool
// noEtcHosts flag should be provided when /etc/hosts file shouldn't be
// used.
@@ -39,88 +69,85 @@ type options struct {
localFrontend bool
}
// functions used for their side-effects
type effect func() error
type arg struct {
description string // a short, English description of the argument
longName string // the name of the argument used after '--'
shortName string // the name of the argument used after '-'
// only one of updateWithValue, updateNoValue, and effect should be present
updateWithValue func(o options, v string) (options, error) // the mutator for arguments with parameters
updateNoValue func(o options) (options, error) // the mutator for arguments without parameters
effect func(o options, exec string) (f effect, err error) // the side-effect closure generator
serialize func(o options) []string // the re-serialization function back to arguments (return nil for omit)
// initCmdLineOpts completes initialization of the global command-line option
// slice. It must only be called once.
func initCmdLineOpts() {
// The --help option cannot be put directly into cmdLineOpts, because that
// causes initialization cycle due to printHelp referencing cmdLineOpts.
cmdLineOpts = append(cmdLineOpts, cmdLineOpt{
updateWithValue: nil,
updateNoValue: nil,
effect: func(o options, exec string) (effect, error) {
return func() error { printHelp(exec); exitWithError(); return nil }, nil
},
serialize: func(o options) (val string, ok bool) { return "", false },
description: "Print this help.",
longName: "help",
shortName: "",
})
}
// {type}SliceOrNil functions check their parameter of type {type}
// against its zero value and return nil if the parameter value is
// zero otherwise they return a string slice of the parameter
// effect is the type for functions used for their side-effects.
type effect func() (err error)
func ipSliceOrNil(ip net.IP) []string {
if ip == nil {
return nil
}
// cmdLineOpt contains the data for a single command-line option. Only one of
// updateWithValue, updateNoValue, and effect must be present.
type cmdLineOpt struct {
updateWithValue func(o options, v string) (updated options, err error)
updateNoValue func(o options) (updated options, err error)
effect func(o options, exec string) (eff effect, err error)
return []string{ip.String()}
// serialize is a function that encodes the option into a slice of
// command-line arguments, if necessary. If ok is false, this option should
// be skipped.
serialize func(o options) (val string, ok bool)
description string
longName string
shortName string
}
func stringSliceOrNil(s string) []string {
if s == "" {
return nil
}
// cmdLineOpts are all command-line options of AdGuard Home.
var cmdLineOpts = []cmdLineOpt{{
updateWithValue: func(o options, v string) (options, error) {
o.confFilename = v
return o, nil
},
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) {
return o.confFilename, o.confFilename != ""
},
description: "Path to the config file.",
longName: "config",
shortName: "c",
}, {
updateWithValue: func(o options, v string) (options, error) { o.workDir = v; return o, nil },
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) { return o.workDir, o.workDir != "" },
description: "Path to the working directory.",
longName: "work-dir",
shortName: "w",
}, {
updateWithValue: func(o options, v string) (options, error) {
o.bindHost = net.ParseIP(v)
return o, nil
},
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) {
if o.bindHost == nil {
return "", false
}
return []string{s}
}
func intSliceOrNil(i int) []string {
if i == 0 {
return nil
}
return []string{strconv.Itoa(i)}
}
func boolSliceOrNil(b bool) []string {
if b {
return []string{}
}
return nil
}
var args []arg
var configArg = arg{
"Path to the config file.",
"config", "c",
func(o options, v string) (options, error) { o.configFilename = v; return o, nil },
nil,
nil,
func(o options) []string { return stringSliceOrNil(o.configFilename) },
}
var workDirArg = arg{
"Path to the working directory.",
"work-dir", "w",
func(o options, v string) (options, error) { o.workDir = v; return o, nil }, nil, nil,
func(o options) []string { return stringSliceOrNil(o.workDir) },
}
var hostArg = arg{
"Host address to bind HTTP server on.",
"host", "h",
func(o options, v string) (options, error) { o.bindHost = net.ParseIP(v); return o, nil }, nil, nil,
func(o options) []string { return ipSliceOrNil(o.bindHost) },
}
var portArg = arg{
"Port to serve HTTP pages on.",
"port", "p",
func(o options, v string) (options, error) {
return o.bindHost.String(), true
},
description: "Host address to bind HTTP server on.",
longName: "host",
shortName: "h",
}, {
updateWithValue: func(o options, v string) (options, error) {
var err error
var p int
minPort, maxPort := 0, 1<<16-1
@@ -131,108 +158,81 @@ var portArg = arg{
} else {
o.bindPort = p
}
return o, err
}, nil, nil,
func(o options) []string { return intSliceOrNil(o.bindPort) },
}
var serviceArg = arg{
"Service control action: status, install, uninstall, start, stop, restart, reload (configuration).",
"service", "s",
func(o options, v string) (options, error) {
return o, err
},
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) {
if o.bindPort == 0 {
return "", false
}
return strconv.Itoa(o.bindPort), true
},
description: "Port to serve HTTP pages on.",
longName: "port",
shortName: "p",
}, {
updateWithValue: func(o options, v string) (options, error) {
o.serviceControlAction = v
return o, nil
}, nil, nil,
func(o options) []string { return stringSliceOrNil(o.serviceControlAction) },
}
var logfileArg = arg{
"Path to log file. If empty: write to stdout; if 'syslog': write to system log.",
"logfile", "l",
func(o options, v string) (options, error) { o.logFile = v; return o, nil }, nil, nil,
func(o options) []string { return stringSliceOrNil(o.logFile) },
}
var pidfileArg = arg{
"Path to a file where PID is stored.",
"pidfile", "",
func(o options, v string) (options, error) { o.pidFile = v; return o, nil }, nil, nil,
func(o options) []string { return stringSliceOrNil(o.pidFile) },
}
var checkConfigArg = arg{
"Check configuration and exit.",
"check-config", "",
nil, func(o options) (options, error) { o.checkConfig = true; return o, nil }, nil,
func(o options) []string { return boolSliceOrNil(o.checkConfig) },
}
var noCheckUpdateArg = arg{
"Don't check for updates.",
"no-check-update", "",
nil, func(o options) (options, error) { o.disableUpdate = true; return o, nil }, nil,
func(o options) []string { return boolSliceOrNil(o.disableUpdate) },
}
var disableMemoryOptimizationArg = arg{
"Deprecated. Disable memory optimization.",
"no-mem-optimization", "",
nil, nil, func(_ options, _ string) (f effect, err error) {
},
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) {
return o.serviceControlAction, o.serviceControlAction != ""
},
description: `Service control action: status, install (as a service), ` +
`uninstall (as a service), start, stop, restart, reload (configuration).`,
longName: "service",
shortName: "s",
}, {
updateWithValue: func(o options, v string) (options, error) { o.logFile = v; return o, nil },
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) { return o.logFile, o.logFile != "" },
description: `Path to log file. If empty, write to stdout; ` +
`if "syslog", write to system log.`,
longName: "logfile",
shortName: "l",
}, {
updateWithValue: func(o options, v string) (options, error) { o.pidFile = v; return o, nil },
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) { return o.pidFile, o.pidFile != "" },
description: "Path to a file where PID is stored.",
longName: "pidfile",
shortName: "",
}, {
updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.checkConfig = true; return o, nil },
effect: nil,
serialize: func(o options) (val string, ok bool) { return "", o.checkConfig },
description: "Check configuration and exit.",
longName: "check-config",
shortName: "",
}, {
updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.disableUpdate = true; return o, nil },
effect: nil,
serialize: func(o options) (val string, ok bool) { return "", o.disableUpdate },
description: "Don't check for updates.",
longName: "no-check-update",
shortName: "",
}, {
updateWithValue: nil,
updateNoValue: nil,
effect: func(_ options, _ string) (f effect, err error) {
log.Info("warning: using --no-mem-optimization flag has no effect and is deprecated")
return nil, nil
},
func(o options) []string { return nil },
}
var verboseArg = arg{
"Enable verbose output.",
"verbose", "v",
nil, func(o options) (options, error) { o.verbose = true; return o, nil }, nil,
func(o options) []string { return boolSliceOrNil(o.verbose) },
}
var glinetArg = arg{
"Run in GL-Inet compatibility mode.",
"glinet", "",
nil, func(o options) (options, error) { o.glinetMode = true; return o, nil }, nil,
func(o options) []string { return boolSliceOrNil(o.glinetMode) },
}
var versionArg = arg{
description: "Show the version and exit. Show more detailed version description with -v.",
longName: "version",
shortName: "",
updateWithValue: nil,
updateNoValue: nil,
effect: func(o options, exec string) (effect, error) {
return func() error {
if o.verbose {
fmt.Println(version.Verbose())
} else {
fmt.Println(version.Full())
}
os.Exit(0)
return nil
}, nil
},
serialize: func(o options) []string { return nil },
}
var helpArg = arg{
"Print this help.",
"help", "",
nil, nil, func(o options, exec string) (effect, error) {
return func() error { _ = printHelp(exec); os.Exit(64); return nil }, nil
},
func(o options) []string { return nil },
}
var noEtcHostsArg = arg{
description: "Deprecated. Do not use the OS-provided hosts.",
longName: "no-etc-hosts",
shortName: "",
serialize: func(o options) (val string, ok bool) { return "", false },
description: "Deprecated. Disable memory optimization.",
longName: "no-mem-optimization",
shortName: "",
}, {
updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
effect: func(_ options, _ string) (f effect, err error) {
@@ -242,146 +242,216 @@ var noEtcHostsArg = arg{
return nil, nil
},
serialize: func(o options) []string { return boolSliceOrNil(o.noEtcHosts) },
}
var localFrontendArg = arg{
description: "Use local frontend directories.",
longName: "local-frontend",
shortName: "",
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
description: "Deprecated. Do not use the OS-provided hosts.",
longName: "no-etc-hosts",
shortName: "",
}, {
updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.localFrontend = true; return o, nil },
effect: nil,
serialize: func(o options) []string { return boolSliceOrNil(o.localFrontend) },
}
serialize: func(o options) (val string, ok bool) { return "", o.localFrontend },
description: "Use local frontend directories.",
longName: "local-frontend",
shortName: "",
}, {
updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.verbose = true; return o, nil },
effect: nil,
serialize: func(o options) (val string, ok bool) { return "", o.verbose },
description: "Enable verbose output.",
longName: "verbose",
shortName: "v",
}, {
updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.glinetMode = true; return o, nil },
effect: nil,
serialize: func(o options) (val string, ok bool) { return "", o.glinetMode },
description: "Run in GL-Inet compatibility mode.",
longName: "glinet",
shortName: "",
}, {
updateWithValue: nil,
updateNoValue: nil,
effect: func(o options, exec string) (effect, error) {
return func() error {
if o.verbose {
fmt.Println(version.Verbose())
} else {
fmt.Println(version.Full())
}
func init() {
args = []arg{
configArg,
workDirArg,
hostArg,
portArg,
serviceArg,
logfileArg,
pidfileArg,
checkConfigArg,
noCheckUpdateArg,
disableMemoryOptimizationArg,
noEtcHostsArg,
localFrontendArg,
verboseArg,
glinetArg,
versionArg,
helpArg,
}
}
os.Exit(0)
func getUsageLines(exec string, args []arg) []string {
usage := []string{
"Usage:",
"",
fmt.Sprintf("%s [options]", exec),
"",
"Options:",
}
for _, arg := range args {
return nil
}, nil
},
serialize: func(o options) (val string, ok bool) { return "", false },
description: "Show the version and exit. Show more detailed version description with -v.",
longName: "version",
shortName: "",
}}
// printHelp prints the entire help message. It exits with an error code if
// there are any I/O errors.
func printHelp(exec string) {
b := &strings.Builder{}
stringutil.WriteToBuilder(
b,
"Usage:\n\n",
fmt.Sprintf("%s [options]\n\n", exec),
"Options:\n",
)
var err error
for _, opt := range cmdLineOpts {
val := ""
if arg.updateWithValue != nil {
if opt.updateWithValue != nil {
val = " VALUE"
}
if arg.shortName != "" {
usage = append(usage, fmt.Sprintf(" -%s, %-30s %s",
arg.shortName,
"--"+arg.longName+val,
arg.description))
longDesc := opt.longName + val
if opt.shortName != "" {
_, err = fmt.Fprintf(b, " -%s, --%-28s %s\n", opt.shortName, longDesc, opt.description)
} else {
usage = append(usage, fmt.Sprintf(" %-34s %s",
"--"+arg.longName+val,
arg.description))
_, err = fmt.Fprintf(b, " --%-32s %s\n", longDesc, opt.description)
}
if err != nil {
// The only error here can be from incorrect Fprintf usage, which is
// a programmer error.
panic(err)
}
}
return usage
_, err = fmt.Print(b)
if err != nil {
// Exit immediately, since not being able to print out a help message
// essentially means that the I/O is very broken at the moment.
exitWithError()
}
}
func printHelp(exec string) error {
for _, line := range getUsageLines(exec, args) {
_, err := fmt.Println(line)
// parseCmdOpts parses the command-line arguments into options and effects.
func parseCmdOpts(cmdName string, args []string) (o options, eff effect, err error) {
// Don't use range since the loop changes the loop variable.
argsLen := len(args)
for i := 0; i < len(args); i++ {
arg := args[i]
isKnown := false
for _, opt := range cmdLineOpts {
isKnown = argMatches(opt, arg)
if !isKnown {
continue
}
if opt.updateWithValue != nil {
i++
if i >= argsLen {
return o, eff, fmt.Errorf("got %s without argument", arg)
}
o, err = opt.updateWithValue(o, args[i])
} else {
o, eff, err = updateOptsNoValue(o, eff, opt, cmdName)
}
if err != nil {
return o, eff, fmt.Errorf("applying option %s: %w", arg, err)
}
break
}
if !isKnown {
return o, eff, fmt.Errorf("unknown option %s", arg)
}
}
return o, eff, err
}
// argMatches returns true if arg matches command-line option opt.
func argMatches(opt cmdLineOpt, arg string) (ok bool) {
if arg == "" || arg[0] != '-' {
return false
}
arg = arg[1:]
if arg == "" {
return false
}
return (opt.shortName != "" && arg == opt.shortName) ||
(arg[0] == '-' && arg[1:] == opt.longName)
}
// updateOptsNoValue sets values or effects from opt into o or prev.
func updateOptsNoValue(
o options,
prev effect,
opt cmdLineOpt,
cmdName string,
) (updated options, chained effect, err error) {
if opt.updateNoValue != nil {
o, err = opt.updateNoValue(o)
if err != nil {
return o, prev, err
}
return o, prev, nil
}
next, err := opt.effect(o, cmdName)
if err != nil {
return o, prev, err
}
chained = chainEffect(prev, next)
return o, chained, nil
}
// chainEffect chans the next effect after the prev one. If prev is nil, eff
// only calls next. If next is nil, eff is prev; if prev is nil, eff is next.
func chainEffect(prev, next effect) (eff effect) {
if prev == nil {
return next
} else if next == nil {
return prev
}
eff = func() (err error) {
err = prev()
if err != nil {
return err
}
return next()
}
return nil
return eff
}
func argMatches(a arg, v string) bool {
return v == "--"+a.longName || (a.shortName != "" && v == "-"+a.shortName)
}
func parse(exec string, ss []string) (o options, f effect, err error) {
for i := 0; i < len(ss); i++ {
v := ss[i]
knownParam := false
for _, arg := range args {
if argMatches(arg, v) {
if arg.updateWithValue != nil {
if i+1 >= len(ss) {
return o, f, fmt.Errorf("got %s without argument", v)
}
i++
o, err = arg.updateWithValue(o, ss[i])
if err != nil {
return
}
} else if arg.updateNoValue != nil {
o, err = arg.updateNoValue(o)
if err != nil {
return
}
} else if arg.effect != nil {
var eff effect
eff, err = arg.effect(o, exec)
if err != nil {
return
}
if eff != nil {
prevf := f
f = func() (ferr error) {
if prevf != nil {
ferr = prevf()
}
if ferr == nil {
ferr = eff()
}
return ferr
}
}
}
knownParam = true
break
}
// optsToArgs converts command line options into a list of arguments.
func optsToArgs(o options) (args []string) {
for _, opt := range cmdLineOpts {
val, ok := opt.serialize(o)
if !ok {
continue
}
if !knownParam {
return o, f, fmt.Errorf("unknown option %v", v)
if opt.shortName != "" {
args = append(args, "-"+opt.shortName)
} else {
args = append(args, "--"+opt.longName)
}
if val != "" {
args = append(args, val)
}
}
return
}
func shortestFlag(a arg) string {
if a.shortName != "" {
return "-" + a.shortName
}
return "--" + a.longName
}
func serialize(o options) []string {
ss := []string{}
for _, arg := range args {
s := arg.serialize(o)
if s != nil {
ss = append(ss, append([]string{shortestFlag(arg)}, s...)...)
}
}
return ss
return args
}

View File

@@ -12,7 +12,7 @@ import (
func testParseOK(t *testing.T, ss ...string) options {
t.Helper()
o, _, err := parse("", ss)
o, _, err := parseCmdOpts("", ss)
require.NoError(t, err)
return o
@@ -21,7 +21,7 @@ func testParseOK(t *testing.T, ss ...string) options {
func testParseErr(t *testing.T, descr string, ss ...string) {
t.Helper()
_, _, err := parse("", ss)
_, _, err := parseCmdOpts("", ss)
require.Error(t, err)
}
@@ -38,11 +38,11 @@ func TestParseVerbose(t *testing.T) {
}
func TestParseConfigFilename(t *testing.T) {
assert.Equal(t, "", testParseOK(t).configFilename, "empty is no config filename")
assert.Equal(t, "path", testParseOK(t, "-c", "path").configFilename, "-c is config filename")
assert.Equal(t, "", testParseOK(t).confFilename, "empty is no config filename")
assert.Equal(t, "path", testParseOK(t, "-c", "path").confFilename, "-c is config filename")
testParseParamMissing(t, "-c")
assert.Equal(t, "path", testParseOK(t, "--config", "path").configFilename, "--config is config filename")
assert.Equal(t, "path", testParseOK(t, "--config", "path").confFilename, "--config is config filename")
testParseParamMissing(t, "--config")
}
@@ -103,7 +103,7 @@ func TestParseDisableUpdate(t *testing.T) {
// TODO(e.burkov): Remove after v0.108.0.
func TestParseDisableMemoryOptimization(t *testing.T) {
o, eff, err := parse("", []string{"--no-mem-optimization"})
o, eff, err := parseCmdOpts("", []string{"--no-mem-optimization"})
require.NoError(t, err)
assert.Nil(t, eff)
@@ -130,73 +130,73 @@ func TestParseUnknown(t *testing.T) {
testParseErr(t, "unknown dash", "-")
}
func TestSerialize(t *testing.T) {
func TestOptsToArgs(t *testing.T) {
testCases := []struct {
name string
args []string
opts options
ss []string
}{{
name: "empty",
args: []string{},
opts: options{},
ss: []string{},
}, {
name: "config_filename",
opts: options{configFilename: "path"},
ss: []string{"-c", "path"},
args: []string{"-c", "path"},
opts: options{confFilename: "path"},
}, {
name: "work_dir",
args: []string{"-w", "path"},
opts: options{workDir: "path"},
ss: []string{"-w", "path"},
}, {
name: "bind_host",
args: []string{"-h", "1.2.3.4"},
opts: options{bindHost: net.IP{1, 2, 3, 4}},
ss: []string{"-h", "1.2.3.4"},
}, {
name: "bind_port",
args: []string{"-p", "666"},
opts: options{bindPort: 666},
ss: []string{"-p", "666"},
}, {
name: "log_file",
args: []string{"-l", "path"},
opts: options{logFile: "path"},
ss: []string{"-l", "path"},
}, {
name: "pid_file",
args: []string{"--pidfile", "path"},
opts: options{pidFile: "path"},
ss: []string{"--pidfile", "path"},
}, {
name: "disable_update",
args: []string{"--no-check-update"},
opts: options{disableUpdate: true},
ss: []string{"--no-check-update"},
}, {
name: "control_action",
args: []string{"-s", "run"},
opts: options{serviceControlAction: "run"},
ss: []string{"-s", "run"},
}, {
name: "glinet_mode",
args: []string{"--glinet"},
opts: options{glinetMode: true},
ss: []string{"--glinet"},
}, {
name: "multiple",
opts: options{
serviceControlAction: "run",
configFilename: "config",
workDir: "work",
pidFile: "pid",
disableUpdate: true,
},
ss: []string{
args: []string{
"-c", "config",
"-w", "work",
"-s", "run",
"--pidfile", "pid",
"--no-check-update",
},
opts: options{
serviceControlAction: "run",
confFilename: "config",
workDir: "work",
pidFile: "pid",
disableUpdate: true,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := serialize(tc.opts)
assert.ElementsMatch(t, tc.ss, result)
result := optsToArgs(tc.opts)
assert.ElementsMatch(t, tc.args, result)
})
}
}

View File

@@ -11,6 +11,7 @@ import (
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/errors"
@@ -175,7 +176,8 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
chooseSystem()
action := opts.serviceControlAction
log.Printf("service: control action: %s", action)
log.Info(version.Full())
log.Info("service: control action: %s", action)
if action == "reload" {
sendSigReload()
@@ -195,7 +197,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
DisplayName: serviceDisplayName,
Description: serviceDescription,
WorkingDirectory: pwd,
Arguments: serialize(runOpts),
Arguments: optsToArgs(runOpts),
}
configureService(svcConfig)
@@ -277,7 +279,7 @@ AdGuard Home is successfully installed and will automatically start on boot.
There are a few more things that must be configured before you can use it.
Click on the link below and follow the Installation Wizard steps to finish setup.
AdGuard Home is now available at the following addresses:`)
printHTTPAddresses(schemeHTTP)
printHTTPAddresses(aghhttp.SchemeHTTP)
}
}

View File

@@ -266,7 +266,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
}
}
if !WebCheckPortAvailable(setts.PortHTTPS) {
if !webCheckPortAvailable(setts.PortHTTPS) {
aghhttp.Error(
r,
w,
@@ -356,7 +356,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
}
// TODO(e.burkov): Investigate and perhaps check other ports.
if !WebCheckPortAvailable(data.PortHTTPS) {
if !webCheckPortAvailable(data.PortHTTPS) {
aghhttp.Error(
r,
w,
@@ -680,8 +680,6 @@ func unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {
}
func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
w.Header().Set("Content-Type", "application/json")
if data.CertificateChain != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
data.CertificateChain = encoded
@@ -692,16 +690,7 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
data.PrivateKey = ""
}
err := json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Failed to marshal json with TLS status: %s",
err,
)
}
_ = aghhttp.WriteJSONResponse(w, r, data)
}
// registerWebHandlers registers HTTP handlers for TLS configuration

View File

@@ -278,11 +278,11 @@ func upgradeSchema4to5(diskConf yobj) error {
log.Fatalf("Can't use password \"%s\": bcrypt.GenerateFromPassword: %s", passStr, err)
return nil
}
u := User{
u := webUser{
Name: nameStr,
PasswordHash: string(hash),
}
users := []User{u}
users := []webUser{u}
diskConf["users"] = users
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
@@ -160,7 +161,7 @@ func assertEqualExcept(t *testing.T, oldConf, newConf yobj, oldKeys, newKeys []s
}
func testDiskConf(schemaVersion int) (diskConf yobj) {
filters := []filter{{
filters := []filtering.FilterYAML{{
URL: "https://filters.adtidy.org/android/filters/111_optimized.txt",
Name: "Latvian filter",
RulesCount: 100,

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