Compare commits

..

2 Commits

Author SHA1 Message Date
Eugene Burkov
de837e4eec all: use netip for web 2022-09-29 14:56:37 +03:00
Eugene Burkov
e528d2f23b add basic lla 2022-09-28 20:30:40 +03:00
118 changed files with 1643 additions and 3097 deletions

View File

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

View File

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

View File

@@ -12,129 +12,11 @@ and this project adheres to
## [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
- As an additional CSRF protection measure, AdGuard Home now ensures that
requests that change its state but have no body (such as `POST
/control/stats_reset` requests) do not have a `Content-Type` header set on
them ([#4970]).
### Added
#### Experimental HTTP/3 Support
See [#3955] and the related issues for more details. These features are still
experimental and may break or change in the future.
- DNS-over-HTTP/3 DNS and web UI client request support. This feature must be
explicitly enabled by setting the new property `dns.serve_http3` in the
configuration file to `true`.
- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration
file property `dns.use_http3_upstreams` is set to `true`.
- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions
using the `h3://` scheme.
### Fixed
- User-specific blocked services not applying correctly ([#4945], [#4982],
[#4983]).
- `only application/json is allowed` errors in various APIs ([#4970]).
[#3955]: https://github.com/AdguardTeam/AdGuardHome/issues/3955
[#4945]: https://github.com/AdguardTeam/AdGuardHome/issues/4945
[#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970
[#4982]: https://github.com/AdguardTeam/AdGuardHome/issues/4982
[#4983]: https://github.com/AdguardTeam/AdGuardHome/issues/4983
[ms-v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/milestone/51?closed=1
## [v0.107.14] - 2022-09-29
See also the [v0.107.14 GitHub milestone][ms-v0.107.14].
### Security
A Cross-Site Request Forgery (CSRF) vulnerability has been discovered. The CVE
number is to be assigned. We thank Daniel Elkabes from Mend.io for reporting
this vulnerability to us.
#### `SameSite` Policy
The `SameSite` policy on the AdGuard Home session cookies is now set to `Lax`.
Which means that the only cross-site HTTP request for which the browser is
allowed to send the session cookie is navigating to the AdGuard Home domain.
**Users are strongly advised to log out, clear browser cache, and log in again
after updating.**
#### Removal Of Plain-Text APIs (BREAKING API CHANGE)
We have implemented several measures to prevent such vulnerabilities in the
future, but some of these measures break backwards compatibility for the sake of
better protection.
The following APIs, which previously accepted or returned `text/plain` data,
now accept or return data as JSON. All new formats for the request and response
bodies are documented in `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
- `GET /control/i18n/current_language`;
- `POST /control/dhcp/find_active_dhcp`;
- `POST /control/filtering/set_rules`;
- `POST /control/i18n/change_language`.
#### Stricter Content-Type Checks (BREAKING API CHANGE)
All JSON APIs that expect a body now check if the request actually has
`Content-Type` set to `application/json`.
#### Other Security Changes
- Weaker cipher suites that use the CBC (cipher block chaining) mode of
operation have been disabled ([#2993]).
@@ -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
[#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
-->
@@ -188,7 +79,7 @@ See also the [v0.107.12 GitHub milestone][ms-v0.107.12].
### 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
[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
- 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
support has also been removed, as it has reached end of life and will not
receive security updates.
@@ -352,7 +243,7 @@ See also the [v0.107.8 GitHub milestone][ms-v0.107.8].
### 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
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
- 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-29804] Go vulnerabilities.
- Enforced password strength policy ([#3503]).
@@ -545,7 +436,7 @@ See also the [v0.107.6 GitHub milestone][ms-v0.107.6].
### Security
- `User-Agent` HTTP header removed from outgoing DNS-over-HTTPS requests.
- Go version 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.
### Added
@@ -600,7 +491,7 @@ were resolved.
### 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]: 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
- 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.
### 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
[v0.107.17]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...v0.107.17
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...HEAD
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...HEAD
[v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...v0.107.16
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...HEAD
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
[v0.107.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12
[v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:5.2'
'dockerGo': 'adguard/golang-ubuntu:5.1'
'stages':
- 'Build frontend':
@@ -322,7 +322,7 @@
# need to build a few of these.
'variables':
'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
# is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
@@ -337,4 +337,4 @@
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerGo': 'adguard/golang-ubuntu:5.2'
'dockerGo': 'adguard/golang-ubuntu:5.1'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -215,7 +215,6 @@
"example_upstream_udp": "regular DNS (over UDP, hostname);",
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>;",
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>;",
"example_upstream_doh3": "encrypted DNS-over-HTTPS with forced <0>HTTP/3</0> and no fallback to HTTP/2 or below;",
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>;",
"example_upstream_sdns": "<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;",
"example_upstream_tcp": "regular DNS (over TCP);",
@@ -606,7 +605,7 @@
"blocklist": "Blocklist",
"milliseconds_abbreviation": "ms",
"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_max_override": "Override maximum TTL",
"enter_cache_size": "Enter cache size (bytes)",

View File

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

View File

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

View File

@@ -635,6 +635,5 @@
"parental_control": "Contrôle parental",
"safe_browsing": "Navigation sécurisée",
"served_from_cache": "{{value}} <i>(depuis le cache)</i>",
"form_error_password_length": "Le mot de passe doit comporter au moins {{value}} caractères",
"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>."
"form_error_password_length": "Le mot de passe doit comporter au moins {{value}} caractères"
}

View File

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

View File

@@ -635,6 +635,5 @@
"parental_control": "Szülői felügyelet",
"safe_browsing": "Biztonságos böngészés",
"served_from_cache": "{{value}} <i>(gyorsítótárból kiszolgálva)</i>",
"form_error_password_length": "A jelszó legalább {{value}} karakter hosszú kell, hogy legyen",
"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> ."
"form_error_password_length": "A jelszó legalább {{value}} karakter hosszú kell, hogy legyen"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -635,6 +635,5 @@
"parental_control": "Quản lý của phụ huynh",
"safe_browsing": "Duyệt web an toàn",
"served_from_cache": "{{value}} <i>(được phục vụ từ bộ nhớ cache)</i>",
"form_error_password_length": "Mật khẩu phải có ít nhất {{value}} ký tự",
"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>."
"form_error_password_length": "Mật khẩu phải có ít nhất {{value}} ký tự"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

16
go.mod
View File

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

35
go.sum
View File

@@ -1,5 +1,5 @@
github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko=
github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE=
github.com/AdguardTeam/dnsproxy v0.44.0 h1:JzIxEXF4OyJq4wZVHeZkM1af4VfuwcgrUzjgdBGljsE=
github.com/AdguardTeam/dnsproxy v0.44.0/go.mod h1:HsxYYW/bC8uo+4eX9pRW21hFD6gWZdrvcfBb1R6/AzU=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
@@ -23,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/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE=
github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
@@ -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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@@ -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-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-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI=
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e h1:WhB000cGjOfbJiedMGvJkMTclI18VD69w27k+sceql8=
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 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0=
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-20190311183353-d8887717615a/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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -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-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-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -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-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-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 h1:C1tElbkWrsSkn3IRl1GCW/gETw1TywWIPgwZtXTZbYg=
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net"
"net/netip"
"syscall"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
@@ -31,6 +32,12 @@ var (
// the IP being static is available.
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.
// If it can't give a definitive answer, it returns false and an error for which
// 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
// 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)
if err != nil {
log.Debug("%s", err)
return nil
return ip
} else if code != 0 {
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
return nil
return ip
}
fields := bytes.Fields(out)
// The meaningful "ip route" command output should contain the word
// "default" at first field and default gateway IP address at third field.
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
@@ -78,9 +90,9 @@ func CanBindPrivilegedPorts() (can bool, err error) {
// NetInterface represents an entry of network interfaces map.
type NetInterface struct {
// 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 []*net.IPNet `json:"-"`
Subnets []netip.Prefix `json:"-"`
Name string `json:"name"`
HardwareAddr net.HardwareAddr `json:"hardware_address"`
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
// WEB only we do not return link-local addresses here.
//
// 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.
func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {
ifaces, err := net.Interfaces()
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 {
return nil, errors.Error("couldn't find any legible interface")
return nil, errors.Error("no legible interfaces")
}
for _, iface := range ifaces {
var addrs []net.Addr
addrs, err = iface.Addrs()
for i := range ifaces {
var niface *NetInterface
niface, err = NetInterfaceFrom(&ifaces[i])
if err != nil {
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
}
netIface := &NetInterface{
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 nil, err
} else if len(niface.Addresses) != 0 {
// Discard interfaces with no addresses.
nifaces = append(nifaces, niface)
}
}
return netIfaces, nil
return nifaces, nil
}
// 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.
//
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func InterfaceByIP(ip net.IP) (ifaceName string) {
func InterfaceByIP(ip netip.Addr) (ifaceName string) {
ifaces, err := GetValidNetInterfacesForWeb()
if err != nil {
return ""
@@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
for _, iface := range ifaces {
for _, addr := range iface.Addresses {
if ip.Equal(addr) {
if ip == addr {
return iface.Name
}
}
@@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
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.
//
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func GetSubnet(ifaceName string) *net.IPNet {
func GetSubnet(ifaceName string) (p netip.Prefix) {
netIfaces, err := GetValidNetInterfacesForWeb()
if err != nil {
log.Error("Could not get network interfaces info: %v", err)
return nil
return p
}
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
// 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
addr := netutil.IPPort{IP: ip, Port: port}.String()
addr := ipp.String()
switch network {
case "tcp":
c, err = net.Listen(network, addr)

View File

@@ -6,7 +6,7 @@ import (
"bufio"
"fmt"
"io"
"net"
"net/netip"
"os"
"strings"
@@ -151,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
// interface through dhcpcd.conf.
func ifaceSetStaticIP(ifaceName string) (err error) {
ipNet := GetSubnet(ifaceName)
if ipNet.IP == nil {
if !ipNet.Addr().IsValid() {
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
// 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{}
stringutil.WriteToBuilder(
b,
@@ -183,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri
" added by AdGuard Home.\ninterface ",
ifaceName,
"\nstatic ip_address=",
ipNet.String(),
subnet.String(),
"\n",
)
if gwIP != nil {
stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n")
if gateway.IsValid() {
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()
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io/fs"
"net"
"net/netip"
"os"
"strings"
"testing"
@@ -93,34 +94,34 @@ func TestGatewayIP(t *testing.T) {
const cmd = "ip route show dev " + ifaceName
testCases := []struct {
name string
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),
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),
want: net.IP{
want: netip.AddrFrom16([16]byte{
0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0xFF, 0xFF,
},
}),
name: "success_v6",
}, {
name: "bad_output",
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")),
want: nil,
want: netip.Addr{},
name: "err_runcmd",
}, {
name: "bad_code",
shell: theOnlyCmd(cmd, 1, "", nil),
want: nil,
want: netip.Addr{},
name: "bad_code",
}}
for _, tc := range testCases {
@@ -198,17 +199,21 @@ func TestBroadcastFromIPNet(t *testing.T) {
}
func TestCheckPort(t *testing.T) {
laddr := netip.AddrPortFrom(IPv4Localhost(), 0)
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)
testutil.CleanupAndRequireSuccess(t, l.Close)
ipp := netutil.IPPortFromAddr(l.Addr())
require.NotNil(t, ipp)
require.NotNil(t, ipp.IP)
require.NotZero(t, ipp.Port)
addr := l.Addr()
require.IsType(t, new(net.TCPAddr), addr)
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{}
require.ErrorAs(t, err, &target)
@@ -216,16 +221,18 @@ func TestCheckPort(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)
testutil.CleanupAndRequireSuccess(t, conn.Close)
ipp := netutil.IPPortFromAddr(conn.LocalAddr())
require.NotNil(t, ipp)
require.NotNil(t, ipp.IP)
require.NotZero(t, ipp.Port)
addr := conn.LocalAddr()
require.IsType(t, new(net.UDPAddr), addr)
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{}
require.ErrorAs(t, err, &target)
@@ -233,12 +240,12 @@ func TestCheckPort(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)
})
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)
})
}
@@ -322,18 +329,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) {
`"mtu":1500` +
`}` + "\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}
mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen)
ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
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{
Addresses: []net.IP{ip4, ip6},
Subnets: []*net.IPNet{{
IP: ip4.Mask(mask4),
Mask: mask4,
}, {
IP: ip6.Mask(mask6),
Mask: mask6,
}},
Addresses: []netip.Addr{ip4, ip6},
Subnets: []netip.Prefix{net4, net6},
Name: "iface0",
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
Flags: net.FlagUp | net.FlagMulticast,

View File

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

View File

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

View File

@@ -5,9 +5,11 @@ package dhcpd
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"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.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) {
@@ -235,7 +248,22 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
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()
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 {
Name string `json:"name"`
HardwareAddr string `json:"hardware_address"`
@@ -341,6 +349,8 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
return
}
// ignore link-local
//
// TODO(e.burkov): Try to listen DHCP on LLA as well.
if ipnet.IP.IsLinkLocalUnicast() {
continue
}
@@ -351,7 +361,7 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
}
}
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
}
}
@@ -402,37 +412,31 @@ type dhcpSearchResult struct {
V6 dhcpSearchV6Result `json:"v6"`
}
// findActiveServerReq is the JSON structure for the request to find active DHCP
// servers.
type findActiveServerReq struct {
Interface string `json:"interface"`
}
// handleDHCPFindActiveServer performs the following tasks:
// 1. searches for another DHCP server in the network;
// 2. check if a static IP is configured for the network interface;
// 3. responds with the results.
// Perform the following tasks:
// . Search for another DHCP server running
// . Check if a static IP is configured for the network interface
// Respond with results
func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
if aghhttp.WriteTextPlainDeprecated(w, r) {
return
}
req := &findActiveServerReq{}
err := json.NewDecoder(r.Body).Decode(req)
// This use of ReadAll is safe, because request's body is now limited.
body, err := io.ReadAll(r.Body)
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
}
ifaceName := req.Interface
ifaceName := strings.TrimSpace(string(body))
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
}
result := &dhcpSearchResult{
result := dhcpSearchResult{
V4: dhcpSearchV4Result{
OtherServer: dhcpSearchOtherResult{
Found: "no",
@@ -457,14 +461,6 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
result.V4.StaticIP.IP = aghnet.GetSubnet(ifaceName).String()
}
setOtherDHCPResult(ifaceName, result)
_ = aghhttp.WriteJSONResponse(w, r, result)
}
// setOtherDHCPResult sets the results of the check for another DHCP server in
// result.
func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
found4, found6, err4, err6 := aghnet.CheckOtherDHCP(ifaceName)
if err4 != nil {
result.V4.OtherServer.Found = "error"
@@ -472,13 +468,24 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
} else if found4 {
result.V4.OtherServer.Found = "yes"
}
if err6 != nil {
result.V6.OtherServer.Found = "error"
result.V6.OtherServer.Error = err6.Error()
} else if found6 {
result.V6.OtherServer.Found = "yes"
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Failed to marshal DHCP found json: %s",
err,
)
}
}
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {

View File

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

View File

@@ -183,7 +183,15 @@ func (s *Server) accessListJSON() (j accessListJSON) {
}
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

View File

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

View File

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

View File

@@ -201,10 +201,6 @@ type ServerConfig struct {
// Register an HTTP handler
HTTPRegister aghhttp.RegisterFunc
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
// resolving PTR queries for local addresses.
LocalPTRResolvers []string
// ResolveClients signals if the RDNS should resolve clients' addresses.
ResolveClients bool
@@ -212,12 +208,9 @@ type ServerConfig struct {
// locally-served networks should be resolved via private PTR resolvers.
UsePrivateRDNS bool
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
ServeHTTP3 bool
// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
// upstreams.
UseHTTP3Upstreams bool
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
// resolving PTR queries for local addresses.
LocalPTRResolvers []string
}
// 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{
UDPListenAddr: srvConf.UDPListenAddrs,
TCPListenAddr: srvConf.TCPListenAddrs,
HTTP3: srvConf.ServeHTTP3,
Ratelimit: int(srvConf.Ratelimit),
RatelimitWhitelist: srvConf.RatelimitWhitelist,
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
func (s *Server) prepareUpstreamSettings() error {
// We're setting a customized set of RootCAs
@@ -375,14 +353,12 @@ func (s *Server) prepareUpstreamSettings() error {
upstreams = s.conf.UpstreamDNS
}
httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams)
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
upstreamConfig, err := proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
},
)
if err != nil {
@@ -395,9 +371,8 @@ func (s *Server) prepareUpstreamSettings() error {
uc, err = proxy.ParseUpstreamsConfig(
defaultDNS,
&upstream.Options{
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout,
},
)
if err != nil {

View File

@@ -151,7 +151,7 @@ func (s *Server) checkHostRules(host string, rrtype uint16, setts *filtering.Set
}
// filterDNSResponse checks each resource record of the response's answer
// section from pctx and returns a non-nil res if at least one of 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.
func (s *Server) filterDNSResponse(
pctx *proxy.DNSContext,

View File

@@ -112,7 +112,13 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
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) {
@@ -343,10 +349,7 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
conf, err = proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: []string{},
Timeout: DefaultTimeout,
},
&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
)
if err != nil {
return nil, err
@@ -409,15 +412,7 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
return nil
}
var protocols = []string{
"h3://",
"https://",
"quic://",
"sdns://",
"tcp://",
"tls://",
"udp://",
}
var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"}
// validateUpstream returns an error if u alongside with domains is not a valid
// 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"
}
_ = 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.
@@ -680,13 +692,11 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
return
}
if !s.IsRunning() {
aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
return
}

View File

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

View File

@@ -453,7 +453,13 @@ func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
}
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) {
@@ -461,7 +467,13 @@ func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Req
list := d.Config.BlockedServices
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) {

View File

@@ -3,11 +3,13 @@ package filtering
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"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) {
if aghhttp.WriteTextPlainDeprecated(w, r) {
return
}
req := &filteringRulesReq{}
err := json.NewDecoder(r.Body).Decode(req)
// This use of ReadAll is safe, because request's body is now limited.
body, err := io.ReadAll(r.Body)
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
}
d.UserRules = req.Rules
d.UserRules = strings.Split(string(body), "\n")
d.ConfigModified()
d.EnableFilters(true)
}
@@ -301,7 +294,14 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
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 {
@@ -354,7 +354,17 @@ func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request
resp.UserRules = d.UserRules
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
@@ -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

View File

@@ -240,7 +240,13 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
}
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) {

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"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) {
resp := &struct {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(&struct {
Enabled bool `json:"enabled"`
}{
Enabled: d.Config.SafeBrowsingEnabled,
}
})
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
_ = aghhttp.WriteJSONResponse(w, r, resp)
return
}
}
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) {
resp := &struct {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(&struct {
Enabled bool `json:"enabled"`
}{
Enabled: d.Config.ParentalEnabled,
})
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/binary"
"encoding/gob"
"encoding/json"
"fmt"
"net"
"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) {
resp := &struct {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(&struct {
Enabled bool `json:"enabled"`
}{
Enabled: d.Config.SafeSearchEnabled,
})
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to write response json: %s",
err,
)
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
var safeSearchDomains = map[string]string{

View File

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

View File

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

View File

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

View File

@@ -93,7 +93,13 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
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
@@ -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

View File

@@ -3,7 +3,7 @@ package home
import (
"bytes"
"fmt"
"net"
"net/netip"
"os"
"path/filepath"
"sync"
@@ -85,10 +85,10 @@ type configuration struct {
// It's reset after config is parsed
fileData []byte
BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
Users []webUser `yaml:"users"` // Users that can access HTTP server
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
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
Users []User `yaml:"users"` // Users that can access HTTP server
// AuthAttempts is the maximum number of failed login attempts a user
// can do before being blocked.
AuthAttempts uint `yaml:"auth_attempts"`
@@ -135,8 +135,8 @@ type configuration struct {
// field ordering is important -- yaml fields will mirror ordering from here
type dnsConfig struct {
BindHosts []net.IP `yaml:"bind_hosts"`
Port int `yaml:"port"`
BindHosts []netip.Addr `yaml:"bind_hosts"`
Port int `yaml:"port"`
// time interval for statistics (in days)
StatsInterval uint32 `yaml:"statistics_interval"`
@@ -166,19 +166,6 @@ type dnsConfig struct {
// LocalPTRResolvers is the slice of addresses to be used as upstreams
// for PTR queries for locally-served networks.
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
//
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
// experimental.
ServeHTTP3 bool `yaml:"serve_http3"`
// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
// upstreams.
//
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
// experimental.
UseHTTP3Upstreams bool `yaml:"use_http3_upstreams"`
}
type tlsConfigSettings struct {
@@ -211,12 +198,12 @@ type tlsConfigSettings struct {
var config = &configuration{
BindPort: 3000,
BetaBindPort: 0,
BindHost: net.IP{0, 0, 0, 0},
BindHost: netip.IPv4Unspecified(),
AuthAttempts: 5,
AuthBlockMin: 15,
WebSessionTTLHours: 30 * 24,
DNS: dnsConfig{
BindHosts: []net.IP{{0, 0, 0, 0}},
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS,
StatsInterval: 1,
QueryLogEnabled: true,

View File

@@ -1,13 +1,13 @@
package home
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"runtime"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
@@ -20,11 +20,11 @@ import (
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
// 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 {
var hostport string
if config.DNS.Port != defaultPortDNS {
hostport = netutil.JoinHostPort(addr.String(), config.DNS.Port)
hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String()
} else {
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
// dst. It also adds the IP addresses of all network interfaces if src contains
// 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
for _, h := range src {
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.
func collectDNSAddresses() (addrs []string, err error) {
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 {
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
if err != nil {
@@ -97,16 +99,16 @@ func collectDNSAddresses() (addrs []string, err error) {
// statusResponse is a response for /control/status endpoint.
type statusResponse struct {
Version string `json:"version"`
Language string `json:"language"`
DNSAddrs []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
IsProtectionEnabled bool `json:"protection_enabled"`
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
// openapi.yaml declares.
IsDHCPAvailable bool `json:"dhcp_available"`
IsRunning bool `json:"running"`
IsDHCPAvailable bool `json:"dhcp_available"`
IsRunning bool `json:"running"`
Version string `json:"version"`
Language string `json:"language"`
}
func handleStatus(w http.ResponseWriter, r *http.Request) {
@@ -125,12 +127,12 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
defer config.RUnlock()
resp = statusResponse{
Version: version.Version(),
DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port,
HTTPPort: config.BindPort,
Language: config.Language,
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
}
_ = 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 {
@@ -154,12 +162,16 @@ type profileJSON struct {
}
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
pj := profileJSON{}
u := Context.auth.getCurrentUser(r)
resp := &profileJSON{
Name: u.Name,
}
pj.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)))))
}
// ensure returns a wrapped handler that makes sure that the request has the
// correct method as well as additional method and header checks.
func ensure(
method string,
handler func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) {
// ----------------------------------
// helper functions for HTTP handlers
// ----------------------------------
func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
m, u := r.Method, r.URL
log.Debug("started %s %s %s", m, r.Host, u)
defer func() { log.Debug("finished %s %s %s in %s", m, r.Host, u, time.Since(start)) }()
if m != method {
aghhttp.Error(r, w, http.StatusMethodNotAllowed, "only method %s is allowed", method)
log.Debug("%s %v", r.Method, r.URL)
if r.Method != method {
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
return
}
if modifiesData(m) {
if !ensureContentType(w, r) {
return
}
if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
Context.controlLock.Lock()
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) {
return ensure(http.MethodPost, handler)
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"os"
"os/exec"
"path/filepath"
@@ -21,7 +21,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/lucas-clemente/quic-go/http3"
)
// getAddrsResponse is the response for /install/get_addresses endpoint.
@@ -60,13 +59,25 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
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 {
IP net.IP `json:"ip"`
Port int `json:"port"`
Autofix bool `json:"autofix"`
IP netip.Addr `json:"ip"`
Port int `json:"port"`
Autofix bool `json:"autofix"`
}
type checkConfReq struct {
@@ -117,7 +128,7 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
// 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
@@ -142,13 +153,13 @@ func (req *checkConfReq) validateDNS(
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 {
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) {
return false, err
}
@@ -160,7 +171,7 @@ func (req *checkConfReq) validateDNS(
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
}
@@ -190,13 +201,19 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
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
// It either checks if we have a static IP
// 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{}
interfaceName := aghnet.InterfaceByIP(ip)
@@ -304,8 +321,8 @@ func disableDNSStubListener() error {
}
type applyConfigReqEnt struct {
IP net.IP `json:"ip"`
Port int `json:"port"`
IP netip.Addr `json:"ip"`
Port int `json:"port"`
}
type applyConfigReq struct {
@@ -329,7 +346,6 @@ func copyInstallSettings(dst, src *configuration) {
// shutdownTimeout is the timeout for shutting HTTP server down operation.
const shutdownTimeout = 5 * time.Second
// shutdownSrv shuts srv down and prints error messages to the log.
func shutdownSrv(ctx context.Context, srv *http.Server) {
defer log.OnPanic("")
@@ -338,38 +354,13 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
}
err := srv.Shutdown(ctx)
if err == nil {
return
}
const msgFmt = "shutting down http server %q: %s"
if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err)
} else {
log.Error(msgFmt, srv.Addr, err)
}
}
// shutdownSrv3 shuts srv down and prints error messages to the log.
//
// TODO(a.garipov): Think of a good way to merge with [shutdownSrv].
func shutdownSrv3(srv *http3.Server) {
defer log.OnPanic("")
if srv == nil {
return
}
err := srv.Close()
if err == nil {
return
}
const msgFmt = "shutting down http/3 server %q: %s"
if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err)
} else {
log.Error(msgFmt, srv.Addr, err)
if err != nil {
const msgFmt = "shutting down http server %q: %s"
if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err)
} else {
log.Error(msgFmt, srv.Addr, err)
}
}
}
@@ -397,14 +388,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
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 {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
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 {
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
config.BindHost = req.Web.IP
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
// 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
}
u := &webUser{
u := &User{
Name: req.Username,
}
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")
}
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 {
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 {
return nil, false, fmt.Errorf(
"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
// functionality will appear in default checkConfigReqEnt.
type checkConfigReqEntBeta struct {
IP []net.IP `json:"ip"`
Port int `json:"port"`
Autofix bool `json:"autofix"`
IP []netip.Addr `json:"ip"`
Port int `json:"port"`
Autofix bool `json:"autofix"`
}
// 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)
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
}
body := nonBetaReqBody.String()
r.Body = io.NopCloser(strings.NewReader(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
// functionality will appear in default applyConfigReqEnt.
type applyConfigReqEntBeta struct {
IP []net.IP `json:"ip"`
Port int `json:"port"`
IP []netip.Addr `json:"ip"`
Port int `json:"port"`
}
// 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)
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
}
@@ -686,7 +688,19 @@ func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Req
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

View File

@@ -28,6 +28,8 @@ type temporaryError interface {
// Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := &versionResponse{}
if Context.disableUpdate {
resp.Disabled = true
@@ -69,7 +71,10 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
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

View File

@@ -3,6 +3,7 @@ package home
import (
"fmt"
"net"
"net/netip"
"net/url"
"os"
"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 {
return nil
}
tcpAddrs = make([]*net.TCPAddr, len(ips))
for i, ip := range ips {
tcpAddrs[i] = &net.TCPAddr{
IP: ip,
Port: port,
}
tcpAddrs = make([]*net.TCPAddr, 0, len(ips))
for _, ip := range ips {
tcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
}
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 {
return nil
}
udpAddrs = make([]*net.UDPAddr, len(ips))
for i, ip := range ips {
udpAddrs[i] = &net.UDPAddr{
IP: ip,
Port: port,
}
udpAddrs = make([]*net.UDPAddr, 0, len(ips))
for _, ip := range ips {
udpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
}
return udpAddrs
@@ -200,7 +195,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
dnsConf := config.DNS
hosts := dnsConf.BindHosts
if len(hosts) == 0 {
hosts = []net.IP{{127, 0, 0, 1}}
hosts = []netip.Addr{aghnet.IPv4Localhost()}
}
newConf = dnsforward.ServerConfig{
@@ -246,18 +241,15 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
newConf.FilterHandler = applyAdditionalFiltering
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
newConf.ResolveClients = config.Clients.Sources.RDNS
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
newConf.ServeHTTP3 = dnsConf.ServeHTTP3
newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
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 == "" {
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)
if c.UseOwnBlockedServices {
// TODO(e.burkov): Get rid of this crutch.
svcs := c.BlockedServices
if svcs == nil {
svcs = []string{}
}
Context.filters.ApplyBlockedServices(setts, svcs)
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
Context.filters.ApplyBlockedServices(setts, c.BlockedServices)
}
setts.ClientName = c.Name

View File

@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"net/http/pprof"
"net/netip"
"net/url"
"os"
"os/signal"
@@ -97,15 +98,9 @@ var Context homeContext
// Main is the entry point
func Main(clientBuildFS fs.FS) {
initCmdLineOpts()
// The configuration file path can be overridden, but other command-line
// options have to override config values. Therefore, do it manually
// instead of using package flag.
//
// TODO(a.garipov): The comment above is most likely false. Replace with
// package flag.
opts := loadCmdLineOpts()
// config can be specified, which reads options from there, but other command line flags have to override config values
// therefore, we must do it manually instead of using a lib
args := loadOptions()
Context.appSignalChannel = make(chan os.Signal)
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
@@ -126,18 +121,26 @@ func Main(clientBuildFS fs.FS) {
}
}()
if opts.serviceControlAction != "" {
handleServiceControlAction(opts, clientBuildFS)
if args.serviceControlAction != "" {
handleServiceControlAction(args, clientBuildFS)
return
}
// run the protection
run(opts, clientBuildFS)
run(args, clientBuildFS)
}
func setupContext(opts options) {
setupContextFlags(opts)
func setupContext(args options) {
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() {
case version.ChannelEdge, version.ChannelDevelopment:
@@ -172,13 +175,13 @@ func setupContext(opts options) {
os.Exit(1)
}
if opts.checkConfig {
if args.checkConfig {
log.Info("configuration file is ok")
os.Exit(0)
}
if !opts.noEtcHosts && config.Clients.Sources.HostsFile {
if !args.noEtcHosts && config.Clients.Sources.HostsFile {
err = setupHostsContainer()
fatalOnError(err)
}
@@ -187,24 +190,6 @@ func setupContext(opts options) {
Context.mux = http.NewServeMux()
}
// setupContextFlags sets global flags and prints their status to the log.
func setupContextFlags(opts options) {
Context.firstRun = detectFirstRun()
if Context.firstRun {
log.Info("This is the first time AdGuard Home is launched")
checkPermissions()
}
Context.runningAsService = opts.runningAsService
// Don't print the runningAsService flag, since that has already been done
// in [run].
Context.disableUpdate = opts.disableUpdate || version.Channel() == version.ChannelDevelopment
if Context.disableUpdate {
log.Info("AdGuard Home updates are disabled")
}
}
// logIfUnsupported logs a formatted warning if the error is one of the
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
// nil. Otherwise, it returns err.
@@ -286,14 +271,13 @@ func setupHostsContainer() (err error) {
return nil
}
func setupConfig(opts options) (err error) {
func setupConfig(args options) (err error) {
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts
config.DNS.DnsfilterConf.ConfigModified = onConfigModified
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
config.DNS.DnsfilterConf.DataDir = Context.getDataDir()
config.DNS.DnsfilterConf.Filters = slices.Clone(config.Filters)
config.DNS.DnsfilterConf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules)
config.DNS.DnsfilterConf.HTTPClient = Context.client
config.DHCP.WorkDir = Context.workDir
@@ -328,9 +312,9 @@ func setupConfig(opts options) (err error) {
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
if opts.bindPort != 0 {
if args.bindPort != 0 {
tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(tcpPorts, tcpPort(opts.bindPort), tcpPort(config.BetaBindPort))
addPorts(tcpPorts, tcpPort(args.bindPort), tcpPort(config.BetaBindPort))
udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(config.DNS.Port))
@@ -352,23 +336,23 @@ func setupConfig(opts options) (err error) {
return fmt.Errorf("validating udp ports: %w", err)
}
config.BindPort = opts.bindPort
config.BindPort = args.bindPort
}
// override bind host/port from the console
if opts.bindHost != nil {
config.BindHost = opts.bindHost
if args.bindHost.IsValid() {
config.BindHost = args.bindHost
}
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
Context.pidFileName = opts.pidFile
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
Context.pidFileName = args.pidFile
}
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
if opts.localFrontend {
if args.localFrontend {
log.Info("warning: using local frontend files")
clientFS = os.DirFS("build/static")
@@ -397,11 +381,9 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
clientFS: clientFS,
clientBetaFS: clientBetaFS,
serveHTTP3: config.DNS.ServeHTTP3,
}
web = newWeb(&webConf)
web = CreateWeb(&webConf)
if web == nil {
return nil, fmt.Errorf("initializing web: %w", err)
}
@@ -416,24 +398,24 @@ func fatalOnError(err error) {
}
// run configures and starts AdGuard Home.
func run(opts options, clientBuildFS fs.FS) {
func run(args options, clientBuildFS fs.FS) {
// configure config filename
initConfigFilename(opts)
initConfigFilename(args)
// configure working dir and config path
initWorkingDir(opts)
initWorkingDir(args)
// configure log level and output
configureLogger(opts)
configureLogger(args)
// 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)
if opts.runningAsService {
if args.runningAsService {
log.Info("AdGuard Home is running as a service")
}
setupContext(opts)
setupContext(args)
err := configureOS(config)
fatalOnError(err)
@@ -443,7 +425,7 @@ func run(opts options, clientBuildFS fs.FS) {
// but also avoid relying on automatic Go init() function
filtering.InitModule()
err = setupConfig(opts)
err = setupConfig(args)
fatalOnError(err)
if !Context.firstRun {
@@ -472,10 +454,10 @@ func run(opts options, clientBuildFS fs.FS) {
}
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
GLMode = opts.glinetMode
var rateLimiter *authRateLimiter
GLMode = args.glinetMode
var arl *authRateLimiter
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
rateLimiter = newAuthRateLimiter(
arl = newAuthRateLimiter(
time.Duration(config.AuthBlockMin)*time.Minute,
config.AuthAttempts,
)
@@ -487,7 +469,7 @@ func run(opts options, clientBuildFS fs.FS) {
sessFilename,
config.Users,
config.WebSessionTTLHours*60*60,
rateLimiter,
arl,
)
if Context.auth == nil {
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")
}
Context.web, err = initWeb(opts, clientBuildFS)
Context.web, err = initWeb(args, clientBuildFS)
fatalOnError(err)
if !Context.firstRun {
@@ -556,7 +538,7 @@ func checkPermissions() {
}
// 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 errors.Is(err, os.ErrPermission) {
log.Fatal(`Permission check failed.
@@ -591,10 +573,10 @@ func writePIDFile(fn string) bool {
return true
}
func initConfigFilename(opts options) {
func initConfigFilename(args options) {
// config file path can be overridden by command-line arguments:
if opts.confFilename != "" {
Context.configFilename = opts.confFilename
if args.configFilename != "" {
Context.configFilename = args.configFilename
} else {
// Default config file name
Context.configFilename = "AdGuardHome.yaml"
@@ -603,15 +585,15 @@ func initConfigFilename(opts options) {
// initWorkingDir initializes the workDir
// 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()
if err != nil {
panic(err)
}
if opts.workDir != "" {
if args.workDir != "" {
// If there is a custom config file, use it's directory as our working dir
Context.workDir = opts.workDir
Context.workDir = args.workDir
} else {
Context.workDir = filepath.Dir(execPath)
}
@@ -625,15 +607,15 @@ func initWorkingDir(opts options) {
}
// configureLogger configures logger level and output
func configureLogger(opts options) {
func configureLogger(args options) {
ls := getLogSettings()
// command-line arguments can override config settings
if opts.verbose || config.Verbose {
if args.verbose || config.Verbose {
ls.Verbose = true
}
if opts.logFile != "" {
ls.File = opts.logFile
if args.logFile != "" {
ls.File = args.logFile
} else if config.File != "" {
ls.File = config.File
}
@@ -654,7 +636,7 @@ func configureLogger(opts options) {
// happen pretty quickly.
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
// else is configured. Otherwise, we'll simply lose the log output.
ls.File = configSyslog
@@ -744,29 +726,25 @@ func exitWithError() {
os.Exit(64)
}
// loadCmdLineOpts reads command line arguments and initializes configuration
// from them. If there is an error or an effect, loadCmdLineOpts processes them
// and exits.
func loadCmdLineOpts() (opts options) {
opts, eff, err := parseCmdOpts(os.Args[0], os.Args[1:])
// loadOptions reads command line arguments and initializes configuration
func loadOptions() options {
o, f, err := parse(os.Args[0], os.Args[1:])
if err != nil {
log.Error(err.Error())
printHelp(os.Args[0])
_ = printHelp(os.Args[0])
exitWithError()
}
if eff != nil {
err = eff()
} else if f != nil {
err = f()
if err != nil {
log.Error(err.Error())
exitWithError()
} else {
os.Exit(0)
}
os.Exit(0)
}
return opts
return o
}
// printWebAddrs prints addresses built from proto, addr, and an appropriate

View File

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

View File

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

View File

@@ -3,12 +3,11 @@ package home
import (
"bytes"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"testing"
"github.com/AdguardTeam/golibs/netutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"howett.net/plist"
@@ -28,7 +27,7 @@ func setupDNSIPs(t testing.TB) {
config = &configuration{
DNS: dnsConfig{
BindHosts: []net.IP{netutil.IPv4Zero()},
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS,
},
}

View File

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

View File

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

View File

@@ -176,8 +176,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
chooseSystem()
action := opts.serviceControlAction
log.Info(version.Full())
log.Info("service: control action: %s", action)
log.Printf("service: control action: %s", action)
if action == "reload" {
sendSigReload()
@@ -197,7 +196,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
DisplayName: serviceDisplayName,
Description: serviceDescription,
WorkingDirectory: pwd,
Arguments: optsToArgs(runOpts),
Arguments: serialize(runOpts),
}
configureService(svcConfig)

View File

@@ -266,7 +266,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
}
}
if !webCheckPortAvailable(setts.PortHTTPS) {
if !WebCheckPortAvailable(setts.PortHTTPS) {
aghhttp.Error(
r,
w,
@@ -356,7 +356,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
}
// TODO(e.burkov): Investigate and perhaps check other ports.
if !webCheckPortAvailable(data.PortHTTPS) {
if !WebCheckPortAvailable(data.PortHTTPS) {
aghhttp.Error(
r,
w,
@@ -680,6 +680,8 @@ func unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {
}
func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
w.Header().Set("Content-Type", "application/json")
if data.CertificateChain != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
data.CertificateChain = encoded
@@ -690,7 +692,16 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
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

View File

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

View File

@@ -4,8 +4,8 @@ import (
"context"
"crypto/tls"
"io/fs"
"net"
"net/http"
"net/netip"
"sync"
"time"
@@ -16,8 +16,6 @@ import (
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"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/h2c"
)
@@ -37,7 +35,7 @@ type webConfig struct {
clientFS fs.FS
clientBetaFS fs.FS
BindHost net.IP
BindHost netip.Addr
BindPort int
BetaBindPort int
PortHTTPS int
@@ -55,56 +53,40 @@ type webConfig struct {
WriteTimeout time.Duration
firstRun bool
serveHTTP3 bool
}
// httpsServer contains the data for the HTTPS server.
type httpsServer struct {
// server is the pre-HTTP/3 HTTPS server.
server *http.Server
// server3 is the HTTP/3 HTTPS server. If it is not nil,
// [httpsServer.server] must also be non-nil.
server3 *http3.Server
// TODO(a.garipov): Why is there a *sync.Cond here? Remove.
cond *sync.Cond
condLock sync.Mutex
cert tls.Certificate
inShutdown bool
enabled bool
// HTTPSServer - HTTPS Server
type HTTPSServer struct {
server *http.Server
cond *sync.Cond
condLock sync.Mutex
shutdown bool // if TRUE, don't restart the server
enabled bool
cert tls.Certificate
}
// Web is the web UI and API server.
// Web - module object
type Web struct {
conf *webConfig
// TODO(a.garipov): Refactor all these servers.
httpServer *http.Server
// httpServerBeta is a server for new client.
httpServerBeta *http.Server
conf *webConfig
forceHTTPS bool
httpServer *http.Server // HTTP module
httpsServer HTTPSServer // HTTPS module
// handlerBeta is the handler for new client.
handlerBeta http.Handler
// installerBeta is the pre-install handler for new client.
installerBeta http.Handler
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
// [Web.http3Server] must also not be nil.
httpsServer httpsServer
forceHTTPS bool
// httpServerBeta is a server for new client.
httpServerBeta *http.Server
}
// newWeb creates a new instance of the web UI and API server.
func newWeb(conf *webConfig) (w *Web) {
log.Info("web: initializing")
// CreateWeb - create module
func CreateWeb(conf *webConfig) *Web {
log.Info("Initialize web module")
w = &Web{
conf: conf,
}
w := Web{}
w.conf = conf
clientFS := http.FileServer(http.FS(conf.clientFS))
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)
return w
return &w
}
// webCheckPortAvailable checks if port, which is considered an HTTPS port, is
// available, unless the HTTPS server isn't active.
//
// TODO(a.garipov): Adapt for HTTP/3.
func webCheckPortAvailable(port int) (ok bool) {
return Context.web.httpsServer.server != nil ||
aghnet.CheckPort("tcp", config.BindHost, port) == nil
// WebCheckPortAvailable - check if port is available
// BUT: if we are already using this port, no need
func WebCheckPortAvailable(port int) bool {
if Context.web.httpsServer.server != nil {
return true
}
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
}
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
// if necessary.
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.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
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
shutdownSrv(ctx, web.httpsServer.server)
shutdownSrv3(web.httpsServer.server3)
cancel()
}
@@ -183,7 +163,7 @@ func (web *Web) Start() {
go web.tlsServerLoop()
// 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)
errs := make(chan error, 2)
@@ -254,7 +234,7 @@ func (web *Web) Close(ctx context.Context) {
log.Info("stopping http server...")
web.httpsServer.cond.L.Lock()
web.httpsServer.inShutdown = true
web.httpsServer.shutdown = true
web.httpsServer.cond.L.Unlock()
var cancel context.CancelFunc
@@ -262,7 +242,6 @@ func (web *Web) Close(ctx context.Context) {
defer cancel()
shutdownSrv(ctx, web.httpsServer.server)
shutdownSrv3(web.httpsServer.server3)
shutdownSrv(ctx, web.httpServer)
shutdownSrv(ctx, web.httpServerBeta)
@@ -272,7 +251,7 @@ func (web *Web) Close(ctx context.Context) {
func (web *Web) tlsServerLoop() {
for {
web.httpsServer.cond.L.Lock()
if web.httpsServer.inShutdown {
if web.httpsServer.shutdown {
web.httpsServer.cond.L.Unlock()
break
}
@@ -280,7 +259,7 @@ func (web *Web) tlsServerLoop() {
// this mechanism doesn't let us through until all conditions are met
for !web.httpsServer.enabled { // sleep until necessary data is supplied
web.httpsServer.cond.Wait()
if web.httpsServer.inShutdown {
if web.httpsServer.shutdown {
web.httpsServer.cond.L.Unlock()
return
}
@@ -288,10 +267,11 @@ func (web *Web) tlsServerLoop() {
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{
ErrorLog: log.StdLog("web: https", log.DEBUG),
Addr: addr,
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: Context.tlsRoots,
@@ -305,40 +285,10 @@ func (web *Web) tlsServerLoop() {
}
printHTTPAddresses(aghhttp.SchemeHTTPS)
if web.conf.serveHTTP3 {
go web.mustStartHTTP3(addr)
}
log.Debug("web: starting https server")
err := web.httpsServer.server.ListenAndServeTLS("", "")
if !errors.Is(err, http.ErrServerClosed) {
if err != http.ErrServerClosed {
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
package websvc
import "time"
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second

View File

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

View File

@@ -1,6 +1,7 @@
package querylog
import (
"encoding/json"
"fmt"
"net"
"net/http"
@@ -47,7 +48,24 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
// convert log entries to JSON
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) {
@@ -56,13 +74,23 @@ func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
// Get configuration
func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
resp := qlogConfig{
Enabled: l.conf.Enabled,
Interval: l.conf.RotationIvl.Hours() / 24,
AnonymizeClientIP: l.conf.AnonymizeClientIP,
resp := qlogConfig{}
resp.Enabled = l.conf.Enabled
resp.Interval = l.conf.RotationIvl.Hours() / 24
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.

View File

@@ -55,7 +55,12 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
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.
@@ -66,7 +71,13 @@ type configResp struct {
// handleStatsInfo handles requests to the GET /control/stats_info endpoint.
func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
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

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