Compare commits
2 Commits
websvc-con
...
2926-lla-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de837e4eec | ||
|
|
e528d2f23b |
82
CHANGELOG.md
82
CHANGELOG.md
@@ -12,75 +12,11 @@ and this project adheres to
|
||||
## [Unreleased]
|
||||
|
||||
<!--
|
||||
## [v0.108.0] - TBA (APPROX.)
|
||||
## [v0.108.0] - 2022-12-01 (APPROX.)
|
||||
-->
|
||||
|
||||
### 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]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- `only application/json is allowed` errors in various APIs ([#4970]).
|
||||
|
||||
[#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
## [v0.107.15] - 2022-10-26 (APPROX.)
|
||||
|
||||
See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
|
||||
|
||||
[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]).
|
||||
|
||||
@@ -97,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
|
||||
-->
|
||||
|
||||
|
||||
|
||||
@@ -1296,12 +1241,11 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...HEAD
|
||||
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
|
||||
-->
|
||||
|
||||
[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.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
|
||||
|
||||
4
Makefile
4
Makefile
@@ -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)'\
|
||||
|
||||
|
||||
18
SECURITY.md
18
SECURITY.md
@@ -1,18 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please send your vulnerability reports to <security@adguard.com>. To make sure
|
||||
that your report reaches us, please:
|
||||
|
||||
1. Include the words “AdGuard Home” and “vulnerability” to the subject line as
|
||||
well as a short description of the vulnerability. For example:
|
||||
|
||||
> AdGuard Home API vulnerability: possible XSS attack
|
||||
|
||||
2. Make sure that the message body contains a clear description of the
|
||||
vulnerability.
|
||||
|
||||
If you have not received a reply to your email within 7 days, please make sure
|
||||
to follow up with us again at <security@adguard.com>. Once again, make sure
|
||||
that the word “vulnerability” is in the subject line.
|
||||
@@ -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}} сімвалаў"
|
||||
}
|
||||
|
||||
@@ -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é"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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ä"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}}文字以上にしてください"
|
||||
}
|
||||
|
||||
@@ -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}}자 이상이어야 합니다"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}} символов"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}} символів"
|
||||
}
|
||||
|
||||
@@ -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ự"
|
||||
}
|
||||
|
||||
@@ -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}} 个字符"
|
||||
}
|
||||
|
||||
@@ -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}} 個字元"
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Package aghchan contains channel utilities.
|
||||
package aghchan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Receive returns an error if it cannot receive a value form c before timeout
|
||||
// runs out.
|
||||
func Receive[T any](c <-chan T, timeout time.Duration) (v T, ok bool, err error) {
|
||||
var zero T
|
||||
timeoutCh := time.After(timeout)
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
// TODO(a.garipov): Consider implementing [errors.Aser] for
|
||||
// os.ErrTimeout.
|
||||
return zero, false, fmt.Errorf("did not receive after %s", timeout)
|
||||
case v, ok = <-c:
|
||||
return v, ok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// MustReceive panics if it cannot receive a value form c before timeout runs
|
||||
// out.
|
||||
func MustReceive[T any](c <-chan T, timeout time.Duration) (v T, ok bool) {
|
||||
v, ok, err := Receive(c, timeout)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return v, ok
|
||||
}
|
||||
@@ -2,12 +2,10 @@
|
||||
package aghhttp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
@@ -33,43 +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", 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) {
|
||||
w.Header().Set(HdrNameContentType, HdrValApplicationJSON)
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package aghhttp
|
||||
|
||||
// HTTP Headers
|
||||
|
||||
// HTTP header name constants.
|
||||
//
|
||||
// TODO(a.garipov): Remove unused.
|
||||
const (
|
||||
HdrNameAcceptEncoding = "Accept-Encoding"
|
||||
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
HdrNameContentEncoding = "Content-Encoding"
|
||||
HdrNameContentType = "Content-Type"
|
||||
HdrNameServer = "Server"
|
||||
HdrNameTrailer = "Trailer"
|
||||
HdrNameUserAgent = "User-Agent"
|
||||
)
|
||||
|
||||
// HTTP header value constants.
|
||||
const (
|
||||
HdrValApplicationJSON = "application/json"
|
||||
HdrValTextPlain = "text/plain"
|
||||
)
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"testing/fstest"
|
||||
"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())
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -347,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
|
||||
}
|
||||
@@ -357,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
|
||||
}
|
||||
}
|
||||
@@ -408,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",
|
||||
@@ -463,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"
|
||||
@@ -478,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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -43,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"))
|
||||
@@ -84,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)
|
||||
@@ -119,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)
|
||||
@@ -151,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
|
||||
@@ -179,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
@@ -198,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,
|
||||
|
||||
@@ -3,12 +3,11 @@ 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"
|
||||
@@ -21,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()
|
||||
}
|
||||
@@ -39,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() {
|
||||
@@ -72,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 {
|
||||
@@ -98,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) {
|
||||
@@ -126,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,
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -147,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 {
|
||||
@@ -157,16 +164,13 @@ type profileJSON struct {
|
||||
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
pj := profileJSON{}
|
||||
u := Context.auth.getCurrentUser(r)
|
||||
|
||||
pj.Name = u.Name
|
||||
|
||||
data, err := json.Marshal(pj)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
@@ -197,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()
|
||||
}
|
||||
@@ -228,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)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -59,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 {
|
||||
@@ -116,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
|
||||
@@ -141,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
|
||||
}
|
||||
@@ -159,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
|
||||
}
|
||||
|
||||
@@ -189,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)
|
||||
@@ -303,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 {
|
||||
@@ -370,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)
|
||||
|
||||
@@ -390,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
|
||||
@@ -406,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)
|
||||
@@ -463,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",
|
||||
@@ -491,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
|
||||
@@ -568,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
|
||||
@@ -670,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
@@ -254,7 +249,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -277,7 +278,6 @@ func setupConfig(args options) (err error) {
|
||||
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
|
||||
@@ -340,7 +340,7 @@ func setupConfig(args options) (err error) {
|
||||
}
|
||||
|
||||
// override bind host/port from the console
|
||||
if args.bindHost != nil {
|
||||
if args.bindHost.IsValid() {
|
||||
config.BindHost = args.bindHost
|
||||
}
|
||||
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
||||
@@ -409,7 +409,7 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
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 args.runningAsService {
|
||||
log.Info("AdGuard Home is running as a service")
|
||||
@@ -455,9 +455,9 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
|
||||
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
||||
GLMode = args.glinetMode
|
||||
var rateLimiter *authRateLimiter
|
||||
var arl *authRateLimiter
|
||||
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
||||
rateLimiter = newAuthRateLimiter(
|
||||
arl = newAuthRateLimiter(
|
||||
time.Duration(config.AuthBlockMin)*time.Minute,
|
||||
config.AuthAttempts,
|
||||
)
|
||||
@@ -469,7 +469,7 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
sessFilename,
|
||||
config.Users,
|
||||
config.WebSessionTTLHours*60*60,
|
||||
rateLimiter,
|
||||
arl,
|
||||
)
|
||||
if Context.auth == nil {
|
||||
log.Fatalf("Couldn't initialize Auth module")
|
||||
@@ -538,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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package home
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
@@ -12,15 +12,15 @@ import (
|
||||
|
||||
// options passed from command-line arguments
|
||||
type options struct {
|
||||
verbose bool // is verbose logging enabled
|
||||
configFilename string // path to the config file
|
||||
workDir string // path to the working directory where we will store the filters data and the querylog
|
||||
bindHost net.IP // host address to bind HTTP server on
|
||||
bindPort int // port to serve HTTP pages on
|
||||
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
||||
pidFile string // File name to save PID to
|
||||
checkConfig bool // Check configuration and exit
|
||||
disableUpdate bool // If set, don't check for updates
|
||||
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
|
||||
|
||||
// service control action (see service.ControlAction array + "status" command)
|
||||
serviceControlAction string
|
||||
@@ -60,8 +60,8 @@ type arg struct {
|
||||
// against its zero value and return nil if the parameter value is
|
||||
// zero otherwise they return a string slice of the parameter
|
||||
|
||||
func ipSliceOrNil(ip net.IP) []string {
|
||||
if ip == nil {
|
||||
func ipSliceOrNil(ip netip.Addr) []string {
|
||||
if !ip.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ var workDirArg = arg{
|
||||
var hostArg = arg{
|
||||
"Host address to bind HTTP server on.",
|
||||
"host", "h",
|
||||
func(o options, v string) (options, error) { o.bindHost = net.ParseIP(v); return o, nil }, nil, nil,
|
||||
func(o options, v string) (options, error) { o.bindHost, _ = netip.ParseAddr(v); return o, nil }, nil, nil,
|
||||
func(o options) []string { return ipSliceOrNil(o.bindHost) },
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package home
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ func TestSerialize(t *testing.T) {
|
||||
ss: []string{"-w", "path"},
|
||||
}, {
|
||||
name: "bind_host",
|
||||
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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -35,7 +35,7 @@ type webConfig struct {
|
||||
clientFS fs.FS
|
||||
clientBetaFS fs.FS
|
||||
|
||||
BindHost net.IP
|
||||
BindHost netip.Addr
|
||||
BindPort int
|
||||
BetaBindPort int
|
||||
PortHTTPS int
|
||||
@@ -114,8 +114,11 @@ func CreateWeb(conf *webConfig) *Web {
|
||||
// WebCheckPortAvailable - check if port is available
|
||||
// BUT: if we are already using this port, no need
|
||||
func WebCheckPortAvailable(port int) bool {
|
||||
return Context.web.httpsServer.server != nil ||
|
||||
aghnet.CheckPort("tcp", config.BindHost, port) == nil
|
||||
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
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
)
|
||||
|
||||
// DNS Settings Handlers
|
||||
|
||||
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
||||
// HTTP API.
|
||||
type ReqPatchSettingsDNS struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
BootstrapServers []string `json:"bootstrap_servers"`
|
||||
UpstreamServers []string `json:"upstream_servers"`
|
||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
|
||||
// DnsSettings object in the OpenAPI specification.
|
||||
type HTTPAPIDNSSettings struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
BootstrapServers []string `json:"bootstrap_servers"`
|
||||
UpstreamServers []string `json:"upstream_servers"`
|
||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
|
||||
// API.
|
||||
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
|
||||
req := &ReqPatchSettingsDNS{
|
||||
Addresses: []netip.AddrPort{},
|
||||
BootstrapServers: []string{},
|
||||
UpstreamServers: []string{},
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newConf := &dnssvc.Config{
|
||||
Addresses: req.Addresses,
|
||||
BootstrapServers: req.BootstrapServers,
|
||||
UpstreamServers: req.UpstreamServers,
|
||||
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newSvc := svc.confMgr.DNS()
|
||||
err = newSvc.Start()
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
||||
Addresses: newConf.Addresses,
|
||||
BootstrapServers: newConf.BootstrapServers,
|
||||
UpstreamServers: newConf.UpstreamServers,
|
||||
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
||||
})
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
|
||||
BootstrapServers: []string{"1.0.0.1"},
|
||||
UpstreamServers: []string{"1.1.1.1"},
|
||||
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
||||
var numStarted uint64
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
||||
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||
OnStart: func() (err error) {
|
||||
atomic.AddUint64(&numStarted, 1)
|
||||
|
||||
return nil
|
||||
},
|
||||
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
|
||||
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
|
||||
}
|
||||
}
|
||||
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsDNS,
|
||||
}
|
||||
|
||||
req := jobj{
|
||||
"addresses": wantDNS.Addresses,
|
||||
"bootstrap_servers": wantDNS.BootstrapServers,
|
||||
"upstream_servers": wantDNS.UpstreamServers,
|
||||
"upstream_timeout": wantDNS.UpstreamTimeout,
|
||||
}
|
||||
|
||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||
resp := &websvc.HTTPAPIDNSSettings{}
|
||||
err := json.Unmarshal(respBody, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, uint64(1), numStarted)
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// HTTP Settings Handlers
|
||||
|
||||
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
||||
// HTTP API.
|
||||
type ReqPatchSettingsHTTP struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
//
|
||||
// TODO(a.garipov): Add wait time.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||
Timeout JSONDuration `json:"timeout"`
|
||||
}
|
||||
|
||||
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
||||
// HttpSettings object in the OpenAPI specification.
|
||||
type HTTPAPIHTTPSettings struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||
Timeout JSONDuration `json:"timeout"`
|
||||
ForceHTTPS bool `json:"force_https"`
|
||||
}
|
||||
|
||||
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
|
||||
// HTTP API.
|
||||
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
req := &ReqPatchSettingsHTTP{}
|
||||
|
||||
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newConf := &Config{
|
||||
ConfigManager: svc.confMgr,
|
||||
TLS: svc.tls,
|
||||
Addresses: req.Addresses,
|
||||
SecureAddresses: req.SecureAddresses,
|
||||
Timeout: time.Duration(req.Timeout),
|
||||
ForceHTTPS: svc.forceHTTPS,
|
||||
}
|
||||
|
||||
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
||||
Addresses: newConf.Addresses,
|
||||
SecureAddresses: newConf.SecureAddresses,
|
||||
Timeout: JSONDuration(newConf.Timeout),
|
||||
ForceHTTPS: newConf.ForceHTTPS,
|
||||
})
|
||||
|
||||
cancelUpd := func() {}
|
||||
updCtx := context.Background()
|
||||
|
||||
ctx := r.Context()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
|
||||
}
|
||||
|
||||
// Launch the new HTTP service in a separate goroutine to let this handler
|
||||
// finish and thus, this server to shutdown.
|
||||
go func() {
|
||||
defer cancelUpd()
|
||||
|
||||
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
||||
if updErr != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Consider better ways to do this.
|
||||
const maxUpdDur = 10 * time.Second
|
||||
updStart := time.Now()
|
||||
var newSvc ServiceWithConfig[*Config]
|
||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||
if time.Since(updStart) >= maxUpdDur {
|
||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("websvc: waiting for new websvc to be configured")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
updErr = newSvc.Start()
|
||||
if updErr != nil {
|
||||
log.Error("websvc: new svc failed to start with error: %s", updErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
||||
Timeout: websvc.JSONDuration(10 * time.Second),
|
||||
ForceHTTPS: false,
|
||||
}
|
||||
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
||||
return websvc.New(&websvc.Config{
|
||||
TLS: &tls.Config{
|
||||
Certificates: []tls.Certificate{{}},
|
||||
},
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||
Timeout: 5 * time.Second,
|
||||
ForceHTTPS: true,
|
||||
})
|
||||
}
|
||||
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsHTTP,
|
||||
}
|
||||
|
||||
req := jobj{
|
||||
"addresses": wantWeb.Addresses,
|
||||
"secure_addresses": wantWeb.SecureAddresses,
|
||||
"timeout": wantWeb.Timeout,
|
||||
"force_https": wantWeb.ForceHTTPS,
|
||||
}
|
||||
|
||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||
resp := &websvc.HTTPAPIHTTPSettings{}
|
||||
err := json.Unmarshal(respBody, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, wantWeb, resp)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// JSON Utilities
|
||||
|
||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||
|
||||
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
||||
// into JSON according to our API conventions.
|
||||
type JSONDuration time.Duration
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONDuration(0)
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
||||
// always nil.
|
||||
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Duration(d)) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
||||
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
||||
if d == nil {
|
||||
return fmt.Errorf("json duration is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*d = JSONDuration(int64(msec * nsecPerMsec))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||
// according to our API conventions.
|
||||
type JSONTime time.Time
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONTime{}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
||||
// always nil.
|
||||
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONTime)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
||||
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||
if t == nil {
|
||||
return fmt.Errorf("json time is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
||||
// and logs any errors it encounters. r is used to get additional information
|
||||
// from the request.
|
||||
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
||||
writeJSONResponse(w, r, v, http.StatusOK)
|
||||
}
|
||||
|
||||
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
||||
// errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
||||
// TODO(a.garipov): Put some of these to a middleware.
|
||||
h := w.Header()
|
||||
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
||||
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
||||
|
||||
w.WriteHeader(code)
|
||||
|
||||
err := json.NewEncoder(w).Encode(v)
|
||||
if err != nil {
|
||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||
// definition in the OpenAPI specification.
|
||||
type ErrorCode string
|
||||
|
||||
// ErrorCode constants.
|
||||
//
|
||||
// TODO(a.garipov): Expand and document codes.
|
||||
const (
|
||||
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
||||
ErrorCodeTMP000 = ""
|
||||
)
|
||||
|
||||
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
||||
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
||||
// specification.
|
||||
type HTTPAPIErrorResp struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
||||
// errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
||||
|
||||
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
||||
Code: ErrorCodeTMP000,
|
||||
Msg: err.Error(),
|
||||
}, http.StatusUnprocessableEntity)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testJSONTime is the JSON time for tests.
|
||||
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||
|
||||
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||
const testJSONTimeStr = "1234567890123.456"
|
||||
|
||||
func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
in websvc.JSONTime
|
||||
want []byte
|
||||
}{{
|
||||
name: "unix_zero",
|
||||
wantErrMsg: "",
|
||||
in: websvc.JSONTime(time.Unix(0, 0)),
|
||||
want: []byte("0"),
|
||||
}, {
|
||||
name: "empty",
|
||||
wantErrMsg: "",
|
||||
in: websvc.JSONTime{},
|
||||
want: []byte("-6795364578871.345"),
|
||||
}, {
|
||||
name: "time",
|
||||
wantErrMsg: "",
|
||||
in: testJSONTime,
|
||||
want: []byte(testJSONTimeStr),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.in.MarshalJSON()
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
in := &struct {
|
||||
A websvc.JSONTime
|
||||
}{
|
||||
A: testJSONTime,
|
||||
}
|
||||
|
||||
got, err := json.Marshal(in)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
want websvc.JSONTime
|
||||
data []byte
|
||||
}{{
|
||||
name: "time",
|
||||
wantErrMsg: "",
|
||||
want: testJSONTime,
|
||||
data: []byte(testJSONTimeStr),
|
||||
}, {
|
||||
name: "bad",
|
||||
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||
`invalid syntax`,
|
||||
want: websvc.JSONTime{},
|
||||
data: []byte(`{}`),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got websvc.JSONTime
|
||||
err := got.UnmarshalJSON(tc.data)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
||||
require.Error(t, err)
|
||||
|
||||
msg := err.Error()
|
||||
assert.Equal(t, "json time is nil", msg)
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
want := testJSONTime
|
||||
var got struct {
|
||||
A websvc.JSONTime
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, want, got.A)
|
||||
})
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package websvc
|
||||
|
||||
// Path constants
|
||||
const (
|
||||
PathHealthCheck = "/health-check"
|
||||
|
||||
PathV1SettingsAll = "/api/v1/settings/all"
|
||||
PathV1SettingsDNS = "/api/v1/settings/dns"
|
||||
PathV1SettingsHTTP = "/api/v1/settings/http"
|
||||
PathV1SystemInfo = "/api/v1/system/info"
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// All Settings Handlers
|
||||
|
||||
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
|
||||
// HTTP API.
|
||||
type RespGetV1SettingsAll struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
DNS *HTTPAPIDNSSettings `json:"dns"`
|
||||
HTTP *HTTPAPIHTTPSettings `json:"http"`
|
||||
}
|
||||
|
||||
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
|
||||
// API.
|
||||
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
|
||||
dnsSvc := svc.confMgr.DNS()
|
||||
dnsConf := dnsSvc.Config()
|
||||
|
||||
webSvc := svc.confMgr.Web()
|
||||
httpConf := webSvc.Config()
|
||||
|
||||
// TODO(a.garipov): Add all currently supported parameters.
|
||||
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
||||
DNS: &HTTPAPIDNSSettings{
|
||||
Addresses: dnsConf.Addresses,
|
||||
BootstrapServers: dnsConf.BootstrapServers,
|
||||
UpstreamServers: dnsConf.UpstreamServers,
|
||||
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
||||
},
|
||||
HTTP: &HTTPAPIHTTPSettings{
|
||||
Addresses: httpConf.Addresses,
|
||||
SecureAddresses: httpConf.SecureAddresses,
|
||||
Timeout: JSONDuration(httpConf.Timeout),
|
||||
ForceHTTPS: httpConf.ForceHTTPS,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||
// TODO(a.garipov): Add all currently supported parameters.
|
||||
|
||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
||||
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
||||
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
||||
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
||||
}
|
||||
|
||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||
Timeout: websvc.JSONDuration(5 * time.Second),
|
||||
ForceHTTPS: true,
|
||||
}
|
||||
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
||||
c, err := dnssvc.New(&dnssvc.Config{
|
||||
Addresses: wantDNS.Addresses,
|
||||
UpstreamServers: wantDNS.UpstreamServers,
|
||||
BootstrapServers: wantDNS.BootstrapServers,
|
||||
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
||||
return websvc.New(&websvc.Config{
|
||||
TLS: &tls.Config{
|
||||
Certificates: []tls.Certificate{{}},
|
||||
},
|
||||
Addresses: wantWeb.Addresses,
|
||||
SecureAddresses: wantWeb.SecureAddresses,
|
||||
Timeout: time.Duration(wantWeb.Timeout),
|
||||
ForceHTTPS: true,
|
||||
})
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsAll,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
resp := &websvc.RespGetV1SettingsAll{}
|
||||
err := json.Unmarshal(body, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, wantDNS, resp.DNS)
|
||||
assert.Equal(t, wantWeb, resp.HTTP)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Wait Listener
|
||||
|
||||
// waitListener is a wrapper around a listener that also calls wg.Done() on the
|
||||
// first call to Accept. It is useful in situations where it is important to
|
||||
// catch the precise moment of the first call to Accept, for example when
|
||||
// starting an HTTP server.
|
||||
//
|
||||
// TODO(a.garipov): Move to aghnet?
|
||||
type waitListener struct {
|
||||
net.Listener
|
||||
|
||||
firstAcceptWG *sync.WaitGroup
|
||||
firstAcceptOnce sync.Once
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ net.Listener = (*waitListener)(nil)
|
||||
|
||||
// Accept implements the [net.Listener] interface for *waitListener.
|
||||
func (l *waitListener) Accept() (conn net.Conn, err error) {
|
||||
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
|
||||
|
||||
return l.Listener.Accept()
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWaitListener_Accept(t *testing.T) {
|
||||
// TODO(a.garipov): use atomic.Bool in Go 1.19.
|
||||
var numAcceptCalls uint32
|
||||
var l net.Listener = &aghtest.Listener{
|
||||
OnAccept: func() (conn net.Conn, err error) {
|
||||
atomic.AddUint32(&numAcceptCalls, 1)
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
OnAddr: func() (addr net.Addr) { panic("not implemented") },
|
||||
OnClose: func() (err error) { panic("not implemented") },
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
done := make(chan struct{})
|
||||
go aghchan.MustReceive(done, testTimeout)
|
||||
|
||||
go func() {
|
||||
var wrapper net.Listener = &waitListener{
|
||||
Listener: l,
|
||||
firstAcceptWG: wg,
|
||||
}
|
||||
|
||||
_, _ = wrapper.Accept()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(done)
|
||||
|
||||
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import "time"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
@@ -1,187 +0,0 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testStart is the server start value for tests.
|
||||
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// type check
|
||||
var _ websvc.ConfigManager = (*configManager)(nil)
|
||||
|
||||
// configManager is a [websvc.ConfigManager] for tests.
|
||||
type configManager struct {
|
||||
onDNS func() (svc websvc.ServiceWithConfig[*dnssvc.Config])
|
||||
onWeb func() (svc websvc.ServiceWithConfig[*websvc.Config])
|
||||
|
||||
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
||||
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
||||
}
|
||||
|
||||
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) DNS() (svc websvc.ServiceWithConfig[*dnssvc.Config]) {
|
||||
return m.onDNS()
|
||||
}
|
||||
|
||||
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) Web() (svc websvc.ServiceWithConfig[*websvc.Config]) {
|
||||
return m.onWeb()
|
||||
}
|
||||
|
||||
// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return m.onUpdateDNS(ctx, c)
|
||||
}
|
||||
|
||||
// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
return m.onUpdateWeb(ctx, c)
|
||||
}
|
||||
|
||||
// newConfigManager returns a *configManager all methods of which panic.
|
||||
func newConfigManager() (m *configManager) {
|
||||
return &configManager{
|
||||
onDNS: func() (svc websvc.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
||||
onWeb: func() (svc websvc.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
||||
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
onUpdateWeb: func(_ context.Context, _ *websvc.Config) (err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newTestServer creates and starts a new web service instance as well as its
|
||||
// sole address. It also registers a cleanup procedure, which shuts the
|
||||
// instance down.
|
||||
//
|
||||
// TODO(a.garipov): Use svc or remove it.
|
||||
func newTestServer(
|
||||
t testing.TB,
|
||||
confMgr websvc.ConfigManager,
|
||||
) (svc *websvc.Service, addr netip.AddrPort) {
|
||||
t.Helper()
|
||||
|
||||
c := &websvc.Config{
|
||||
ConfigManager: confMgr,
|
||||
TLS: nil,
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||
SecureAddresses: nil,
|
||||
Timeout: testTimeout,
|
||||
Start: testStart,
|
||||
ForceHTTPS: false,
|
||||
}
|
||||
|
||||
svc = websvc.New(c)
|
||||
|
||||
err := svc.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err = svc.Shutdown(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
c = svc.Config()
|
||||
require.NotNil(t, c)
|
||||
require.Len(t, c.Addresses, 1)
|
||||
|
||||
return svc, c.Addresses[0]
|
||||
}
|
||||
|
||||
// jobj is a utility alias for JSON objects.
|
||||
type jobj map[string]any
|
||||
|
||||
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||
// the response as well as checks that the status code is correct.
|
||||
//
|
||||
// TODO(a.garipov): Add helpers for other methods.
|
||||
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
||||
t.Helper()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoErrorf(t, err, "creating req")
|
||||
|
||||
httpCli := &http.Client{
|
||||
Timeout: testTimeout,
|
||||
}
|
||||
resp, err := httpCli.Do(req)
|
||||
require.NoErrorf(t, err, "performing req")
|
||||
require.Equal(t, wantCode, resp.StatusCode)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoErrorf(t, err, "reading body")
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded
|
||||
// reqBody as the request body and returns the body of the response as well as
|
||||
// checks that the status code is correct.
|
||||
//
|
||||
// TODO(a.garipov): Add helpers for other methods.
|
||||
func httpPatch(t testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) {
|
||||
t.Helper()
|
||||
|
||||
b, err := json.Marshal(reqBody)
|
||||
require.NoErrorf(t, err, "marshaling reqBody")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b))
|
||||
require.NoErrorf(t, err, "creating req")
|
||||
|
||||
httpCli := &http.Client{
|
||||
Timeout: testTimeout,
|
||||
}
|
||||
resp, err := httpCli.Do(req)
|
||||
require.NoErrorf(t, err, "performing req")
|
||||
require.Equal(t, wantCode, resp.StatusCode)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoErrorf(t, err, "reading body")
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||
confMgr := newConfigManager()
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathHealthCheck,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
}
|
||||
@@ -11,32 +11,29 @@ import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Main is the entry point of application.
|
||||
func Main(clientBuildFS fs.FS) {
|
||||
// Initial Configuration
|
||||
// # Initial Configuration
|
||||
|
||||
start := time.Now()
|
||||
rand.Seed(start.UnixNano())
|
||||
|
||||
// TODO(a.garipov): Set up logging.
|
||||
|
||||
// Web Service
|
||||
// # Web Service
|
||||
|
||||
// TODO(a.garipov): Use in the Web service.
|
||||
_ = clientBuildFS
|
||||
|
||||
// TODO(a.garipov): Make configurable.
|
||||
web := websvc.New(&websvc.Config{
|
||||
// TODO(a.garipov): Use an actual implementation.
|
||||
ConfigManager: nil,
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
|
||||
Start: start,
|
||||
Timeout: 60 * time.Second,
|
||||
ForceHTTPS: false,
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
|
||||
Start: start,
|
||||
Timeout: 60 * time.Second,
|
||||
})
|
||||
|
||||
err := web.Start()
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
||||
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
||||
// and replacement of module dnsproxy.
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
@@ -48,14 +47,6 @@ type Config struct {
|
||||
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
||||
// [agh.Service] that does nothing.
|
||||
type Service struct {
|
||||
// running is an atomic boolean value. Keep it the first value in the
|
||||
// struct to ensure atomic alignment. 0 means that the service is not
|
||||
// running, 1 means that it is running.
|
||||
//
|
||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19 or get rid of it
|
||||
// completely.
|
||||
running uint64
|
||||
|
||||
proxy *proxy.Proxy
|
||||
bootstraps []string
|
||||
upstreams []string
|
||||
@@ -169,17 +160,6 @@ func (svc *Service) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to
|
||||
// tell when all servers are actually up, so at best this is merely an
|
||||
// assumption.
|
||||
if err != nil {
|
||||
atomic.StoreUint64(&svc.running, 0)
|
||||
} else {
|
||||
atomic.StoreUint64(&svc.running, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
return svc.proxy.Start()
|
||||
}
|
||||
|
||||
@@ -193,27 +173,13 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||
return svc.proxy.Stop()
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the web service. Config must not
|
||||
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||
// addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
// Config returns the current configuration of the web service.
|
||||
func (svc *Service) Config() (c *Config) {
|
||||
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
||||
|
||||
var addrs []netip.AddrPort
|
||||
if atomic.LoadUint64(&svc.running) == 1 {
|
||||
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
||||
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
||||
}
|
||||
} else {
|
||||
conf := svc.proxy.Config
|
||||
udpAddrs := conf.UDPListenAddr
|
||||
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.AddrPort()
|
||||
}
|
||||
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
||||
addrs := make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
||||
}
|
||||
|
||||
c = &Config{
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/dnssvc"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
61
internal/v1/websvc/json.go
Normal file
61
internal/v1/websvc/json.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// JSON Utilities
|
||||
|
||||
// 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{}
|
||||
|
||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||
|
||||
// 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', 3, 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
|
||||
}
|
||||
|
||||
// writeJSONResponse encodes v into w and logs any errors it encounters. r is
|
||||
// used to get additional information from the request.
|
||||
func writeJSONResponse(w io.Writer, r *http.Request, v any) {
|
||||
err := json.NewEncoder(w).Encode(v)
|
||||
if err != nil {
|
||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||
}
|
||||
}
|
||||
8
internal/v1/websvc/path.go
Normal file
8
internal/v1/websvc/path.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package websvc
|
||||
|
||||
// Path constants
|
||||
const (
|
||||
PathHealthCheck = "/health-check"
|
||||
|
||||
PathV1SystemInfo = "/api/v1/system/info"
|
||||
)
|
||||
@@ -16,20 +16,20 @@ type RespGetV1SystemInfo struct {
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
NewVersion string `json:"new_version,omitempty"`
|
||||
Start JSONTime `json:"start"`
|
||||
Start jsonTime `json:"start"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||
// API.
|
||||
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
|
||||
writeJSONResponse(w, r, &RespGetV1SystemInfo{
|
||||
Arch: runtime.GOARCH,
|
||||
Channel: version.Channel(),
|
||||
OS: runtime.GOOS,
|
||||
// TODO(a.garipov): Fill this when we have an updater.
|
||||
NewVersion: "",
|
||||
Start: JSONTime(svc.start),
|
||||
Start: jsonTime(svc.start),
|
||||
Version: version.Version(),
|
||||
})
|
||||
}
|
||||
@@ -8,17 +8,16 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||
confMgr := newConfigManager()
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
_, addr := newTestServer(t)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Host: addr,
|
||||
Path: websvc.PathV1SystemInfo,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// Package websvc contains the AdGuard Home HTTP API service.
|
||||
//
|
||||
// NOTE: Packages other than cmd must not import this package, as it imports
|
||||
// most other packages.
|
||||
// Package websvc contains the AdGuard Home web service.
|
||||
//
|
||||
// TODO(a.garipov): Add tests.
|
||||
package websvc
|
||||
@@ -17,46 +14,18 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||
)
|
||||
|
||||
// ServiceWithConfig is an extension of the [agh.Service] interface for services
|
||||
// that can return their configuration.
|
||||
//
|
||||
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||
// how to make it testable in a better way.
|
||||
type ServiceWithConfig[ConfigType any] interface {
|
||||
agh.Service
|
||||
|
||||
Config() (c ConfigType)
|
||||
}
|
||||
|
||||
// ConfigManager is the configuration manager interface.
|
||||
type ConfigManager interface {
|
||||
DNS() (svc ServiceWithConfig[*dnssvc.Config])
|
||||
Web() (svc ServiceWithConfig[*Config])
|
||||
|
||||
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
|
||||
UpdateWeb(ctx context.Context, c *Config) (err error)
|
||||
}
|
||||
|
||||
// Config is the AdGuard Home web service configuration structure.
|
||||
type Config struct {
|
||||
// ConfigManager is used to show information about services as well as
|
||||
// dynamically reconfigure them.
|
||||
ConfigManager ConfigManager
|
||||
|
||||
// TLS is the optional TLS configuration. If TLS is not nil,
|
||||
// SecureAddresses must not be empty.
|
||||
TLS *tls.Config
|
||||
|
||||
// Start is the time of start of AdGuard Home.
|
||||
Start time.Time
|
||||
|
||||
// Addresses are the addresses on which to serve the plain HTTP API.
|
||||
Addresses []netip.AddrPort
|
||||
|
||||
@@ -64,48 +33,40 @@ type Config struct {
|
||||
// SecureAddresses is not empty, TLS must not be nil.
|
||||
SecureAddresses []netip.AddrPort
|
||||
|
||||
// Start is the time of start of AdGuard Home.
|
||||
Start time.Time
|
||||
|
||||
// Timeout is the timeout for all server operations.
|
||||
Timeout time.Duration
|
||||
|
||||
// ForceHTTPS tells if all requests to Addresses should be redirected to a
|
||||
// secure address instead.
|
||||
//
|
||||
// TODO(a.garipov): Use; define rules, which address to redirect to.
|
||||
ForceHTTPS bool
|
||||
}
|
||||
|
||||
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||
// [agh.Service] that does nothing.
|
||||
type Service struct {
|
||||
confMgr ConfigManager
|
||||
tls *tls.Config
|
||||
start time.Time
|
||||
servers []*http.Server
|
||||
timeout time.Duration
|
||||
forceHTTPS bool
|
||||
tls *tls.Config
|
||||
servers []*http.Server
|
||||
start time.Time
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||
// *Service that does nothing. The fields of c must not be modified after
|
||||
// calling New.
|
||||
// *Service that does nothing.
|
||||
func New(c *Config) (svc *Service) {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
svc = &Service{
|
||||
confMgr: c.ConfigManager,
|
||||
tls: c.TLS,
|
||||
start: c.Start,
|
||||
timeout: c.Timeout,
|
||||
forceHTTPS: c.ForceHTTPS,
|
||||
tls: c.TLS,
|
||||
start: c.Start,
|
||||
timeout: c.Timeout,
|
||||
}
|
||||
|
||||
mux := newMux(svc)
|
||||
|
||||
for _, a := range c.Addresses {
|
||||
addr := a.String()
|
||||
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
|
||||
errLog := log.StdLog("websvc: http: "+addr, log.ERROR)
|
||||
svc.servers = append(svc.servers, &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
@@ -150,21 +111,6 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
method: http.MethodGet,
|
||||
path: PathHealthCheck,
|
||||
isJSON: false,
|
||||
}, {
|
||||
handler: svc.handleGetSettingsAll,
|
||||
method: http.MethodGet,
|
||||
path: PathV1SettingsAll,
|
||||
isJSON: true,
|
||||
}, {
|
||||
handler: svc.handlePatchSettingsDNS,
|
||||
method: http.MethodPatch,
|
||||
path: PathV1SettingsDNS,
|
||||
isJSON: true,
|
||||
}, {
|
||||
handler: svc.handlePatchSettingsHTTP,
|
||||
method: http.MethodPatch,
|
||||
path: PathV1SettingsHTTP,
|
||||
isJSON: true,
|
||||
}, {
|
||||
handler: svc.handleGetV1SystemInfo,
|
||||
method: http.MethodGet,
|
||||
@@ -173,41 +119,29 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
}}
|
||||
|
||||
for _, r := range routes {
|
||||
var h http.HandlerFunc
|
||||
if r.isJSON {
|
||||
mux.Handle(r.method, r.path, jsonMw(r.handler))
|
||||
// TODO(a.garipov): Consider using httptreemux's MiddlewareFunc.
|
||||
h = jsonMw(r.handler)
|
||||
} else {
|
||||
mux.Handle(r.method, r.path, r.handler)
|
||||
h = r.handler
|
||||
}
|
||||
|
||||
mux.Handle(r.method, r.path, h)
|
||||
}
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
||||
// must not be called simultaneously with Start. If svc was initialized with
|
||||
// ":0" addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||
// Addrs returns all addresses on which this server serves the HTTP API. Addrs
|
||||
// must not be called until Start returns.
|
||||
func (svc *Service) Addrs() (addrs []string) {
|
||||
addrs = make([]string, 0, len(svc.servers))
|
||||
for _, srv := range svc.servers {
|
||||
addrPort, err := netip.ParseAddrPort(srv.Addr)
|
||||
if err != nil {
|
||||
// Technically shouldn't happen, since all servers must have a valid
|
||||
// address.
|
||||
panic(fmt.Errorf("websvc: server %q: bad address: %w", srv.Addr, err))
|
||||
}
|
||||
|
||||
// srv.Serve will set TLSConfig to an almost empty value, so, instead of
|
||||
// relying only on the nilness of TLSConfig, check the length of the
|
||||
// certificates field as well.
|
||||
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
|
||||
addrs = append(addrs, addrPort)
|
||||
} else {
|
||||
secureAddrs = append(secureAddrs, addrPort)
|
||||
}
|
||||
|
||||
addrs = append(addrs, srv.Addr)
|
||||
}
|
||||
|
||||
return addrs, secureAddrs
|
||||
return addrs
|
||||
}
|
||||
|
||||
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
||||
@@ -215,6 +149,9 @@ func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request)
|
||||
_, _ = io.WriteString(w, "OK")
|
||||
}
|
||||
|
||||
// unit is a convenient alias for struct{}.
|
||||
type unit = struct{}
|
||||
|
||||
// type check
|
||||
var _ agh.Service = (*Service)(nil)
|
||||
|
||||
@@ -226,9 +163,11 @@ func (svc *Service) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
srvs := svc.servers
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(svc.servers))
|
||||
for _, srv := range svc.servers {
|
||||
wg.Add(len(srvs))
|
||||
for _, srv := range srvs {
|
||||
go serve(srv, wg)
|
||||
}
|
||||
|
||||
@@ -242,14 +181,11 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
||||
addr := srv.Addr
|
||||
defer log.OnPanic(addr)
|
||||
|
||||
var proto string
|
||||
var l net.Listener
|
||||
var err error
|
||||
if srv.TLSConfig == nil {
|
||||
proto = "http"
|
||||
l, err = net.Listen("tcp", addr)
|
||||
} else {
|
||||
proto = "https"
|
||||
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -260,12 +196,8 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
||||
// would mean that a random available port was automatically chosen.
|
||||
srv.Addr = l.Addr().String()
|
||||
|
||||
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
|
||||
|
||||
l = &waitListener{
|
||||
Listener: l,
|
||||
firstAcceptWG: wg,
|
||||
}
|
||||
log.Info("websvc: starting srv http://%s", srv.Addr)
|
||||
wg.Done()
|
||||
|
||||
err = srv.Serve(l)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
@@ -289,28 +221,8 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.List("shutting down", errs...)
|
||||
return errors.List("shutting down")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the web service. Config must not
|
||||
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||
// addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) Config() (c *Config) {
|
||||
c = &Config{
|
||||
ConfigManager: svc.confMgr,
|
||||
TLS: svc.tls,
|
||||
// Leave Addresses and SecureAddresses empty and get the actual
|
||||
// addresses that include the :0 ones later.
|
||||
Start: svc.start,
|
||||
Timeout: svc.timeout,
|
||||
ForceHTTPS: svc.forceHTTPS,
|
||||
}
|
||||
|
||||
c.Addresses, c.SecureAddresses = svc.addrs()
|
||||
|
||||
return c
|
||||
}
|
||||
93
internal/v1/websvc/websvc_test.go
Normal file
93
internal/v1/websvc/websvc_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
// 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) (svc *websvc.Service, addr string) {
|
||||
t.Helper()
|
||||
|
||||
c := &websvc.Config{
|
||||
TLS: nil,
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||
SecureAddresses: nil,
|
||||
Timeout: testTimeout,
|
||||
Start: testStart,
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
addrs := svc.Addrs()
|
||||
require.Len(t, addrs, 1)
|
||||
|
||||
return svc, addrs[0]
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||
_, addr := newTestServer(t)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr,
|
||||
Path: websvc.PathHealthCheck,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
}
|
||||
@@ -63,6 +63,14 @@ func Version() (v string) {
|
||||
return version
|
||||
}
|
||||
|
||||
// Constants defining the format of module information string.
|
||||
const (
|
||||
modInfoAtSep = "@"
|
||||
modInfoDevSep = " "
|
||||
modInfoSumLeft = " (sum: "
|
||||
modInfoSumRight = ")"
|
||||
)
|
||||
|
||||
// fmtModule returns formatted information about module. The result looks like:
|
||||
//
|
||||
// github.com/Username/module@v1.2.3 (sum: someHASHSUM=)
|
||||
@@ -79,16 +87,14 @@ func fmtModule(m *debug.Module) (formatted string) {
|
||||
|
||||
stringutil.WriteToBuilder(b, m.Path)
|
||||
if ver := m.Version; ver != "" {
|
||||
sep := "@"
|
||||
sep := modInfoAtSep
|
||||
if ver == "(devel)" {
|
||||
sep = " "
|
||||
sep = modInfoDevSep
|
||||
}
|
||||
|
||||
stringutil.WriteToBuilder(b, sep, ver)
|
||||
}
|
||||
|
||||
if sum := m.Sum; sum != "" {
|
||||
stringutil.WriteToBuilder(b, "(sum: ", sum, ")")
|
||||
stringutil.WriteToBuilder(b, modInfoSumLeft, sum, modInfoSumRight)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
|
||||
4
main.go
4
main.go
@@ -1,5 +1,5 @@
|
||||
//go:build !next
|
||||
// +build !next
|
||||
//go:build !v1
|
||||
// +build !v1
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//go:build next
|
||||
// +build next
|
||||
//go:build v1
|
||||
// +build v1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/cmd"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/cmd"
|
||||
)
|
||||
|
||||
// Embed the prebuilt client here since we strive to keep .go files inside the
|
||||
@@ -4,89 +4,6 @@
|
||||
|
||||
## v0.108.0: API changes
|
||||
|
||||
|
||||
|
||||
## v0.107.15: `POST` Requests Without Bodies
|
||||
|
||||
As an additional CSRF protection measure, AdGuard Home now ensures that requests
|
||||
that change its state but have no body do not have a `Content-Type` header set
|
||||
on them.
|
||||
|
||||
This concerns the following APIs:
|
||||
|
||||
* `POST /control/dhcp/reset_leases`;
|
||||
* `POST /control/dhcp/reset`;
|
||||
* `POST /control/parental/disable`;
|
||||
* `POST /control/parental/enable`;
|
||||
* `POST /control/querylog_clear`;
|
||||
* `POST /control/safebrowsing/disable`;
|
||||
* `POST /control/safebrowsing/enable`;
|
||||
* `POST /control/safesearch/disable`;
|
||||
* `POST /control/safesearch/enable`;
|
||||
* `POST /control/stats_reset`;
|
||||
* `POST /control/update`.
|
||||
|
||||
|
||||
|
||||
## v0.107.14: BREAKING API CHANGES
|
||||
|
||||
A Cross-Site Request Forgery (CSRF) vulnerability has been discovered. 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.
|
||||
|
||||
All JSON APIs that expect a body now check if the request actually has
|
||||
`Content-Type` set to `application/json`.
|
||||
|
||||
All new formats for the request and response bodies are documented in
|
||||
`openapi.yaml`.
|
||||
|
||||
### `POST /control/filtering/set_rules` And Other Plain-Text APIs
|
||||
|
||||
The following APIs, which previously accepted or returned `text/plain` data,
|
||||
now accept or return data as JSON.
|
||||
|
||||
#### `POST /control/filtering/set_rules`
|
||||
|
||||
Previously, the API accepted a raw list of filters as a plain-text file. Now,
|
||||
the filters must be presented in a JSON object with the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"rules":
|
||||
[
|
||||
"||example.com^",
|
||||
"# comment",
|
||||
"@@||www.example.com^"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /control/i18n/current_language` And `POST /control/i18n/change_language`
|
||||
|
||||
Previously, these APIs accepted and returned the language code in plain text.
|
||||
Now, they accept and return them in a JSON object with the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "en"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /control/dhcp/find_active_dhcp`
|
||||
|
||||
Previously, the API accepted the name of the network interface as a plain-text
|
||||
string. Now, it must be contained within a JSON object with the following
|
||||
format:
|
||||
|
||||
```json
|
||||
{
|
||||
"interface": "eth0"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## v0.107.12: API changes
|
||||
|
||||
### `GET /control/blocked_services/services`
|
||||
@@ -94,8 +11,6 @@ format:
|
||||
* The new `GET /control/blocked_services/services` HTTP API allows inspecting
|
||||
all available services.
|
||||
|
||||
|
||||
|
||||
## v0.107.7: API changes
|
||||
|
||||
### The new optional field `"ecs"` in `QueryLogItem`
|
||||
@@ -109,8 +24,6 @@ format:
|
||||
`POST /install/configure` which means that the specified password does not
|
||||
meet the strength requirements.
|
||||
|
||||
|
||||
|
||||
## v0.107.3: API changes
|
||||
|
||||
### The new field `"version"` in `AddressesInfo`
|
||||
@@ -118,8 +31,6 @@ format:
|
||||
* The new field `"version"` in `GET /install/get_addresses` is the version of
|
||||
the AdGuard Home instance.
|
||||
|
||||
|
||||
|
||||
## v0.107.0: API changes
|
||||
|
||||
### The new field `"cached"` in `QueryLogItem`
|
||||
|
||||
@@ -413,11 +413,6 @@
|
||||
- 'dhcp'
|
||||
'operationId': 'checkActiveDhcp'
|
||||
'summary': 'Searches for an active DHCP server on the network'
|
||||
'requestBody':
|
||||
'content':
|
||||
'application/json':
|
||||
'schema':
|
||||
'$ref': '#/components/schemas/DhcpFindActiveReq'
|
||||
'responses':
|
||||
'200':
|
||||
'description': 'OK.'
|
||||
@@ -601,10 +596,11 @@
|
||||
'summary': 'Set user-defined filter rules'
|
||||
'requestBody':
|
||||
'content':
|
||||
'application/json':
|
||||
'text/plain':
|
||||
'schema':
|
||||
'$ref': '#/components/schemas/SetRulesRequest'
|
||||
'description': 'Custom filtering rules.'
|
||||
'type': 'string'
|
||||
'example': '@@||yandex.ru^|'
|
||||
'description': 'All filtering rules, one line per rule'
|
||||
'responses':
|
||||
'200':
|
||||
'description': 'OK.'
|
||||
@@ -671,6 +667,24 @@
|
||||
- 'parental'
|
||||
'operationId': 'parentalEnable'
|
||||
'summary': 'Enable parental filtering'
|
||||
'requestBody':
|
||||
'content':
|
||||
'text/plain':
|
||||
'schema':
|
||||
'type': 'string'
|
||||
'enum':
|
||||
- 'EARLY_CHILDHOOD'
|
||||
- 'YOUNG'
|
||||
- 'TEEN'
|
||||
- 'MATURE'
|
||||
'example': 'sensitivity=TEEN'
|
||||
'description': |
|
||||
Age sensitivity for parental filtering,
|
||||
EARLY_CHILDHOOD is 3
|
||||
YOUNG is 10
|
||||
TEEN is 13
|
||||
MATURE is 17
|
||||
'required': true
|
||||
'responses':
|
||||
'200':
|
||||
'description': 'OK.'
|
||||
@@ -944,9 +958,10 @@
|
||||
Change current language. Argument must be an ISO 639-1 two-letter code.
|
||||
'requestBody':
|
||||
'content':
|
||||
'application/json':
|
||||
'text/plain':
|
||||
'schema':
|
||||
'$ref': '#/components/schemas/LanguageSettings'
|
||||
'type': 'string'
|
||||
'example': 'en'
|
||||
'description': >
|
||||
New language. It must be known to the server and must be an ISO 639-1
|
||||
two-letter code.
|
||||
@@ -965,9 +980,10 @@
|
||||
'200':
|
||||
'description': 'OK.'
|
||||
'content':
|
||||
'application/json':
|
||||
'schema':
|
||||
'$ref': '#/components/schemas/LanguageSettings'
|
||||
'text/plain':
|
||||
'examples':
|
||||
'response':
|
||||
'value': 'en'
|
||||
'/install/get_addresses_beta':
|
||||
'get':
|
||||
'tags':
|
||||
@@ -1537,19 +1553,6 @@
|
||||
'properties':
|
||||
'updated':
|
||||
'type': 'integer'
|
||||
'SetRulesRequest':
|
||||
'description': 'Custom filtering rules setting request.'
|
||||
'example':
|
||||
'rules':
|
||||
- '||example.com^'
|
||||
- '# comment'
|
||||
- '@@||www.example.com^'
|
||||
'properties':
|
||||
'rules':
|
||||
'items':
|
||||
'type': 'string'
|
||||
'type': 'array'
|
||||
'type': 'object'
|
||||
'GetVersionRequest':
|
||||
'type': 'object'
|
||||
'description': '/version.json request data'
|
||||
@@ -1774,16 +1777,6 @@
|
||||
'additionalProperties':
|
||||
'$ref': '#/components/schemas/NetInterface'
|
||||
|
||||
'DhcpFindActiveReq':
|
||||
'description': >
|
||||
Request for checking for other DHCP servers in the network.
|
||||
'properties':
|
||||
'interface':
|
||||
'description': 'The name of the network interface'
|
||||
'example': 'eth0'
|
||||
'type': 'string'
|
||||
'type': 'object'
|
||||
|
||||
'DhcpSearchResult':
|
||||
'type': 'object'
|
||||
'description': >
|
||||
@@ -2699,15 +2692,6 @@
|
||||
'description': 'The error message, an opaque string.'
|
||||
'type': 'string'
|
||||
'type': 'object'
|
||||
'LanguageSettings':
|
||||
'description': 'Language settings object.'
|
||||
'properties':
|
||||
'language':
|
||||
'description': 'The current language or the language to set.'
|
||||
'type': 'string'
|
||||
'required':
|
||||
- 'language'
|
||||
'type': 'object'
|
||||
'securitySchemes':
|
||||
'basicAuth':
|
||||
'type': 'http'
|
||||
|
||||
@@ -2289,7 +2289,7 @@
|
||||
'upstream_servers':
|
||||
- '1.1.1.1'
|
||||
- '8.8.8.8'
|
||||
'upstream_timeout': 1000
|
||||
'upstream_timeout': '1s'
|
||||
'required':
|
||||
- 'addresses'
|
||||
- 'blocking_mode'
|
||||
@@ -2397,9 +2397,8 @@
|
||||
'type': 'array'
|
||||
'upstream_timeout':
|
||||
'description': >
|
||||
Upstream request timeout, in milliseconds.
|
||||
'format': 'double'
|
||||
'type': 'number'
|
||||
Upstream request timeout, as a human readable duration.
|
||||
'type': 'string'
|
||||
'type': 'object'
|
||||
|
||||
'DnsType':
|
||||
@@ -3506,16 +3505,14 @@
|
||||
'addresses':
|
||||
- '127.0.0.1:80'
|
||||
- '192.168.1.1:80'
|
||||
'force_https': true
|
||||
'secure_addresses':
|
||||
- '127.0.0.1:443'
|
||||
- '192.168.1.1:443'
|
||||
'timeout': 10000
|
||||
'force_https': true
|
||||
'required':
|
||||
- 'addresses'
|
||||
- 'force_https'
|
||||
- 'secure_addresses'
|
||||
- 'timeout'
|
||||
- 'force_https'
|
||||
|
||||
'HttpSettingsPatch':
|
||||
'description': >
|
||||
@@ -3542,11 +3539,6 @@
|
||||
'items':
|
||||
'type': 'string'
|
||||
'type': 'array'
|
||||
'timeout':
|
||||
'description': >
|
||||
HTTP request timeout, in milliseconds.
|
||||
'format': 'double'
|
||||
'type': 'number'
|
||||
'type': 'object'
|
||||
|
||||
'InternalServerErrorResp':
|
||||
|
||||
@@ -136,11 +136,11 @@ underscores() {
|
||||
-e '_freebsd.go'\
|
||||
-e '_linux.go'\
|
||||
-e '_little.go'\
|
||||
-e '_next.go'\
|
||||
-e '_openbsd.go'\
|
||||
-e '_others.go'\
|
||||
-e '_test.go'\
|
||||
-e '_unix.go'\
|
||||
-e '_v1.go'\
|
||||
-e '_windows.go' \
|
||||
-v\
|
||||
| sed -e 's/./\t\0/'
|
||||
@@ -229,7 +229,7 @@ gocyclo --over 13 ./internal/filtering/
|
||||
# Apply stricter standards to new or somewhat refactored code.
|
||||
gocyclo --over 10 ./internal/aghio/ ./internal/aghnet/ ./internal/aghos/\
|
||||
./internal/aghtest/ ./internal/dnsforward/ ./internal/stats/\
|
||||
./internal/tools/ ./internal/updater/ ./internal/next/ ./internal/version/\
|
||||
./internal/tools/ ./internal/updater/ ./internal/v1/ ./internal/version/\
|
||||
./main.go
|
||||
|
||||
ineffassign ./...
|
||||
|
||||
Reference in New Issue
Block a user