Compare commits
2 Commits
v0.108.0-b
...
2926-lla-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de837e4eec | ||
|
|
e528d2f23b |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
'name': 'build'
|
'name': 'build'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.18.7'
|
'GO_VERSION': '1.18.6'
|
||||||
'NODE_VERSION': '14'
|
'NODE_VERSION': '14'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
'name': 'lint'
|
'name': 'lint'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.18.7'
|
'GO_VERSION': '1.18.6'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
'push':
|
'push':
|
||||||
|
|||||||
152
CHANGELOG.md
152
CHANGELOG.md
@@ -12,129 +12,11 @@ and this project adheres to
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
## [v0.108.0] - TBA (APPROX.)
|
## [v0.108.0] - 2022-12-01 (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
|
### 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
|
- Weaker cipher suites that use the CBC (cipher block chaining) mode of
|
||||||
operation have been disabled ([#2993]).
|
operation have been disabled ([#2993]).
|
||||||
|
|
||||||
@@ -151,7 +33,16 @@ All JSON APIs that expect a body now check if the request actually has
|
|||||||
[#4927]: https://github.com/AdguardTeam/AdGuardHome/issues/4927
|
[#4927]: https://github.com/AdguardTeam/AdGuardHome/issues/4927
|
||||||
[#4930]: https://github.com/AdguardTeam/AdGuardHome/issues/4930
|
[#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
|
[ms-v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/milestone/50?closed=1
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -188,7 +79,7 @@ See also the [v0.107.12 GitHub milestone][ms-v0.107.12].
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version has been updated to prevent the possibility of exploiting the
|
- Go version was updated to prevent the possibility of exploiting the
|
||||||
CVE-2022-27664 and CVE-2022-32190 Go vulnerabilities fixed in
|
CVE-2022-27664 and CVE-2022-32190 Go vulnerabilities fixed in
|
||||||
[Go 1.18.6][go-1.18.6].
|
[Go 1.18.6][go-1.18.6].
|
||||||
|
|
||||||
@@ -309,7 +200,7 @@ See also the [v0.107.9 GitHub milestone][ms-v0.107.9].
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version has been updated to prevent the possibility of exploiting the
|
- Go version was 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
|
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
|
support has also been removed, as it has reached end of life and will not
|
||||||
receive security updates.
|
receive security updates.
|
||||||
@@ -352,7 +243,7 @@ See also the [v0.107.8 GitHub milestone][ms-v0.107.8].
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version has been updated to prevent the possibility of exploiting the
|
- Go version was updated to prevent the possibility of exploiting the
|
||||||
CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, and other Go vulnerabilities
|
CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, and other Go vulnerabilities
|
||||||
fixed in [Go 1.17.12][go-1.17.12].
|
fixed in [Go 1.17.12][go-1.17.12].
|
||||||
|
|
||||||
@@ -388,7 +279,7 @@ See also the [v0.107.7 GitHub milestone][ms-v0.107.7].
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version has been updated to prevent the possibility of exploiting the
|
- Go version was updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and
|
[CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and
|
||||||
[CVE-2022-29804] Go vulnerabilities.
|
[CVE-2022-29804] Go vulnerabilities.
|
||||||
- Enforced password strength policy ([#3503]).
|
- Enforced password strength policy ([#3503]).
|
||||||
@@ -545,7 +436,7 @@ See also the [v0.107.6 GitHub milestone][ms-v0.107.6].
|
|||||||
### Security
|
### Security
|
||||||
|
|
||||||
- `User-Agent` HTTP header removed from outgoing DNS-over-HTTPS requests.
|
- `User-Agent` HTTP header removed from outgoing DNS-over-HTTPS requests.
|
||||||
- Go version has been updated to prevent the possibility of exploiting the
|
- Go version was updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-24675], [CVE-2022-27536], and [CVE-2022-28327] Go vulnerabilities.
|
[CVE-2022-24675], [CVE-2022-27536], and [CVE-2022-28327] Go vulnerabilities.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -600,7 +491,7 @@ were resolved.
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version has been updated to prevent the possibility of exploiting the
|
- Go version was updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-24921] Go vulnerability.
|
[CVE-2022-24921] Go vulnerability.
|
||||||
|
|
||||||
[CVE-2022-24921]: https://www.cvedetails.com/cve/CVE-2022-24921
|
[CVE-2022-24921]: https://www.cvedetails.com/cve/CVE-2022-24921
|
||||||
@@ -613,7 +504,7 @@ See also the [v0.107.4 GitHub milestone][ms-v0.107.4].
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version has been updated to prevent the possibility of exploiting the
|
- Go version was updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-23806], [CVE-2022-23772], and [CVE-2022-23773] Go vulnerabilities.
|
[CVE-2022-23806], [CVE-2022-23772], and [CVE-2022-23773] Go vulnerabilities.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -1350,14 +1241,11 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...HEAD
|
||||||
[v0.107.17]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...v0.107.17
|
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...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.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.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
|
[v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -34,7 +34,7 @@ YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
|
|||||||
--ignore-engines --ignore-optional --ignore-platform\
|
--ignore-engines --ignore-optional --ignore-platform\
|
||||||
--ignore-scripts
|
--ignore-scripts
|
||||||
|
|
||||||
NEXTAPI = 0
|
V1API = 0
|
||||||
|
|
||||||
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
||||||
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
||||||
@@ -63,7 +63,7 @@ ENV = env\
|
|||||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||||
RACE='$(RACE)'\
|
RACE='$(RACE)'\
|
||||||
SIGN='$(SIGN)'\
|
SIGN='$(SIGN)'\
|
||||||
NEXTAPI='$(NEXTAPI)'\
|
V1API='$(V1API)'\
|
||||||
VERBOSE='$(VERBOSE)'\
|
VERBOSE='$(VERBOSE)'\
|
||||||
VERSION='$(VERSION)'\
|
VERSION='$(VERSION)'\
|
||||||
|
|
||||||
|
|||||||
18
SECURITY.md
18
SECURITY.md
@@ -1,18 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# Make sure to sync any changes with the branch overrides below.
|
# Make sure to sync any changes with the branch overrides below.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'edge'
|
'channel': 'edge'
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
- 'Build frontend':
|
- 'Build frontend':
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
# need to build a few of these.
|
# need to build a few of these.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'beta'
|
'channel': 'beta'
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
||||||
# release-vX.Y.Z branches are the branches from which the actual final release
|
# release-vX.Y.Z branches are the branches from which the actual final release
|
||||||
# is built.
|
# is built.
|
||||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||||
@@ -337,4 +337,4 @@
|
|||||||
# are the ones that actually get released.
|
# are the ones that actually get released.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'release'
|
'channel': 'release'
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
'key': 'AHBRTSPECS'
|
'key': 'AHBRTSPECS'
|
||||||
'name': 'AdGuard Home - Build and run tests'
|
'name': 'AdGuard Home - Build and run tests'
|
||||||
'variables':
|
'variables':
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
- 'Tests':
|
- 'Tests':
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Бацькоўскі кантроль",
|
"parental_control": "Бацькоўскі кантроль",
|
||||||
"safe_browsing": "Бяспечны інтэрнэт",
|
"safe_browsing": "Бяспечны інтэрнэт",
|
||||||
"served_from_cache": "{{value}} <i>(атрымана з кэша)</i>",
|
"served_from_cache": "{{value}} <i>(атрымана з кэша)</i>",
|
||||||
"form_error_password_length": "Пароль павінен быць не менш за {{value}} сімвалаў",
|
"form_error_password_length": "Пароль павінен быць не менш за {{value}} сімвалаў"
|
||||||
"anonymizer_notification": "<0>Заўвага:</0> Ананімізацыя IP уключана. Вы можаце адключыць яго ў <1>Агульных наладах</1> ."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Rodičovská ochrana",
|
"parental_control": "Rodičovská ochrana",
|
||||||
"safe_browsing": "Bezpečné prohlížení",
|
"safe_browsing": "Bezpečné prohlížení",
|
||||||
"served_from_cache": "{{value}} <i>(převzato z mezipaměti)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Forældrekontrol",
|
"parental_control": "Forældrekontrol",
|
||||||
"safe_browsing": "Sikker Browsing",
|
"safe_browsing": "Sikker Browsing",
|
||||||
"served_from_cache": "{{value}} <i>(leveret fra cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Kindersicherung",
|
"parental_control": "Kindersicherung",
|
||||||
"safe_browsing": "Internetsicherheit",
|
"safe_browsing": "Internetsicherheit",
|
||||||
"served_from_cache": "{{value}} <i>(aus dem Cache abgerufen)</i>",
|
"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."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,6 @@
|
|||||||
"example_upstream_udp": "regular DNS (over UDP, hostname);",
|
"example_upstream_udp": "regular DNS (over UDP, hostname);",
|
||||||
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>;",
|
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>;",
|
||||||
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</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_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_sdns": "<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;",
|
||||||
"example_upstream_tcp": "regular DNS (over TCP);",
|
"example_upstream_tcp": "regular DNS (over TCP);",
|
||||||
@@ -606,7 +605,7 @@
|
|||||||
"blocklist": "Blocklist",
|
"blocklist": "Blocklist",
|
||||||
"milliseconds_abbreviation": "ms",
|
"milliseconds_abbreviation": "ms",
|
||||||
"cache_size": "Cache size",
|
"cache_size": "Cache size",
|
||||||
"cache_size_desc": "DNS cache size (in bytes). To disable caching, leave empty.",
|
"cache_size_desc": "DNS cache size (in bytes).",
|
||||||
"cache_ttl_min_override": "Override minimum TTL",
|
"cache_ttl_min_override": "Override minimum TTL",
|
||||||
"cache_ttl_max_override": "Override maximum TTL",
|
"cache_ttl_max_override": "Override maximum TTL",
|
||||||
"enter_cache_size": "Enter cache size (bytes)",
|
"enter_cache_size": "Enter cache size (bytes)",
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Control parental",
|
"parental_control": "Control parental",
|
||||||
"safe_browsing": "Navegación segura",
|
"safe_browsing": "Navegación segura",
|
||||||
"served_from_cache": "{{value}} <i>(servido desde la caché)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Lapsilukko",
|
"parental_control": "Lapsilukko",
|
||||||
"safe_browsing": "Turvallinen selaus",
|
"safe_browsing": "Turvallinen selaus",
|
||||||
"served_from_cache": "{{value}} <i>(jaettu välimuistista)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Contrôle parental",
|
"parental_control": "Contrôle parental",
|
||||||
"safe_browsing": "Navigation sécurisée",
|
"safe_browsing": "Navigation sécurisée",
|
||||||
"served_from_cache": "{{value}} <i>(depuis le cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Roditeljska zaštita",
|
"parental_control": "Roditeljska zaštita",
|
||||||
"safe_browsing": "Sigurno surfanje",
|
"safe_browsing": "Sigurno surfanje",
|
||||||
"served_from_cache": "{{value}} <i>(dohvaćeno iz predmemorije)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Szülői felügyelet",
|
"parental_control": "Szülői felügyelet",
|
||||||
"safe_browsing": "Biztonságos böngészés",
|
"safe_browsing": "Biztonságos böngészés",
|
||||||
"served_from_cache": "{{value}} <i>(gyorsítótárból kiszolgálva)</i>",
|
"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> ."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Kontrol Orang Tua",
|
"parental_control": "Kontrol Orang Tua",
|
||||||
"safe_browsing": "Penjelajahan Aman",
|
"safe_browsing": "Penjelajahan Aman",
|
||||||
"served_from_cache": "{{value}} <i>(disajikan dari cache)</i>",
|
"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> ."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Controllo Parentale",
|
"parental_control": "Controllo Parentale",
|
||||||
"safe_browsing": "Navigazione Sicura",
|
"safe_browsing": "Navigazione Sicura",
|
||||||
"served_from_cache": "{{value}} <i>(fornito dalla cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "ペアレンタルコントロール",
|
"parental_control": "ペアレンタルコントロール",
|
||||||
"safe_browsing": "セーフブラウジング",
|
"safe_browsing": "セーフブラウジング",
|
||||||
"served_from_cache": "{{value}} <i>(キャッシュから応答)</i>",
|
"served_from_cache": "{{value}} <i>(キャッシュから応答)</i>",
|
||||||
"form_error_password_length": "パスワードは{{value}}文字以上にしてください",
|
"form_error_password_length": "パスワードは{{value}}文字以上にしてください"
|
||||||
"anonymizer_notification": "【<0>注意</0>】IPの匿名化が有効になっています。 <1>一般設定</1>で無効にできます。"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "자녀 보호",
|
"parental_control": "자녀 보호",
|
||||||
"safe_browsing": "세이프 브라우징",
|
"safe_browsing": "세이프 브라우징",
|
||||||
"served_from_cache": "{{value}} <i>(캐시에서 제공)</i>",
|
"served_from_cache": "{{value}} <i>(캐시에서 제공)</i>",
|
||||||
"form_error_password_length": "비밀번호는 {{value}}자 이상이어야 합니다",
|
"form_error_password_length": "비밀번호는 {{value}}자 이상이어야 합니다"
|
||||||
"anonymizer_notification": "<0>참고:</0> IP 익명화가 활성화되었습니다. <1>일반 설정</1>에서 비활성화할 수 있습니다."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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_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_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-aanvragen van je systeem standaard door AdGuard Home verwerkt.",
|
"autofix_warning_result": "Als gevolg hiervan worden alle DNS-verzoeken van je systeem standaard door AdGuard Home verwerkt.",
|
||||||
"tags_title": "Labels",
|
"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>.",
|
"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",
|
"form_select_tags": "Client tags selecteren",
|
||||||
@@ -628,13 +628,12 @@
|
|||||||
"original_response": "Oorspronkelijke reactie",
|
"original_response": "Oorspronkelijke reactie",
|
||||||
"click_to_view_queries": "Klik om queries te bekijken",
|
"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.",
|
"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-aanvragen van deze cliënt laten vervallen.",
|
"adg_will_drop_dns_queries": "AdGuard Home zal alle DNS-verzoeken van deze cliënt laten vervallen.",
|
||||||
"filter_allowlist": "WAARSCHUWING: Deze actie zal ook de regel \"{{disallowed_rule}}\" uitsluiten van de lijst met toegestane clients.",
|
"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.",
|
"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",
|
"use_saved_key": "De eerder opgeslagen sleutel gebruiken",
|
||||||
"parental_control": "Ouderlijk toezicht",
|
"parental_control": "Ouderlijk toezicht",
|
||||||
"safe_browsing": "Veilig browsen",
|
"safe_browsing": "Veilig browsen",
|
||||||
"served_from_cache": "{{value}} <i>(geleverd vanuit cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Kontrola rodzicielska",
|
"parental_control": "Kontrola rodzicielska",
|
||||||
"safe_browsing": "Bezpieczne przeglądanie",
|
"safe_browsing": "Bezpieczne przeglądanie",
|
||||||
"served_from_cache": "{{value}} <i>(podawane z pamięci podręcznej)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Controle parental",
|
"parental_control": "Controle parental",
|
||||||
"safe_browsing": "Navegação segura",
|
"safe_browsing": "Navegação segura",
|
||||||
"served_from_cache": "{{value}} <i>(servido do cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Controlo parental",
|
"parental_control": "Controlo parental",
|
||||||
"safe_browsing": "Navegação segura",
|
"safe_browsing": "Navegação segura",
|
||||||
"served_from_cache": "{{value}} <i>(servido do cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Control Parental",
|
"parental_control": "Control Parental",
|
||||||
"safe_browsing": "Navigare în siguranță",
|
"safe_browsing": "Navigare în siguranță",
|
||||||
"served_from_cache": "{{value}} <i>(furnizat din cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Родительский контроль",
|
"parental_control": "Родительский контроль",
|
||||||
"safe_browsing": "Безопасный интернет",
|
"safe_browsing": "Безопасный интернет",
|
||||||
"served_from_cache": "{{value}} <i>(получено из кеша)</i>",
|
"served_from_cache": "{{value}} <i>(получено из кеша)</i>",
|
||||||
"form_error_password_length": "Пароль должен быть длиной не меньше {{value}} символов",
|
"form_error_password_length": "Пароль должен быть длиной не меньше {{value}} символов"
|
||||||
"anonymizer_notification": "<0>Внимание:</0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки</1>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Rodičovská kontrola",
|
"parental_control": "Rodičovská kontrola",
|
||||||
"safe_browsing": "Bezpečné prehliadanie",
|
"safe_browsing": "Bezpečné prehliadanie",
|
||||||
"served_from_cache": "{{value}} <i>(prevzatá z cache pamäte)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Starševski nadzor",
|
"parental_control": "Starševski nadzor",
|
||||||
"safe_browsing": "Varno brskanje",
|
"safe_browsing": "Varno brskanje",
|
||||||
"served_from_cache": "{{value}} <i>(postreženo iz predpomnilnika)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Roditeljska kontrola",
|
"parental_control": "Roditeljska kontrola",
|
||||||
"safe_browsing": "Sigurno pregledanje",
|
"safe_browsing": "Sigurno pregledanje",
|
||||||
"served_from_cache": "{{value}} <i>(posluženo iz predmemorije)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Föräldrakontroll",
|
"parental_control": "Föräldrakontroll",
|
||||||
"safe_browsing": "Säker surfning",
|
"safe_browsing": "Säker surfning",
|
||||||
"served_from_cache": "{{value}} <i>(levereras från cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,7 +368,7 @@
|
|||||||
"encryption_server_enter": "Alan adınızı girin",
|
"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_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": "Otomatik olarak HTTPS'e yönlendir",
|
||||||
"encryption_redirect_desc": "İşaretlenirse, AdGuard Home sizi otomatik olarak HTTP adresinden HTTPS adreslerine yönlendirecektir.",
|
"encryption_redirect_desc": "Etkinleştirirseniz, AdGuard Home sizi HTTP adresi yerine HTTPS adresine yönlendirir.",
|
||||||
"encryption_https": "HTTPS bağlantı noktası",
|
"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_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ı",
|
"encryption_dot": "DNS-over-TLS bağlantı noktası",
|
||||||
@@ -408,7 +408,7 @@
|
|||||||
"fix": "Düzelt",
|
"fix": "Düzelt",
|
||||||
"dns_providers": "Aralarından seçim yapabileceğiniz, bilinen <0>DNS sağlayıcıların listesi</0>.",
|
"dns_providers": "Aralarından seçim yapabileceğiniz, bilinen <0>DNS sağlayıcıların listesi</0>.",
|
||||||
"update_now": "Şimdi güncelle",
|
"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ı izleyin</a>.",
|
"update_failed": "Otomatik güncelleme başarısız oldu. Elle güncellemek için lütfen <a>bu adımları uygulayın</a>.",
|
||||||
"manual_update": "Elle güncellemek için lütfen <a>bu adımları uygulayın</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",
|
"processing_update": "Lütfen bekleyin, AdGuard Home güncelleniyor",
|
||||||
"clients_title": "Kalıcı istemciler",
|
"clients_title": "Kalıcı istemciler",
|
||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Ebeveyn Denetimi",
|
"parental_control": "Ebeveyn Denetimi",
|
||||||
"safe_browsing": "Güvenli Gezinti",
|
"safe_browsing": "Güvenli Gezinti",
|
||||||
"served_from_cache": "{{value}} <i>(önbellekten kullanıldı)</i>",
|
"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."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Батьківський контроль",
|
"parental_control": "Батьківський контроль",
|
||||||
"safe_browsing": "Безпечний перегляд",
|
"safe_browsing": "Безпечний перегляд",
|
||||||
"served_from_cache": "{{value}} <i>(отримано з кешу)</i>",
|
"served_from_cache": "{{value}} <i>(отримано з кешу)</i>",
|
||||||
"form_error_password_length": "Пароль мусить мати принаймні {{value}} символів",
|
"form_error_password_length": "Пароль мусить мати принаймні {{value}} символів"
|
||||||
"anonymizer_notification": "<0>Примітка:</0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування</1> ."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "Quản lý của phụ huynh",
|
"parental_control": "Quản lý của phụ huynh",
|
||||||
"safe_browsing": "Duyệt web an toàn",
|
"safe_browsing": "Duyệt web an toàn",
|
||||||
"served_from_cache": "{{value}} <i>(được phục vụ từ bộ nhớ cache)</i>",
|
"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>."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "家长控制",
|
"parental_control": "家长控制",
|
||||||
"safe_browsing": "安全浏览",
|
"safe_browsing": "安全浏览",
|
||||||
"served_from_cache": "{{value}}<i>(由缓存提供)</i>",
|
"served_from_cache": "{{value}}<i>(由缓存提供)</i>",
|
||||||
"form_error_password_length": "密码必须至少有 {{value}} 个字符",
|
"form_error_password_length": "密码必须至少有 {{value}} 个字符"
|
||||||
"anonymizer_notification": "<0>注意:</0> IP 匿名化已启用。您可以在<1>常规设置</1>中禁用它。"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,5 @@
|
|||||||
"parental_control": "家長控制",
|
"parental_control": "家長控制",
|
||||||
"safe_browsing": "安全瀏覽",
|
"safe_browsing": "安全瀏覽",
|
||||||
"served_from_cache": "{{value}} <i>(由快取提供)</i>",
|
"served_from_cache": "{{value}} <i>(由快取提供)</i>",
|
||||||
"form_error_password_length": "密碼必須為至少長 {{value}} 個字元",
|
"form_error_password_length": "密碼必須為至少長 {{value}} 個字元"
|
||||||
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
|
|||||||
export const setRules = (rules) => async (dispatch) => {
|
export const setRules = (rules) => async (dispatch) => {
|
||||||
dispatch(setRulesRequest());
|
dispatch(setRulesRequest());
|
||||||
try {
|
try {
|
||||||
const normalizedRules = {
|
const normalizedRules = normalizeRulesTextarea(rules);
|
||||||
rules: normalizeRulesTextarea(rules)?.split('\n'),
|
|
||||||
};
|
|
||||||
await apiClient.setRules(normalizedRules);
|
await apiClient.setRules(normalizedRules);
|
||||||
dispatch(addSuccessToast('updated_custom_filtering_toast'));
|
dispatch(addSuccessToast('updated_custom_filtering_toast'));
|
||||||
dispatch(setRulesSuccess());
|
dispatch(setRulesSuccess());
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');
|
|||||||
export const changeLanguage = (lang) => async (dispatch) => {
|
export const changeLanguage = (lang) => async (dispatch) => {
|
||||||
dispatch(changeLanguageRequest());
|
dispatch(changeLanguageRequest());
|
||||||
try {
|
try {
|
||||||
await apiClient.changeLanguage({ language: lang });
|
await apiClient.changeLanguage(lang);
|
||||||
dispatch(changeLanguageSuccess());
|
dispatch(changeLanguageSuccess());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
@@ -370,8 +370,8 @@ export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS');
|
|||||||
export const getLanguage = () => async (dispatch) => {
|
export const getLanguage = () => async (dispatch) => {
|
||||||
dispatch(getLanguageRequest());
|
dispatch(getLanguageRequest());
|
||||||
try {
|
try {
|
||||||
const langSettings = await apiClient.getCurrentLanguage();
|
const language = await apiClient.getCurrentLanguage();
|
||||||
dispatch(getLanguageSuccess(langSettings.language));
|
dispatch(getLanguageSuccess(language));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(getLanguageFailure());
|
dispatch(getLanguageFailure());
|
||||||
@@ -421,10 +421,7 @@ export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
|
|||||||
export const findActiveDhcp = (name) => async (dispatch, getState) => {
|
export const findActiveDhcp = (name) => async (dispatch, getState) => {
|
||||||
dispatch(findActiveDhcpRequest());
|
dispatch(findActiveDhcpRequest());
|
||||||
try {
|
try {
|
||||||
const req = {
|
const activeDhcp = await apiClient.findActiveDhcp(name);
|
||||||
interface: name,
|
|
||||||
};
|
|
||||||
const activeDhcp = await apiClient.findActiveDhcp(req);
|
|
||||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
dispatch(findActiveDhcpSuccess(activeDhcp));
|
||||||
const { check, interface_name, interfaces } = getState().dhcp;
|
const { check, interface_name, interfaces } = getState().dhcp;
|
||||||
const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name;
|
const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name;
|
||||||
|
|||||||
@@ -10,17 +10,11 @@ class Api {
|
|||||||
async makeRequest(path, method = 'POST', config) {
|
async makeRequest(path, method = 'POST', config) {
|
||||||
const url = `${this.baseUrl}/${path}`;
|
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 {
|
try {
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
...axiosConfig,
|
...config,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -61,6 +55,7 @@ class Api {
|
|||||||
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
|
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
|
||||||
const config = {
|
const config = {
|
||||||
data: servers,
|
data: servers,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, config);
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
@@ -69,6 +64,7 @@ class Api {
|
|||||||
const { path, method } = this.GLOBAL_VERSION;
|
const { path, method } = this.GLOBAL_VERSION;
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, config);
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
@@ -104,6 +100,7 @@ class Api {
|
|||||||
const { path, method } = this.FILTERING_REFRESH;
|
const { path, method } = this.FILTERING_REFRESH;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
@@ -113,6 +110,7 @@ class Api {
|
|||||||
const { path, method } = this.FILTERING_ADD_FILTER;
|
const { path, method } = this.FILTERING_ADD_FILTER;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
@@ -122,6 +120,7 @@ class Api {
|
|||||||
const { path, method } = this.FILTERING_REMOVE_FILTER;
|
const { path, method } = this.FILTERING_REMOVE_FILTER;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
@@ -131,6 +130,7 @@ class Api {
|
|||||||
const { path, method } = this.FILTERING_SET_RULES;
|
const { path, method } = this.FILTERING_SET_RULES;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: rules,
|
data: rules,
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -139,6 +139,7 @@ class Api {
|
|||||||
const { path, method } = this.FILTERING_CONFIG;
|
const { path, method } = this.FILTERING_CONFIG;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -147,6 +148,7 @@ class Api {
|
|||||||
const { path, method } = this.FILTERING_SET_URL;
|
const { path, method } = this.FILTERING_SET_URL;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -171,7 +173,12 @@ class Api {
|
|||||||
|
|
||||||
enableParentalControl() {
|
enableParentalControl() {
|
||||||
const { path, method } = this.PARENTAL_ENABLE;
|
const { path, method } = this.PARENTAL_ENABLE;
|
||||||
return this.makeRequest(path, method);
|
const parameter = 'sensitivity=TEEN'; // this parameter TEEN is hardcoded
|
||||||
|
const config = {
|
||||||
|
data: parameter,
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
disableParentalControl() {
|
disableParentalControl() {
|
||||||
@@ -233,10 +240,11 @@ class Api {
|
|||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeLanguage(config) {
|
changeLanguage(lang) {
|
||||||
const { path, method } = this.CHANGE_LANGUAGE;
|
const { path, method } = this.CHANGE_LANGUAGE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: lang,
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -272,14 +280,16 @@ class Api {
|
|||||||
const { path, method } = this.DHCP_SET_CONFIG;
|
const { path, method } = this.DHCP_SET_CONFIG;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
findActiveDhcp(req) {
|
findActiveDhcp(name) {
|
||||||
const { path, method } = this.DHCP_FIND_ACTIVE;
|
const { path, method } = this.DHCP_FIND_ACTIVE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: req,
|
data: name,
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -288,6 +298,7 @@ class Api {
|
|||||||
const { path, method } = this.DHCP_ADD_STATIC_LEASE;
|
const { path, method } = this.DHCP_ADD_STATIC_LEASE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -296,6 +307,7 @@ class Api {
|
|||||||
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
|
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -326,6 +338,7 @@ class Api {
|
|||||||
const { path, method } = this.INSTALL_CONFIGURE;
|
const { path, method } = this.INSTALL_CONFIGURE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -334,6 +347,7 @@ class Api {
|
|||||||
const { path, method } = this.INSTALL_CHECK_CONFIG;
|
const { path, method } = this.INSTALL_CHECK_CONFIG;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -354,6 +368,7 @@ class Api {
|
|||||||
const { path, method } = this.TLS_CONFIG;
|
const { path, method } = this.TLS_CONFIG;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -362,6 +377,7 @@ class Api {
|
|||||||
const { path, method } = this.TLS_VALIDATE;
|
const { path, method } = this.TLS_VALIDATE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -386,6 +402,7 @@ class Api {
|
|||||||
const { path, method } = this.ADD_CLIENT;
|
const { path, method } = this.ADD_CLIENT;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -394,6 +411,7 @@ class Api {
|
|||||||
const { path, method } = this.DELETE_CLIENT;
|
const { path, method } = this.DELETE_CLIENT;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -402,6 +420,7 @@ class Api {
|
|||||||
const { path, method } = this.UPDATE_CLIENT;
|
const { path, method } = this.UPDATE_CLIENT;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -426,6 +445,7 @@ class Api {
|
|||||||
const { path, method } = this.ACCESS_SET;
|
const { path, method } = this.ACCESS_SET;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -446,6 +466,7 @@ class Api {
|
|||||||
const { path, method } = this.REWRITE_ADD;
|
const { path, method } = this.REWRITE_ADD;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -454,6 +475,7 @@ class Api {
|
|||||||
const { path, method } = this.REWRITE_DELETE;
|
const { path, method } = this.REWRITE_DELETE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -479,6 +501,7 @@ class Api {
|
|||||||
const { path, method } = this.BLOCKED_SERVICES_SET;
|
const { path, method } = this.BLOCKED_SERVICES_SET;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
@@ -506,6 +529,7 @@ class Api {
|
|||||||
const { path, method } = this.STATS_CONFIG;
|
const { path, method } = this.STATS_CONFIG;
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, config);
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
@@ -541,6 +565,7 @@ class Api {
|
|||||||
const { path, method } = this.QUERY_LOG_CONFIG;
|
const { path, method } = this.QUERY_LOG_CONFIG;
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, config);
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
@@ -557,6 +582,7 @@ class Api {
|
|||||||
const { path, method } = this.LOGIN;
|
const { path, method } = this.LOGIN;
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, config);
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
@@ -583,6 +609,7 @@ class Api {
|
|||||||
const { path, method } = this.SET_DNS_CONFIG;
|
const { path, method } = this.SET_DNS_CONFIG;
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, config);
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,22 +57,6 @@ const Examples = (props) => (
|
|||||||
example_upstream_doh
|
example_upstream_doh
|
||||||
</Trans>
|
</Trans>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<code>quic://unfiltered.adguard-dns.com</code>: <Trans
|
<code>quic://unfiltered.adguard-dns.com</code>: <Trans
|
||||||
components={[
|
components={[
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.45.2
|
github.com/AdguardTeam/dnsproxy v0.44.0
|
||||||
github.com/AdguardTeam/golibs v0.10.9
|
github.com/AdguardTeam/golibs v0.10.9
|
||||||
github.com/AdguardTeam/urlfilter v0.16.0
|
github.com/AdguardTeam/urlfilter v0.16.0
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
@@ -18,7 +18,7 @@ require (
|
|||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
|
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
|
||||||
github.com/kardianos/service v1.2.1
|
github.com/kardianos/service v1.2.1
|
||||||
github.com/lucas-clemente/quic-go v0.29.1
|
github.com/lucas-clemente/quic-go v0.29.0
|
||||||
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
||||||
github.com/mdlayher/netlink v1.6.0
|
github.com/mdlayher/netlink v1.6.0
|
||||||
// TODO(a.garipov): This package is deprecated; find a new one or use
|
// 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/stretchr/testify v1.8.0
|
||||||
github.com/ti-mo/netfilter v0.4.0
|
github.com/ti-mo/netfilter v0.4.0
|
||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
|
||||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9
|
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91
|
||||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
|
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
@@ -43,12 +43,10 @@ require (
|
|||||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // 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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/josharian/native v1.0.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-18 v0.1.2 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
||||||
github.com/mdlayher/packet v1.0.0 // indirect
|
github.com/mdlayher/packet v1.0.0 // indirect
|
||||||
@@ -59,7 +57,7 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e // indirect
|
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 // indirect
|
||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/tools v0.1.12 // indirect
|
golang.org/x/tools v0.1.12 // indirect
|
||||||
|
|||||||
35
go.sum
35
go.sum
@@ -1,5 +1,5 @@
|
|||||||
github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko=
|
github.com/AdguardTeam/dnsproxy v0.44.0 h1:JzIxEXF4OyJq4wZVHeZkM1af4VfuwcgrUzjgdBGljsE=
|
||||||
github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE=
|
github.com/AdguardTeam/dnsproxy v0.44.0/go.mod h1:HsxYYW/bC8uo+4eX9pRW21hFD6gWZdrvcfBb1R6/AzU=
|
||||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
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.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||||
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
|
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
|
||||||
@@ -23,8 +23,6 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O
|
|||||||
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
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 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -90,10 +88,8 @@ 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
|
github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE=
|
||||||
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
|
github.com/lucas-clemente/quic-go v0.29.0/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 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
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=
|
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
|
||||||
@@ -129,7 +125,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
|||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
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.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.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 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
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=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
@@ -173,16 +168,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-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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/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-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
|
||||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
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.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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/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.20220922195421-2adab6b8c60e h1:WhB000cGjOfbJiedMGvJkMTclI18VD69w27k+sceql8=
|
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -194,7 +189,6 @@ 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-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-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-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-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-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
@@ -206,8 +200,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-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-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-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
|
||||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/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-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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -230,7 +224,6 @@ 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-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-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-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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -254,8 +247,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-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-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-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 h1:C1tElbkWrsSkn3IRl1GCW/gETw1TywWIPgwZtXTZbYg=
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,10 @@
|
|||||||
package aghhttp
|
package aghhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,50 +31,6 @@ func OK(w http.ResponseWriter) {
|
|||||||
// Error writes formatted message to w and also logs it.
|
// Error writes formatted message to w and also logs it.
|
||||||
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
|
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
|
||||||
text := fmt.Sprintf(format, args...)
|
text := fmt.Sprintf(format, args...)
|
||||||
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text)
|
log.Error("%s %s: %s", r.Method, r.URL, text)
|
||||||
http.Error(w, text, code)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/urlfilter"
|
"github.com/AdguardTeam/urlfilter"
|
||||||
@@ -163,9 +163,15 @@ func TestHostsContainer_refresh(t *testing.T) {
|
|||||||
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
upd, ok := aghchan.MustReceive(hc.Upd(), 1*time.Second)
|
var ok bool
|
||||||
require.True(t, ok)
|
var upd *netutil.IPMap
|
||||||
require.NotNil(t, upd)
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, 1, upd.Len())
|
assert.Equal(t, 1, upd.Len())
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
@@ -31,6 +32,12 @@ var (
|
|||||||
// the IP being static is available.
|
// the IP being static is available.
|
||||||
const ErrNoStaticIPInfo errors.Error = "no information about static ip"
|
const ErrNoStaticIPInfo errors.Error = "no information about static ip"
|
||||||
|
|
||||||
|
// IPv4Localhost returns 127.0.0.1, which returns true for [netip.Addr.Is4].
|
||||||
|
func IPv4Localhost() (ip netip.Addr) { return netip.AddrFrom4([4]byte{0: 127, 3: 1}) }
|
||||||
|
|
||||||
|
// IPv6Localhost returns ::1, which returns true for [netip.Addr.Is6].
|
||||||
|
func IPv6Localhost() (ip netip.Addr) { return netip.AddrFrom16([16]byte{15: 1}) }
|
||||||
|
|
||||||
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
||||||
// If it can't give a definitive answer, it returns false and an error for which
|
// If it can't give a definitive answer, it returns false and an error for which
|
||||||
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
||||||
@@ -47,26 +54,31 @@ func IfaceSetStaticIP(ifaceName string) (err error) {
|
|||||||
//
|
//
|
||||||
// TODO(e.burkov): Investigate if the gateway address may be fetched in another
|
// TODO(e.burkov): Investigate if the gateway address may be fetched in another
|
||||||
// way since not every machine has the software installed.
|
// way since not every machine has the software installed.
|
||||||
func GatewayIP(ifaceName string) (ip net.IP) {
|
func GatewayIP(ifaceName string) (ip netip.Addr) {
|
||||||
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
|
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("%s", err)
|
log.Debug("%s", err)
|
||||||
|
|
||||||
return nil
|
return ip
|
||||||
} else if code != 0 {
|
} else if code != 0 {
|
||||||
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
|
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
|
||||||
|
|
||||||
return nil
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := bytes.Fields(out)
|
fields := bytes.Fields(out)
|
||||||
// The meaningful "ip route" command output should contain the word
|
// The meaningful "ip route" command output should contain the word
|
||||||
// "default" at first field and default gateway IP address at third field.
|
// "default" at first field and default gateway IP address at third field.
|
||||||
if len(fields) < 3 || string(fields[0]) != "default" {
|
if len(fields) < 3 || string(fields[0]) != "default" {
|
||||||
return nil
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
return net.ParseIP(string(fields[2]))
|
ip, err = netip.ParseAddr(string(fields[2]))
|
||||||
|
if err != nil {
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanBindPrivilegedPorts checks if current process can bind to privileged
|
// CanBindPrivilegedPorts checks if current process can bind to privileged
|
||||||
@@ -78,9 +90,9 @@ func CanBindPrivilegedPorts() (can bool, err error) {
|
|||||||
// NetInterface represents an entry of network interfaces map.
|
// NetInterface represents an entry of network interfaces map.
|
||||||
type NetInterface struct {
|
type NetInterface struct {
|
||||||
// Addresses are the network interface addresses.
|
// Addresses are the network interface addresses.
|
||||||
Addresses []net.IP `json:"ip_addresses,omitempty"`
|
Addresses []netip.Addr `json:"ip_addresses,omitempty"`
|
||||||
// Subnets are the IP networks for this network interface.
|
// Subnets are the IP networks for this network interface.
|
||||||
Subnets []*net.IPNet `json:"-"`
|
Subnets []netip.Prefix `json:"-"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
HardwareAddr net.HardwareAddr `json:"hardware_address"`
|
HardwareAddr net.HardwareAddr `json:"hardware_address"`
|
||||||
Flags net.Flags `json:"flags"`
|
Flags net.Flags `json:"flags"`
|
||||||
@@ -101,57 +113,78 @@ func (iface NetInterface) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NetInterfaceFrom(iface *net.Interface) (niface *NetInterface, err error) {
|
||||||
|
niface = &NetInterface{
|
||||||
|
Name: iface.Name,
|
||||||
|
HardwareAddr: iface.HardwareAddr,
|
||||||
|
Flags: iface.Flags,
|
||||||
|
MTU: iface.MTU,
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect network interface addresses.
|
||||||
|
for _, addr := range addrs {
|
||||||
|
n, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
// Should be *net.IPNet, this is weird.
|
||||||
|
return nil, fmt.Errorf("expected %[2]s to be %[1]T, got %[2]T", n, addr)
|
||||||
|
} else if ip4 := n.IP.To4(); ip4 != nil {
|
||||||
|
n.IP = ip4
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, ok := netip.AddrFromSlice(n.IP)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("bad address %s", n.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.IsLinkLocalUnicast() {
|
||||||
|
// Ignore link-local IPv4.
|
||||||
|
if ip.Is4() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip = ip.WithZone(iface.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ones, _ := n.Mask.Size()
|
||||||
|
p := netip.PrefixFrom(ip, ones)
|
||||||
|
|
||||||
|
niface.Addresses = append(niface.Addresses, ip)
|
||||||
|
niface.Subnets = append(niface.Subnets, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return niface, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and
|
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and
|
||||||
// WEB only we do not return link-local addresses here.
|
// WEB only we do not return link-local addresses here.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Can't properly test the function since it's nontrivial to
|
// TODO(e.burkov): Can't properly test the function since it's nontrivial to
|
||||||
// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.
|
// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.
|
||||||
func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
|
func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't get interfaces: %w", err)
|
return nil, fmt.Errorf("getting interfaces: %w", err)
|
||||||
} else if len(ifaces) == 0 {
|
} else if len(ifaces) == 0 {
|
||||||
return nil, errors.Error("couldn't find any legible interface")
|
return nil, errors.Error("no legible interfaces")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, iface := range ifaces {
|
for i := range ifaces {
|
||||||
var addrs []net.Addr
|
var niface *NetInterface
|
||||||
addrs, err = iface.Addrs()
|
niface, err = NetInterfaceFrom(&ifaces[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
|
return nil, err
|
||||||
}
|
} else if len(niface.Addresses) != 0 {
|
||||||
|
// Discard interfaces with no addresses.
|
||||||
netIface := &NetInterface{
|
nifaces = append(nifaces, niface)
|
||||||
MTU: iface.MTU,
|
|
||||||
Name: iface.Name,
|
|
||||||
HardwareAddr: iface.HardwareAddr,
|
|
||||||
Flags: iface.Flags,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect network interface addresses.
|
|
||||||
for _, addr := range addrs {
|
|
||||||
ipNet, ok := addr.(*net.IPNet)
|
|
||||||
if !ok {
|
|
||||||
// Should be net.IPNet, this is weird.
|
|
||||||
return nil, fmt.Errorf("got %s that is not net.IPNet, it is %T", addr, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore link-local.
|
|
||||||
if ipNet.IP.IsLinkLocalUnicast() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
netIface.Addresses = append(netIface.Addresses, ipNet.IP)
|
|
||||||
netIface.Subnets = append(netIface.Subnets, ipNet)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard interfaces with no addresses.
|
|
||||||
if len(netIface.Addresses) != 0 {
|
|
||||||
netIfaces = append(netIfaces, netIface)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return netIfaces, nil
|
return nifaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfaceByIP returns the name of the interface bound to ip.
|
// InterfaceByIP returns the name of the interface bound to ip.
|
||||||
@@ -160,7 +193,7 @@ func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
|
|||||||
// IP address can be shared by multiple interfaces in some configurations.
|
// IP address can be shared by multiple interfaces in some configurations.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
||||||
func InterfaceByIP(ip net.IP) (ifaceName string) {
|
func InterfaceByIP(ip netip.Addr) (ifaceName string) {
|
||||||
ifaces, err := GetValidNetInterfacesForWeb()
|
ifaces, err := GetValidNetInterfacesForWeb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
|
|||||||
|
|
||||||
for _, iface := range ifaces {
|
for _, iface := range ifaces {
|
||||||
for _, addr := range iface.Addresses {
|
for _, addr := range iface.Addresses {
|
||||||
if ip.Equal(addr) {
|
if ip == addr {
|
||||||
return iface.Name
|
return iface.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubnet returns pointer to net.IPNet for the specified interface or nil if
|
// GetSubnet returns the subnet corresponding to the interface of zero prefix if
|
||||||
// the search fails.
|
// the search fails.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
||||||
func GetSubnet(ifaceName string) *net.IPNet {
|
func GetSubnet(ifaceName string) (p netip.Prefix) {
|
||||||
netIfaces, err := GetValidNetInterfacesForWeb()
|
netIfaces, err := GetValidNetInterfacesForWeb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Could not get network interfaces info: %v", err)
|
log.Error("Could not get network interfaces info: %v", err)
|
||||||
return nil
|
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, netIface := range netIfaces {
|
for _, netIface := range netIfaces {
|
||||||
@@ -194,14 +228,14 @@ func GetSubnet(ifaceName string) *net.IPNet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPort checks if the port is available for binding. network is expected
|
// CheckPort checks if the port is available for binding. network is expected
|
||||||
// to be one of "udp" and "tcp".
|
// to be one of "udp" and "tcp".
|
||||||
func CheckPort(network string, ip net.IP, port int) (err error) {
|
func CheckPort(network string, ipp netip.AddrPort) (err error) {
|
||||||
var c io.Closer
|
var c io.Closer
|
||||||
addr := netutil.IPPort{IP: ip, Port: port}.String()
|
addr := ipp.String()
|
||||||
switch network {
|
switch network {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
c, err = net.Listen(network, addr)
|
c, err = net.Listen(network, addr)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
|
|||||||
// interface through dhcpcd.conf.
|
// interface through dhcpcd.conf.
|
||||||
func ifaceSetStaticIP(ifaceName string) (err error) {
|
func ifaceSetStaticIP(ifaceName string) (err error) {
|
||||||
ipNet := GetSubnet(ifaceName)
|
ipNet := GetSubnet(ifaceName)
|
||||||
if ipNet.IP == nil {
|
if !ipNet.Addr().IsValid() {
|
||||||
return errors.Error("can't get IP address")
|
return errors.Error("can't get IP address")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
|
|||||||
|
|
||||||
// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that
|
// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that
|
||||||
// configure the interface to have a static IP.
|
// configure the interface to have a static IP.
|
||||||
func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf string) {
|
func dhcpcdConfIface(ifaceName string, subnet netip.Prefix, gateway netip.Addr) (conf string) {
|
||||||
b := &strings.Builder{}
|
b := &strings.Builder{}
|
||||||
stringutil.WriteToBuilder(
|
stringutil.WriteToBuilder(
|
||||||
b,
|
b,
|
||||||
@@ -183,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri
|
|||||||
" added by AdGuard Home.\ninterface ",
|
" added by AdGuard Home.\ninterface ",
|
||||||
ifaceName,
|
ifaceName,
|
||||||
"\nstatic ip_address=",
|
"\nstatic ip_address=",
|
||||||
ipNet.String(),
|
subnet.String(),
|
||||||
"\n",
|
"\n",
|
||||||
)
|
)
|
||||||
|
|
||||||
if gwIP != nil {
|
if gateway.IsValid() {
|
||||||
stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n")
|
stringutil.WriteToBuilder(b, "static routers=", gateway.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
stringutil.WriteToBuilder(b, "static domain_name_servers=", ipNet.IP.String(), "\n\n")
|
stringutil.WriteToBuilder(b, "static domain_name_servers=", subnet.Addr().String(), "\n\n")
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -93,34 +94,34 @@ func TestGatewayIP(t *testing.T) {
|
|||||||
const cmd = "ip route show dev " + ifaceName
|
const cmd = "ip route show dev " + ifaceName
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
|
||||||
shell mapShell
|
shell mapShell
|
||||||
want net.IP
|
want netip.Addr
|
||||||
|
name string
|
||||||
}{{
|
}{{
|
||||||
name: "success_v4",
|
|
||||||
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
||||||
want: net.IP{1, 2, 3, 4}.To16(),
|
want: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
|
||||||
|
name: "success_v4",
|
||||||
}, {
|
}, {
|
||||||
name: "success_v6",
|
|
||||||
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
||||||
want: net.IP{
|
want: netip.AddrFrom16([16]byte{
|
||||||
0x0, 0x0, 0x0, 0x0,
|
0x0, 0x0, 0x0, 0x0,
|
||||||
0x0, 0x0, 0x0, 0x0,
|
0x0, 0x0, 0x0, 0x0,
|
||||||
0x0, 0x0, 0x0, 0x0,
|
0x0, 0x0, 0x0, 0x0,
|
||||||
0x0, 0x0, 0xFF, 0xFF,
|
0x0, 0x0, 0xFF, 0xFF,
|
||||||
},
|
}),
|
||||||
|
name: "success_v6",
|
||||||
}, {
|
}, {
|
||||||
name: "bad_output",
|
|
||||||
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
||||||
want: nil,
|
want: netip.Addr{},
|
||||||
|
name: "bad_output",
|
||||||
}, {
|
}, {
|
||||||
name: "err_runcmd",
|
|
||||||
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
||||||
want: nil,
|
want: netip.Addr{},
|
||||||
|
name: "err_runcmd",
|
||||||
}, {
|
}, {
|
||||||
name: "bad_code",
|
|
||||||
shell: theOnlyCmd(cmd, 1, "", nil),
|
shell: theOnlyCmd(cmd, 1, "", nil),
|
||||||
want: nil,
|
want: netip.Addr{},
|
||||||
|
name: "bad_code",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -198,17 +199,21 @@ func TestBroadcastFromIPNet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckPort(t *testing.T) {
|
func TestCheckPort(t *testing.T) {
|
||||||
|
laddr := netip.AddrPortFrom(IPv4Localhost(), 0)
|
||||||
|
|
||||||
t.Run("tcp_bound", func(t *testing.T) {
|
t.Run("tcp_bound", func(t *testing.T) {
|
||||||
l, err := net.Listen("tcp", "127.0.0.1:")
|
l, err := net.Listen("tcp", laddr.String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||||
|
|
||||||
ipp := netutil.IPPortFromAddr(l.Addr())
|
addr := l.Addr()
|
||||||
require.NotNil(t, ipp)
|
require.IsType(t, new(net.TCPAddr), addr)
|
||||||
require.NotNil(t, ipp.IP)
|
|
||||||
require.NotZero(t, ipp.Port)
|
|
||||||
|
|
||||||
err = CheckPort("tcp", ipp.IP, ipp.Port)
|
ipp := addr.(*net.TCPAddr).AddrPort()
|
||||||
|
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||||
|
require.NotZero(t, ipp.Port())
|
||||||
|
|
||||||
|
err = CheckPort("tcp", ipp)
|
||||||
target := &net.OpError{}
|
target := &net.OpError{}
|
||||||
require.ErrorAs(t, err, &target)
|
require.ErrorAs(t, err, &target)
|
||||||
|
|
||||||
@@ -216,16 +221,18 @@ func TestCheckPort(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("udp_bound", func(t *testing.T) {
|
t.Run("udp_bound", func(t *testing.T) {
|
||||||
conn, err := net.ListenPacket("udp", "127.0.0.1:")
|
conn, err := net.ListenPacket("udp", laddr.String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testutil.CleanupAndRequireSuccess(t, conn.Close)
|
testutil.CleanupAndRequireSuccess(t, conn.Close)
|
||||||
|
|
||||||
ipp := netutil.IPPortFromAddr(conn.LocalAddr())
|
addr := conn.LocalAddr()
|
||||||
require.NotNil(t, ipp)
|
require.IsType(t, new(net.UDPAddr), addr)
|
||||||
require.NotNil(t, ipp.IP)
|
|
||||||
require.NotZero(t, ipp.Port)
|
|
||||||
|
|
||||||
err = CheckPort("udp", ipp.IP, ipp.Port)
|
ipp := addr.(*net.UDPAddr).AddrPort()
|
||||||
|
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||||
|
require.NotZero(t, ipp.Port())
|
||||||
|
|
||||||
|
err = CheckPort("udp", ipp)
|
||||||
target := &net.OpError{}
|
target := &net.OpError{}
|
||||||
require.ErrorAs(t, err, &target)
|
require.ErrorAs(t, err, &target)
|
||||||
|
|
||||||
@@ -233,12 +240,12 @@ func TestCheckPort(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bad_network", func(t *testing.T) {
|
t.Run("bad_network", func(t *testing.T) {
|
||||||
err := CheckPort("bad_network", nil, 0)
|
err := CheckPort("bad_network", netip.AddrPortFrom(netip.Addr{}, 0))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("can_bind", func(t *testing.T) {
|
t.Run("can_bind", func(t *testing.T) {
|
||||||
err := CheckPort("udp", net.IP{0, 0, 0, 0}, 0)
|
err := CheckPort("udp", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -322,18 +329,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) {
|
|||||||
`"mtu":1500` +
|
`"mtu":1500` +
|
||||||
`}` + "\n"
|
`}` + "\n"
|
||||||
|
|
||||||
ip4, ip6 := net.IP{1, 2, 3, 4}, net.IP{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
|
||||||
mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen)
|
require.True(t, ok)
|
||||||
|
|
||||||
|
ip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
net4 := netip.PrefixFrom(ip4, 24)
|
||||||
|
net6 := netip.PrefixFrom(ip6, 8)
|
||||||
|
|
||||||
iface := &NetInterface{
|
iface := &NetInterface{
|
||||||
Addresses: []net.IP{ip4, ip6},
|
Addresses: []netip.Addr{ip4, ip6},
|
||||||
Subnets: []*net.IPNet{{
|
Subnets: []netip.Prefix{net4, net6},
|
||||||
IP: ip4.Mask(mask4),
|
|
||||||
Mask: mask4,
|
|
||||||
}, {
|
|
||||||
IP: ip6.Mask(mask6),
|
|
||||||
Mask: mask6,
|
|
||||||
}},
|
|
||||||
Name: "iface0",
|
Name: "iface0",
|
||||||
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
||||||
Flags: net.FlagUp | net.FlagMulticast,
|
Flags: net.FlagUp | net.FlagMulticast,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package aghtest
|
package aghtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
@@ -16,8 +15,6 @@ import (
|
|||||||
|
|
||||||
// Standard Library
|
// Standard Library
|
||||||
|
|
||||||
// Package fs
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ fs.FS = &FS{}
|
var _ fs.FS = &FS{}
|
||||||
|
|
||||||
@@ -61,8 +58,6 @@ func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
|
|||||||
return fsys.OnStat(name)
|
return fsys.OnStat(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package net
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ net.Listener = (*Listener)(nil)
|
var _ net.Listener = (*Listener)(nil)
|
||||||
|
|
||||||
@@ -88,9 +83,31 @@ func (l *Listener) Close() (err error) {
|
|||||||
return l.OnClose()
|
return l.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module AdGuardHome
|
// Module dnsproxy
|
||||||
|
|
||||||
// Package aghos
|
// 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
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
||||||
@@ -116,57 +133,3 @@ func (w *FSWatcher) Add(name string) (err error) {
|
|||||||
func (w *FSWatcher) Close() (err error) {
|
func (w *FSWatcher) Close() (err error) {
|
||||||
return w.OnClose()
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package aghtest_test
|
package aghtest_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ websvc.ServiceWithConfig[struct{}] = (*aghtest.ServiceWithConfig[struct{}])(nil)
|
var _ aghos.FSWatcher = (*aghtest.FSWatcher)(nil)
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ package dhcpd
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
@@ -78,7 +80,18 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
status.Leases = s.Leases(LeasesDynamic)
|
status.Leases = s.Leases(LeasesDynamic)
|
||||||
status.StaticLeases = s.Leases(LeasesStatic)
|
status.StaticLeases = s.Leases(LeasesStatic)
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, status)
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
||||||
@@ -235,7 +248,22 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.setConfFromJSON(conf, srv4, srv6)
|
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.conf.ConfigModified()
|
s.conf.ConfigModified()
|
||||||
|
|
||||||
err = s.dbLoad()
|
err = s.dbLoad()
|
||||||
@@ -254,26 +282,6 @@ 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 {
|
type netInterfaceJSON struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
HardwareAddr string `json:"hardware_address"`
|
HardwareAddr string `json:"hardware_address"`
|
||||||
@@ -341,6 +349,8 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// ignore link-local
|
// ignore link-local
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Try to listen DHCP on LLA as well.
|
||||||
if ipnet.IP.IsLinkLocalUnicast() {
|
if ipnet.IP.IsLinkLocalUnicast() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -351,7 +361,7 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {
|
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {
|
||||||
jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name)
|
jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name).AsSlice()
|
||||||
response[iface.Name] = jsonIface
|
response[iface.Name] = jsonIface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,37 +412,31 @@ type dhcpSearchResult struct {
|
|||||||
V6 dhcpSearchV6Result `json:"v6"`
|
V6 dhcpSearchV6Result `json:"v6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// findActiveServerReq is the JSON structure for the request to find active DHCP
|
// Perform the following tasks:
|
||||||
// servers.
|
// . Search for another DHCP server running
|
||||||
type findActiveServerReq struct {
|
// . Check if a static IP is configured for the network interface
|
||||||
Interface string `json:"interface"`
|
// Respond with results
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
|
||||||
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
// This use of ReadAll is safe, because request's body is now limited.
|
||||||
return
|
body, err := io.ReadAll(r.Body)
|
||||||
}
|
|
||||||
|
|
||||||
req := &findActiveServerReq{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
|
msg := fmt.Sprintf("failed to read request body: %s", err)
|
||||||
|
log.Error(msg)
|
||||||
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ifaceName := req.Interface
|
ifaceName := strings.TrimSpace(string(body))
|
||||||
if ifaceName == "" {
|
if ifaceName == "" {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "empty interface name")
|
msg := "empty interface name specified"
|
||||||
|
log.Error(msg)
|
||||||
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &dhcpSearchResult{
|
result := dhcpSearchResult{
|
||||||
V4: dhcpSearchV4Result{
|
V4: dhcpSearchV4Result{
|
||||||
OtherServer: dhcpSearchOtherResult{
|
OtherServer: dhcpSearchOtherResult{
|
||||||
Found: "no",
|
Found: "no",
|
||||||
@@ -457,14 +461,6 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
|
|||||||
result.V4.StaticIP.IP = aghnet.GetSubnet(ifaceName).String()
|
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)
|
found4, found6, err4, err6 := aghnet.CheckOtherDHCP(ifaceName)
|
||||||
if err4 != nil {
|
if err4 != nil {
|
||||||
result.V4.OtherServer.Found = "error"
|
result.V4.OtherServer.Found = "error"
|
||||||
@@ -472,13 +468,24 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
|
|||||||
} else if found4 {
|
} else if found4 {
|
||||||
result.V4.OtherServer.Found = "yes"
|
result.V4.OtherServer.Found = "yes"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err6 != nil {
|
if err6 != nil {
|
||||||
result.V6.OtherServer.Found = "error"
|
result.V6.OtherServer.Found = "error"
|
||||||
result.V6.OtherServer.Error = err6.Error()
|
result.V6.OtherServer.Error = err6.Error()
|
||||||
} else if found6 {
|
} else if found6 {
|
||||||
result.V6.OtherServer.Found = "yes"
|
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) {
|
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// jsonError is a generic JSON error response.
|
// jsonError is a generic JSON error response.
|
||||||
@@ -24,9 +25,15 @@ type jsonError struct {
|
|||||||
// TODO(a.garipov): Either take the logger from the server after we've
|
// TODO(a.garipov): Either take the logger from the server after we've
|
||||||
// refactored logging or make this not a method of *Server.
|
// refactored logging or make this not a method of *Server.
|
||||||
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponseCode(w, r, http.StatusNotImplemented, &jsonError{
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(&jsonError{
|
||||||
Message: aghos.Unsupported("dhcp").Error(),
|
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
|
// registerHandlers sets the handlers for DHCP HTTP API that always respond with
|
||||||
|
|||||||
@@ -183,7 +183,15 @@ func (s *Server) accessListJSON() (j accessListJSON) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON())
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAccessSet checks the internal accessListJSON lists. To search for
|
// validateAccessSet checks the internal accessListJSON lists. To search for
|
||||||
|
|||||||
@@ -123,14 +123,7 @@ type quicConnection interface {
|
|||||||
func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
|
func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
|
||||||
proto := pctx.Proto
|
proto := pctx.Proto
|
||||||
if proto == proxy.ProtoHTTPS {
|
if proto == proxy.ProtoHTTPS {
|
||||||
clientID, err = clientIDFromDNSContextHTTPS(pctx)
|
return 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 {
|
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@@ -140,9 +133,31 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cliSrvName, err := clientServerName(pctx, proto)
|
cliSrvName := ""
|
||||||
if err != nil {
|
switch proto {
|
||||||
return "", err
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
clientID, err = clientIDFromClientServerName(
|
clientID, err = clientIDFromClientServerName(
|
||||||
@@ -156,35 +171,3 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
|||||||
|
|
||||||
return clientID, nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -160,22 +160,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
|||||||
wantClientID: "insensitive",
|
wantClientID: "insensitive",
|
||||||
wantErrMsg: ``,
|
wantErrMsg: ``,
|
||||||
strictSNI: true,
|
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 {
|
for _, tc := range testCases {
|
||||||
@@ -189,32 +173,16 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
|||||||
conf: ServerConfig{TLSConfig: tlsConf},
|
conf: ServerConfig{TLSConfig: tlsConf},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var conn net.Conn
|
||||||
conn net.Conn
|
if tc.proto == proxy.ProtoTLS {
|
||||||
qconn quic.Connection
|
conn = testTLSConn{
|
||||||
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,
|
serverName: tc.cliSrvName,
|
||||||
}
|
}
|
||||||
case proxy.ProtoTLS:
|
}
|
||||||
conn = testTLSConn{
|
|
||||||
|
var qconn quic.Connection
|
||||||
|
if tc.proto == proxy.ProtoQUIC {
|
||||||
|
qconn = testQUICConnection{
|
||||||
serverName: tc.cliSrvName,
|
serverName: tc.cliSrvName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +190,6 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
|||||||
pctx := &proxy.DNSContext{
|
pctx := &proxy.DNSContext{
|
||||||
Proto: tc.proto,
|
Proto: tc.proto,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
HTTPRequest: httpReq,
|
|
||||||
QUICConnection: qconn,
|
QUICConnection: qconn,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,76 +205,56 @@ func TestClientIDFromDNSContextHTTPS(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
path string
|
path string
|
||||||
cliSrvName string
|
|
||||||
wantClientID string
|
wantClientID string
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
}{{
|
}{{
|
||||||
name: "no_clientid",
|
name: "no_clientid",
|
||||||
path: "/dns-query",
|
path: "/dns-query",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "no_clientid_slash",
|
name: "no_clientid_slash",
|
||||||
path: "/dns-query/",
|
path: "/dns-query/",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "clientid",
|
name: "clientid",
|
||||||
path: "/dns-query/cli",
|
path: "/dns-query/cli",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "cli",
|
wantClientID: "cli",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "clientid_slash",
|
name: "clientid_slash",
|
||||||
path: "/dns-query/cli/",
|
path: "/dns-query/cli/",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "cli",
|
wantClientID: "cli",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "clientid_case",
|
name: "clientid_case",
|
||||||
path: "/dns-query/InSeNsItIvE",
|
path: "/dns-query/InSeNsItIvE",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "insensitive",
|
wantClientID: "insensitive",
|
||||||
wantErrMsg: ``,
|
wantErrMsg: ``,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_url",
|
name: "bad_url",
|
||||||
path: "/foo",
|
path: "/foo",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `clientid check: invalid path "/foo"`,
|
wantErrMsg: `clientid check: invalid path "/foo"`,
|
||||||
}, {
|
}, {
|
||||||
name: "extra",
|
name: "extra",
|
||||||
path: "/dns-query/cli/foo",
|
path: "/dns-query/cli/foo",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `clientid check: invalid path "/dns-query/cli/foo": extra parts`,
|
wantErrMsg: `clientid check: invalid path "/dns-query/cli/foo": extra parts`,
|
||||||
}, {
|
}, {
|
||||||
name: "invalid_clientid",
|
name: "invalid_clientid",
|
||||||
path: "/dns-query/!!!",
|
path: "/dns-query/!!!",
|
||||||
cliSrvName: "example.com",
|
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `clientid check: invalid clientid "!!!": bad domain name label rune '!'`,
|
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 {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
connState := &tls.ConnectionState{
|
|
||||||
ServerName: tc.cliSrvName,
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &http.Request{
|
r := &http.Request{
|
||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
Path: tc.path,
|
Path: tc.path,
|
||||||
},
|
},
|
||||||
TLS: connState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pctx := &proxy.DNSContext{
|
pctx := &proxy.DNSContext{
|
||||||
|
|||||||
@@ -201,10 +201,6 @@ type ServerConfig struct {
|
|||||||
// Register an HTTP handler
|
// Register an HTTP handler
|
||||||
HTTPRegister aghhttp.RegisterFunc
|
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 signals if the RDNS should resolve clients' addresses.
|
||||||
ResolveClients bool
|
ResolveClients bool
|
||||||
|
|
||||||
@@ -212,12 +208,9 @@ type ServerConfig struct {
|
|||||||
// locally-served networks should be resolved via private PTR resolvers.
|
// locally-served networks should be resolved via private PTR resolvers.
|
||||||
UsePrivateRDNS bool
|
UsePrivateRDNS bool
|
||||||
|
|
||||||
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
|
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
|
||||||
ServeHTTP3 bool
|
// resolving PTR queries for local addresses.
|
||||||
|
LocalPTRResolvers []string
|
||||||
// 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
|
// if any of ServerConfig values are zero, then default values from below are used
|
||||||
@@ -233,7 +226,6 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
|||||||
conf = proxy.Config{
|
conf = proxy.Config{
|
||||||
UDPListenAddr: srvConf.UDPListenAddrs,
|
UDPListenAddr: srvConf.UDPListenAddrs,
|
||||||
TCPListenAddr: srvConf.TCPListenAddrs,
|
TCPListenAddr: srvConf.TCPListenAddrs,
|
||||||
HTTP3: srvConf.ServeHTTP3,
|
|
||||||
Ratelimit: int(srvConf.Ratelimit),
|
Ratelimit: int(srvConf.Ratelimit),
|
||||||
RatelimitWhitelist: srvConf.RatelimitWhitelist,
|
RatelimitWhitelist: srvConf.RatelimitWhitelist,
|
||||||
RefuseAny: srvConf.RefuseAny,
|
RefuseAny: srvConf.RefuseAny,
|
||||||
@@ -332,20 +324,6 @@ 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
|
// prepareUpstreamSettings - prepares upstream DNS server settings
|
||||||
func (s *Server) prepareUpstreamSettings() error {
|
func (s *Server) prepareUpstreamSettings() error {
|
||||||
// We're setting a customized set of RootCAs
|
// We're setting a customized set of RootCAs
|
||||||
@@ -375,14 +353,12 @@ func (s *Server) prepareUpstreamSettings() error {
|
|||||||
upstreams = s.conf.UpstreamDNS
|
upstreams = s.conf.UpstreamDNS
|
||||||
}
|
}
|
||||||
|
|
||||||
httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams)
|
|
||||||
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
||||||
upstreamConfig, err := proxy.ParseUpstreamsConfig(
|
upstreamConfig, err := proxy.ParseUpstreamsConfig(
|
||||||
upstreams,
|
upstreams,
|
||||||
&upstream.Options{
|
&upstream.Options{
|
||||||
Bootstrap: s.conf.BootstrapDNS,
|
Bootstrap: s.conf.BootstrapDNS,
|
||||||
Timeout: s.conf.UpstreamTimeout,
|
Timeout: s.conf.UpstreamTimeout,
|
||||||
HTTPVersions: httpVersions,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -395,9 +371,8 @@ func (s *Server) prepareUpstreamSettings() error {
|
|||||||
uc, err = proxy.ParseUpstreamsConfig(
|
uc, err = proxy.ParseUpstreamsConfig(
|
||||||
defaultDNS,
|
defaultDNS,
|
||||||
&upstream.Options{
|
&upstream.Options{
|
||||||
Bootstrap: s.conf.BootstrapDNS,
|
Bootstrap: s.conf.BootstrapDNS,
|
||||||
Timeout: s.conf.UpstreamTimeout,
|
Timeout: s.conf.UpstreamTimeout,
|
||||||
HTTPVersions: httpVersions,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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
|
// filterDNSResponse checks each resource record of the response's answer
|
||||||
// section from pctx and returns a non-nil res if at least one of canonical
|
// section from pctx and returns a non-nil res if at least one of canonnical
|
||||||
// names or IP addresses in it matches the filtering rules.
|
// names or IP addresses in it matches the filtering rules.
|
||||||
func (s *Server) filterDNSResponse(
|
func (s *Server) filterDNSResponse(
|
||||||
pctx *proxy.DNSContext,
|
pctx *proxy.DNSContext,
|
||||||
|
|||||||
@@ -112,7 +112,13 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
DefautLocalPTRUpstreams: defLocalPTRUps,
|
DefautLocalPTRUpstreams: defLocalPTRUps,
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
||||||
@@ -343,10 +349,7 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
|
|||||||
|
|
||||||
conf, err = proxy.ParseUpstreamsConfig(
|
conf, err = proxy.ParseUpstreamsConfig(
|
||||||
upstreams,
|
upstreams,
|
||||||
&upstream.Options{
|
&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
|
||||||
Bootstrap: []string{},
|
|
||||||
Timeout: DefaultTimeout,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -409,15 +412,7 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var protocols = []string{
|
var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"}
|
||||||
"h3://",
|
|
||||||
"https://",
|
|
||||||
"quic://",
|
|
||||||
"sdns://",
|
|
||||||
"tcp://",
|
|
||||||
"tls://",
|
|
||||||
"udp://",
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateUpstream returns an error if u alongside with domains is not a valid
|
// validateUpstream returns an error if u alongside with domains is not a valid
|
||||||
// upstream configuration. useDefault is true if the upstream is
|
// upstream configuration. useDefault is true if the upstream is
|
||||||
@@ -664,7 +659,24 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
|||||||
result[host] = "OK"
|
result[host] = "OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, result)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDoH is the DNS-over-HTTPs handler.
|
// handleDoH is the DNS-over-HTTPs handler.
|
||||||
@@ -680,13 +692,11 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
|
||||||
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
|
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
|
||||||
aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
|
aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.IsRunning() {
|
if !s.IsRunning() {
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
|
aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
@@ -117,8 +116,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
|||||||
s.conf = tc.conf()
|
s.conf = tc.conf()
|
||||||
s.handleGetConfig(w, nil)
|
s.handleGetConfig(w, nil)
|
||||||
|
|
||||||
cType := w.Header().Get(aghhttp.HdrNameContentType)
|
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
||||||
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
|
|
||||||
assert.JSONEq(t, string(caseWant), w.Body.String())
|
assert.JSONEq(t, string(caseWant), w.Body.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,7 +453,13 @@ func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesAvailableServices(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesAvailableServices(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, serviceIDs)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -461,7 +467,13 @@ func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Req
|
|||||||
list := d.Config.BlockedServices
|
list := d.Config.BlockedServices
|
||||||
d.confLock.RUnlock()
|
d.confLock.RUnlock()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, list)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package filtering
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
@@ -247,25 +249,16 @@ func (d *DNSFilter) handleFilteringSetURL(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func (d *DNSFilter) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
|
||||||
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
// This use of ReadAll is safe, because request's body is now limited.
|
||||||
return
|
body, err := io.ReadAll(r.Body)
|
||||||
}
|
|
||||||
|
|
||||||
req := &filteringRulesReq{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
d.UserRules = req.Rules
|
d.UserRules = strings.Split(string(body), "\n")
|
||||||
d.ConfigModified()
|
d.ConfigModified()
|
||||||
d.EnableFilters(true)
|
d.EnableFilters(true)
|
||||||
}
|
}
|
||||||
@@ -301,7 +294,14 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type filterJSON struct {
|
type filterJSON struct {
|
||||||
@@ -354,7 +354,17 @@ func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request
|
|||||||
resp.UserRules = d.UserRules
|
resp.UserRules = d.UserRules
|
||||||
d.filtersMu.RUnlock()
|
d.filtersMu.RUnlock()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set filtering configuration
|
// Set filtering configuration
|
||||||
@@ -456,7 +466,11 @@ func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err = json.NewEncoder(w).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterFilteringHandlers - register handlers
|
// RegisterFilteringHandlers - register handlers
|
||||||
@@ -240,7 +240,13 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
d.confLock.Unlock()
|
d.confLock.Unlock()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, arr)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -380,13 +381,17 @@ func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := &struct {
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(&struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}{
|
}{
|
||||||
Enabled: d.Config.SafeBrowsingEnabled,
|
Enabled: d.Config.SafeBrowsingEnabled,
|
||||||
}
|
})
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -400,11 +405,13 @@ func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := &struct {
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(&struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}{
|
}{
|
||||||
Enabled: d.Config.ParentalEnabled,
|
Enabled: d.Config.ParentalEnabled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -145,13 +146,21 @@ func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := &struct {
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(&struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}{
|
}{
|
||||||
Enabled: d.Config.SafeSearchEnabled,
|
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{
|
var safeSearchDomains = map[string]string{
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
@@ -34,8 +32,7 @@ const sessionTokenSize = 16
|
|||||||
|
|
||||||
type session struct {
|
type session struct {
|
||||||
userName string
|
userName string
|
||||||
// expire is the expiration time, in seconds.
|
expire uint32 // expiration time (in seconds)
|
||||||
expire uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) serialize() []byte {
|
func (s *session) serialize() []byte {
|
||||||
@@ -67,29 +64,29 @@ func (s *session) deserialize(data []byte) bool {
|
|||||||
|
|
||||||
// Auth - global object
|
// Auth - global object
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
db *bbolt.DB
|
db *bbolt.DB
|
||||||
raleLimiter *authRateLimiter
|
blocker *authRateLimiter
|
||||||
sessions map[string]*session
|
sessions map[string]*session
|
||||||
users []webUser
|
users []User
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
sessionTTL uint32
|
sessionTTL uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// webUser represents a user of the Web UI.
|
// User object
|
||||||
type webUser struct {
|
type User struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
PasswordHash string `yaml:"password"`
|
PasswordHash string `yaml:"password"` // bcrypt hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitAuth - create a global object
|
// InitAuth - create a global object
|
||||||
func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter *authRateLimiter) *Auth {
|
func InitAuth(dbFilename string, users []User, sessionTTL uint32, blocker *authRateLimiter) *Auth {
|
||||||
log.Info("Initializing auth module: %s", dbFilename)
|
log.Info("Initializing auth module: %s", dbFilename)
|
||||||
|
|
||||||
a := &Auth{
|
a := &Auth{
|
||||||
sessionTTL: sessionTTL,
|
sessionTTL: sessionTTL,
|
||||||
raleLimiter: rateLimiter,
|
blocker: blocker,
|
||||||
sessions: make(map[string]*session),
|
sessions: make(map[string]*session),
|
||||||
users: users,
|
users: users,
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
a.db, err = bbolt.Open(dbFilename, 0o644, nil)
|
a.db, err = bbolt.Open(dbFilename, 0o644, nil)
|
||||||
@@ -329,25 +326,35 @@ func newSessionToken() (data []byte, err error) {
|
|||||||
return randData, nil
|
return randData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCookie creates a new authentication cookie.
|
// cookieTimeFormat is the format to be used in (time.Time).Format for cookie's
|
||||||
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) {
|
// expiry field.
|
||||||
rateLimiter := a.raleLimiter
|
const cookieTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||||
u, ok := a.findUser(req.Name, req.Password)
|
|
||||||
if !ok {
|
// cookieExpiryFormat returns the formatted exp to be used in cookie string.
|
||||||
if rateLimiter != nil {
|
// It's quite simple for now, but probably will be expanded in the future.
|
||||||
rateLimiter.inc(addr)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.Error("invalid username or password")
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if rateLimiter != nil {
|
if blocker != nil {
|
||||||
rateLimiter.remove(addr)
|
blocker.remove(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
sess, err := newSessionToken()
|
var sess []byte
|
||||||
|
sess, err = newSessionToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generating token: %w", err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
@@ -357,15 +364,11 @@ func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error)
|
|||||||
expire: uint32(now.Unix()) + a.sessionTTL,
|
expire: uint32(now.Unix()) + a.sessionTTL,
|
||||||
})
|
})
|
||||||
|
|
||||||
return &http.Cookie{
|
return fmt.Sprintf(
|
||||||
Name: sessionCookieName,
|
"%s=%s; Path=/; HttpOnly; Expires=%s",
|
||||||
Value: hex.EncodeToString(sess),
|
sessionCookieName, hex.EncodeToString(sess),
|
||||||
Path: "/",
|
cookieExpiryFormat(now.Add(cookieTTL)),
|
||||||
Expires: now.Add(cookieTTL),
|
), nil
|
||||||
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// realIP extracts the real IP address of the client from an HTTP request using
|
// realIP extracts the real IP address of the client from an HTTP request using
|
||||||
@@ -433,8 +436,8 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
|
if blocker := Context.auth.blocker; blocker != nil {
|
||||||
if left := rateLimiter.check(remoteAddr); left > 0 {
|
if left := blocker.check(remoteAddr); left > 0 {
|
||||||
w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds())))
|
w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds())))
|
||||||
aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left)
|
aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left)
|
||||||
|
|
||||||
@@ -442,9 +445,10 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := Context.auth.newCookie(req, remoteAddr)
|
var cookie string
|
||||||
|
cookie, err = Context.auth.httpCookie(req, remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusForbidden, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "crypto rand reader: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -458,11 +462,20 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Error("auth: unknown ip")
|
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)
|
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
|
||||||
|
|
||||||
http.SetCookie(w, cookie)
|
|
||||||
|
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
|
h.Set("Set-Cookie", cookie)
|
||||||
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||||
h.Set("Pragma", "no-cache")
|
h.Set("Pragma", "no-cache")
|
||||||
h.Set("Expires", "0")
|
h.Set("Expires", "0")
|
||||||
@@ -471,31 +484,17 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
respHdr := w.Header()
|
cookie := r.Header.Get("Cookie")
|
||||||
c, err := r.Cookie(sessionCookieName)
|
sess := parseCookie(cookie)
|
||||||
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)
|
|
||||||
|
|
||||||
return
|
Context.auth.RemoveSession(sess)
|
||||||
}
|
|
||||||
|
|
||||||
Context.auth.RemoveSession(c.Value)
|
w.Header().Set("Location", "/login.html")
|
||||||
|
|
||||||
c = &http.Cookie{
|
s := fmt.Sprintf("%s=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
||||||
Name: sessionCookieName,
|
sessionCookieName)
|
||||||
Value: "",
|
w.Header().Set("Set-Cookie", s)
|
||||||
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)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,108 +504,101 @@ func RegisterAuthHandlers() {
|
|||||||
httpRegister(http.MethodGet, "/control/logout", handleLogout)
|
httpRegister(http.MethodGet, "/control/logout", handleLogout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionalAuthThird return true if user should authenticate first.
|
func parseCookie(cookie string) string {
|
||||||
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
|
pairs := strings.Split(cookie, ";")
|
||||||
if glProcessCookie(r) {
|
for _, pair := range pairs {
|
||||||
log.Debug("auth: authentication is handled by GL-Inet submodule")
|
pair = strings.TrimSpace(pair)
|
||||||
|
kv := strings.SplitN(pair, "=", 2)
|
||||||
return false
|
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
|
||||||
|
|
||||||
// redirect to login page if not authenticated
|
// redirect to login page if not authenticated
|
||||||
isAuthenticated := false
|
ok := false
|
||||||
cookie, err := r.Cookie(sessionCookieName)
|
cookie, err := r.Cookie(sessionCookieName)
|
||||||
if err != nil {
|
|
||||||
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
|
if glProcessCookie(r) {
|
||||||
// Check Basic authentication.
|
log.Debug("auth: authentication was handled by GL-Inet submodule")
|
||||||
user, pass, hasBasic := r.BasicAuth()
|
ok = true
|
||||||
if hasBasic {
|
} else if err == nil {
|
||||||
_, isAuthenticated = Context.auth.findUser(user, pass)
|
r := Context.auth.checkSession(cookie.Value)
|
||||||
if !isAuthenticated {
|
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 {
|
||||||
log.Info("auth: invalid Basic Authorization value")
|
log.Info("auth: invalid Basic Authorization value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
res := Context.auth.checkSession(cookie.Value)
|
|
||||||
isAuthenticated = res == checkSessionOK
|
|
||||||
if !isAuthenticated {
|
|
||||||
log.Debug("auth: invalid cookie value: %s", cookie)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
if isAuthenticated {
|
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
||||||
return false
|
if glProcessRedirect(w, r) {
|
||||||
}
|
log.Debug("auth: redirected to login page by GL-Inet submodule")
|
||||||
|
} else {
|
||||||
if p := r.URL.Path; p == "/" || p == "/index.html" {
|
w.Header().Set("Location", "/login.html")
|
||||||
if glProcessRedirect(w, r) {
|
w.WriteHeader(http.StatusFound)
|
||||||
log.Debug("auth: redirected to login page by GL-Inet submodule")
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debug("auth: redirected to login page")
|
w.WriteHeader(http.StatusForbidden)
|
||||||
w.Header().Set("Location", "/login.html")
|
_, _ = w.Write([]byte("Forbidden"))
|
||||||
w.WriteHeader(http.StatusFound)
|
|
||||||
}
|
}
|
||||||
} else {
|
authFirst = true
|
||||||
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
_, _ = w.Write([]byte("Forbidden"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return authFirst
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the
|
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||||
// project.
|
|
||||||
func optionalAuth(
|
|
||||||
h func(http.ResponseWriter, *http.Request),
|
|
||||||
) (wrapped func(http.ResponseWriter, *http.Request)) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
p := r.URL.Path
|
if r.URL.Path == "/login.html" {
|
||||||
authRequired := Context.auth != nil && Context.auth.AuthRequired()
|
// redirect to dashboard if already authenticated
|
||||||
if p == "/login.html" {
|
authRequired := Context.auth != nil && Context.auth.AuthRequired()
|
||||||
cookie, err := r.Cookie(sessionCookieName)
|
cookie, err := r.Cookie(sessionCookieName)
|
||||||
if authRequired && err == nil {
|
if authRequired && err == nil {
|
||||||
// Redirect to the dashboard if already authenticated.
|
r := Context.auth.checkSession(cookie.Value)
|
||||||
res := Context.auth.checkSession(cookie.Value)
|
if r == checkSessionOK {
|
||||||
if res == checkSessionOK {
|
|
||||||
w.Header().Set("Location", "/")
|
w.Header().Set("Location", "/")
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
} else if r == checkSessionNotFound {
|
||||||
|
log.Debug("auth: invalid cookie value: %s", cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("auth: invalid cookie value: %s", cookie)
|
|
||||||
}
|
}
|
||||||
} else if isPublicResource(p) {
|
|
||||||
// Process as usual, no additional auth requirements.
|
} else if strings.HasPrefix(r.URL.Path, "/assets/") ||
|
||||||
} else if authRequired {
|
strings.HasPrefix(r.URL.Path, "/login.") {
|
||||||
|
// process as usual
|
||||||
|
// no additional auth requirements
|
||||||
|
} else if Context.auth != nil && Context.auth.AuthRequired() {
|
||||||
if optionalAuthThird(w, r) {
|
if optionalAuthThird(w, r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h(w, r)
|
handler(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 {
|
type authHandler struct {
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
}
|
}
|
||||||
@@ -620,7 +612,7 @@ func optionalAuthHandler(handler http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserAdd - add new user
|
// UserAdd - add new user
|
||||||
func (a *Auth) UserAdd(u *webUser, password string) {
|
func (a *Auth) UserAdd(u *User, password string) {
|
||||||
if len(password) == 0 {
|
if len(password) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -639,35 +631,31 @@ func (a *Auth) UserAdd(u *webUser, password string) {
|
|||||||
log.Debug("auth: added user: %s", u.Name)
|
log.Debug("auth: added user: %s", u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findUser returns a user if there is one.
|
// UserFind - find a user
|
||||||
func (a *Auth) findUser(login, password string) (u webUser, ok bool) {
|
func (a *Auth) UserFind(login, password string) User {
|
||||||
a.lock.Lock()
|
a.lock.Lock()
|
||||||
defer a.lock.Unlock()
|
defer a.lock.Unlock()
|
||||||
|
for _, u := range a.users {
|
||||||
for _, u = range a.users {
|
|
||||||
if u.Name == login &&
|
if u.Name == login &&
|
||||||
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
|
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
|
||||||
return u, true
|
return u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return User{}
|
||||||
return webUser{}, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCurrentUser returns the current user. It returns an empty User if the
|
// getCurrentUser returns the current user. It returns an empty User if the
|
||||||
// user is not found.
|
// user is not found.
|
||||||
func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
|
func (a *Auth) getCurrentUser(r *http.Request) User {
|
||||||
cookie, err := r.Cookie(sessionCookieName)
|
cookie, err := r.Cookie(sessionCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// There's no Cookie, check Basic authentication.
|
// There's no Cookie, check Basic authentication.
|
||||||
user, pass, ok := r.BasicAuth()
|
user, pass, ok := r.BasicAuth()
|
||||||
if ok {
|
if ok {
|
||||||
u, _ = Context.auth.findUser(user, pass)
|
return Context.auth.UserFind(user, pass)
|
||||||
|
|
||||||
return u
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return webUser{}
|
return User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.lock.Lock()
|
a.lock.Lock()
|
||||||
@@ -675,20 +663,20 @@ func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
|
|||||||
|
|
||||||
s, ok := a.sessions[cookie.Value]
|
s, ok := a.sessions[cookie.Value]
|
||||||
if !ok {
|
if !ok {
|
||||||
return webUser{}
|
return User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u = range a.users {
|
for _, u := range a.users {
|
||||||
if u.Name == s.userName {
|
if u.Name == s.userName {
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return webUser{}
|
return User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsers - get users
|
// GetUsers - get users
|
||||||
func (a *Auth) GetUsers() []webUser {
|
func (a *Auth) GetUsers() []User {
|
||||||
a.lock.Lock()
|
a.lock.Lock()
|
||||||
users := a.users
|
users := a.users
|
||||||
a.lock.Unlock()
|
a.lock.Unlock()
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
aghtest.DiscardLogOutput(m)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewSessionToken(t *testing.T) {
|
func TestNewSessionToken(t *testing.T) {
|
||||||
// Successful case.
|
// Successful case.
|
||||||
token, err := newSessionToken()
|
token, err := newSessionToken()
|
||||||
@@ -38,14 +43,14 @@ func TestAuth(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
fn := filepath.Join(dir, "sessions.db")
|
fn := filepath.Join(dir, "sessions.db")
|
||||||
|
|
||||||
users := []webUser{{
|
users := []User{{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
|
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
|
||||||
}}
|
}}
|
||||||
a := InitAuth(fn, nil, 60, nil)
|
a := InitAuth(fn, nil, 60, nil)
|
||||||
s := session{}
|
s := session{}
|
||||||
|
|
||||||
user := webUser{Name: "name"}
|
user := User{Name: "name"}
|
||||||
a.UserAdd(&user, "password")
|
a.UserAdd(&user, "password")
|
||||||
|
|
||||||
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
|
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
|
||||||
@@ -79,8 +84,7 @@ func TestAuth(t *testing.T) {
|
|||||||
a.storeSession(sess, &s)
|
a.storeSession(sess, &s)
|
||||||
a.Close()
|
a.Close()
|
||||||
|
|
||||||
u, ok := a.findUser("name", "password")
|
u := a.UserFind("name", "password")
|
||||||
assert.True(t, ok)
|
|
||||||
assert.NotEmpty(t, u.Name)
|
assert.NotEmpty(t, u.Name)
|
||||||
|
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
@@ -114,7 +118,7 @@ func TestAuthHTTP(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
fn := filepath.Join(dir, "sessions.db")
|
fn := filepath.Join(dir, "sessions.db")
|
||||||
|
|
||||||
users := []webUser{
|
users := []User{
|
||||||
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
|
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
|
||||||
}
|
}
|
||||||
Context.auth = InitAuth(fn, users, 60, nil)
|
Context.auth = InitAuth(fn, users, 60, nil)
|
||||||
@@ -146,19 +150,18 @@ func TestAuthHTTP(t *testing.T) {
|
|||||||
assert.True(t, handlerCalled)
|
assert.True(t, handlerCalled)
|
||||||
|
|
||||||
// perform login
|
// perform login
|
||||||
cookie, err := Context.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "")
|
cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}, "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, cookie)
|
assert.NotEmpty(t, cookie)
|
||||||
|
|
||||||
// get /
|
// get /
|
||||||
handler2 = optionalAuth(handler)
|
handler2 = optionalAuth(handler)
|
||||||
w.hdr = make(http.Header)
|
w.hdr = make(http.Header)
|
||||||
r.Header.Set("Cookie", cookie.String())
|
r.Header.Set("Cookie", cookie)
|
||||||
r.URL = &url.URL{Path: "/"}
|
r.URL = &url.URL{Path: "/"}
|
||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
assert.True(t, handlerCalled)
|
assert.True(t, handlerCalled)
|
||||||
|
|
||||||
r.Header.Del("Cookie")
|
r.Header.Del("Cookie")
|
||||||
|
|
||||||
// get / with basic auth
|
// get / with basic auth
|
||||||
@@ -174,7 +177,7 @@ func TestAuthHTTP(t *testing.T) {
|
|||||||
// get login page with a valid cookie - we're redirected to /
|
// get login page with a valid cookie - we're redirected to /
|
||||||
handler2 = optionalAuth(handler)
|
handler2 = optionalAuth(handler)
|
||||||
w.hdr = make(http.Header)
|
w.hdr = make(http.Header)
|
||||||
r.Header.Set("Cookie", cookie.String())
|
r.Header.Set("Cookie", cookie)
|
||||||
r.URL = &url.URL{Path: loginURL}
|
r.URL = &url.URL{Path: loginURL}
|
||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
|
|||||||
@@ -456,9 +456,8 @@ func (clients *clientsContainer) findUpstreams(
|
|||||||
conf, err = proxy.ParseUpstreamsConfig(
|
conf, err = proxy.ParseUpstreamsConfig(
|
||||||
upstreams,
|
upstreams,
|
||||||
&upstream.Options{
|
&upstream.Options{
|
||||||
Bootstrap: config.DNS.BootstrapDNS,
|
Bootstrap: config.DNS.BootstrapDNS,
|
||||||
Timeout: config.DNS.UpstreamTimeout.Duration,
|
Timeout: config.DNS.UpstreamTimeout.Duration,
|
||||||
HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -93,7 +93,13 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
|
|||||||
|
|
||||||
data.Tags = clientTags
|
data.Tags = clientTags
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert JSON object to Client object
|
// Convert JSON object to Client object
|
||||||
@@ -243,7 +249,11 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// findRuntime looks up the IP in runtime and temporary storages, like
|
// findRuntime looks up the IP in runtime and temporary storages, like
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package home
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -85,10 +85,10 @@ type configuration struct {
|
|||||||
// It's reset after config is parsed
|
// It's reset after config is parsed
|
||||||
fileData []byte
|
fileData []byte
|
||||||
|
|
||||||
BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
BindHost netip.Addr `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
|
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
|
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
|
||||||
Users []webUser `yaml:"users"` // Users that can access HTTP server
|
Users []User `yaml:"users"` // Users that can access HTTP server
|
||||||
// AuthAttempts is the maximum number of failed login attempts a user
|
// AuthAttempts is the maximum number of failed login attempts a user
|
||||||
// can do before being blocked.
|
// can do before being blocked.
|
||||||
AuthAttempts uint `yaml:"auth_attempts"`
|
AuthAttempts uint `yaml:"auth_attempts"`
|
||||||
@@ -135,8 +135,8 @@ type configuration struct {
|
|||||||
|
|
||||||
// field ordering is important -- yaml fields will mirror ordering from here
|
// field ordering is important -- yaml fields will mirror ordering from here
|
||||||
type dnsConfig struct {
|
type dnsConfig struct {
|
||||||
BindHosts []net.IP `yaml:"bind_hosts"`
|
BindHosts []netip.Addr `yaml:"bind_hosts"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
|
|
||||||
// time interval for statistics (in days)
|
// time interval for statistics (in days)
|
||||||
StatsInterval uint32 `yaml:"statistics_interval"`
|
StatsInterval uint32 `yaml:"statistics_interval"`
|
||||||
@@ -166,19 +166,6 @@ type dnsConfig struct {
|
|||||||
// LocalPTRResolvers is the slice of addresses to be used as upstreams
|
// LocalPTRResolvers is the slice of addresses to be used as upstreams
|
||||||
// for PTR queries for locally-served networks.
|
// for PTR queries for locally-served networks.
|
||||||
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
|
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 {
|
type tlsConfigSettings struct {
|
||||||
@@ -211,12 +198,12 @@ type tlsConfigSettings struct {
|
|||||||
var config = &configuration{
|
var config = &configuration{
|
||||||
BindPort: 3000,
|
BindPort: 3000,
|
||||||
BetaBindPort: 0,
|
BetaBindPort: 0,
|
||||||
BindHost: net.IP{0, 0, 0, 0},
|
BindHost: netip.IPv4Unspecified(),
|
||||||
AuthAttempts: 5,
|
AuthAttempts: 5,
|
||||||
AuthBlockMin: 15,
|
AuthBlockMin: 15,
|
||||||
WebSessionTTLHours: 30 * 24,
|
WebSessionTTLHours: 30 * 24,
|
||||||
DNS: dnsConfig{
|
DNS: dnsConfig{
|
||||||
BindHosts: []net.IP{{0, 0, 0, 0}},
|
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
|
||||||
Port: defaultPortDNS,
|
Port: defaultPortDNS,
|
||||||
StatsInterval: 1,
|
StatsInterval: 1,
|
||||||
QueryLogEnabled: true,
|
QueryLogEnabled: true,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package home
|
package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
@@ -20,11 +20,11 @@ import (
|
|||||||
|
|
||||||
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
|
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
|
||||||
// addresses to a slice of strings.
|
// addresses to a slice of strings.
|
||||||
func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
|
func appendDNSAddrs(dst []string, addrs ...netip.Addr) (res []string) {
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
var hostport string
|
var hostport string
|
||||||
if config.DNS.Port != defaultPortDNS {
|
if config.DNS.Port != defaultPortDNS {
|
||||||
hostport = netutil.JoinHostPort(addr.String(), config.DNS.Port)
|
hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String()
|
||||||
} else {
|
} else {
|
||||||
hostport = addr.String()
|
hostport = addr.String()
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
|
|||||||
// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to
|
// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to
|
||||||
// dst. It also adds the IP addresses of all network interfaces if src contains
|
// dst. It also adds the IP addresses of all network interfaces if src contains
|
||||||
// an unspecified IP address.
|
// an unspecified IP address.
|
||||||
func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err error) {
|
func appendDNSAddrsWithIfaces(dst []string, src []netip.Addr) (res []string, err error) {
|
||||||
ifacesAdded := false
|
ifacesAdded := false
|
||||||
for _, h := range src {
|
for _, h := range src {
|
||||||
if !h.IsUnspecified() {
|
if !h.IsUnspecified() {
|
||||||
@@ -71,7 +71,9 @@ func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err err
|
|||||||
// on, including the addresses on all interfaces in cases of unspecified IPs.
|
// on, including the addresses on all interfaces in cases of unspecified IPs.
|
||||||
func collectDNSAddresses() (addrs []string, err error) {
|
func collectDNSAddresses() (addrs []string, err error) {
|
||||||
if hosts := config.DNS.BindHosts; len(hosts) == 0 {
|
if hosts := config.DNS.BindHosts; len(hosts) == 0 {
|
||||||
addrs = appendDNSAddrs(addrs, net.IP{127, 0, 0, 1})
|
addr := aghnet.IPv4Localhost()
|
||||||
|
|
||||||
|
addrs = appendDNSAddrs(addrs, addr)
|
||||||
} else {
|
} else {
|
||||||
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
|
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -97,16 +99,16 @@ func collectDNSAddresses() (addrs []string, err error) {
|
|||||||
|
|
||||||
// statusResponse is a response for /control/status endpoint.
|
// statusResponse is a response for /control/status endpoint.
|
||||||
type statusResponse struct {
|
type statusResponse struct {
|
||||||
Version string `json:"version"`
|
|
||||||
Language string `json:"language"`
|
|
||||||
DNSAddrs []string `json:"dns_addresses"`
|
DNSAddrs []string `json:"dns_addresses"`
|
||||||
DNSPort int `json:"dns_port"`
|
DNSPort int `json:"dns_port"`
|
||||||
HTTPPort int `json:"http_port"`
|
HTTPPort int `json:"http_port"`
|
||||||
IsProtectionEnabled bool `json:"protection_enabled"`
|
IsProtectionEnabled bool `json:"protection_enabled"`
|
||||||
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
|
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
|
||||||
// openapi.yaml declares.
|
// openapi.yaml declares.
|
||||||
IsDHCPAvailable bool `json:"dhcp_available"`
|
IsDHCPAvailable bool `json:"dhcp_available"`
|
||||||
IsRunning bool `json:"running"`
|
IsRunning bool `json:"running"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Language string `json:"language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -125,12 +127,12 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer config.RUnlock()
|
defer config.RUnlock()
|
||||||
|
|
||||||
resp = statusResponse{
|
resp = statusResponse{
|
||||||
Version: version.Version(),
|
|
||||||
DNSAddrs: dnsAddrs,
|
DNSAddrs: dnsAddrs,
|
||||||
DNSPort: config.DNS.Port,
|
DNSPort: config.DNS.Port,
|
||||||
HTTPPort: config.BindPort,
|
HTTPPort: config.BindPort,
|
||||||
Language: config.Language,
|
|
||||||
IsRunning: isRunning(),
|
IsRunning: isRunning(),
|
||||||
|
Version: version.Version(),
|
||||||
|
Language: config.Language,
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -146,7 +148,13 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
resp.IsDHCPAvailable = Context.dhcpServer != nil
|
resp.IsDHCPAvailable = Context.dhcpServer != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type profileJSON struct {
|
type profileJSON struct {
|
||||||
@@ -154,12 +162,16 @@ type profileJSON struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pj := profileJSON{}
|
||||||
u := Context.auth.getCurrentUser(r)
|
u := Context.auth.getCurrentUser(r)
|
||||||
resp := &profileJSON{
|
pj.Name = u.Name
|
||||||
Name: u.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
data, err := json.Marshal(pj)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
@@ -189,29 +201,19 @@ func httpRegister(method, url string, handler http.HandlerFunc) {
|
|||||||
Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
|
Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure returns a wrapped handler that makes sure that the request has the
|
// ----------------------------------
|
||||||
// correct method as well as additional method and header checks.
|
// helper functions for HTTP handlers
|
||||||
func ensure(
|
// ----------------------------------
|
||||||
method string,
|
func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||||
handler func(http.ResponseWriter, *http.Request),
|
|
||||||
) (wrapped func(http.ResponseWriter, *http.Request)) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
log.Debug("%s %v", r.Method, r.URL)
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if modifiesData(m) {
|
if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
|
||||||
if !ensureContentType(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Context.controlLock.Lock()
|
Context.controlLock.Lock()
|
||||||
defer Context.controlLock.Unlock()
|
defer Context.controlLock.Unlock()
|
||||||
}
|
}
|
||||||
@@ -220,42 +222,6 @@ func ensure(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||||
return ensure(http.MethodPost, handler)
|
return ensure(http.MethodPost, handler)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// getAddrsResponse is the response for /install/get_addresses endpoint.
|
// getAddrsResponse is the response for /install/get_addresses endpoint.
|
||||||
@@ -60,13 +59,25 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
|
|||||||
data.Interfaces[iface.Name] = iface
|
data.Interfaces[iface.Name] = iface
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkConfReqEnt struct {
|
type checkConfReqEnt struct {
|
||||||
IP net.IP `json:"ip"`
|
IP netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Autofix bool `json:"autofix"`
|
Autofix bool `json:"autofix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkConfReq struct {
|
type checkConfReq struct {
|
||||||
@@ -117,7 +128,7 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
|
|||||||
// unbound after install.
|
// unbound after install.
|
||||||
}
|
}
|
||||||
|
|
||||||
return aghnet.CheckPort("tcp", req.Web.IP, portInt)
|
return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(portInt)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateDNS returns error if the DNS part of the initial configuration can't
|
// validateDNS returns error if the DNS part of the initial configuration can't
|
||||||
@@ -142,13 +153,13 @@ func (req *checkConfReq) validateDNS(
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("tcp", req.DNS.IP, port)
|
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("udp", req.DNS.IP, port)
|
err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
|
||||||
if !aghnet.IsAddrInUse(err) {
|
if !aghnet.IsAddrInUse(err) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -160,7 +171,7 @@ func (req *checkConfReq) validateDNS(
|
|||||||
log.Error("disabling DNSStubListener: %s", err)
|
log.Error("disabling DNSStubListener: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("udp", req.DNS.IP, port)
|
err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
|
||||||
canAutofix = false
|
canAutofix = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,13 +201,19 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
|
|||||||
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
|
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStaticIP - handles static IP request
|
// handleStaticIP - handles static IP request
|
||||||
// It either checks if we have a static IP
|
// It either checks if we have a static IP
|
||||||
// Or if set=true, it tries to set it
|
// Or if set=true, it tries to set it
|
||||||
func handleStaticIP(ip net.IP, set bool) staticIPJSON {
|
func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
|
||||||
resp := staticIPJSON{}
|
resp := staticIPJSON{}
|
||||||
|
|
||||||
interfaceName := aghnet.InterfaceByIP(ip)
|
interfaceName := aghnet.InterfaceByIP(ip)
|
||||||
@@ -304,8 +321,8 @@ func disableDNSStubListener() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type applyConfigReqEnt struct {
|
type applyConfigReqEnt struct {
|
||||||
IP net.IP `json:"ip"`
|
IP netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type applyConfigReq struct {
|
type applyConfigReq struct {
|
||||||
@@ -329,7 +346,6 @@ func copyInstallSettings(dst, src *configuration) {
|
|||||||
// shutdownTimeout is the timeout for shutting HTTP server down operation.
|
// shutdownTimeout is the timeout for shutting HTTP server down operation.
|
||||||
const shutdownTimeout = 5 * time.Second
|
const shutdownTimeout = 5 * time.Second
|
||||||
|
|
||||||
// shutdownSrv shuts srv down and prints error messages to the log.
|
|
||||||
func shutdownSrv(ctx context.Context, srv *http.Server) {
|
func shutdownSrv(ctx context.Context, srv *http.Server) {
|
||||||
defer log.OnPanic("")
|
defer log.OnPanic("")
|
||||||
|
|
||||||
@@ -338,38 +354,13 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := srv.Shutdown(ctx)
|
err := srv.Shutdown(ctx)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return
|
const msgFmt = "shutting down http server %q: %s"
|
||||||
}
|
if errors.Is(err, context.Canceled) {
|
||||||
|
log.Debug(msgFmt, srv.Addr, err)
|
||||||
const msgFmt = "shutting down http server %q: %s"
|
} else {
|
||||||
if errors.Is(err, context.Canceled) {
|
log.Error(msgFmt, srv.Addr, err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,14 +388,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("udp", req.DNS.IP, req.DNS.Port)
|
err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("tcp", req.DNS.IP, req.DNS.Port)
|
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
@@ -417,7 +408,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
|||||||
Context.firstRun = false
|
Context.firstRun = false
|
||||||
config.BindHost = req.Web.IP
|
config.BindHost = req.Web.IP
|
||||||
config.BindPort = req.Web.Port
|
config.BindPort = req.Web.Port
|
||||||
config.DNS.BindHosts = []net.IP{req.DNS.IP}
|
config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
|
||||||
config.DNS.Port = req.DNS.Port
|
config.DNS.Port = req.DNS.Port
|
||||||
|
|
||||||
// TODO(e.burkov): StartMods() should be put in a separate goroutine at the
|
// TODO(e.burkov): StartMods() should be put in a separate goroutine at the
|
||||||
@@ -433,7 +424,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
u := &webUser{
|
u := &User{
|
||||||
Name: req.Username,
|
Name: req.Username,
|
||||||
}
|
}
|
||||||
Context.auth.UserAdd(u, req.Password)
|
Context.auth.UserAdd(u, req.Password)
|
||||||
@@ -490,9 +481,9 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
|
|||||||
return nil, false, errors.Error("ports cannot be 0")
|
return nil, false, errors.Error("ports cannot be 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
restartHTTP = !config.BindHost.Equal(req.Web.IP) || config.BindPort != req.Web.Port
|
restartHTTP = config.BindHost != req.Web.IP || config.BindPort != req.Web.Port
|
||||||
if restartHTTP {
|
if restartHTTP {
|
||||||
err = aghnet.CheckPort("tcp", req.Web.IP, req.Web.Port)
|
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf(
|
return nil, false, fmt.Errorf(
|
||||||
"checking address %s:%d: %w",
|
"checking address %s:%d: %w",
|
||||||
@@ -518,9 +509,9 @@ func (web *Web) registerInstallHandlers() {
|
|||||||
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
||||||
// functionality will appear in default checkConfigReqEnt.
|
// functionality will appear in default checkConfigReqEnt.
|
||||||
type checkConfigReqEntBeta struct {
|
type checkConfigReqEntBeta struct {
|
||||||
IP []net.IP `json:"ip"`
|
IP []netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Autofix bool `json:"autofix"`
|
Autofix bool `json:"autofix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkConfigReqBeta is a struct representing new client's config check request
|
// checkConfigReqBeta is a struct representing new client's config check request
|
||||||
@@ -572,11 +563,16 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "encoding check_config: %s", err)
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"Failed to encode 'check_config' JSON data: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body := nonBetaReqBody.String()
|
body := nonBetaReqBody.String()
|
||||||
r.Body = io.NopCloser(strings.NewReader(body))
|
r.Body = io.NopCloser(strings.NewReader(body))
|
||||||
r.ContentLength = int64(len(body))
|
r.ContentLength = int64(len(body))
|
||||||
@@ -590,8 +586,8 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
|
|||||||
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
||||||
// functionality will appear in default applyConfigReqEnt.
|
// functionality will appear in default applyConfigReqEnt.
|
||||||
type applyConfigReqEntBeta struct {
|
type applyConfigReqEntBeta struct {
|
||||||
IP []net.IP `json:"ip"`
|
IP []netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyConfigReqBeta is a struct representing new client's config setting
|
// applyConfigReqBeta is a struct representing new client's config setting
|
||||||
@@ -644,7 +640,13 @@ func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "encoding configure: %s", err)
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"Failed to encode 'check_config' JSON data: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -686,7 +688,19 @@ func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
data.Interfaces = ifaces
|
data.Interfaces = ifaces
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerBetaInstallHandlers registers the install handlers for new client
|
// registerBetaInstallHandlers registers the install handlers for new client
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ type temporaryError interface {
|
|||||||
|
|
||||||
// Get the latest available version from the Internet
|
// Get the latest available version from the Internet
|
||||||
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp := &versionResponse{}
|
resp := &versionResponse{}
|
||||||
if Context.disableUpdate {
|
if Context.disableUpdate {
|
||||||
resp.Disabled = true
|
resp.Disabled = true
|
||||||
@@ -69,7 +71,10 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
err = json.NewEncoder(w).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "writing body: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// requestVersionInfo sets the VersionInfo field of resp if it can reach the
|
// requestVersionInfo sets the VersionInfo field of resp if it can reach the
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package home
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -164,33 +165,27 @@ func onDNSRequest(pctx *proxy.DNSContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ipsToTCPAddrs(ips []net.IP, port int) (tcpAddrs []*net.TCPAddr) {
|
func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
|
||||||
if ips == nil {
|
if ips == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tcpAddrs = make([]*net.TCPAddr, len(ips))
|
tcpAddrs = make([]*net.TCPAddr, 0, len(ips))
|
||||||
for i, ip := range ips {
|
for _, ip := range ips {
|
||||||
tcpAddrs[i] = &net.TCPAddr{
|
tcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
|
||||||
IP: ip,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tcpAddrs
|
return tcpAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func ipsToUDPAddrs(ips []net.IP, port int) (udpAddrs []*net.UDPAddr) {
|
func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
|
||||||
if ips == nil {
|
if ips == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
udpAddrs = make([]*net.UDPAddr, len(ips))
|
udpAddrs = make([]*net.UDPAddr, 0, len(ips))
|
||||||
for i, ip := range ips {
|
for _, ip := range ips {
|
||||||
udpAddrs[i] = &net.UDPAddr{
|
udpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
|
||||||
IP: ip,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return udpAddrs
|
return udpAddrs
|
||||||
@@ -200,7 +195,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
|||||||
dnsConf := config.DNS
|
dnsConf := config.DNS
|
||||||
hosts := dnsConf.BindHosts
|
hosts := dnsConf.BindHosts
|
||||||
if len(hosts) == 0 {
|
if len(hosts) == 0 {
|
||||||
hosts = []net.IP{{127, 0, 0, 1}}
|
hosts = []netip.Addr{aghnet.IPv4Localhost()}
|
||||||
}
|
}
|
||||||
|
|
||||||
newConf = dnsforward.ServerConfig{
|
newConf = dnsforward.ServerConfig{
|
||||||
@@ -246,18 +241,15 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
|||||||
newConf.FilterHandler = applyAdditionalFiltering
|
newConf.FilterHandler = applyAdditionalFiltering
|
||||||
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
|
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
|
||||||
|
|
||||||
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
|
|
||||||
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
|
|
||||||
|
|
||||||
newConf.ResolveClients = config.Clients.Sources.RDNS
|
newConf.ResolveClients = config.Clients.Sources.RDNS
|
||||||
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
|
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
|
||||||
newConf.ServeHTTP3 = dnsConf.ServeHTTP3
|
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
|
||||||
newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams
|
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
|
||||||
|
|
||||||
return newConf, nil
|
return newConf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDNSCrypt(hosts []net.IP, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) {
|
func newDNSCrypt(hosts []netip.Addr, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) {
|
||||||
if tlsConf.DNSCryptConfigFile == "" {
|
if tlsConf.DNSCryptConfigFile == "" {
|
||||||
return dnscc, errors.Error("no dnscrypt_config_file")
|
return dnscc, errors.Error("no dnscrypt_config_file")
|
||||||
}
|
}
|
||||||
@@ -361,13 +353,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
|
|||||||
log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID)
|
log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID)
|
||||||
|
|
||||||
if c.UseOwnBlockedServices {
|
if c.UseOwnBlockedServices {
|
||||||
// TODO(e.burkov): Get rid of this crutch.
|
Context.filters.ApplyBlockedServices(setts, c.BlockedServices)
|
||||||
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
|
setts.ClientName = c.Name
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -97,15 +98,9 @@ var Context homeContext
|
|||||||
|
|
||||||
// Main is the entry point
|
// Main is the entry point
|
||||||
func Main(clientBuildFS fs.FS) {
|
func Main(clientBuildFS fs.FS) {
|
||||||
initCmdLineOpts()
|
// 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
|
||||||
// The configuration file path can be overridden, but other command-line
|
args := loadOptions()
|
||||||
// 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)
|
Context.appSignalChannel = make(chan os.Signal)
|
||||||
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||||
@@ -126,18 +121,26 @@ func Main(clientBuildFS fs.FS) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if opts.serviceControlAction != "" {
|
if args.serviceControlAction != "" {
|
||||||
handleServiceControlAction(opts, clientBuildFS)
|
handleServiceControlAction(args, clientBuildFS)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the protection
|
// run the protection
|
||||||
run(opts, clientBuildFS)
|
run(args, clientBuildFS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupContext(opts options) {
|
func setupContext(args options) {
|
||||||
setupContextFlags(opts)
|
Context.runningAsService = args.runningAsService
|
||||||
|
Context.disableUpdate = args.disableUpdate ||
|
||||||
|
version.Channel() == version.ChannelDevelopment
|
||||||
|
|
||||||
|
Context.firstRun = detectFirstRun()
|
||||||
|
if Context.firstRun {
|
||||||
|
log.Info("This is the first time AdGuard Home is launched")
|
||||||
|
checkPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
switch version.Channel() {
|
switch version.Channel() {
|
||||||
case version.ChannelEdge, version.ChannelDevelopment:
|
case version.ChannelEdge, version.ChannelDevelopment:
|
||||||
@@ -172,13 +175,13 @@ func setupContext(opts options) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.checkConfig {
|
if args.checkConfig {
|
||||||
log.Info("configuration file is ok")
|
log.Info("configuration file is ok")
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.noEtcHosts && config.Clients.Sources.HostsFile {
|
if !args.noEtcHosts && config.Clients.Sources.HostsFile {
|
||||||
err = setupHostsContainer()
|
err = setupHostsContainer()
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
}
|
}
|
||||||
@@ -187,24 +190,6 @@ func setupContext(opts options) {
|
|||||||
Context.mux = http.NewServeMux()
|
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
|
// logIfUnsupported logs a formatted warning if the error is one of the
|
||||||
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
|
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
|
||||||
// nil. Otherwise, it returns err.
|
// nil. Otherwise, it returns err.
|
||||||
@@ -286,14 +271,13 @@ func setupHostsContainer() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupConfig(opts options) (err error) {
|
func setupConfig(args options) (err error) {
|
||||||
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts
|
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts
|
||||||
config.DNS.DnsfilterConf.ConfigModified = onConfigModified
|
config.DNS.DnsfilterConf.ConfigModified = onConfigModified
|
||||||
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
|
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
|
||||||
config.DNS.DnsfilterConf.DataDir = Context.getDataDir()
|
config.DNS.DnsfilterConf.DataDir = Context.getDataDir()
|
||||||
config.DNS.DnsfilterConf.Filters = slices.Clone(config.Filters)
|
config.DNS.DnsfilterConf.Filters = slices.Clone(config.Filters)
|
||||||
config.DNS.DnsfilterConf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
|
config.DNS.DnsfilterConf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
|
||||||
config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules)
|
|
||||||
config.DNS.DnsfilterConf.HTTPClient = Context.client
|
config.DNS.DnsfilterConf.HTTPClient = Context.client
|
||||||
|
|
||||||
config.DHCP.WorkDir = Context.workDir
|
config.DHCP.WorkDir = Context.workDir
|
||||||
@@ -328,9 +312,9 @@ func setupConfig(opts options) (err error) {
|
|||||||
|
|
||||||
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
|
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
|
||||||
|
|
||||||
if opts.bindPort != 0 {
|
if args.bindPort != 0 {
|
||||||
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
||||||
addPorts(tcpPorts, tcpPort(opts.bindPort), tcpPort(config.BetaBindPort))
|
addPorts(tcpPorts, tcpPort(args.bindPort), tcpPort(config.BetaBindPort))
|
||||||
|
|
||||||
udpPorts := aghalg.UniqChecker[udpPort]{}
|
udpPorts := aghalg.UniqChecker[udpPort]{}
|
||||||
addPorts(udpPorts, udpPort(config.DNS.Port))
|
addPorts(udpPorts, udpPort(config.DNS.Port))
|
||||||
@@ -352,23 +336,23 @@ func setupConfig(opts options) (err error) {
|
|||||||
return fmt.Errorf("validating udp ports: %w", err)
|
return fmt.Errorf("validating udp ports: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.BindPort = opts.bindPort
|
config.BindPort = args.bindPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// override bind host/port from the console
|
// override bind host/port from the console
|
||||||
if opts.bindHost != nil {
|
if args.bindHost.IsValid() {
|
||||||
config.BindHost = opts.bindHost
|
config.BindHost = args.bindHost
|
||||||
}
|
}
|
||||||
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
|
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
||||||
Context.pidFileName = opts.pidFile
|
Context.pidFileName = args.pidFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
|
func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
|
||||||
var clientFS, clientBetaFS fs.FS
|
var clientFS, clientBetaFS fs.FS
|
||||||
if opts.localFrontend {
|
if args.localFrontend {
|
||||||
log.Info("warning: using local frontend files")
|
log.Info("warning: using local frontend files")
|
||||||
|
|
||||||
clientFS = os.DirFS("build/static")
|
clientFS = os.DirFS("build/static")
|
||||||
@@ -397,11 +381,9 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
|
|||||||
|
|
||||||
clientFS: clientFS,
|
clientFS: clientFS,
|
||||||
clientBetaFS: clientBetaFS,
|
clientBetaFS: clientBetaFS,
|
||||||
|
|
||||||
serveHTTP3: config.DNS.ServeHTTP3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
web = newWeb(&webConf)
|
web = CreateWeb(&webConf)
|
||||||
if web == nil {
|
if web == nil {
|
||||||
return nil, fmt.Errorf("initializing web: %w", err)
|
return nil, fmt.Errorf("initializing web: %w", err)
|
||||||
}
|
}
|
||||||
@@ -416,24 +398,24 @@ func fatalOnError(err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run configures and starts AdGuard Home.
|
// run configures and starts AdGuard Home.
|
||||||
func run(opts options, clientBuildFS fs.FS) {
|
func run(args options, clientBuildFS fs.FS) {
|
||||||
// configure config filename
|
// configure config filename
|
||||||
initConfigFilename(opts)
|
initConfigFilename(args)
|
||||||
|
|
||||||
// configure working dir and config path
|
// configure working dir and config path
|
||||||
initWorkingDir(opts)
|
initWorkingDir(args)
|
||||||
|
|
||||||
// configure log level and output
|
// configure log level and output
|
||||||
configureLogger(opts)
|
configureLogger(args)
|
||||||
|
|
||||||
// Print the first message after logger is configured.
|
// Print the first message after logger is configured.
|
||||||
log.Info(version.Full())
|
log.Println(version.Full())
|
||||||
log.Debug("current working directory is %s", Context.workDir)
|
log.Debug("current working directory is %s", Context.workDir)
|
||||||
if opts.runningAsService {
|
if args.runningAsService {
|
||||||
log.Info("AdGuard Home is running as a service")
|
log.Info("AdGuard Home is running as a service")
|
||||||
}
|
}
|
||||||
|
|
||||||
setupContext(opts)
|
setupContext(args)
|
||||||
|
|
||||||
err := configureOS(config)
|
err := configureOS(config)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
@@ -443,7 +425,7 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
// but also avoid relying on automatic Go init() function
|
// but also avoid relying on automatic Go init() function
|
||||||
filtering.InitModule()
|
filtering.InitModule()
|
||||||
|
|
||||||
err = setupConfig(opts)
|
err = setupConfig(args)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
@@ -472,10 +454,10 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
||||||
GLMode = opts.glinetMode
|
GLMode = args.glinetMode
|
||||||
var rateLimiter *authRateLimiter
|
var arl *authRateLimiter
|
||||||
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
||||||
rateLimiter = newAuthRateLimiter(
|
arl = newAuthRateLimiter(
|
||||||
time.Duration(config.AuthBlockMin)*time.Minute,
|
time.Duration(config.AuthBlockMin)*time.Minute,
|
||||||
config.AuthAttempts,
|
config.AuthAttempts,
|
||||||
)
|
)
|
||||||
@@ -487,7 +469,7 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
sessFilename,
|
sessFilename,
|
||||||
config.Users,
|
config.Users,
|
||||||
config.WebSessionTTLHours*60*60,
|
config.WebSessionTTLHours*60*60,
|
||||||
rateLimiter,
|
arl,
|
||||||
)
|
)
|
||||||
if Context.auth == nil {
|
if Context.auth == nil {
|
||||||
log.Fatalf("Couldn't initialize Auth module")
|
log.Fatalf("Couldn't initialize Auth module")
|
||||||
@@ -499,7 +481,7 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
log.Fatalf("Can't initialize TLS module")
|
log.Fatalf("Can't initialize TLS module")
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.web, err = initWeb(opts, clientBuildFS)
|
Context.web, err = initWeb(args, clientBuildFS)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
@@ -556,7 +538,7 @@ func checkPermissions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We should check if AdGuard Home is able to bind to port 53
|
// We should check if AdGuard Home is able to bind to port 53
|
||||||
err := aghnet.CheckPort("tcp", net.IP{127, 0, 0, 1}, defaultPortDNS)
|
err := aghnet.CheckPort("tcp", netip.AddrPortFrom(aghnet.IPv4Localhost(), defaultPortDNS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrPermission) {
|
if errors.Is(err, os.ErrPermission) {
|
||||||
log.Fatal(`Permission check failed.
|
log.Fatal(`Permission check failed.
|
||||||
@@ -591,10 +573,10 @@ func writePIDFile(fn string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfigFilename(opts options) {
|
func initConfigFilename(args options) {
|
||||||
// config file path can be overridden by command-line arguments:
|
// config file path can be overridden by command-line arguments:
|
||||||
if opts.confFilename != "" {
|
if args.configFilename != "" {
|
||||||
Context.configFilename = opts.confFilename
|
Context.configFilename = args.configFilename
|
||||||
} else {
|
} else {
|
||||||
// Default config file name
|
// Default config file name
|
||||||
Context.configFilename = "AdGuardHome.yaml"
|
Context.configFilename = "AdGuardHome.yaml"
|
||||||
@@ -603,15 +585,15 @@ func initConfigFilename(opts options) {
|
|||||||
|
|
||||||
// initWorkingDir initializes the workDir
|
// initWorkingDir initializes the workDir
|
||||||
// if no command-line arguments specified, we use the directory where our binary file is located
|
// if no command-line arguments specified, we use the directory where our binary file is located
|
||||||
func initWorkingDir(opts options) {
|
func initWorkingDir(args options) {
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.workDir != "" {
|
if args.workDir != "" {
|
||||||
// If there is a custom config file, use it's directory as our working dir
|
// If there is a custom config file, use it's directory as our working dir
|
||||||
Context.workDir = opts.workDir
|
Context.workDir = args.workDir
|
||||||
} else {
|
} else {
|
||||||
Context.workDir = filepath.Dir(execPath)
|
Context.workDir = filepath.Dir(execPath)
|
||||||
}
|
}
|
||||||
@@ -625,15 +607,15 @@ func initWorkingDir(opts options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// configureLogger configures logger level and output
|
// configureLogger configures logger level and output
|
||||||
func configureLogger(opts options) {
|
func configureLogger(args options) {
|
||||||
ls := getLogSettings()
|
ls := getLogSettings()
|
||||||
|
|
||||||
// command-line arguments can override config settings
|
// command-line arguments can override config settings
|
||||||
if opts.verbose || config.Verbose {
|
if args.verbose || config.Verbose {
|
||||||
ls.Verbose = true
|
ls.Verbose = true
|
||||||
}
|
}
|
||||||
if opts.logFile != "" {
|
if args.logFile != "" {
|
||||||
ls.File = opts.logFile
|
ls.File = args.logFile
|
||||||
} else if config.File != "" {
|
} else if config.File != "" {
|
||||||
ls.File = config.File
|
ls.File = config.File
|
||||||
}
|
}
|
||||||
@@ -654,7 +636,7 @@ func configureLogger(opts options) {
|
|||||||
// happen pretty quickly.
|
// happen pretty quickly.
|
||||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||||
|
|
||||||
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
if args.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
||||||
// When running as a Windows service, use eventlog by default if nothing
|
// When running as a Windows service, use eventlog by default if nothing
|
||||||
// else is configured. Otherwise, we'll simply lose the log output.
|
// else is configured. Otherwise, we'll simply lose the log output.
|
||||||
ls.File = configSyslog
|
ls.File = configSyslog
|
||||||
@@ -744,29 +726,25 @@ func exitWithError() {
|
|||||||
os.Exit(64)
|
os.Exit(64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadCmdLineOpts reads command line arguments and initializes configuration
|
// loadOptions reads command line arguments and initializes configuration
|
||||||
// from them. If there is an error or an effect, loadCmdLineOpts processes them
|
func loadOptions() options {
|
||||||
// and exits.
|
o, f, err := parse(os.Args[0], os.Args[1:])
|
||||||
func loadCmdLineOpts() (opts options) {
|
|
||||||
opts, eff, err := parseCmdOpts(os.Args[0], os.Args[1:])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
printHelp(os.Args[0])
|
_ = printHelp(os.Args[0])
|
||||||
|
|
||||||
exitWithError()
|
exitWithError()
|
||||||
}
|
} else if f != nil {
|
||||||
|
err = f()
|
||||||
if eff != nil {
|
|
||||||
err = eff()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
exitWithError()
|
exitWithError()
|
||||||
|
} else {
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
// printWebAddrs prints addresses built from proto, addr, and an appropriate
|
// printWebAddrs prints addresses built from proto, addr, and an appropriate
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package home
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
aghtest.DiscardLogOutput(m)
|
|
||||||
initCmdLineOpts()
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package home
|
package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
@@ -49,35 +51,43 @@ var allowedLanguages = stringutil.NewSet(
|
|||||||
"zh-tw",
|
"zh-tw",
|
||||||
)
|
)
|
||||||
|
|
||||||
// languageJSON is the JSON structure for language requests and responses.
|
func handleI18nCurrentLanguage(w http.ResponseWriter, _ *http.Request) {
|
||||||
type languageJSON struct {
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
Language string `json:"language"`
|
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)
|
||||||
|
|
||||||
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
return
|
||||||
log.Printf("home: language is %s", config.Language)
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, &languageJSON{
|
|
||||||
Language: config.Language,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
|
func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
|
||||||
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
// This use of ReadAll is safe, because request's body is now limited.
|
||||||
return
|
body, err := io.ReadAll(r.Body)
|
||||||
}
|
|
||||||
|
|
||||||
langReq := &languageJSON{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(langReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "reading req: %s", err)
|
msg := fmt.Sprintf("failed to read request body: %s", err)
|
||||||
|
log.Println(msg)
|
||||||
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lang := langReq.Language
|
language := strings.TrimSpace(string(body))
|
||||||
if !allowedLanguages.Has(lang) {
|
if language == "" {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "unknown language: %q", lang)
|
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)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,8 +96,7 @@ func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
|
|||||||
config.Lock()
|
config.Lock()
|
||||||
defer config.Unlock()
|
defer config.Unlock()
|
||||||
|
|
||||||
config.Language = lang
|
config.Language = language
|
||||||
log.Printf("home: language is set to %s", lang)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
onConfigModified()
|
onConfigModified()
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ package home
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"howett.net/plist"
|
"howett.net/plist"
|
||||||
@@ -28,7 +27,7 @@ func setupDNSIPs(t testing.TB) {
|
|||||||
|
|
||||||
config = &configuration{
|
config = &configuration{
|
||||||
DNS: dnsConfig{
|
DNS: dnsConfig{
|
||||||
BindHosts: []net.IP{netutil.IPv4Zero()},
|
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
|
||||||
Port: defaultPortDNS,
|
Port: defaultPortDNS,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,63 +2,33 @@ package home
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(a.garipov): Replace with package flag.
|
// options passed from command-line arguments
|
||||||
|
|
||||||
// options represents the command-line options.
|
|
||||||
type options struct {
|
type options struct {
|
||||||
// confFilename is the path to the configuration file.
|
verbose bool // is verbose logging enabled
|
||||||
confFilename string
|
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 netip.Addr // 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
|
||||||
|
|
||||||
// workDir is the path to the working directory where AdGuard Home stores
|
// service control action (see service.ControlAction array + "status" command)
|
||||||
// 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
|
serviceControlAction string
|
||||||
|
|
||||||
// bindHost is the address on which to serve the HTTP UI.
|
// runningAsService flag is set to true when options are passed from the service runner
|
||||||
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
|
runningAsService bool
|
||||||
|
|
||||||
// glinetMode shows if the GL-Inet compatibility mode is enabled.
|
glinetMode bool // Activate GL-Inet compatibility mode
|
||||||
glinetMode bool
|
|
||||||
|
|
||||||
// noEtcHosts flag should be provided when /etc/hosts file shouldn't be
|
// noEtcHosts flag should be provided when /etc/hosts file shouldn't be
|
||||||
// used.
|
// used.
|
||||||
@@ -69,85 +39,88 @@ type options struct {
|
|||||||
localFrontend bool
|
localFrontend bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// initCmdLineOpts completes initialization of the global command-line option
|
// functions used for their side-effects
|
||||||
// slice. It must only be called once.
|
type effect func() error
|
||||||
func initCmdLineOpts() {
|
|
||||||
// The --help option cannot be put directly into cmdLineOpts, because that
|
type arg struct {
|
||||||
// causes initialization cycle due to printHelp referencing cmdLineOpts.
|
description string // a short, English description of the argument
|
||||||
cmdLineOpts = append(cmdLineOpts, cmdLineOpt{
|
longName string // the name of the argument used after '--'
|
||||||
updateWithValue: nil,
|
shortName string // the name of the argument used after '-'
|
||||||
updateNoValue: nil,
|
|
||||||
effect: func(o options, exec string) (effect, error) {
|
// only one of updateWithValue, updateNoValue, and effect should be present
|
||||||
return func() error { printHelp(exec); exitWithError(); return nil }, nil
|
|
||||||
},
|
updateWithValue func(o options, v string) (options, error) // the mutator for arguments with parameters
|
||||||
serialize: func(o options) (val string, ok bool) { return "", false },
|
updateNoValue func(o options) (options, error) // the mutator for arguments without parameters
|
||||||
description: "Print this help.",
|
effect func(o options, exec string) (f effect, err error) // the side-effect closure generator
|
||||||
longName: "help",
|
|
||||||
shortName: "",
|
serialize func(o options) []string // the re-serialization function back to arguments (return nil for omit)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// effect is the type for functions used for their side-effects.
|
// {type}SliceOrNil functions check their parameter of type {type}
|
||||||
type effect func() (err error)
|
// against its zero value and return nil if the parameter value is
|
||||||
|
// zero otherwise they return a string slice of the parameter
|
||||||
|
|
||||||
// cmdLineOpt contains the data for a single command-line option. Only one of
|
func ipSliceOrNil(ip netip.Addr) []string {
|
||||||
// updateWithValue, updateNoValue, and effect must be present.
|
if !ip.IsValid() {
|
||||||
type cmdLineOpt struct {
|
return nil
|
||||||
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)
|
|
||||||
|
|
||||||
// serialize is a function that encodes the option into a slice of
|
return []string{ip.String()}
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdLineOpts are all command-line options of AdGuard Home.
|
func stringSliceOrNil(s string) []string {
|
||||||
var cmdLineOpts = []cmdLineOpt{{
|
if s == "" {
|
||||||
updateWithValue: func(o options, v string) (options, error) {
|
return nil
|
||||||
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 o.bindHost.String(), true
|
return []string{s}
|
||||||
},
|
}
|
||||||
description: "Host address to bind HTTP server on.",
|
|
||||||
longName: "host",
|
func intSliceOrNil(i int) []string {
|
||||||
shortName: "h",
|
if i == 0 {
|
||||||
}, {
|
return nil
|
||||||
updateWithValue: func(o options, v string) (options, error) {
|
}
|
||||||
|
|
||||||
|
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, _ = netip.ParseAddr(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) {
|
||||||
var err error
|
var err error
|
||||||
var p int
|
var p int
|
||||||
minPort, maxPort := 0, 1<<16-1
|
minPort, maxPort := 0, 1<<16-1
|
||||||
@@ -158,119 +131,78 @@ var cmdLineOpts = []cmdLineOpt{{
|
|||||||
} else {
|
} else {
|
||||||
o.bindPort = p
|
o.bindPort = p
|
||||||
}
|
}
|
||||||
|
|
||||||
return o, err
|
return o, err
|
||||||
},
|
}, nil, nil,
|
||||||
updateNoValue: nil,
|
func(o options) []string { return intSliceOrNil(o.bindPort) },
|
||||||
effect: nil,
|
}
|
||||||
serialize: func(o options) (val string, ok bool) {
|
|
||||||
if o.bindPort == 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return strconv.Itoa(o.bindPort), true
|
var serviceArg = arg{
|
||||||
},
|
"Service control action: status, install, uninstall, start, stop, restart, reload (configuration).",
|
||||||
description: "Port to serve HTTP pages on.",
|
"service", "s",
|
||||||
longName: "port",
|
func(o options, v string) (options, error) {
|
||||||
shortName: "p",
|
|
||||||
}, {
|
|
||||||
updateWithValue: func(o options, v string) (options, error) {
|
|
||||||
o.serviceControlAction = v
|
o.serviceControlAction = v
|
||||||
return o, nil
|
return o, nil
|
||||||
},
|
}, nil, nil,
|
||||||
updateNoValue: nil,
|
func(o options) []string { return stringSliceOrNil(o.serviceControlAction) },
|
||||||
effect: nil,
|
}
|
||||||
serialize: func(o options) (val string, ok bool) {
|
|
||||||
return o.serviceControlAction, o.serviceControlAction != ""
|
var logfileArg = arg{
|
||||||
},
|
"Path to log file. If empty: write to stdout; if 'syslog': write to system log.",
|
||||||
description: `Service control action: status, install (as a service), ` +
|
"logfile", "l",
|
||||||
`uninstall (as a service), start, stop, restart, reload (configuration).`,
|
func(o options, v string) (options, error) { o.logFile = v; return o, nil }, nil, nil,
|
||||||
longName: "service",
|
func(o options) []string { return stringSliceOrNil(o.logFile) },
|
||||||
shortName: "s",
|
}
|
||||||
}, {
|
|
||||||
updateWithValue: func(o options, v string) (options, error) { o.logFile = v; return o, nil },
|
var pidfileArg = arg{
|
||||||
updateNoValue: nil,
|
"Path to a file where PID is stored.",
|
||||||
effect: nil,
|
"pidfile", "",
|
||||||
serialize: func(o options) (val string, ok bool) { return o.logFile, o.logFile != "" },
|
func(o options, v string) (options, error) { o.pidFile = v; return o, nil }, nil, nil,
|
||||||
description: `Path to log file. If empty, write to stdout; ` +
|
func(o options) []string { return stringSliceOrNil(o.pidFile) },
|
||||||
`if "syslog", write to system log.`,
|
}
|
||||||
longName: "logfile",
|
|
||||||
shortName: "l",
|
var checkConfigArg = arg{
|
||||||
}, {
|
"Check configuration and exit.",
|
||||||
updateWithValue: func(o options, v string) (options, error) { o.pidFile = v; return o, nil },
|
"check-config", "",
|
||||||
updateNoValue: nil,
|
nil, func(o options) (options, error) { o.checkConfig = true; return o, nil }, nil,
|
||||||
effect: nil,
|
func(o options) []string { return boolSliceOrNil(o.checkConfig) },
|
||||||
serialize: func(o options) (val string, ok bool) { return o.pidFile, o.pidFile != "" },
|
}
|
||||||
description: "Path to a file where PID is stored.",
|
|
||||||
longName: "pidfile",
|
var noCheckUpdateArg = arg{
|
||||||
shortName: "",
|
"Don't check for updates.",
|
||||||
}, {
|
"no-check-update", "",
|
||||||
updateWithValue: nil,
|
nil, func(o options) (options, error) { o.disableUpdate = true; return o, nil }, nil,
|
||||||
updateNoValue: func(o options) (options, error) { o.checkConfig = true; return o, nil },
|
func(o options) []string { return boolSliceOrNil(o.disableUpdate) },
|
||||||
effect: nil,
|
}
|
||||||
serialize: func(o options) (val string, ok bool) { return "", o.checkConfig },
|
|
||||||
description: "Check configuration and exit.",
|
var disableMemoryOptimizationArg = arg{
|
||||||
longName: "check-config",
|
"Deprecated. Disable memory optimization.",
|
||||||
shortName: "",
|
"no-mem-optimization", "",
|
||||||
}, {
|
nil, nil, func(_ options, _ string) (f effect, err error) {
|
||||||
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")
|
log.Info("warning: using --no-mem-optimization flag has no effect and is deprecated")
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
serialize: func(o options) (val string, ok bool) { return "", false },
|
func(o options) []string { return nil },
|
||||||
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) {
|
|
||||||
log.Info(
|
|
||||||
"warning: --no-etc-hosts flag is deprecated and will be removed in the future versions",
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil, nil
|
var verboseArg = arg{
|
||||||
},
|
"Enable verbose output.",
|
||||||
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
|
"verbose", "v",
|
||||||
description: "Deprecated. Do not use the OS-provided hosts.",
|
nil, func(o options) (options, error) { o.verbose = true; return o, nil }, nil,
|
||||||
longName: "no-etc-hosts",
|
func(o options) []string { return boolSliceOrNil(o.verbose) },
|
||||||
shortName: "",
|
}
|
||||||
}, {
|
|
||||||
updateWithValue: nil,
|
var glinetArg = arg{
|
||||||
updateNoValue: func(o options) (options, error) { o.localFrontend = true; return o, nil },
|
"Run in GL-Inet compatibility mode.",
|
||||||
effect: nil,
|
"glinet", "",
|
||||||
serialize: func(o options) (val string, ok bool) { return "", o.localFrontend },
|
nil, func(o options) (options, error) { o.glinetMode = true; return o, nil }, nil,
|
||||||
description: "Use local frontend directories.",
|
func(o options) []string { return boolSliceOrNil(o.glinetMode) },
|
||||||
longName: "local-frontend",
|
}
|
||||||
|
|
||||||
|
var versionArg = arg{
|
||||||
|
description: "Show the version and exit. Show more detailed version description with -v.",
|
||||||
|
longName: "version",
|
||||||
shortName: "",
|
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,
|
updateWithValue: nil,
|
||||||
updateNoValue: nil,
|
updateNoValue: nil,
|
||||||
effect: func(o options, exec string) (effect, error) {
|
effect: func(o options, exec string) (effect, error) {
|
||||||
@@ -280,178 +212,176 @@ var cmdLineOpts = []cmdLineOpt{{
|
|||||||
} else {
|
} else {
|
||||||
fmt.Println(version.Full())
|
fmt.Println(version.Full())
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
serialize: func(o options) (val string, ok bool) { return "", false },
|
serialize: func(o options) []string { return nil },
|
||||||
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
|
var helpArg = arg{
|
||||||
// there are any I/O errors.
|
"Print this help.",
|
||||||
func printHelp(exec string) {
|
"help", "",
|
||||||
b := &strings.Builder{}
|
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 },
|
||||||
|
}
|
||||||
|
|
||||||
stringutil.WriteToBuilder(
|
var noEtcHostsArg = arg{
|
||||||
b,
|
description: "Deprecated. Do not use the OS-provided hosts.",
|
||||||
"Usage:\n\n",
|
longName: "no-etc-hosts",
|
||||||
fmt.Sprintf("%s [options]\n\n", exec),
|
shortName: "",
|
||||||
"Options:\n",
|
updateWithValue: nil,
|
||||||
)
|
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
|
||||||
|
effect: func(_ options, _ string) (f effect, err error) {
|
||||||
|
log.Info(
|
||||||
|
"warning: --no-etc-hosts flag is deprecated and will be removed in the future versions",
|
||||||
|
)
|
||||||
|
|
||||||
var err error
|
return nil, nil
|
||||||
for _, opt := range cmdLineOpts {
|
},
|
||||||
|
serialize: func(o options) []string { return boolSliceOrNil(o.noEtcHosts) },
|
||||||
|
}
|
||||||
|
|
||||||
|
var localFrontendArg = arg{
|
||||||
|
description: "Use local frontend directories.",
|
||||||
|
longName: "local-frontend",
|
||||||
|
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) },
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
args = []arg{
|
||||||
|
configArg,
|
||||||
|
workDirArg,
|
||||||
|
hostArg,
|
||||||
|
portArg,
|
||||||
|
serviceArg,
|
||||||
|
logfileArg,
|
||||||
|
pidfileArg,
|
||||||
|
checkConfigArg,
|
||||||
|
noCheckUpdateArg,
|
||||||
|
disableMemoryOptimizationArg,
|
||||||
|
noEtcHostsArg,
|
||||||
|
localFrontendArg,
|
||||||
|
verboseArg,
|
||||||
|
glinetArg,
|
||||||
|
versionArg,
|
||||||
|
helpArg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUsageLines(exec string, args []arg) []string {
|
||||||
|
usage := []string{
|
||||||
|
"Usage:",
|
||||||
|
"",
|
||||||
|
fmt.Sprintf("%s [options]", exec),
|
||||||
|
"",
|
||||||
|
"Options:",
|
||||||
|
}
|
||||||
|
for _, arg := range args {
|
||||||
val := ""
|
val := ""
|
||||||
if opt.updateWithValue != nil {
|
if arg.updateWithValue != nil {
|
||||||
val = " VALUE"
|
val = " VALUE"
|
||||||
}
|
}
|
||||||
|
if arg.shortName != "" {
|
||||||
longDesc := opt.longName + val
|
usage = append(usage, fmt.Sprintf(" -%s, %-30s %s",
|
||||||
if opt.shortName != "" {
|
arg.shortName,
|
||||||
_, err = fmt.Fprintf(b, " -%s, --%-28s %s\n", opt.shortName, longDesc, opt.description)
|
"--"+arg.longName+val,
|
||||||
|
arg.description))
|
||||||
} else {
|
} else {
|
||||||
_, err = fmt.Fprintf(b, " --%-32s %s\n", longDesc, opt.description)
|
usage = append(usage, fmt.Sprintf(" %-34s %s",
|
||||||
}
|
"--"+arg.longName+val,
|
||||||
|
arg.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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCmdOpts parses the command-line arguments into options and effects.
|
func printHelp(exec string) error {
|
||||||
func parseCmdOpts(cmdName string, args []string) (o options, eff effect, err error) {
|
for _, line := range getUsageLines(exec, args) {
|
||||||
// Don't use range since the loop changes the loop variable.
|
_, err := fmt.Println(line)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return eff
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// optsToArgs converts command line options into a list of arguments.
|
func argMatches(a arg, v string) bool {
|
||||||
func optsToArgs(o options) (args []string) {
|
return v == "--"+a.longName || (a.shortName != "" && v == "-"+a.shortName)
|
||||||
for _, opt := range cmdLineOpts {
|
}
|
||||||
val, ok := opt.serialize(o)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.shortName != "" {
|
func parse(exec string, ss []string) (o options, f effect, err error) {
|
||||||
args = append(args, "-"+opt.shortName)
|
for i := 0; i < len(ss); i++ {
|
||||||
} else {
|
v := ss[i]
|
||||||
args = append(args, "--"+opt.longName)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if !knownParam {
|
||||||
if val != "" {
|
return o, f, fmt.Errorf("unknown option %v", v)
|
||||||
args = append(args, val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package home
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func testParseOK(t *testing.T, ss ...string) options {
|
func testParseOK(t *testing.T, ss ...string) options {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
o, _, err := parseCmdOpts("", ss)
|
o, _, err := parse("", ss)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return o
|
return o
|
||||||
@@ -21,7 +21,7 @@ func testParseOK(t *testing.T, ss ...string) options {
|
|||||||
func testParseErr(t *testing.T, descr string, ss ...string) {
|
func testParseErr(t *testing.T, descr string, ss ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
_, _, err := parseCmdOpts("", ss)
|
_, _, err := parse("", ss)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,11 +38,11 @@ func TestParseVerbose(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseConfigFilename(t *testing.T) {
|
func TestParseConfigFilename(t *testing.T) {
|
||||||
assert.Equal(t, "", testParseOK(t).confFilename, "empty is no config filename")
|
assert.Equal(t, "", testParseOK(t).configFilename, "empty is no config filename")
|
||||||
assert.Equal(t, "path", testParseOK(t, "-c", "path").confFilename, "-c is config filename")
|
assert.Equal(t, "path", testParseOK(t, "-c", "path").configFilename, "-c is config filename")
|
||||||
testParseParamMissing(t, "-c")
|
testParseParamMissing(t, "-c")
|
||||||
|
|
||||||
assert.Equal(t, "path", testParseOK(t, "--config", "path").confFilename, "--config is config filename")
|
assert.Equal(t, "path", testParseOK(t, "--config", "path").configFilename, "--config is config filename")
|
||||||
testParseParamMissing(t, "--config")
|
testParseParamMissing(t, "--config")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +56,13 @@ func TestParseWorkDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseBindHost(t *testing.T) {
|
func TestParseBindHost(t *testing.T) {
|
||||||
assert.Nil(t, testParseOK(t).bindHost, "empty is not host")
|
wantAddr := netip.AddrFrom4([4]byte{1, 2, 3, 4})
|
||||||
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
|
|
||||||
|
assert.Zero(t, testParseOK(t).bindHost, "empty is not host")
|
||||||
|
assert.Equal(t, wantAddr, testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
|
||||||
testParseParamMissing(t, "-h")
|
testParseParamMissing(t, "-h")
|
||||||
|
|
||||||
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host")
|
assert.Equal(t, wantAddr, testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host")
|
||||||
testParseParamMissing(t, "--host")
|
testParseParamMissing(t, "--host")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ func TestParseDisableUpdate(t *testing.T) {
|
|||||||
|
|
||||||
// TODO(e.burkov): Remove after v0.108.0.
|
// TODO(e.burkov): Remove after v0.108.0.
|
||||||
func TestParseDisableMemoryOptimization(t *testing.T) {
|
func TestParseDisableMemoryOptimization(t *testing.T) {
|
||||||
o, eff, err := parseCmdOpts("", []string{"--no-mem-optimization"})
|
o, eff, err := parse("", []string{"--no-mem-optimization"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Nil(t, eff)
|
assert.Nil(t, eff)
|
||||||
@@ -130,73 +132,73 @@ func TestParseUnknown(t *testing.T) {
|
|||||||
testParseErr(t, "unknown dash", "-")
|
testParseErr(t, "unknown dash", "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOptsToArgs(t *testing.T) {
|
func TestSerialize(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
args []string
|
|
||||||
opts options
|
opts options
|
||||||
|
ss []string
|
||||||
}{{
|
}{{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
args: []string{},
|
|
||||||
opts: options{},
|
opts: options{},
|
||||||
|
ss: []string{},
|
||||||
}, {
|
}, {
|
||||||
name: "config_filename",
|
name: "config_filename",
|
||||||
args: []string{"-c", "path"},
|
opts: options{configFilename: "path"},
|
||||||
opts: options{confFilename: "path"},
|
ss: []string{"-c", "path"},
|
||||||
}, {
|
}, {
|
||||||
name: "work_dir",
|
name: "work_dir",
|
||||||
args: []string{"-w", "path"},
|
|
||||||
opts: options{workDir: "path"},
|
opts: options{workDir: "path"},
|
||||||
|
ss: []string{"-w", "path"},
|
||||||
}, {
|
}, {
|
||||||
name: "bind_host",
|
name: "bind_host",
|
||||||
args: []string{"-h", "1.2.3.4"},
|
opts: options{bindHost: netip.AddrFrom4([4]byte{1, 2, 3, 4})},
|
||||||
opts: options{bindHost: net.IP{1, 2, 3, 4}},
|
ss: []string{"-h", "1.2.3.4"},
|
||||||
}, {
|
}, {
|
||||||
name: "bind_port",
|
name: "bind_port",
|
||||||
args: []string{"-p", "666"},
|
|
||||||
opts: options{bindPort: 666},
|
opts: options{bindPort: 666},
|
||||||
|
ss: []string{"-p", "666"},
|
||||||
}, {
|
}, {
|
||||||
name: "log_file",
|
name: "log_file",
|
||||||
args: []string{"-l", "path"},
|
|
||||||
opts: options{logFile: "path"},
|
opts: options{logFile: "path"},
|
||||||
|
ss: []string{"-l", "path"},
|
||||||
}, {
|
}, {
|
||||||
name: "pid_file",
|
name: "pid_file",
|
||||||
args: []string{"--pidfile", "path"},
|
|
||||||
opts: options{pidFile: "path"},
|
opts: options{pidFile: "path"},
|
||||||
|
ss: []string{"--pidfile", "path"},
|
||||||
}, {
|
}, {
|
||||||
name: "disable_update",
|
name: "disable_update",
|
||||||
args: []string{"--no-check-update"},
|
|
||||||
opts: options{disableUpdate: true},
|
opts: options{disableUpdate: true},
|
||||||
|
ss: []string{"--no-check-update"},
|
||||||
}, {
|
}, {
|
||||||
name: "control_action",
|
name: "control_action",
|
||||||
args: []string{"-s", "run"},
|
|
||||||
opts: options{serviceControlAction: "run"},
|
opts: options{serviceControlAction: "run"},
|
||||||
|
ss: []string{"-s", "run"},
|
||||||
}, {
|
}, {
|
||||||
name: "glinet_mode",
|
name: "glinet_mode",
|
||||||
args: []string{"--glinet"},
|
|
||||||
opts: options{glinetMode: true},
|
opts: options{glinetMode: true},
|
||||||
|
ss: []string{"--glinet"},
|
||||||
}, {
|
}, {
|
||||||
name: "multiple",
|
name: "multiple",
|
||||||
args: []string{
|
opts: options{
|
||||||
|
serviceControlAction: "run",
|
||||||
|
configFilename: "config",
|
||||||
|
workDir: "work",
|
||||||
|
pidFile: "pid",
|
||||||
|
disableUpdate: true,
|
||||||
|
},
|
||||||
|
ss: []string{
|
||||||
"-c", "config",
|
"-c", "config",
|
||||||
"-w", "work",
|
"-w", "work",
|
||||||
"-s", "run",
|
"-s", "run",
|
||||||
"--pidfile", "pid",
|
"--pidfile", "pid",
|
||||||
"--no-check-update",
|
"--no-check-update",
|
||||||
},
|
},
|
||||||
opts: options{
|
|
||||||
serviceControlAction: "run",
|
|
||||||
confFilename: "config",
|
|
||||||
workDir: "work",
|
|
||||||
pidFile: "pid",
|
|
||||||
disableUpdate: true,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
result := optsToArgs(tc.opts)
|
result := serialize(tc.opts)
|
||||||
assert.ElementsMatch(t, tc.args, result)
|
assert.ElementsMatch(t, tc.ss, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,8 +176,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
|
|||||||
chooseSystem()
|
chooseSystem()
|
||||||
|
|
||||||
action := opts.serviceControlAction
|
action := opts.serviceControlAction
|
||||||
log.Info(version.Full())
|
log.Printf("service: control action: %s", action)
|
||||||
log.Info("service: control action: %s", action)
|
|
||||||
|
|
||||||
if action == "reload" {
|
if action == "reload" {
|
||||||
sendSigReload()
|
sendSigReload()
|
||||||
@@ -197,7 +196,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
|
|||||||
DisplayName: serviceDisplayName,
|
DisplayName: serviceDisplayName,
|
||||||
Description: serviceDescription,
|
Description: serviceDescription,
|
||||||
WorkingDirectory: pwd,
|
WorkingDirectory: pwd,
|
||||||
Arguments: optsToArgs(runOpts),
|
Arguments: serialize(runOpts),
|
||||||
}
|
}
|
||||||
configureService(svcConfig)
|
configureService(svcConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !webCheckPortAvailable(setts.PortHTTPS) {
|
if !WebCheckPortAvailable(setts.PortHTTPS) {
|
||||||
aghhttp.Error(
|
aghhttp.Error(
|
||||||
r,
|
r,
|
||||||
w,
|
w,
|
||||||
@@ -356,7 +356,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): Investigate and perhaps check other ports.
|
// TODO(e.burkov): Investigate and perhaps check other ports.
|
||||||
if !webCheckPortAvailable(data.PortHTTPS) {
|
if !WebCheckPortAvailable(data.PortHTTPS) {
|
||||||
aghhttp.Error(
|
aghhttp.Error(
|
||||||
r,
|
r,
|
||||||
w,
|
w,
|
||||||
@@ -680,6 +680,8 @@ func unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
|
func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if data.CertificateChain != "" {
|
if data.CertificateChain != "" {
|
||||||
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
|
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
|
||||||
data.CertificateChain = encoded
|
data.CertificateChain = encoded
|
||||||
@@ -690,7 +692,16 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
|
|||||||
data.PrivateKey = ""
|
data.PrivateKey = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
err := json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
"Failed to marshal json with TLS status: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerWebHandlers registers HTTP handlers for TLS configuration
|
// registerWebHandlers registers HTTP handlers for TLS configuration
|
||||||
|
|||||||
@@ -278,11 +278,11 @@ func upgradeSchema4to5(diskConf yobj) error {
|
|||||||
log.Fatalf("Can't use password \"%s\": bcrypt.GenerateFromPassword: %s", passStr, err)
|
log.Fatalf("Can't use password \"%s\": bcrypt.GenerateFromPassword: %s", passStr, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
u := webUser{
|
u := User{
|
||||||
Name: nameStr,
|
Name: nameStr,
|
||||||
PasswordHash: string(hash),
|
PasswordHash: string(hash),
|
||||||
}
|
}
|
||||||
users := []webUser{u}
|
users := []User{u}
|
||||||
diskConf["users"] = users
|
diskConf["users"] = users
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,8 +16,6 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/NYTimes/gziphandler"
|
"github.com/NYTimes/gziphandler"
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
)
|
)
|
||||||
@@ -37,7 +35,7 @@ type webConfig struct {
|
|||||||
clientFS fs.FS
|
clientFS fs.FS
|
||||||
clientBetaFS fs.FS
|
clientBetaFS fs.FS
|
||||||
|
|
||||||
BindHost net.IP
|
BindHost netip.Addr
|
||||||
BindPort int
|
BindPort int
|
||||||
BetaBindPort int
|
BetaBindPort int
|
||||||
PortHTTPS int
|
PortHTTPS int
|
||||||
@@ -55,56 +53,40 @@ type webConfig struct {
|
|||||||
WriteTimeout time.Duration
|
WriteTimeout time.Duration
|
||||||
|
|
||||||
firstRun bool
|
firstRun bool
|
||||||
|
|
||||||
serveHTTP3 bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpsServer contains the data for the HTTPS server.
|
// HTTPSServer - HTTPS Server
|
||||||
type httpsServer struct {
|
type HTTPSServer struct {
|
||||||
// server is the pre-HTTP/3 HTTPS server.
|
server *http.Server
|
||||||
server *http.Server
|
cond *sync.Cond
|
||||||
// server3 is the HTTP/3 HTTPS server. If it is not nil,
|
condLock sync.Mutex
|
||||||
// [httpsServer.server] must also be non-nil.
|
shutdown bool // if TRUE, don't restart the server
|
||||||
server3 *http3.Server
|
enabled bool
|
||||||
|
cert tls.Certificate
|
||||||
// TODO(a.garipov): Why is there a *sync.Cond here? Remove.
|
|
||||||
cond *sync.Cond
|
|
||||||
condLock sync.Mutex
|
|
||||||
cert tls.Certificate
|
|
||||||
inShutdown bool
|
|
||||||
enabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web is the web UI and API server.
|
// Web - module object
|
||||||
type Web struct {
|
type Web struct {
|
||||||
conf *webConfig
|
conf *webConfig
|
||||||
|
forceHTTPS bool
|
||||||
// TODO(a.garipov): Refactor all these servers.
|
httpServer *http.Server // HTTP module
|
||||||
httpServer *http.Server
|
httpsServer HTTPSServer // HTTPS module
|
||||||
|
|
||||||
// httpServerBeta is a server for new client.
|
|
||||||
httpServerBeta *http.Server
|
|
||||||
|
|
||||||
// handlerBeta is the handler for new client.
|
// handlerBeta is the handler for new client.
|
||||||
handlerBeta http.Handler
|
handlerBeta http.Handler
|
||||||
|
|
||||||
// installerBeta is the pre-install handler for new client.
|
// installerBeta is the pre-install handler for new client.
|
||||||
installerBeta http.Handler
|
installerBeta http.Handler
|
||||||
|
|
||||||
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
|
// httpServerBeta is a server for new client.
|
||||||
// [Web.http3Server] must also not be nil.
|
httpServerBeta *http.Server
|
||||||
httpsServer httpsServer
|
|
||||||
|
|
||||||
forceHTTPS bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWeb creates a new instance of the web UI and API server.
|
// CreateWeb - create module
|
||||||
func newWeb(conf *webConfig) (w *Web) {
|
func CreateWeb(conf *webConfig) *Web {
|
||||||
log.Info("web: initializing")
|
log.Info("Initialize web module")
|
||||||
|
|
||||||
w = &Web{
|
w := Web{}
|
||||||
conf: conf,
|
w.conf = conf
|
||||||
}
|
|
||||||
|
|
||||||
clientFS := http.FileServer(http.FS(conf.clientFS))
|
clientFS := http.FileServer(http.FS(conf.clientFS))
|
||||||
betaClientFS := http.FileServer(http.FS(conf.clientBetaFS))
|
betaClientFS := http.FileServer(http.FS(conf.clientBetaFS))
|
||||||
@@ -126,23 +108,23 @@ func newWeb(conf *webConfig) (w *Web) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock)
|
w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock)
|
||||||
|
return &w
|
||||||
return w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// webCheckPortAvailable checks if port, which is considered an HTTPS port, is
|
// WebCheckPortAvailable - check if port is available
|
||||||
// available, unless the HTTPS server isn't active.
|
// BUT: if we are already using this port, no need
|
||||||
//
|
func WebCheckPortAvailable(port int) bool {
|
||||||
// TODO(a.garipov): Adapt for HTTP/3.
|
if Context.web.httpsServer.server != nil {
|
||||||
func webCheckPortAvailable(port int) (ok bool) {
|
return true
|
||||||
return Context.web.httpsServer.server != nil ||
|
}
|
||||||
aghnet.CheckPort("tcp", config.BindHost, port) == nil
|
|
||||||
|
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
|
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
|
||||||
// if necessary.
|
// if necessary.
|
||||||
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
|
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
|
||||||
log.Debug("web: applying new tls configuration")
|
log.Debug("Web: applying new TLS configuration")
|
||||||
web.conf.PortHTTPS = tlsConf.PortHTTPS
|
web.conf.PortHTTPS = tlsConf.PortHTTPS
|
||||||
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
|
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
|
||||||
|
|
||||||
@@ -164,8 +146,6 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
|
|||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
|
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
|
||||||
shutdownSrv(ctx, web.httpsServer.server)
|
shutdownSrv(ctx, web.httpsServer.server)
|
||||||
shutdownSrv3(web.httpsServer.server3)
|
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +163,7 @@ func (web *Web) Start() {
|
|||||||
go web.tlsServerLoop()
|
go web.tlsServerLoop()
|
||||||
|
|
||||||
// this loop is used as an ability to change listening host and/or port
|
// this loop is used as an ability to change listening host and/or port
|
||||||
for !web.httpsServer.inShutdown {
|
for !web.httpsServer.shutdown {
|
||||||
printHTTPAddresses(aghhttp.SchemeHTTP)
|
printHTTPAddresses(aghhttp.SchemeHTTP)
|
||||||
errs := make(chan error, 2)
|
errs := make(chan error, 2)
|
||||||
|
|
||||||
@@ -254,7 +234,7 @@ func (web *Web) Close(ctx context.Context) {
|
|||||||
log.Info("stopping http server...")
|
log.Info("stopping http server...")
|
||||||
|
|
||||||
web.httpsServer.cond.L.Lock()
|
web.httpsServer.cond.L.Lock()
|
||||||
web.httpsServer.inShutdown = true
|
web.httpsServer.shutdown = true
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
@@ -262,7 +242,6 @@ func (web *Web) Close(ctx context.Context) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
shutdownSrv(ctx, web.httpsServer.server)
|
shutdownSrv(ctx, web.httpsServer.server)
|
||||||
shutdownSrv3(web.httpsServer.server3)
|
|
||||||
shutdownSrv(ctx, web.httpServer)
|
shutdownSrv(ctx, web.httpServer)
|
||||||
shutdownSrv(ctx, web.httpServerBeta)
|
shutdownSrv(ctx, web.httpServerBeta)
|
||||||
|
|
||||||
@@ -272,7 +251,7 @@ func (web *Web) Close(ctx context.Context) {
|
|||||||
func (web *Web) tlsServerLoop() {
|
func (web *Web) tlsServerLoop() {
|
||||||
for {
|
for {
|
||||||
web.httpsServer.cond.L.Lock()
|
web.httpsServer.cond.L.Lock()
|
||||||
if web.httpsServer.inShutdown {
|
if web.httpsServer.shutdown {
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -280,7 +259,7 @@ func (web *Web) tlsServerLoop() {
|
|||||||
// this mechanism doesn't let us through until all conditions are met
|
// this mechanism doesn't let us through until all conditions are met
|
||||||
for !web.httpsServer.enabled { // sleep until necessary data is supplied
|
for !web.httpsServer.enabled { // sleep until necessary data is supplied
|
||||||
web.httpsServer.cond.Wait()
|
web.httpsServer.cond.Wait()
|
||||||
if web.httpsServer.inShutdown {
|
if web.httpsServer.shutdown {
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -288,10 +267,11 @@ func (web *Web) tlsServerLoop() {
|
|||||||
|
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
|
|
||||||
addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
|
// prepare HTTPS server
|
||||||
|
address := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
|
||||||
web.httpsServer.server = &http.Server{
|
web.httpsServer.server = &http.Server{
|
||||||
ErrorLog: log.StdLog("web: https", log.DEBUG),
|
ErrorLog: log.StdLog("web: https", log.DEBUG),
|
||||||
Addr: addr,
|
Addr: address,
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
Certificates: []tls.Certificate{web.httpsServer.cert},
|
Certificates: []tls.Certificate{web.httpsServer.cert},
|
||||||
RootCAs: Context.tlsRoots,
|
RootCAs: Context.tlsRoots,
|
||||||
@@ -305,40 +285,10 @@ func (web *Web) tlsServerLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printHTTPAddresses(aghhttp.SchemeHTTPS)
|
printHTTPAddresses(aghhttp.SchemeHTTPS)
|
||||||
|
|
||||||
if web.conf.serveHTTP3 {
|
|
||||||
go web.mustStartHTTP3(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("web: starting https server")
|
|
||||||
err := web.httpsServer.server.ListenAndServeTLS("", "")
|
err := web.httpsServer.server.ListenAndServeTLS("", "")
|
||||||
if !errors.Is(err, http.ErrServerClosed) {
|
if err != http.ErrServerClosed {
|
||||||
cleanupAlways()
|
cleanupAlways()
|
||||||
log.Fatalf("web: https: %s", err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) mustStartHTTP3(address string) {
|
|
||||||
defer log.OnPanic("web: http3")
|
|
||||||
|
|
||||||
web.httpsServer.server3 = &http3.Server{
|
|
||||||
// TODO(a.garipov): See if there is a way to use the error log as
|
|
||||||
// well as timeouts here.
|
|
||||||
Addr: address,
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{web.httpsServer.cert},
|
|
||||||
RootCAs: Context.tlsRoots,
|
|
||||||
CipherSuites: aghtls.SaferCipherSuites(),
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
},
|
|
||||||
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("web: starting http/3 server")
|
|
||||||
err := web.httpsServer.server3.ListenAndServe()
|
|
||||||
if !errors.Is(err, quic.ErrServerClosed) {
|
|
||||||
cleanupAlways()
|
|
||||||
log.Fatalf("web: http3: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNS Settings Handlers
|
|
||||||
|
|
||||||
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
|
||||||
// HTTP API.
|
|
||||||
type ReqPatchSettingsDNS struct {
|
|
||||||
// TODO(a.garipov): Add more as we go.
|
|
||||||
|
|
||||||
Addresses []netip.AddrPort `json:"addresses"`
|
|
||||||
BootstrapServers []string `json:"bootstrap_servers"`
|
|
||||||
UpstreamServers []string `json:"upstream_servers"`
|
|
||||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
|
|
||||||
// DnsSettings object in the OpenAPI specification.
|
|
||||||
type HTTPAPIDNSSettings struct {
|
|
||||||
// TODO(a.garipov): Add more as we go.
|
|
||||||
|
|
||||||
Addresses []netip.AddrPort `json:"addresses"`
|
|
||||||
BootstrapServers []string `json:"bootstrap_servers"`
|
|
||||||
UpstreamServers []string `json:"upstream_servers"`
|
|
||||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
|
|
||||||
// API.
|
|
||||||
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &ReqPatchSettingsDNS{
|
|
||||||
Addresses: []netip.AddrPort{},
|
|
||||||
BootstrapServers: []string{},
|
|
||||||
UpstreamServers: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil {
|
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newConf := &dnssvc.Config{
|
|
||||||
Addresses: req.Addresses,
|
|
||||||
BootstrapServers: req.BootstrapServers,
|
|
||||||
UpstreamServers: req.UpstreamServers,
|
|
||||||
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
|
||||||
if err != nil {
|
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newSvc := svc.confMgr.DNS()
|
|
||||||
err = newSvc.Start()
|
|
||||||
if err != nil {
|
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
|
||||||
Addresses: newConf.Addresses,
|
|
||||||
BootstrapServers: newConf.BootstrapServers,
|
|
||||||
UpstreamServers: newConf.UpstreamServers,
|
|
||||||
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package websvc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
|
||||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
|
|
||||||
BootstrapServers: []string{"1.0.0.1"},
|
|
||||||
UpstreamServers: []string{"1.1.1.1"},
|
|
||||||
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
|
||||||
var numStarted uint64
|
|
||||||
confMgr := newConfigManager()
|
|
||||||
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
|
||||||
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
|
||||||
OnStart: func() (err error) {
|
|
||||||
atomic.AddUint64(&numStarted, 1)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
|
|
||||||
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, addr := newTestServer(t, confMgr)
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: addr.String(),
|
|
||||||
Path: websvc.PathV1SettingsDNS,
|
|
||||||
}
|
|
||||||
|
|
||||||
req := jobj{
|
|
||||||
"addresses": wantDNS.Addresses,
|
|
||||||
"bootstrap_servers": wantDNS.BootstrapServers,
|
|
||||||
"upstream_servers": wantDNS.UpstreamServers,
|
|
||||||
"upstream_timeout": wantDNS.UpstreamTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
|
||||||
resp := &websvc.HTTPAPIDNSSettings{}
|
|
||||||
err := json.Unmarshal(respBody, resp)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, uint64(1), numStarted)
|
|
||||||
assert.Equal(t, wantDNS, resp)
|
|
||||||
assert.Equal(t, wantDNS, resp)
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTP Settings Handlers
|
|
||||||
|
|
||||||
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
|
||||||
// HTTP API.
|
|
||||||
type ReqPatchSettingsHTTP struct {
|
|
||||||
// TODO(a.garipov): Add more as we go.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Add wait time.
|
|
||||||
|
|
||||||
Addresses []netip.AddrPort `json:"addresses"`
|
|
||||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
|
||||||
Timeout JSONDuration `json:"timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
|
||||||
// HttpSettings object in the OpenAPI specification.
|
|
||||||
type HTTPAPIHTTPSettings struct {
|
|
||||||
// TODO(a.garipov): Add more as we go.
|
|
||||||
|
|
||||||
Addresses []netip.AddrPort `json:"addresses"`
|
|
||||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
|
||||||
Timeout JSONDuration `json:"timeout"`
|
|
||||||
ForceHTTPS bool `json:"force_https"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
|
|
||||||
// HTTP API.
|
|
||||||
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &ReqPatchSettingsHTTP{}
|
|
||||||
|
|
||||||
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil {
|
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newConf := &Config{
|
|
||||||
ConfigManager: svc.confMgr,
|
|
||||||
TLS: svc.tls,
|
|
||||||
Addresses: req.Addresses,
|
|
||||||
SecureAddresses: req.SecureAddresses,
|
|
||||||
Timeout: time.Duration(req.Timeout),
|
|
||||||
ForceHTTPS: svc.forceHTTPS,
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
|
||||||
Addresses: newConf.Addresses,
|
|
||||||
SecureAddresses: newConf.SecureAddresses,
|
|
||||||
Timeout: JSONDuration(newConf.Timeout),
|
|
||||||
ForceHTTPS: newConf.ForceHTTPS,
|
|
||||||
})
|
|
||||||
|
|
||||||
cancelUpd := func() {}
|
|
||||||
updCtx := context.Background()
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
if deadline, ok := ctx.Deadline(); ok {
|
|
||||||
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch the new HTTP service in a separate goroutine to let this handler
|
|
||||||
// finish and thus, this server to shutdown.
|
|
||||||
go func() {
|
|
||||||
defer cancelUpd()
|
|
||||||
|
|
||||||
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
|
||||||
if updErr != nil {
|
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(a.garipov): Consider better ways to do this.
|
|
||||||
const maxUpdDur = 10 * time.Second
|
|
||||||
updStart := time.Now()
|
|
||||||
var newSvc ServiceWithConfig[*Config]
|
|
||||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
|
||||||
if time.Since(updStart) >= maxUpdDur {
|
|
||||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("websvc: waiting for new websvc to be configured")
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
updErr = newSvc.Start()
|
|
||||||
if updErr != nil {
|
|
||||||
log.Error("websvc: new svc failed to start with error: %s", updErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package websvc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
|
||||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
|
||||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
|
||||||
Timeout: websvc.JSONDuration(10 * time.Second),
|
|
||||||
ForceHTTPS: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
confMgr := newConfigManager()
|
|
||||||
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
|
||||||
return websvc.New(&websvc.Config{
|
|
||||||
TLS: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{{}},
|
|
||||||
},
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
|
||||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
ForceHTTPS: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, addr := newTestServer(t, confMgr)
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: addr.String(),
|
|
||||||
Path: websvc.PathV1SettingsHTTP,
|
|
||||||
}
|
|
||||||
|
|
||||||
req := jobj{
|
|
||||||
"addresses": wantWeb.Addresses,
|
|
||||||
"secure_addresses": wantWeb.SecureAddresses,
|
|
||||||
"timeout": wantWeb.Timeout,
|
|
||||||
"force_https": wantWeb.ForceHTTPS,
|
|
||||||
}
|
|
||||||
|
|
||||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
|
||||||
resp := &websvc.HTTPAPIHTTPSettings{}
|
|
||||||
err := json.Unmarshal(respBody, resp)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, wantWeb, resp)
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSON Utilities
|
|
||||||
|
|
||||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
|
||||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
|
||||||
|
|
||||||
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
|
||||||
// into JSON according to our API conventions.
|
|
||||||
type JSONDuration time.Duration
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ json.Marshaler = JSONDuration(0)
|
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
|
||||||
// always nil.
|
|
||||||
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
|
||||||
msec := float64(time.Duration(d)) / nsecPerMsec
|
|
||||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
|
||||||
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
|
||||||
if d == nil {
|
|
||||||
return fmt.Errorf("json duration is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
msec, err := strconv.ParseFloat(string(b), 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing json time: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*d = JSONDuration(int64(msec * nsecPerMsec))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
|
||||||
// according to our API conventions.
|
|
||||||
type JSONTime time.Time
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ json.Marshaler = JSONTime{}
|
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
|
||||||
// always nil.
|
|
||||||
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
|
||||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
|
||||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ json.Unmarshaler = (*JSONTime)(nil)
|
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
|
||||||
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
|
||||||
if t == nil {
|
|
||||||
return fmt.Errorf("json time is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
msec, err := strconv.ParseFloat(string(b), 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing json time: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
|
||||||
// and logs any errors it encounters. r is used to get additional information
|
|
||||||
// from the request.
|
|
||||||
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
|
||||||
writeJSONResponse(w, r, v, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
|
||||||
// errors it encounters. r is used to get additional information from the
|
|
||||||
// request.
|
|
||||||
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
|
||||||
// TODO(a.garipov): Put some of these to a middleware.
|
|
||||||
h := w.Header()
|
|
||||||
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
|
||||||
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
|
||||||
|
|
||||||
w.WriteHeader(code)
|
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode(v)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
|
||||||
// definition in the OpenAPI specification.
|
|
||||||
type ErrorCode string
|
|
||||||
|
|
||||||
// ErrorCode constants.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Expand and document codes.
|
|
||||||
const (
|
|
||||||
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
|
||||||
ErrorCodeTMP000 = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
|
||||||
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
|
||||||
// specification.
|
|
||||||
type HTTPAPIErrorResp struct {
|
|
||||||
Code ErrorCode `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
|
||||||
// errors it encounters. r is used to get additional information from the
|
|
||||||
// request.
|
|
||||||
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
|
||||||
|
|
||||||
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
|
||||||
Code: ErrorCodeTMP000,
|
|
||||||
Msg: err.Error(),
|
|
||||||
}, http.StatusUnprocessableEntity)
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package websvc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// testJSONTime is the JSON time for tests.
|
|
||||||
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
|
||||||
|
|
||||||
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
|
||||||
const testJSONTimeStr = "1234567890123.456"
|
|
||||||
|
|
||||||
func TestJSONTime_MarshalJSON(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
wantErrMsg string
|
|
||||||
in websvc.JSONTime
|
|
||||||
want []byte
|
|
||||||
}{{
|
|
||||||
name: "unix_zero",
|
|
||||||
wantErrMsg: "",
|
|
||||||
in: websvc.JSONTime(time.Unix(0, 0)),
|
|
||||||
want: []byte("0"),
|
|
||||||
}, {
|
|
||||||
name: "empty",
|
|
||||||
wantErrMsg: "",
|
|
||||||
in: websvc.JSONTime{},
|
|
||||||
want: []byte("-6795364578871.345"),
|
|
||||||
}, {
|
|
||||||
name: "time",
|
|
||||||
wantErrMsg: "",
|
|
||||||
in: testJSONTime,
|
|
||||||
want: []byte(testJSONTimeStr),
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
got, err := tc.in.MarshalJSON()
|
|
||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
|
||||||
|
|
||||||
assert.Equal(t, tc.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("json", func(t *testing.T) {
|
|
||||||
in := &struct {
|
|
||||||
A websvc.JSONTime
|
|
||||||
}{
|
|
||||||
A: testJSONTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := json.Marshal(in)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
wantErrMsg string
|
|
||||||
want websvc.JSONTime
|
|
||||||
data []byte
|
|
||||||
}{{
|
|
||||||
name: "time",
|
|
||||||
wantErrMsg: "",
|
|
||||||
want: testJSONTime,
|
|
||||||
data: []byte(testJSONTimeStr),
|
|
||||||
}, {
|
|
||||||
name: "bad",
|
|
||||||
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
|
||||||
`invalid syntax`,
|
|
||||||
want: websvc.JSONTime{},
|
|
||||||
data: []byte(`{}`),
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
var got websvc.JSONTime
|
|
||||||
err := got.UnmarshalJSON(tc.data)
|
|
||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
|
||||||
|
|
||||||
assert.Equal(t, tc.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("nil", func(t *testing.T) {
|
|
||||||
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
msg := err.Error()
|
|
||||||
assert.Equal(t, "json time is nil", msg)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("json", func(t *testing.T) {
|
|
||||||
want := testJSONTime
|
|
||||||
var got struct {
|
|
||||||
A websvc.JSONTime
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, want, got.A)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
// Path constants
|
|
||||||
const (
|
|
||||||
PathHealthCheck = "/health-check"
|
|
||||||
|
|
||||||
PathV1SettingsAll = "/api/v1/settings/all"
|
|
||||||
PathV1SettingsDNS = "/api/v1/settings/dns"
|
|
||||||
PathV1SettingsHTTP = "/api/v1/settings/http"
|
|
||||||
PathV1SystemInfo = "/api/v1/system/info"
|
|
||||||
)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// All Settings Handlers
|
|
||||||
|
|
||||||
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
|
|
||||||
// HTTP API.
|
|
||||||
type RespGetV1SettingsAll struct {
|
|
||||||
// TODO(a.garipov): Add more as we go.
|
|
||||||
|
|
||||||
DNS *HTTPAPIDNSSettings `json:"dns"`
|
|
||||||
HTTP *HTTPAPIHTTPSettings `json:"http"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
|
|
||||||
// API.
|
|
||||||
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
|
|
||||||
dnsSvc := svc.confMgr.DNS()
|
|
||||||
dnsConf := dnsSvc.Config()
|
|
||||||
|
|
||||||
webSvc := svc.confMgr.Web()
|
|
||||||
httpConf := webSvc.Config()
|
|
||||||
|
|
||||||
// TODO(a.garipov): Add all currently supported parameters.
|
|
||||||
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
|
||||||
DNS: &HTTPAPIDNSSettings{
|
|
||||||
Addresses: dnsConf.Addresses,
|
|
||||||
BootstrapServers: dnsConf.BootstrapServers,
|
|
||||||
UpstreamServers: dnsConf.UpstreamServers,
|
|
||||||
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
|
||||||
},
|
|
||||||
HTTP: &HTTPAPIHTTPSettings{
|
|
||||||
Addresses: httpConf.Addresses,
|
|
||||||
SecureAddresses: httpConf.SecureAddresses,
|
|
||||||
Timeout: JSONDuration(httpConf.Timeout),
|
|
||||||
ForceHTTPS: httpConf.ForceHTTPS,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package websvc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestService_HandleGetSettingsAll(t *testing.T) {
|
|
||||||
// TODO(a.garipov): Add all currently supported parameters.
|
|
||||||
|
|
||||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
|
||||||
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
|
||||||
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
|
||||||
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
|
||||||
}
|
|
||||||
|
|
||||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
|
||||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
|
||||||
Timeout: websvc.JSONDuration(5 * time.Second),
|
|
||||||
ForceHTTPS: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
confMgr := newConfigManager()
|
|
||||||
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
|
||||||
c, err := dnssvc.New(&dnssvc.Config{
|
|
||||||
Addresses: wantDNS.Addresses,
|
|
||||||
UpstreamServers: wantDNS.UpstreamServers,
|
|
||||||
BootstrapServers: wantDNS.BootstrapServers,
|
|
||||||
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
|
||||||
return websvc.New(&websvc.Config{
|
|
||||||
TLS: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{{}},
|
|
||||||
},
|
|
||||||
Addresses: wantWeb.Addresses,
|
|
||||||
SecureAddresses: wantWeb.SecureAddresses,
|
|
||||||
Timeout: time.Duration(wantWeb.Timeout),
|
|
||||||
ForceHTTPS: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_, addr := newTestServer(t, confMgr)
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: addr.String(),
|
|
||||||
Path: websvc.PathV1SettingsAll,
|
|
||||||
}
|
|
||||||
|
|
||||||
body := httpGet(t, u, http.StatusOK)
|
|
||||||
resp := &websvc.RespGetV1SettingsAll{}
|
|
||||||
err := json.Unmarshal(body, resp)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, wantDNS, resp.DNS)
|
|
||||||
assert.Equal(t, wantWeb, resp.HTTP)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait Listener
|
|
||||||
|
|
||||||
// waitListener is a wrapper around a listener that also calls wg.Done() on the
|
|
||||||
// first call to Accept. It is useful in situations where it is important to
|
|
||||||
// catch the precise moment of the first call to Accept, for example when
|
|
||||||
// starting an HTTP server.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Move to aghnet?
|
|
||||||
type waitListener struct {
|
|
||||||
net.Listener
|
|
||||||
|
|
||||||
firstAcceptWG *sync.WaitGroup
|
|
||||||
firstAcceptOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ net.Listener = (*waitListener)(nil)
|
|
||||||
|
|
||||||
// Accept implements the [net.Listener] interface for *waitListener.
|
|
||||||
func (l *waitListener) Accept() (conn net.Conn, err error) {
|
|
||||||
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
|
|
||||||
|
|
||||||
return l.Listener.Accept()
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWaitListener_Accept(t *testing.T) {
|
|
||||||
// TODO(a.garipov): use atomic.Bool in Go 1.19.
|
|
||||||
var numAcceptCalls uint32
|
|
||||||
var l net.Listener = &aghtest.Listener{
|
|
||||||
OnAccept: func() (conn net.Conn, err error) {
|
|
||||||
atomic.AddUint32(&numAcceptCalls, 1)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
},
|
|
||||||
OnAddr: func() (addr net.Addr) { panic("not implemented") },
|
|
||||||
OnClose: func() (err error) { panic("not implemented") },
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go aghchan.MustReceive(done, testTimeout)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var wrapper net.Listener = &waitListener{
|
|
||||||
Listener: l,
|
|
||||||
firstAcceptWG: wg,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = wrapper.Accept()
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(done)
|
|
||||||
|
|
||||||
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// testTimeout is the common timeout for tests.
|
|
||||||
const testTimeout = 1 * time.Second
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
package websvc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
aghtest.DiscardLogOutput(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testTimeout is the common timeout for tests.
|
|
||||||
const testTimeout = 1 * time.Second
|
|
||||||
|
|
||||||
// testStart is the server start value for tests.
|
|
||||||
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ websvc.ConfigManager = (*configManager)(nil)
|
|
||||||
|
|
||||||
// configManager is a [websvc.ConfigManager] for tests.
|
|
||||||
type configManager struct {
|
|
||||||
onDNS func() (svc websvc.ServiceWithConfig[*dnssvc.Config])
|
|
||||||
onWeb func() (svc websvc.ServiceWithConfig[*websvc.Config])
|
|
||||||
|
|
||||||
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
|
||||||
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
|
||||||
func (m *configManager) DNS() (svc websvc.ServiceWithConfig[*dnssvc.Config]) {
|
|
||||||
return m.onDNS()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
|
||||||
func (m *configManager) Web() (svc websvc.ServiceWithConfig[*websvc.Config]) {
|
|
||||||
return m.onWeb()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager.
|
|
||||||
func (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
|
||||||
return m.onUpdateDNS(ctx, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager.
|
|
||||||
func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
|
||||||
return m.onUpdateWeb(ctx, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newConfigManager returns a *configManager all methods of which panic.
|
|
||||||
func newConfigManager() (m *configManager) {
|
|
||||||
return &configManager{
|
|
||||||
onDNS: func() (svc websvc.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
|
||||||
onWeb: func() (svc websvc.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
|
||||||
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
|
||||||
panic("not implemented")
|
|
||||||
},
|
|
||||||
onUpdateWeb: func(_ context.Context, _ *websvc.Config) (err error) {
|
|
||||||
panic("not implemented")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTestServer creates and starts a new web service instance as well as its
|
|
||||||
// sole address. It also registers a cleanup procedure, which shuts the
|
|
||||||
// instance down.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Use svc or remove it.
|
|
||||||
func newTestServer(
|
|
||||||
t testing.TB,
|
|
||||||
confMgr websvc.ConfigManager,
|
|
||||||
) (svc *websvc.Service, addr netip.AddrPort) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
c := &websvc.Config{
|
|
||||||
ConfigManager: confMgr,
|
|
||||||
TLS: nil,
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
|
||||||
SecureAddresses: nil,
|
|
||||||
Timeout: testTimeout,
|
|
||||||
Start: testStart,
|
|
||||||
ForceHTTPS: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
svc = websvc.New(c)
|
|
||||||
|
|
||||||
err := svc.Start()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
|
||||||
t.Cleanup(cancel)
|
|
||||||
|
|
||||||
err = svc.Shutdown(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
c = svc.Config()
|
|
||||||
require.NotNil(t, c)
|
|
||||||
require.Len(t, c.Addresses, 1)
|
|
||||||
|
|
||||||
return svc, c.Addresses[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// jobj is a utility alias for JSON objects.
|
|
||||||
type jobj map[string]any
|
|
||||||
|
|
||||||
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
|
||||||
// the response as well as checks that the status code is correct.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Add helpers for other methods.
|
|
||||||
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
|
||||||
require.NoErrorf(t, err, "creating req")
|
|
||||||
|
|
||||||
httpCli := &http.Client{
|
|
||||||
Timeout: testTimeout,
|
|
||||||
}
|
|
||||||
resp, err := httpCli.Do(req)
|
|
||||||
require.NoErrorf(t, err, "performing req")
|
|
||||||
require.Equal(t, wantCode, resp.StatusCode)
|
|
||||||
|
|
||||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
|
||||||
|
|
||||||
body, err = io.ReadAll(resp.Body)
|
|
||||||
require.NoErrorf(t, err, "reading body")
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded
|
|
||||||
// reqBody as the request body and returns the body of the response as well as
|
|
||||||
// checks that the status code is correct.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Add helpers for other methods.
|
|
||||||
func httpPatch(t testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
b, err := json.Marshal(reqBody)
|
|
||||||
require.NoErrorf(t, err, "marshaling reqBody")
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b))
|
|
||||||
require.NoErrorf(t, err, "creating req")
|
|
||||||
|
|
||||||
httpCli := &http.Client{
|
|
||||||
Timeout: testTimeout,
|
|
||||||
}
|
|
||||||
resp, err := httpCli.Do(req)
|
|
||||||
require.NoErrorf(t, err, "performing req")
|
|
||||||
require.Equal(t, wantCode, resp.StatusCode)
|
|
||||||
|
|
||||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
|
||||||
|
|
||||||
body, err = io.ReadAll(resp.Body)
|
|
||||||
require.NoErrorf(t, err, "reading body")
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
|
||||||
confMgr := newConfigManager()
|
|
||||||
_, addr := newTestServer(t, confMgr)
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: addr.String(),
|
|
||||||
Path: websvc.PathHealthCheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
body := httpGet(t, u, http.StatusOK)
|
|
||||||
|
|
||||||
assert.Equal(t, []byte("OK"), body)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -47,7 +48,24 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
|||||||
// convert log entries to JSON
|
// convert log entries to JSON
|
||||||
data := l.entriesToJSON(entries, oldest)
|
data := l.entriesToJSON(entries, oldest)
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
jsonVal, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
"Couldn't marshal data into json: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, err = w.Write(jsonVal)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
||||||
@@ -56,13 +74,23 @@ func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
|||||||
|
|
||||||
// Get configuration
|
// Get configuration
|
||||||
func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
|
func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := qlogConfig{
|
resp := qlogConfig{}
|
||||||
Enabled: l.conf.Enabled,
|
resp.Enabled = l.conf.Enabled
|
||||||
Interval: l.conf.RotationIvl.Hours() / 24,
|
resp.Interval = l.conf.RotationIvl.Hours() / 24
|
||||||
AnonymizeClientIP: l.conf.AnonymizeClientIP,
|
resp.AnonymizeClientIP = l.conf.AnonymizeClientIP
|
||||||
|
|
||||||
|
jsonVal, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, err = w.Write(jsonVal)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
|
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// configResp is the response to the GET /control/stats_info.
|
// configResp is the response to the GET /control/stats_info.
|
||||||
@@ -66,7 +71,13 @@ type configResp struct {
|
|||||||
// handleStatsInfo handles requests to the GET /control/stats_info endpoint.
|
// handleStatsInfo handles requests to the GET /control/stats_info endpoint.
|
||||||
func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
|
func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := configResp{IntervalDays: atomic.LoadUint32(&s.limitHours) / 24}
|
resp := configResp{IntervalDays: atomic.LoadUint32(&s.limitHours) / 24}
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStatsConfig handles requests to the POST /control/stats_config
|
// handleStatsConfig handles requests to the POST /control/stats_config
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user