Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c69639c013 | ||
|
|
5f6fbe8e08 | ||
|
|
b40bbf0260 | ||
|
|
a11c8e91ab |
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,9 +7,9 @@
|
||||
'name': 'AdGuard filters issues'
|
||||
'url': 'https://link.adtidy.org/forward.html?action=report&app=home&from=github'
|
||||
- 'about': >
|
||||
Please send requests for addition to the vetted filtering lists to the
|
||||
Hostlists Registry repository.
|
||||
'name': 'AdGuard Hostlists Registry'
|
||||
Please send requests for new blocked services and vetted filtering lists
|
||||
to the Hostlists Registry repository
|
||||
'name': 'Blocked services and vetted filtering rule lists: AdGuard Hostlists Registry'
|
||||
'url': 'https://github.com/AdguardTeam/HostlistsRegistry'
|
||||
- 'about': >
|
||||
Please use GitHub Discussions for questions
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.18.8'
|
||||
'GO_VERSION': '1.18.9'
|
||||
'NODE_VERSION': '14'
|
||||
|
||||
'on':
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.18.8'
|
||||
'GO_VERSION': '1.18.9'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -13,17 +13,77 @@ and this project adheres to
|
||||
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.23] - 2023-02-15 (APPROX.)
|
||||
|
||||
See also the [v0.107.23 GitHub milestone][ms-v0.107.23].
|
||||
|
||||
[ms-v0.107.23]: https://github.com/AdguardTeam/AdGuardHome/milestone/59?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
## [v0.107.21] - 2122-12-28 (APPROX.)
|
||||
## [v0.107.22] - 2023-01-19
|
||||
|
||||
See also the [v0.107.22 GitHub milestone][ms-v0.107.22].
|
||||
|
||||
### Added
|
||||
|
||||
- Experimental Dark UI theme ([#613]).
|
||||
- The new HTTP API `PUT /control/profile/update`, that updates current user
|
||||
language and UI theme. The format of request body is described in
|
||||
`openapi/openapi.yaml`.
|
||||
|
||||
### Changed
|
||||
|
||||
- The HTTP API `GET /control/profile` now returns enhanced object with
|
||||
current user's name, language, and UI theme. The format of response body is
|
||||
described in `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `AdGuardHome --update` freezing when another instance of AdGuard Home is
|
||||
running ([#4223], [#5191]).
|
||||
- The `--update` flag performing an update even when there is no version change.
|
||||
- Failing HTTPS redirection on saving the encryption settings ([#4898]).
|
||||
- Zeroing rules counter of erroneusly edited filtering rule lists ([#5290]).
|
||||
- Filters updating strategy, which could sometimes lead to use of broken or
|
||||
incompletely downloaded lists ([#5258]).
|
||||
|
||||
[#613]: https://github.com/AdguardTeam/AdGuardHome/issues/613
|
||||
[#5191]: https://github.com/AdguardTeam/AdGuardHome/issues/5191
|
||||
[#5290]: https://github.com/AdguardTeam/AdGuardHome/issues/5290
|
||||
[#5258]: https://github.com/AdguardTeam/AdGuardHome/issues/5258
|
||||
|
||||
[ms-v0.107.22]: https://github.com/AdguardTeam/AdGuardHome/milestone/58?closed=1
|
||||
|
||||
|
||||
|
||||
|
||||
## [v0.107.21] - 2022-12-15
|
||||
|
||||
See also the [v0.107.21 GitHub milestone][ms-v0.107.21].
|
||||
|
||||
### Changed
|
||||
|
||||
- The URLs of the default filters for new installations are synchronized to
|
||||
those introduced in v0.107.20 ([#5238]).
|
||||
|
||||
**NOTE:** Some users may need to re-add the lists from the vetted filter lists
|
||||
to update the URLs to the new ones. Custom filters added by users themselves
|
||||
do not require re-adding.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Errors popping up during updates of settings, which could sometimes cause the
|
||||
server to stop responding ([#5251]).
|
||||
|
||||
[#5238]: https://github.com/AdguardTeam/AdGuardHome/issues/5238
|
||||
[#5251]: https://github.com/AdguardTeam/AdGuardHome/issues/5251
|
||||
|
||||
[ms-v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/milestone/57?closed=1
|
||||
-->
|
||||
|
||||
|
||||
|
||||
@@ -1482,11 +1542,13 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...HEAD
|
||||
[v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.23...HEAD
|
||||
[v0.107.23]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.22...v0.107.23
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.22...HEAD
|
||||
[v0.107.22]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...v0.107.22
|
||||
[v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21
|
||||
[v0.107.20]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.19...v0.107.20
|
||||
[v0.107.19]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.18...v0.107.19
|
||||
[v0.107.18]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...v0.107.18
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
CHANNEL=${bamboo.channel}\
|
||||
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
||||
FRONTEND_PREBUILT=1\
|
||||
VERBOSE=1\
|
||||
PARALLELISM=1\
|
||||
VERBOSE=2\
|
||||
build-release
|
||||
# TODO(a.garipov): Use more fine-grained artifact rules.
|
||||
'artifacts':
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Налады DHCP IPv6",
|
||||
"form_error_required": "Абавязковае поле",
|
||||
"form_error_ip4_format": "Няслушны IPv4-адрас",
|
||||
"form_error_ip4_range_start_format": "Няслушны IPv4-адрас пачатку дыяпазону",
|
||||
"form_error_ip4_range_end_format": "Няслушны IPv4-адрас канца дыяпазону",
|
||||
"form_error_ip4_gateway_format": "Няслушны IPv4-адрас шлюза",
|
||||
"form_error_ip6_format": "Няслушны IPv6-адрас",
|
||||
"form_error_ip_format": "Няслушны IP-адрас",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Павінна быць па-за дыяпазонам «{{start}}»-«{{end}}»",
|
||||
"lower_range_start_error": "Павінна быць менш за пачатак дыяпазону",
|
||||
"greater_range_start_error": "Павінна быць больш за пачатак дыяпазону",
|
||||
"greater_range_end_error": "Павінна быць больш за канец дыяпазону",
|
||||
"subnet_error": "Адрасы павінны быць усярэдзіне адной падсеткі",
|
||||
"gateway_or_subnet_invalid": "Некарэктная маска падсеткі",
|
||||
"dhcp_form_gateway_input": "IP-адрас шлюза",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Nastavení DHCP IPv6",
|
||||
"form_error_required": "Povinné pole",
|
||||
"form_error_ip4_format": "Neplatná adresa IPv4",
|
||||
"form_error_ip4_range_start_format": "Neplatná adresa IPv4 na začátku rozsahu",
|
||||
"form_error_ip4_range_end_format": "Neplatná adresa IPv4 na konci rozsahu",
|
||||
"form_error_ip4_gateway_format": "Neplatná adresa IPv4 brány",
|
||||
"form_error_ip6_format": "Neplatná adresa IPv6",
|
||||
"form_error_ip_format": "Neplatná IP adresa",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Musí být mimo rozsah \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Musí být menší než začátek rozsahu",
|
||||
"greater_range_start_error": "Musí být větší než začátek rozsahu",
|
||||
"greater_range_end_error": "Musí být větší než konec rozsahu",
|
||||
"subnet_error": "Adresy musí být v jedné podsíti",
|
||||
"gateway_or_subnet_invalid": "Neplatná maska podsítě",
|
||||
"dhcp_form_gateway_input": "IP brána",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6-indstillinger",
|
||||
"form_error_required": "Obligatorisk felt",
|
||||
"form_error_ip4_format": "Ugyldig IPv4-adresse",
|
||||
"form_error_ip4_range_start_format": "Ugyldig IPv4-startadresse for området",
|
||||
"form_error_ip4_range_end_format": "Ugyldig IPv4-slutadresse for området",
|
||||
"form_error_ip4_gateway_format": "Ugyldig IPv4 gateway-adresse",
|
||||
"form_error_ip6_format": "Ugyldig IPv6-adresse",
|
||||
"form_error_ip_format": "Ugyldig IP-adresse",
|
||||
@@ -51,9 +49,8 @@
|
||||
"out_of_range_error": "Skal være uden for området \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Skal være mindre end starten på området",
|
||||
"greater_range_start_error": "Skal være større end starten på området",
|
||||
"greater_range_end_error": "Skal være større end områdeslutning",
|
||||
"subnet_error": "Adresser ska være i ét undernet",
|
||||
"gateway_or_subnet_invalid": "Undernetmaske ugyldig",
|
||||
"gateway_or_subnet_invalid": "Ugyldig undernetmaske",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Undernetmaske",
|
||||
"dhcp_form_range_title": "Interval af IP-adresser",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen",
|
||||
"form_error_required": "Pflichtfeld",
|
||||
"form_error_ip4_format": "Ungültige IPv4-Adresse",
|
||||
"form_error_ip4_range_start_format": "Ungültiger Bereichsbeginn der IPv4-Adresse",
|
||||
"form_error_ip4_range_end_format": "Ungültiges Bereichsende der IPv4-Adresse",
|
||||
"form_error_ip4_gateway_format": "Ungültige IPv4-Adresse des Gateways",
|
||||
"form_error_ip6_format": "Ungültige IPv6-Adresse",
|
||||
"form_error_ip_format": "Ungültige IP-Adresse",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Muss außerhalb des Bereichs „{{start}}“-„{{end}}“ liegen",
|
||||
"lower_range_start_error": "Muss niedriger als der Bereichsbeginn sein",
|
||||
"greater_range_start_error": "Muss größer als der Bereichsbeginn sein",
|
||||
"greater_range_end_error": "Muss größer als das Bereichsende sein",
|
||||
"subnet_error": "Die Adressen müssen innerhalb eines Subnetzes liegen",
|
||||
"gateway_or_subnet_invalid": "Ungültige Subnetzmaske",
|
||||
"dhcp_form_gateway_input": "Gateway-IP",
|
||||
|
||||
@@ -298,6 +298,9 @@
|
||||
"blocking_mode_nxdomain": "NXDOMAIN: Respond with NXDOMAIN code",
|
||||
"blocking_mode_null_ip": "Null IP: Respond with zero IP address (0.0.0.0 for A; :: for AAAA)",
|
||||
"blocking_mode_custom_ip": "Custom IP: Respond with a manually set IP address",
|
||||
"theme_auto": "Auto",
|
||||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"upstream_dns_client_desc": "If you keep this field empty, AdGuard Home will use the servers configured in the <0>DNS settings</0>.",
|
||||
"tracker_source": "Tracker source",
|
||||
"source_label": "Source",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Configuración DHCP IPv6",
|
||||
"form_error_required": "Campo obligatorio",
|
||||
"form_error_ip4_format": "Dirección IPv4 no válida",
|
||||
"form_error_ip4_range_start_format": "Dirección IPv4 no válida del inicio de rango",
|
||||
"form_error_ip4_range_end_format": "Dirección IPv4 no válida del final de rango",
|
||||
"form_error_ip4_gateway_format": "Dirección IPv4 no válida de la puerta de enlace",
|
||||
"form_error_ip6_format": "Dirección IPv6 no válida",
|
||||
"form_error_ip_format": "Dirección IP no válida",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Debe estar fuera del rango \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Debe ser inferior que el inicio de rango",
|
||||
"greater_range_start_error": "Debe ser mayor que el inicio de rango",
|
||||
"greater_range_end_error": "Debe ser mayor que el final de rango",
|
||||
"subnet_error": "Las direcciones deben estar en una subred",
|
||||
"gateway_or_subnet_invalid": "Máscara de subred no válida",
|
||||
"dhcp_form_gateway_input": "IP de puerta de enlace",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP:n IPv6-asetukset",
|
||||
"form_error_required": "Pakollinen kenttä",
|
||||
"form_error_ip4_format": "Virheellinen IPv4-osoite",
|
||||
"form_error_ip4_range_start_format": "Virheellinen IPv4-osoitealueen aloitusosoite",
|
||||
"form_error_ip4_range_end_format": "Virheellinen IPv4-osoitealueen päätösosoite",
|
||||
"form_error_ip4_gateway_format": "Virheellinen yhdyskäytävän IPv4-osoite",
|
||||
"form_error_ip6_format": "Virheellinen IPv6-osoite",
|
||||
"form_error_ip_format": "Virheellinen IP-osoite",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Oltava alueen \"{{start}}\" - \"{{end}}\" ulkopuolella",
|
||||
"lower_range_start_error": "Oltava alueen aloitusarvoa pienempi",
|
||||
"greater_range_start_error": "Oltava alueen aloitusarvoa suurempi",
|
||||
"greater_range_end_error": "Oltava alueen päätösarvoa pienempi",
|
||||
"subnet_error": "Osoitteiden tulee olla yhdessä aliverkossa",
|
||||
"gateway_or_subnet_invalid": "Virheellinen aliverkon peite",
|
||||
"dhcp_form_gateway_input": "Yhdyskäytävän IP-osoite",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Paramètres IPv6 du DHCP",
|
||||
"form_error_required": "Champ requis",
|
||||
"form_error_ip4_format": "Adresse IPv4 invalide",
|
||||
"form_error_ip4_range_start_format": "Adresse de début de plage IPv4 incorrecte",
|
||||
"form_error_ip4_range_end_format": "Adresse de fin de plage IPv4 incorrecte",
|
||||
"form_error_ip4_gateway_format": "Adresse de passerelle IPv4 invalide",
|
||||
"form_error_ip6_format": "Adresse IPv6 invalide",
|
||||
"form_error_ip_format": "Adresse IP invalide",
|
||||
@@ -51,9 +49,8 @@
|
||||
"out_of_range_error": "Doit être hors plage « {{start}} » - « {{end}} »",
|
||||
"lower_range_start_error": "Doit être inférieur au début de plage",
|
||||
"greater_range_start_error": "Doit être supérieur au début de plage",
|
||||
"greater_range_end_error": "Doit être supérieur à la fin de plage",
|
||||
"subnet_error": "Les adresses doivent être dans le même sous-réseau",
|
||||
"gateway_or_subnet_invalid": "Masque de sous-réseau invalide",
|
||||
"gateway_or_subnet_invalid": "Masque de sous-réseau invalide.",
|
||||
"dhcp_form_gateway_input": "IP de la passerelle",
|
||||
"dhcp_form_subnet_input": "Masque de sous-réseau",
|
||||
"dhcp_form_range_title": "Rangée des adresses IP",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
|
||||
"form_error_required": "Obavezno polje",
|
||||
"form_error_ip4_format": "Nevažeća IPv4 adresa",
|
||||
"form_error_ip4_range_start_format": "Nepravilan početak ranga IPv4 adresa",
|
||||
"form_error_ip4_range_end_format": "Nepravilan kraj ranga IPv4 adresa",
|
||||
"form_error_ip4_gateway_format": "Nepravilna IPV4 adresa čvora",
|
||||
"form_error_ip6_format": "Nevažeći IPv6 adresa",
|
||||
"form_error_ip_format": "Nepravilna IP adresa",
|
||||
@@ -51,9 +49,8 @@
|
||||
"out_of_range_error": "Mora biti izvan ranga \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Mora biti niže od početnog ranga",
|
||||
"greater_range_start_error": "Mora biti veće od krajnjeg ranga",
|
||||
"greater_range_end_error": "Mora biti veće od krajnjeg ranga",
|
||||
"subnet_error": "Adrese moraju biti iz iste podmreže",
|
||||
"gateway_or_subnet_invalid": "Maska podmreže je neprvilna",
|
||||
"gateway_or_subnet_invalid": "Nevažeća podmrežna maska",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Subnet maskiranje",
|
||||
"dhcp_form_range_title": "Raspon IP adresa",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 Beállítások",
|
||||
"form_error_required": "Kötelező mező",
|
||||
"form_error_ip4_format": "Érvénytelen IPv4 cím",
|
||||
"form_error_ip4_range_start_format": "Érvénytelen IPv4-cím a tartomány kezdetéhez",
|
||||
"form_error_ip4_range_end_format": "Érvénytelen IPv4-cím a tartomány végén",
|
||||
"form_error_ip4_gateway_format": "Az átjáróhoz (gateway) érvénytelen IPv4 cím lett megadva",
|
||||
"form_error_ip6_format": "Érvénytelen IPv6 cím",
|
||||
"form_error_ip_format": "Érvénytelen IP-cím",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "A következő tartományon kívül legyen: \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Kisebb legyen, mint a tartomány kezdete",
|
||||
"greater_range_start_error": "Nagyobbnak kell lennie, mint a tartomány kezdete",
|
||||
"greater_range_end_error": "Nagyobb legyen, mint a tartomány vége",
|
||||
"subnet_error": "A címeknek egy alhálózatban kell lenniük",
|
||||
"gateway_or_subnet_invalid": "Az alhálózati maszk érvénytelen",
|
||||
"dhcp_form_gateway_input": "Átjáró IP",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Pengaturan DHCP IPv6",
|
||||
"form_error_required": "Kolom yang harus diisi",
|
||||
"form_error_ip4_format": "Alamat IPv4 tidak valid",
|
||||
"form_error_ip4_range_start_format": "Alamat IPv4 tidak valid dari rentang awal",
|
||||
"form_error_ip4_range_end_format": "Alamat IPv4 tidak valid dari rentang akhir",
|
||||
"form_error_ip4_gateway_format": "Alamat IPv4 gateway tidak valid",
|
||||
"form_error_ip6_format": "Alamat IPv6 tidak valid",
|
||||
"form_error_ip_format": "Alamat IP tidak valid",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Harus di luar rentang \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Harus lebih rendah dari rentang awal",
|
||||
"greater_range_start_error": "Harus lebih besar dari rentang awal",
|
||||
"greater_range_end_error": "Harus lebih besar dari rentang akhir",
|
||||
"subnet_error": "Alamat harus dalam satu subnet",
|
||||
"gateway_or_subnet_invalid": "Subnet mask tidak valid",
|
||||
"dhcp_form_gateway_input": "IP gateway",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Impostazioni DHCP IPv6",
|
||||
"form_error_required": "Campo richiesto",
|
||||
"form_error_ip4_format": "Indirizzo IPv4 non valido",
|
||||
"form_error_ip4_range_start_format": "Indirizzo IPV4 non valido dell'intervallo iniziale",
|
||||
"form_error_ip4_range_end_format": "Indirizzo IPV4 non valido dell'intervallo finale",
|
||||
"form_error_ip4_gateway_format": "Indirizzo gateway IPv4 non valido",
|
||||
"form_error_ip6_format": "Indirizzo IPv6 non valido",
|
||||
"form_error_ip_format": "Indirizzo IP non valido",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Deve essere fuori intervallo \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Deve essere inferiore dell'intervallo di inizio",
|
||||
"greater_range_start_error": "Deve essere maggiore dell'intervallo di inizio",
|
||||
"greater_range_end_error": "Deve essere maggiore dell'intervallo di fine",
|
||||
"subnet_error": "Gli indirizzi devono trovarsi in una sottorete",
|
||||
"gateway_or_subnet_invalid": "Maschera di sottorete non valida",
|
||||
"dhcp_form_gateway_input": "IP Gateway",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 設定",
|
||||
"form_error_required": "必須項目です",
|
||||
"form_error_ip4_format": "IPv4アドレスが無効です",
|
||||
"form_error_ip4_range_start_format": "範囲開始のIPv4アドレスが無効です",
|
||||
"form_error_ip4_range_end_format": "範囲終了のIPv4アドレスが無効です",
|
||||
"form_error_ip4_gateway_format": "ゲートウェイのIPv4アドレスが無効です",
|
||||
"form_error_ip6_format": "IPv6アドレスが無効です",
|
||||
"form_error_ip_format": "IPアドレスが無効です",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "\"{{start}}\"〜\"{{end}}\" の範囲外である必要があります",
|
||||
"lower_range_start_error": "範囲開始よりも低い値である必要があります",
|
||||
"greater_range_start_error": "範囲開始値より大きい値でなければなりません",
|
||||
"greater_range_end_error": "範囲終了値より大きい値でなければなりません",
|
||||
"subnet_error": "両アドレスが同じサブネット内にある必要があります",
|
||||
"gateway_or_subnet_invalid": "サブネットマスクが無効です",
|
||||
"dhcp_form_gateway_input": "ゲートウェイIP",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 설정",
|
||||
"form_error_required": "필수 영역",
|
||||
"form_error_ip4_format": "잘못된 IPv4 형식",
|
||||
"form_error_ip4_range_start_format": "잘못된 범위 시작 IPv4 형식",
|
||||
"form_error_ip4_range_end_format": "잘못된 범위 종료 IPv4 형식",
|
||||
"form_error_ip4_gateway_format": "잘못된 게이트웨이 IPv4 형식",
|
||||
"form_error_ip6_format": "잘못된 IPv6 주소",
|
||||
"form_error_ip_format": "잘못된 IP 주소",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "'{{start}}'-'{{end}}' 범위 밖이어야 합니다",
|
||||
"lower_range_start_error": "범위 시작보다 작은 값이어야 합니다",
|
||||
"greater_range_start_error": "범위 시작보다 큰 값이어야 합니다",
|
||||
"greater_range_end_error": "범위 종료보다 큰 값이어야 합니다",
|
||||
"subnet_error": "주소는 하나의 서브넷에 있어야 합니다",
|
||||
"gateway_or_subnet_invalid": "잘못된 서브넷 마스크",
|
||||
"dhcp_form_gateway_input": "게이트웨이 IP",
|
||||
@@ -223,7 +220,7 @@
|
||||
"example_upstream_tcp_hostname": "일반 DNS (TCP를 통한, 호스트명);",
|
||||
"all_lists_up_to_date_toast": "모든 리스트가 이미 최신입니다",
|
||||
"updated_upstream_dns_toast": "업스트림 서버가 성공적으로 저장되었습니다",
|
||||
"dns_test_ok_toast": "특정 DNS 서버들은 정상적으로 동작 중입니다",
|
||||
"dns_test_ok_toast": "지정된 DNS 서버가 올바르게 작동하고 있습니다.",
|
||||
"dns_test_not_ok_toast": "서버 '{{key}}': 사용할 수 없습니다, 제대로 작성했는지 확인하세요",
|
||||
"dns_test_warning_toast": "업스트림 '{{key}}'이(가) 테스트 요청에 응답하지 않으며 제대로 작동하지 않을 수 있습니다",
|
||||
"unblock": "차단 해제",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 instellingen",
|
||||
"form_error_required": "Vereist veld",
|
||||
"form_error_ip4_format": "Ongeldig IPv4-adres",
|
||||
"form_error_ip4_range_start_format": "Ongeldig IPv4-adres start bereik",
|
||||
"form_error_ip4_range_end_format": "Ongeldig IPv4-adres einde bereik",
|
||||
"form_error_ip4_gateway_format": "Ongeldig IPv4-adres van de gateway",
|
||||
"form_error_ip6_format": "Ongeldig IPv6-adres",
|
||||
"form_error_ip_format": "Ongeldig IP-adres",
|
||||
@@ -51,9 +49,8 @@
|
||||
"out_of_range_error": "Moet buiten bereik zijn \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Moet lager zijn dan begin reeks",
|
||||
"greater_range_start_error": "Moet groter zijn dan begin reeks",
|
||||
"greater_range_end_error": "Moet groter zijn dan einde reeks",
|
||||
"subnet_error": "Adressen moeten in één subnet vallen",
|
||||
"gateway_or_subnet_invalid": "Subnetmasker ongeldig",
|
||||
"gateway_or_subnet_invalid": "Ongeldig subnetmasker",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Subnet mask",
|
||||
"dhcp_form_range_title": "Bereik van IP adressen",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Ustawienia serwera DHCP IPv6",
|
||||
"form_error_required": "Pole wymagane",
|
||||
"form_error_ip4_format": "Nieprawidłowy adres IPv4",
|
||||
"form_error_ip4_range_start_format": "Nieprawidłowy adres IPv4 początku zakresu",
|
||||
"form_error_ip4_range_end_format": "Nieprawidłowy adres IPv4 końca zakresu",
|
||||
"form_error_ip4_gateway_format": "Nieprawidłowy adres IPv4 bramy",
|
||||
"form_error_ip6_format": "Nieprawidłowy adres IPv6",
|
||||
"form_error_ip_format": "Nieprawidłowy adres IP",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Musi być spoza zakresu \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Musi być niższy niż początek zakresu",
|
||||
"greater_range_start_error": "Musi być większy niż początek zakresu",
|
||||
"greater_range_end_error": "Musi być większy niż koniec zakresu",
|
||||
"subnet_error": "Adresy muszą należeć do jednej podsieci",
|
||||
"gateway_or_subnet_invalid": "Nieprawidłowa maska podsieci",
|
||||
"dhcp_form_gateway_input": "Adres IP bramy",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Configurações DHCP IPv6",
|
||||
"form_error_required": "Campo obrigatório",
|
||||
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
||||
"form_error_ip4_range_start_format": "Endereço IPv4 de início de intervalo inválido",
|
||||
"form_error_ip4_range_end_format": "Endereço IPv4 de fim de intervalo inválido.",
|
||||
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
|
||||
"form_error_ip6_format": "Endereço de IPv6 inválido",
|
||||
"form_error_ip_format": "Endereço de IP inválido",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Deve ser inferior ao início do intervalo",
|
||||
"greater_range_start_error": "Deve ser maior que o início do intervalo",
|
||||
"greater_range_end_error": "Deve ser maior que o fim do intervalo",
|
||||
"subnet_error": "Endereços devem estar em uma sub-rede",
|
||||
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
|
||||
"dhcp_form_gateway_input": "IP do gateway",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Definições DHCP IPv6",
|
||||
"form_error_required": "Campo obrigatório",
|
||||
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
||||
"form_error_ip4_range_start_format": "Endereço IPv4 de início de intervalo inválido",
|
||||
"form_error_ip4_range_end_format": "Endereço IPv4 de fim de intervalo inválido",
|
||||
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
|
||||
"form_error_ip6_format": "Endereço de IPv6 inválido",
|
||||
"form_error_ip_format": "Endereço de email inválido",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Deve ser inferior ao início do intervalo",
|
||||
"greater_range_start_error": "Deve ser maior que o início do intervalo",
|
||||
"greater_range_end_error": "Deve ser maior que o fim do intervalo",
|
||||
"subnet_error": "Os endereços devem estar em uma sub-rede",
|
||||
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
|
||||
"dhcp_form_gateway_input": "IP do gateway",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Setări DHCP IPv6",
|
||||
"form_error_required": "Câmp obligatoriu",
|
||||
"form_error_ip4_format": "Adresă IPv4 nevalidă",
|
||||
"form_error_ip4_range_start_format": "Adresă IPv4 nevalidă pentru începutul intervalului",
|
||||
"form_error_ip4_range_end_format": "Adresă IPv4 nevalidă a sfârșitului intervalului",
|
||||
"form_error_ip4_gateway_format": "Adresă IPv4 nevalidă a gateway-ului",
|
||||
"form_error_ip6_format": "Adresa IPv6 nevalidă",
|
||||
"form_error_ip_format": "Adresă IP nevalidă",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Trebuie să fie în afara intervalului „{{start}}”-„{{end}}”",
|
||||
"lower_range_start_error": "Trebuie să fie mai mică decât începutul intervalului",
|
||||
"greater_range_start_error": "Trebuie să fie mai mare decât începutul intervalului",
|
||||
"greater_range_end_error": "Trebuie să fie mai mare decât sfârșitul intervalului",
|
||||
"subnet_error": "Adresele trebuie să fie în aceeași subrețea",
|
||||
"gateway_or_subnet_invalid": "Mască de subrețea nevalidă",
|
||||
"dhcp_form_gateway_input": "IP Gateway",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Настройки DHCP IPv6",
|
||||
"form_error_required": "Обязательное поле",
|
||||
"form_error_ip4_format": "Некорректный IPv4-адрес",
|
||||
"form_error_ip4_range_start_format": "Некорректный IPv4-адрес начала диапазона",
|
||||
"form_error_ip4_range_end_format": "Некорректный IPv4-адрес конца диапазона",
|
||||
"form_error_ip4_gateway_format": "Некорректный IPv4-адрес шлюза",
|
||||
"form_error_ip6_format": "Некорректный IPv6-адрес",
|
||||
"form_error_ip_format": "Некорректный IP-адрес",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Должно быть вне диапазона «{{start}}»-«{{end}}»",
|
||||
"lower_range_start_error": "Должно быть меньше начала диапазона",
|
||||
"greater_range_start_error": "Должно быть больше начала диапазона",
|
||||
"greater_range_end_error": "Должно быть больше конца диапазона",
|
||||
"subnet_error": "Адреса должны быть внутри одной подсети",
|
||||
"gateway_or_subnet_invalid": "Некорректная маска подсети",
|
||||
"dhcp_form_gateway_input": "IP-адрес шлюза",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Nastavenia DHCP IPv6",
|
||||
"form_error_required": "Povinná položka.",
|
||||
"form_error_ip4_format": "Neplatná IPv4 adresa",
|
||||
"form_error_ip4_range_start_format": "Neplatný začiatok rozsahu IPv4 formátu",
|
||||
"form_error_ip4_range_end_format": "Neplatný koniec rozsahu IPv4 formátu",
|
||||
"form_error_ip4_gateway_format": "Neplatná IPv4 adresa brány",
|
||||
"form_error_ip6_format": "Neplatná IPv6 adresa",
|
||||
"form_error_ip_format": "Neplatná IP adresa",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Musí byť mimo rozsahu \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Musí byť nižšie ako začiatok rozsahu",
|
||||
"greater_range_start_error": "Musí byť väčšie ako začiatok rozsahu",
|
||||
"greater_range_end_error": "Musí byť väčšie ako koniec rozsahu",
|
||||
"subnet_error": "Adresy musia byť v spoločnej podsieti",
|
||||
"gateway_or_subnet_invalid": "Maska podsiete je neplatná",
|
||||
"dhcp_form_gateway_input": "IP brána",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Nastavitve DHCP IPv6",
|
||||
"form_error_required": "Zahtevano polje.",
|
||||
"form_error_ip4_format": "Neveljaven naslov IPv4.",
|
||||
"form_error_ip4_range_start_format": "Neveljaven začetek razpona naslova IPv4",
|
||||
"form_error_ip4_range_end_format": "Neveljaven konec razpona naslova IPv4",
|
||||
"form_error_ip4_gateway_format": "Neveljaven naslov IPv4 prehoda",
|
||||
"form_error_ip6_format": "Neveljaven naslov IPv6",
|
||||
"form_error_ip_format": "Neveljaven naslov IP",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Mora biti izven razpona \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Mora biti manjši od začetka razpona",
|
||||
"greater_range_start_error": "Mora biti večji od začetka razpona",
|
||||
"greater_range_end_error": "Mora biti večji od konca razpona",
|
||||
"subnet_error": "Naslovi morajo biti v enem podomrežju",
|
||||
"gateway_or_subnet_invalid": "Maska podomrežja ni veljavna",
|
||||
"dhcp_form_gateway_input": "IP prehoda",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
|
||||
"form_error_required": "Obavezno polje",
|
||||
"form_error_ip4_format": "Nevažeća IPv4 adresa",
|
||||
"form_error_ip4_range_start_format": "Nevažeća IPv4 addresa početnog opsega",
|
||||
"form_error_ip4_range_end_format": "Nevažeća IPv4 addresa završnog opsega",
|
||||
"form_error_ip4_gateway_format": "Nevažeća IPv4 addresa prozala",
|
||||
"form_error_ip6_format": "Nevažeća IPv6 adresa",
|
||||
"form_error_ip_format": "Nevažeća IP adresa",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Mora biti izvan opsega \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Mora biti manje od početnog opsega",
|
||||
"greater_range_start_error": "Mora biti veće od početnog opsega",
|
||||
"greater_range_end_error": "Mora biti veće od završnog opsega",
|
||||
"subnet_error": "Asrese moraju biti u jednoj subnet",
|
||||
"gateway_or_subnet_invalid": "Subnet mask nevažeća",
|
||||
"dhcp_form_gateway_input": "IP mrežnog prolaza",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 inställningar",
|
||||
"form_error_required": "Obligatoriskt fält",
|
||||
"form_error_ip4_format": "Ogiltig IPv4-adress",
|
||||
"form_error_ip4_range_start_format": "Ogiltig IPv4-adress för starten av intervallet",
|
||||
"form_error_ip4_range_end_format": "Ogiltig IPv4-adress för slutet av intervallet",
|
||||
"form_error_ip4_gateway_format": "Ogiltig IPv4 adress för gatewayen",
|
||||
"form_error_ip6_format": "Ogiltig IPv6-adress",
|
||||
"form_error_ip_format": "Ogiltig IP-adress",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Måste vara utanför intervallet \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Måste vara lägre än starten på intervallet",
|
||||
"greater_range_start_error": "Måste vara högre än starten på intervallet",
|
||||
"greater_range_end_error": "Måste vara större än intervallets slut",
|
||||
"subnet_error": "Adresser måste finnas i ett subnät",
|
||||
"gateway_or_subnet_invalid": "Subnätmask ogiltig",
|
||||
"dhcp_form_gateway_input": "Gateway-IP",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
|
||||
"form_error_required": "Gerekli alan",
|
||||
"form_error_ip4_format": "Geçersiz IPv4 adresi",
|
||||
"form_error_ip4_range_start_format": "Geçersiz başlangıç aralığı IPv4 biçimi",
|
||||
"form_error_ip4_range_end_format": "Geçersiz bitiş aralığı IPv4 adresi",
|
||||
"form_error_ip4_gateway_format": "Geçersiz ağ geçidi IPv4 adresi",
|
||||
"form_error_ip6_format": "Geçersiz IPv6 adresi",
|
||||
"form_error_ip_format": "Geçersiz IP adresi",
|
||||
@@ -51,9 +49,8 @@
|
||||
"out_of_range_error": "\"{{start}}\"-\"{{end}}\" aralığının dışında olmalıdır",
|
||||
"lower_range_start_error": "Başlangıç aralığından daha düşük olmalıdır",
|
||||
"greater_range_start_error": "Başlangıç aralığından daha büyük olmalıdır",
|
||||
"greater_range_end_error": "Bitiş aralığından daha büyük olmalıdır",
|
||||
"subnet_error": "Adresler bir alt ağda olmalıdır",
|
||||
"gateway_or_subnet_invalid": "Alt ağ maskesi geçersiz",
|
||||
"gateway_or_subnet_invalid": "Geçersiz alt ağ maskesi",
|
||||
"dhcp_form_gateway_input": "Ağ geçidi IP",
|
||||
"dhcp_form_subnet_input": "Alt ağ maskesi",
|
||||
"dhcp_form_range_title": "IP adresi aralığı",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Налаштування DHCP IPv6",
|
||||
"form_error_required": "Обов'язкове поле",
|
||||
"form_error_ip4_format": "Неправильна IPv4-адреса",
|
||||
"form_error_ip4_range_start_format": "Неправильна IPv4-адреса початку діапазону",
|
||||
"form_error_ip4_range_end_format": "Неправильна IPv4-адреса кінця діапазону",
|
||||
"form_error_ip4_gateway_format": "Неправильна IPv4-адреса шлюзу",
|
||||
"form_error_ip6_format": "Неправильна IPv6-адреса",
|
||||
"form_error_ip_format": "Неправильна IP-адреса",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Не повинна бути в діапазоні «{{start}}»−«{{end}}»",
|
||||
"lower_range_start_error": "Має бути меншим за початкову адресу",
|
||||
"greater_range_start_error": "Має бути більшим за початкову адресу",
|
||||
"greater_range_end_error": "Має бути більшим за кінцеву адресу",
|
||||
"subnet_error": "Адреси повинні бути в одній підмережі",
|
||||
"gateway_or_subnet_invalid": "Неправильна маска підмережі",
|
||||
"dhcp_form_gateway_input": "IP-адреса шлюзу",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "Cài đặt DHCP IPv6",
|
||||
"form_error_required": "Trường bắt buộc",
|
||||
"form_error_ip4_format": "Địa chỉ IPv4 không hợp lệ",
|
||||
"form_error_ip4_range_start_format": "Địa chỉ IPv4 không hợp lệ của phạm vi bắt đầu",
|
||||
"form_error_ip4_range_end_format": "Địa chỉ IPv4 không hợp lệ của cuối phạm vi",
|
||||
"form_error_ip4_gateway_format": "Địa chỉ IPv4 không hợp lệ của cổng kết nối",
|
||||
"form_error_ip6_format": "Địa chỉ IPv6 không hợp lệ",
|
||||
"form_error_ip_format": "Địa chỉ IP không hợp lệ",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "Phải nằm ngoài phạm vi \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Phải thấp hơn khởi động phạm vi",
|
||||
"greater_range_start_error": "Phải lớn hơn khoảng bắt đầu",
|
||||
"greater_range_end_error": "Phải lớn hơn phạm vi kết thúc",
|
||||
"subnet_error": "Địa chỉ phải nằm trong một mạng con",
|
||||
"gateway_or_subnet_invalid": "Mặt nạ mạng con không hợp lệ",
|
||||
"dhcp_form_gateway_input": "Cổng IP",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6设置",
|
||||
"form_error_required": "必填字段",
|
||||
"form_error_ip4_format": "无效的 IPv4 地址",
|
||||
"form_error_ip4_range_start_format": "范围起始值的 IPv4 地址无效",
|
||||
"form_error_ip4_range_end_format": "范围终值的 IPv4 地址无效",
|
||||
"form_error_ip4_gateway_format": "网关 IPv4 地址无效",
|
||||
"form_error_ip6_format": "无效的 IPv6 地址",
|
||||
"form_error_ip_format": "无效的 IP 地址",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "必定超出了范围 \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "必须小于范围起始值",
|
||||
"greater_range_start_error": "必须大于范围起始值",
|
||||
"greater_range_end_error": "必须大于范围终值",
|
||||
"subnet_error": "地址必须在一个子网内",
|
||||
"gateway_or_subnet_invalid": "子网掩码无效",
|
||||
"dhcp_form_gateway_input": "网关 IP",
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 設定",
|
||||
"form_error_required": "必填的欄位",
|
||||
"form_error_ip4_format": "無效的 IPv4 位址",
|
||||
"form_error_ip4_range_start_format": "無效起始範圍的 IPv4 位址",
|
||||
"form_error_ip4_range_end_format": "無效結束範圍的 IPv4 位址",
|
||||
"form_error_ip4_gateway_format": "無效閘道的 IPv4 位址",
|
||||
"form_error_ip6_format": "無效的 IPv6 位址",
|
||||
"form_error_ip_format": "無效的 IP 位址",
|
||||
@@ -51,7 +49,6 @@
|
||||
"out_of_range_error": "必須在\"{{start}}\"-\"{{end}}\"範圍之外",
|
||||
"lower_range_start_error": "必須低於起始範圍",
|
||||
"greater_range_start_error": "必須大於起始範圍",
|
||||
"greater_range_end_error": "必須大於結束範圍",
|
||||
"subnet_error": "位址必須在子網路中",
|
||||
"gateway_or_subnet_invalid": "無效的子網路遮罩",
|
||||
"dhcp_form_gateway_input": "閘道 IP",
|
||||
|
||||
@@ -41,6 +41,12 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
|
||||
response.certificate_chain = atob(response.certificate_chain);
|
||||
response.private_key = atob(response.private_key);
|
||||
|
||||
if (values.enabled && values.force_https && window.location.protocol === 'http:') {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
redirectToCurrentProtocol(response, httpPort);
|
||||
|
||||
const dnsStatus = await apiClient.getGlobalStatus();
|
||||
if (dnsStatus) {
|
||||
dispatch(dnsStatusSuccess(dnsStatus));
|
||||
@@ -48,7 +54,6 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
|
||||
|
||||
dispatch(setTlsConfigSuccess(response));
|
||||
dispatch(addSuccessToast('encryption_config_saved'));
|
||||
redirectToCurrentProtocol(response, httpPort);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setTlsConfigFailure());
|
||||
|
||||
@@ -363,18 +363,18 @@ export const changeLanguage = (lang) => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getLanguageRequest = createAction('GET_LANGUAGE_REQUEST');
|
||||
export const getLanguageFailure = createAction('GET_LANGUAGE_FAILURE');
|
||||
export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS');
|
||||
export const changeThemeRequest = createAction('CHANGE_THEME_REQUEST');
|
||||
export const changeThemeFailure = createAction('CHANGE_THEME_FAILURE');
|
||||
export const changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS');
|
||||
|
||||
export const getLanguage = () => async (dispatch) => {
|
||||
dispatch(getLanguageRequest());
|
||||
export const changeTheme = (theme) => async (dispatch) => {
|
||||
dispatch(changeThemeRequest());
|
||||
try {
|
||||
const langSettings = await apiClient.getCurrentLanguage();
|
||||
dispatch(getLanguageSuccess(langSettings.language));
|
||||
await apiClient.changeTheme({ theme });
|
||||
dispatch(changeThemeSuccess({ theme }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getLanguageFailure());
|
||||
dispatch(changeThemeFailure());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { getPathWithQueryString } from '../helpers/helpers';
|
||||
import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART } from '../helpers/constants';
|
||||
import {
|
||||
QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES,
|
||||
} from '../helpers/constants';
|
||||
import { BASE_URL } from '../../constants';
|
||||
import i18n from '../i18n';
|
||||
import { LANGUAGES } from '../helpers/twosky';
|
||||
|
||||
class Api {
|
||||
baseUrl = BASE_URL;
|
||||
@@ -224,21 +228,21 @@ class Api {
|
||||
}
|
||||
|
||||
// Language
|
||||
CURRENT_LANGUAGE = { path: 'i18n/current_language', method: 'GET' };
|
||||
|
||||
CHANGE_LANGUAGE = { path: 'i18n/change_language', method: 'POST' };
|
||||
async changeLanguage(config) {
|
||||
const profile = await this.getProfile();
|
||||
profile.language = config.language;
|
||||
|
||||
getCurrentLanguage() {
|
||||
const { path, method } = this.CURRENT_LANGUAGE;
|
||||
return this.makeRequest(path, method);
|
||||
return this.setProfile(profile);
|
||||
}
|
||||
|
||||
changeLanguage(config) {
|
||||
const { path, method } = this.CHANGE_LANGUAGE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
// Theme
|
||||
|
||||
async changeTheme(config) {
|
||||
const profile = await this.getProfile();
|
||||
profile.theme = config.theme;
|
||||
|
||||
return this.setProfile(profile);
|
||||
}
|
||||
|
||||
// DHCP
|
||||
@@ -571,11 +575,24 @@ class Api {
|
||||
// Profile
|
||||
GET_PROFILE = { path: 'profile', method: 'GET' };
|
||||
|
||||
UPDATE_PROFILE = { path: 'profile/update', method: 'PUT' };
|
||||
|
||||
getProfile() {
|
||||
const { path, method } = this.GET_PROFILE;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setProfile(data) {
|
||||
const theme = data.theme ? data.theme : THEMES.auto;
|
||||
const defaultLanguage = i18n.language ? i18n.language : LANGUAGES.en;
|
||||
const language = data.language ? data.language : defaultLanguage;
|
||||
|
||||
const { path, method } = this.UPDATE_PROFILE;
|
||||
const config = { data: { theme, language } };
|
||||
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
|
||||
// DNS config
|
||||
GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };
|
||||
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
:root {
|
||||
--bgcolor: #f5f7fb;
|
||||
--mcolor: #495057;
|
||||
--scolor: rgba(74, 74, 74, 0.7);
|
||||
--border-color: rgba(0, 40, 100, 0.12);
|
||||
--header-bgcolor: #fff;
|
||||
--card-bgcolor: #fff;
|
||||
--card-border-color: rgba(0, 40, 100, 0.12);
|
||||
--ctrl-bgcolor: #fff;
|
||||
--ctrl-select-bgcolor: rgba(69, 79, 94, 0.12);
|
||||
--ctrl-dropdown-color: #212529;
|
||||
--ctrl-dropdown-bgcolor-focus: #f8f9fa;
|
||||
--ctrl-dropdown-color-focus: #16181b;
|
||||
--btn-success-bgcolor: #5eba00;
|
||||
--form-disabled-bgcolor: #f8f9fa;
|
||||
--form-disabled-color: #495057;
|
||||
--rt-nodata-bgcolor: rgba(255,255,255,0.8);
|
||||
--rt-nodata-color: rgba(0,0,0,0.5);
|
||||
--modal-overlay-bgcolor: rgba(255, 255, 255, 0.75);
|
||||
--logs__table-bgcolor: #fff;
|
||||
--logs__row--blue-bgcolor: #e5effd;
|
||||
--logs__row--white-bgcolor: #fff;
|
||||
--detailed-info-color: #888888;
|
||||
--yellow-pale: rgba(247, 181, 0, 0.1);
|
||||
--green79: #67b279;
|
||||
--gray-a5: #a5a5a5;
|
||||
@@ -8,6 +30,32 @@
|
||||
--font-size-disable-autozoom: 1rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bgcolor: #131313;
|
||||
--mcolor: #e6e6e6;
|
||||
--scolor: #a5a5a5;
|
||||
--header-bgcolor: #131313;
|
||||
--border-color: #222;
|
||||
--card-bgcolor: #1c1c1c;
|
||||
--card-border-color: #3d3d3d;
|
||||
--ctrl-bgcolor: #1c1c1c;
|
||||
--ctrl-select-bgcolor: #3d3d3d;
|
||||
--ctrl-dropdown-color: #fff;
|
||||
--ctrl-dropdown-bgcolor-focus: #000;
|
||||
--ctrl-dropdown-color-focus: #fff;
|
||||
--btn-success-bgcolor: #67b279;
|
||||
--form-disabled-bgcolor: #3d3d3d;
|
||||
--form-disabled-color: #a5a5a5;
|
||||
--logs__text-color: #f3f3f3;
|
||||
--rt-nodata-bgcolor: #1c1c1c;
|
||||
--rt-nodata-color: #fff;
|
||||
--modal-overlay-bgcolor: #1c1c1c;
|
||||
--logs__table-bgcolor: #3d3d3d;
|
||||
--logs__row--blue-bgcolor: #467fcf;
|
||||
--logs__row--white-bgcolor: #1c1c1c;
|
||||
--detailed-info-color: #fff;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -20,8 +20,13 @@ import EncryptionTopline from '../ui/EncryptionTopline';
|
||||
import Icons from '../ui/Icons';
|
||||
import i18n from '../../i18n';
|
||||
import Loading from '../ui/Loading';
|
||||
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants';
|
||||
import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers';
|
||||
import {
|
||||
FILTERS_URLS,
|
||||
MENU_URLS,
|
||||
SETTINGS_URLS,
|
||||
THEMES,
|
||||
} from '../../helpers/constants';
|
||||
import { getLogsUrlParams, setHtmlLangAttr, setUITheme } from '../../helpers/helpers';
|
||||
import Header from '../Header';
|
||||
import { changeLanguage, getDnsStatus } from '../../actions';
|
||||
|
||||
@@ -109,6 +114,7 @@ const App = () => {
|
||||
isCoreRunning,
|
||||
isUpdateAvailable,
|
||||
processing,
|
||||
theme,
|
||||
} = useSelector((state) => state.dashboard, shallowEqual);
|
||||
|
||||
const { processing: processingEncryption } = useSelector((
|
||||
@@ -138,6 +144,41 @@ const App = () => {
|
||||
setLanguage();
|
||||
}, [language]);
|
||||
|
||||
const handleAutoTheme = (e, accountTheme) => {
|
||||
if (accountTheme !== THEMES.auto) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.matches) {
|
||||
setUITheme(THEMES.dark);
|
||||
} else {
|
||||
setUITheme(THEMES.light);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (theme !== THEMES.auto) {
|
||||
setUITheme(theme);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const prefersDark = colorSchemeMedia.matches;
|
||||
setUITheme(prefersDark ? THEMES.dark : THEMES.light);
|
||||
|
||||
if (colorSchemeMedia.addEventListener !== undefined) {
|
||||
colorSchemeMedia.addEventListener('change', (e) => {
|
||||
handleAutoTheme(e, theme);
|
||||
});
|
||||
} else {
|
||||
// Deprecated addListener for older versions of Safari.
|
||||
colorSchemeMedia.addListener((e) => {
|
||||
handleAutoTheme(e, theme);
|
||||
});
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const reloadPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
transition: transform 0.3s ease;
|
||||
background-color: #fff;
|
||||
background-color: var(--header-bgcolor);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px !important;
|
||||
pointer-events: auto !important;
|
||||
background-color: var(--white);
|
||||
background-color: var(--ctrl-bgcolor);
|
||||
color: var(--scolor);
|
||||
z-index: 102;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
|
||||
@@ -155,7 +155,7 @@ const Form = (props) => {
|
||||
name={FORM_NAMES.search}
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={classNames('form-control--search form-control--transparent', className)}
|
||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onClearInputClick={onInputClear}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
overflow: hidden;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
color: var(--gray-4d);
|
||||
color: var(--logs__text-color);
|
||||
letter-spacing: 0;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
.detailed-info {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
color: #888888;
|
||||
color: var(--detailed-info-color);
|
||||
}
|
||||
|
||||
.logs__text--link {
|
||||
@@ -103,14 +103,12 @@
|
||||
}
|
||||
|
||||
.form-control--search {
|
||||
box-shadow: 0 1px 0 #ddd;
|
||||
padding: 0 2.5rem;
|
||||
height: 2.25rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.form-control--transparent {
|
||||
border: 0 solid transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@@ -174,10 +172,8 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
--size: 2.5rem;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0;
|
||||
margin-left: 0.9375rem;
|
||||
background-color: transparent;
|
||||
@@ -373,7 +369,7 @@
|
||||
|
||||
/* QUERY_STATUS_COLORS */
|
||||
.logs__row--blue {
|
||||
background-color: var(--blue);
|
||||
background-color: var(--logs__row--blue-bgcolor);
|
||||
}
|
||||
|
||||
.logs__row--green {
|
||||
@@ -385,7 +381,7 @@
|
||||
}
|
||||
|
||||
.logs__row--white {
|
||||
background-color: var(--white);
|
||||
background-color: var(--logs__row--white-bgcolor);
|
||||
}
|
||||
|
||||
.logs__row--yellow {
|
||||
@@ -393,8 +389,8 @@
|
||||
}
|
||||
|
||||
.logs__no-data {
|
||||
color: var(--gray-4d);
|
||||
background-color: var(--white80);
|
||||
color: var(--mcolor);
|
||||
background-color: var(--logs__table-bgcolor);
|
||||
pointer-events: none;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
@@ -407,7 +403,7 @@
|
||||
}
|
||||
|
||||
.logs__table {
|
||||
background-color: var(--white);
|
||||
background-color: var(--logs__table-bgcolor);
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
min-height: 43rem;
|
||||
@@ -474,7 +470,7 @@
|
||||
|
||||
.filteringRules__filter {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-weight: 400;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ import Select from 'react-select';
|
||||
import i18n from '../../../i18n';
|
||||
import Tabs from '../../ui/Tabs';
|
||||
import Examples from '../Dns/Upstream/Examples';
|
||||
import { toggleAllServices } from '../../../helpers/helpers';
|
||||
import { toggleAllServices, trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||
import {
|
||||
renderInputField,
|
||||
renderGroupField,
|
||||
CheckboxField,
|
||||
renderServiceField,
|
||||
renderTextareaField,
|
||||
} from '../../../helpers/form';
|
||||
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
|
||||
import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants';
|
||||
@@ -230,10 +231,11 @@ let Form = (props) => {
|
||||
<Field
|
||||
id="upstreams"
|
||||
name="upstreams"
|
||||
component="textarea"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control form-control--textarea mb-5"
|
||||
placeholder={t('upstream_dns')}
|
||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||
/>
|
||||
<Examples />
|
||||
</div>,
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
.form__desc {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: rgba(74, 74, 74, 0.7);
|
||||
color: var(--scolor);
|
||||
}
|
||||
|
||||
.form__desc--top {
|
||||
|
||||
@@ -107,5 +107,5 @@
|
||||
.checkbox__label-subtitle {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
color: rgba(74, 74, 74, 0.7);
|
||||
color: var(--scolor);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer__column--theme {
|
||||
min-width: 220px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer__column--language {
|
||||
min-width: 220px;
|
||||
margin-bottom: 0;
|
||||
@@ -49,6 +54,11 @@
|
||||
}
|
||||
|
||||
.footer__column--language {
|
||||
min-width: initial;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.footer__column--theme {
|
||||
min-width: initial;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { REPOSITORY, PRIVACY_POLICY_LINK } from '../../helpers/constants';
|
||||
import { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from '../../helpers/constants';
|
||||
import { LANGUAGES } from '../../helpers/twosky';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
@@ -10,6 +11,7 @@ import Version from './Version';
|
||||
import './Footer.css';
|
||||
import './Select.css';
|
||||
import { setHtmlLangAttr } from '../../helpers/helpers';
|
||||
import { changeTheme } from '../../actions';
|
||||
|
||||
const linksData = [
|
||||
{
|
||||
@@ -29,6 +31,11 @@ const linksData = [
|
||||
|
||||
const Footer = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const currentTheme = useSelector((state) => (state.dashboard ? state.dashboard.theme : 'auto'));
|
||||
const profileName = useSelector((state) => (state.dashboard ? state.dashboard.name : ''));
|
||||
const isLoggedIn = profileName !== '';
|
||||
|
||||
const getYear = () => {
|
||||
const today = new Date();
|
||||
@@ -41,6 +48,11 @@ const Footer = () => {
|
||||
setHtmlLangAttr(value);
|
||||
};
|
||||
|
||||
const onThemeChanged = (event) => {
|
||||
const { value } = event.target;
|
||||
dispatch(changeTheme(value));
|
||||
};
|
||||
|
||||
const renderCopyright = () => <div className="footer__column">
|
||||
<div className="footer__copyright">
|
||||
{t('copyright')} © {getYear()}{' '}
|
||||
@@ -58,6 +70,25 @@ const Footer = () => {
|
||||
{t(name)}
|
||||
</a>);
|
||||
|
||||
const renderThemeSelect = (currentTheme, isLoggedIn) => {
|
||||
if (!isLoggedIn) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return <select
|
||||
className="form-control select select--theme"
|
||||
value={currentTheme}
|
||||
onChange={onThemeChanged}
|
||||
>
|
||||
{Object.values(THEMES)
|
||||
.map((theme) => (
|
||||
<option key={theme} value={theme}>
|
||||
{t(`theme_${theme}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer className="footer">
|
||||
@@ -66,6 +97,9 @@ const Footer = () => {
|
||||
<div className="footer__column footer__column--links">
|
||||
{renderLinks(linksData)}
|
||||
</div>
|
||||
<div className="footer__column footer__column--theme">
|
||||
{renderThemeSelect(currentTheme, isLoggedIn)}
|
||||
</div>
|
||||
<div className="footer__column footer__column--language">
|
||||
<select
|
||||
className="form-control select select--language"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
.ReactModal__Overlay--after-open {
|
||||
opacity: 1;
|
||||
transition: opacity 150ms ease-out;
|
||||
background-color: var(--modal-overlay-bgcolor) !important;
|
||||
}
|
||||
|
||||
.ReactModal__Content {
|
||||
|
||||
@@ -13,6 +13,26 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ReactTable .rt-noData {
|
||||
color: var(--rt-nodata-color);
|
||||
background-color: var(--rt-nodata-bgcolor);
|
||||
}
|
||||
|
||||
.ReactTable .-loading {
|
||||
color: var(--rt-nodata-color);
|
||||
background-color: var(--rt-nodata-bgcolor);
|
||||
}
|
||||
|
||||
.ReactTable .-pagination input, .ReactTable .-pagination select {
|
||||
color: var(--rt-nodata-color);
|
||||
background-color: var(--rt-nodata-bgcolor);
|
||||
}
|
||||
|
||||
[data-theme=dark] .ReactTable .-pagination .-btn {
|
||||
color: var(--scolor);
|
||||
background-color: var(--ctrl-bgcolor);
|
||||
}
|
||||
|
||||
.rt-tr-group.logs__row--red {
|
||||
background-color: rgba(223, 56, 18, 0.05);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
.select.select--theme {
|
||||
height: 45px;
|
||||
padding: 0 32px 2px 11px;
|
||||
outline: 0;
|
||||
border-color: var(--ctrl-select-bgcolor);
|
||||
background-image: url("./svg/chevron-down.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 9px center;
|
||||
background-size: 17px 20px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select--theme::-ms-expand {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.select.select--language {
|
||||
height: 45px;
|
||||
padding: 0 32px 2px 33px;
|
||||
outline: 0;
|
||||
border-color: rgba(69, 79, 94, 0.12);
|
||||
border-color: var(--ctrl-select-bgcolor);
|
||||
background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg");
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-position: left 11px center, right 9px center;
|
||||
@@ -16,8 +33,9 @@
|
||||
}
|
||||
|
||||
.basic-multi-select .select__control {
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border: 1px solid var(--card-border-color);;
|
||||
border-radius: 3px;
|
||||
background-color: var(--ctrl-bgcolor);
|
||||
}
|
||||
|
||||
.basic-multi-select .select__control:hover {
|
||||
@@ -36,4 +54,16 @@
|
||||
|
||||
.basic-multi-select .select__menu {
|
||||
z-index: 3;
|
||||
background-color: var(--ctrl-bgcolor);
|
||||
}
|
||||
|
||||
[data-theme=dark] .basic-multi-select .select__option:hover,
|
||||
[data-theme=dark] .basic-multi-select .select__option--is-focused,
|
||||
[data-theme=dark] .basic-multi-select .select__option--is-focused:hover {
|
||||
background-color: var(--ctrl-select-bgcolor);
|
||||
color: var(--ctrl-dropdown-color);
|
||||
}
|
||||
|
||||
[data-theme=dark] .select__multi-value__remove svg {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ body {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
color: var(--mcolor);
|
||||
text-align: left;
|
||||
background-color: #f5f7fb;
|
||||
background-color: var(--bgcolor);
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
@@ -1943,10 +1943,10 @@ pre code {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
color: var(--mcolor);
|
||||
background-color: var(--card-bgcolor);
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 3px;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
@@ -1957,8 +1957,8 @@ pre code {
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
color: var(--mcolor);
|
||||
background-color: var(--ctrl-bgcolor);
|
||||
border-color: #1991eb;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||
@@ -1991,7 +1991,8 @@ pre code {
|
||||
|
||||
.form-control:disabled,
|
||||
.form-control[readonly] {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--form-disabled-bgcolor);
|
||||
color: var(--form-disabled-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -2580,7 +2581,7 @@ fieldset:disabled a.btn {
|
||||
|
||||
.btn-success {
|
||||
color: #fff;
|
||||
background-color: #5eba00;
|
||||
background-color: var(--btn-success-bgcolor);
|
||||
border-color: #5eba00;
|
||||
}
|
||||
|
||||
@@ -3244,7 +3245,7 @@ tbody.collapse.show {
|
||||
color: #495057;
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
background-color: #fff;
|
||||
background-color: var(--ctrl-bgcolor);
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 3px;
|
||||
@@ -3348,7 +3349,7 @@ tbody.collapse.show {
|
||||
padding: 0.25rem 1.5rem;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: #212529;
|
||||
color: var(--ctrl-dropdown-color);
|
||||
text-align: inherit;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
@@ -3357,9 +3358,9 @@ tbody.collapse.show {
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:focus {
|
||||
color: #16181b;
|
||||
color: var(--ctrl-dropdown-color-focus);
|
||||
text-decoration: none;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ctrl-dropdown-bgcolor-focus);
|
||||
}
|
||||
|
||||
.dropdown-item.active,
|
||||
@@ -3794,11 +3795,11 @@ tbody.collapse.show {
|
||||
height: 2.375rem;
|
||||
padding: 0.5rem 1.75rem 0.5rem 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
color: var(--mcolor);
|
||||
vertical-align: middle;
|
||||
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
||||
background: var(--card-bgcolor) url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
||||
background-size: 8px 10px;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 3px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
@@ -4469,9 +4470,9 @@ tbody.collapse.show {
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
background-color: var(--card-bgcolor);
|
||||
background-clip: border-box;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -5475,9 +5476,9 @@ button.close {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
pointer-events: auto;
|
||||
background-color: #fff;
|
||||
background-color: var(--card-bgcolor);
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 3px;
|
||||
outline: 0;
|
||||
}
|
||||
@@ -10268,8 +10269,8 @@ body.fixed-header .page {
|
||||
.header {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid rgba(0, 40, 100, 0.12);
|
||||
background: var(--header-bgcolor);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
body.fixed-header .header {
|
||||
@@ -10325,6 +10326,10 @@ body.fixed-header .header {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
[data-theme=dark] .header-brand-img {
|
||||
filter:invert(1);
|
||||
}
|
||||
|
||||
.header-avatar {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
@@ -10382,8 +10387,8 @@ body.fixed-header .header {
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #fff;
|
||||
border-top: 1px solid rgba(0, 40, 100, 0.12);
|
||||
background: var(--card-bgcolor);
|
||||
border-top: 1px solid var(--card-border-color);
|
||||
font-size: 0.875rem;
|
||||
padding: 1.25rem 0;
|
||||
color: #9aa0ac;
|
||||
@@ -13686,13 +13691,17 @@ Card alert
|
||||
content: "";
|
||||
}
|
||||
|
||||
[data-theme=dark] .dropdown-menu-arrow:before {
|
||||
border-bottom-color: var(--ctrl-bgcolor);
|
||||
}
|
||||
|
||||
.dropdown-menu-arrow:after {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 12px;
|
||||
display: inline-block;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 5px solid #fff;
|
||||
border-bottom: 5px solid var(--ctrl-bgcolor);
|
||||
border-left: 5px solid transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
[data-theme=dark] .tooltip-container {
|
||||
background-color: var(--ctrl-select-bgcolor);
|
||||
color: var(--mcolor);
|
||||
}
|
||||
|
||||
.tooltip-custom--narrow {
|
||||
max-width: 14rem;
|
||||
}
|
||||
|
||||
@@ -227,6 +227,14 @@ export const BLOCKING_MODES = {
|
||||
custom_ip: 'custom_ip',
|
||||
};
|
||||
|
||||
// Note that translation strings contain these modes (theme_CONSTANT)
|
||||
// i.e. theme_auto, theme_light.
|
||||
export const THEMES = {
|
||||
auto: 'auto',
|
||||
dark: 'dark',
|
||||
light: 'light',
|
||||
};
|
||||
|
||||
export const WHOIS_ICONS = {
|
||||
location: 'location',
|
||||
orgname: 'network',
|
||||
|
||||
@@ -28,6 +28,12 @@ export default {
|
||||
"homepage": "https://badmojr.github.io/1Hosts/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
|
||||
},
|
||||
"1hosts_mini": {
|
||||
"name": "1Hosts (mini)",
|
||||
"categoryId": "general",
|
||||
"homepage": "https://badmojr.github.io/1Hosts/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_38.txt"
|
||||
},
|
||||
"CHN_adrules": {
|
||||
"name": "CHN: AdRules DNS List",
|
||||
"categoryId": "regional",
|
||||
@@ -40,6 +46,12 @@ export default {
|
||||
"homepage": "https://anti-ad.net/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_21.txt"
|
||||
},
|
||||
"HUN_hufilter": {
|
||||
"name": "HUN: Hufilter",
|
||||
"categoryId": "regional",
|
||||
"homepage": "https://github.com/hufilter/hufilter",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_35.txt"
|
||||
},
|
||||
"IDN_abpindo": {
|
||||
"name": "IDN: ABPindo",
|
||||
"categoryId": "regional",
|
||||
@@ -70,6 +82,12 @@ export default {
|
||||
"homepage": "https://github.com/yous/YousList",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_15.txt"
|
||||
},
|
||||
"LIT_easylist_lithuania": {
|
||||
"name": "LIT: EasyList Lithuania",
|
||||
"categoryId": "regional",
|
||||
"homepage": "https://github.com/EasyList-Lithuania/easylist_lithuania",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_36.txt"
|
||||
},
|
||||
"MKD_macedonian_pi_hole_blocklist": {
|
||||
"name": "MKD: Macedonian Pi-hole Blocklist",
|
||||
"categoryId": "regional",
|
||||
@@ -148,6 +166,18 @@ export default {
|
||||
"homepage": "https://energized.pro/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_28.txt"
|
||||
},
|
||||
"hagezi_personal": {
|
||||
"name": "HaGeZi Personal Black \u0026 White",
|
||||
"categoryId": "general",
|
||||
"homepage": "https://github.com/hagezi/dns-blocklists",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_34.txt"
|
||||
},
|
||||
"no_google": {
|
||||
"name": "No Google",
|
||||
"categoryId": "other",
|
||||
"homepage": "https://github.com/nickspaargaren/no-google",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_37.txt"
|
||||
},
|
||||
"nocoin_filter_list": {
|
||||
"name": "NoCoin Filter List",
|
||||
"categoryId": "security",
|
||||
|
||||
@@ -670,6 +670,15 @@ export const setHtmlLangAttr = (language) => {
|
||||
window.document.documentElement.lang = language;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets UI theme.
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
export const setUITheme = (theme) => {
|
||||
document.body.dataset.theme = theme;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param values {object}
|
||||
* @returns {object}
|
||||
|
||||
@@ -112,14 +112,6 @@ const dashboard = handleActions(
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.getLanguageSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
language: payload,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.getClientsRequest]: (state) => ({
|
||||
...state,
|
||||
processingClients: true,
|
||||
@@ -148,8 +140,13 @@ const dashboard = handleActions(
|
||||
[actions.getProfileSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
name: payload.name,
|
||||
theme: payload.theme,
|
||||
processingProfile: false,
|
||||
}),
|
||||
[actions.changeThemeSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
theme: payload.theme,
|
||||
}),
|
||||
},
|
||||
{
|
||||
processing: true,
|
||||
@@ -168,6 +165,7 @@ const dashboard = handleActions(
|
||||
autoClients: [],
|
||||
supportedTags: [],
|
||||
name: '',
|
||||
theme: 'auto',
|
||||
checkUpdateFlag: false,
|
||||
},
|
||||
);
|
||||
|
||||
8
go.mod
8
go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.46.4
|
||||
github.com/AdguardTeam/dnsproxy v0.46.5
|
||||
github.com/AdguardTeam/golibs v0.11.3
|
||||
github.com/AdguardTeam/urlfilter v0.16.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
@@ -30,8 +30,8 @@ require (
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/sys v0.2.0
|
||||
golang.org/x/net v0.4.0
|
||||
golang.org/x/sys v0.3.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
howett.net/plist v1.0.0
|
||||
@@ -61,6 +61,6 @@ require (
|
||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,5 +1,5 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.46.4 h1:/+wnTG0q2TkGQyA1PeSsjv4/f5ZprGduKPSoOcG+rOU=
|
||||
github.com/AdguardTeam/dnsproxy v0.46.4/go.mod h1:yYDMAH6ay2PxLcLtfVM3FUiyv/U9B/zYO+cIIssuJNU=
|
||||
github.com/AdguardTeam/dnsproxy v0.46.5 h1:TiJZhwaIDDaKkqEfJ9AD9aroFjcHN8oEbKB8WfTjSIs=
|
||||
github.com/AdguardTeam/dnsproxy v0.46.5/go.mod h1:yKBVgFlE6CqTQtye++3e7SATaMPc4Ixij+KkHsM6HhM=
|
||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||
github.com/AdguardTeam/golibs v0.11.3 h1:Oif+REq2WLycQ2Xm3ZPmJdfftptss0HbGWbxdFaC310=
|
||||
@@ -187,8 +187,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -228,8 +228,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -237,8 +237,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
||||
@@ -530,14 +530,14 @@ func validateBlockingMode(mode BlockingMode, blockingIPv4, blockingIPv6 net.IP)
|
||||
// prepareInternalProxy initializes the DNS proxy that is used for internal DNS
|
||||
// queries, such as public clients PTR resolving and updater hostname resolving.
|
||||
func (s *Server) prepareInternalProxy() (err error) {
|
||||
srvConf := s.conf
|
||||
conf := &proxy.Config{
|
||||
CacheEnabled: true,
|
||||
CacheSizeBytes: 4096,
|
||||
UpstreamConfig: s.conf.UpstreamConfig,
|
||||
UpstreamConfig: srvConf.UpstreamConfig,
|
||||
MaxGoroutines: int(s.conf.MaxGoroutines),
|
||||
}
|
||||
|
||||
srvConf := s.conf
|
||||
setProxyUpstreamMode(
|
||||
conf,
|
||||
srvConf.AllServers,
|
||||
@@ -570,46 +570,32 @@ func (s *Server) Stop() error {
|
||||
|
||||
// stopLocked stops the DNS server without locking. For internal use only.
|
||||
func (s *Server) stopLocked() (err error) {
|
||||
// TODO(e.burkov, a.garipov): Return critical errors, not just log them.
|
||||
// This will require filtering all the non-critical errors in
|
||||
// [upstream.Upstream] implementations.
|
||||
|
||||
if s.dnsProxy != nil {
|
||||
err = s.dnsProxy.Stop()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing primary resolvers: %w", err)
|
||||
log.Error("dnsforward: closing primary resolvers: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if upsConf := s.internalProxy.UpstreamConfig; upsConf != nil {
|
||||
const action = "closing internal resolvers"
|
||||
|
||||
err = upsConf.Close()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Debug("dnsforward: %s: %s", action, err)
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", action, err))
|
||||
}
|
||||
log.Error("dnsforward: closing internal resolvers: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if upsConf := s.localResolvers.UpstreamConfig; upsConf != nil {
|
||||
const action = "closing local resolvers"
|
||||
|
||||
err = upsConf.Close()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Debug("dnsforward: %s: %s", action, err)
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", action, err))
|
||||
}
|
||||
log.Error("dnsforward: closing local resolvers: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.List("stopping dns server", errs...)
|
||||
} else {
|
||||
s.isRunning = false
|
||||
}
|
||||
s.isRunning = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package filtering
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
@@ -97,14 +99,15 @@ func (d *DNSFilter) filterSetProperties(
|
||||
filt.URL,
|
||||
)
|
||||
|
||||
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time) {
|
||||
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time, oldRulesCount int) {
|
||||
if err != nil {
|
||||
filt.URL = oldURL
|
||||
filt.Name = oldName
|
||||
filt.Enabled = oldEnabled
|
||||
filt.LastUpdated = oldUpdated
|
||||
filt.RulesCount = oldRulesCount
|
||||
}
|
||||
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated)
|
||||
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated, filt.RulesCount)
|
||||
|
||||
filt.Name = newList.Name
|
||||
|
||||
@@ -134,8 +137,8 @@ func (d *DNSFilter) filterSetProperties(
|
||||
// TODO(e.burkov): The validation of the contents of the new URL is
|
||||
// currently skipped if the rule list is disabled. This makes it
|
||||
// possible to set a bad rules source, but the validation should still
|
||||
// kick in when the filter is enabled. Consider making changing this
|
||||
// behavior to be stricter.
|
||||
// kick in when the filter is enabled. Consider changing this behavior
|
||||
// to be stricter.
|
||||
filt.unload()
|
||||
}
|
||||
|
||||
@@ -269,10 +272,10 @@ func (d *DNSFilter) periodicallyRefreshFilters() {
|
||||
// already going on.
|
||||
//
|
||||
// TODO(e.burkov): Get rid of the concurrency pattern which requires the
|
||||
// sync.Mutex.TryLock.
|
||||
// [sync.Mutex.TryLock].
|
||||
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
|
||||
if ok = d.refreshLock.TryLock(); !ok {
|
||||
return 0, false, ok
|
||||
return 0, false, false
|
||||
}
|
||||
defer d.refreshLock.Unlock()
|
||||
|
||||
@@ -427,52 +430,124 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
|
||||
return updNum, false
|
||||
}
|
||||
|
||||
// Allows printable UTF-8 text with CR, LF, TAB characters
|
||||
func isPrintableText(data []byte, len int) bool {
|
||||
for i := 0; i < len; i++ {
|
||||
c := data[i]
|
||||
// isPrintableText returns true if data is printable UTF-8 text with CR, LF, TAB
|
||||
// characters.
|
||||
//
|
||||
// TODO(e.burkov): Investigate the purpose of this and improve the
|
||||
// implementation. Perhaps, use something from the unicode package.
|
||||
func isPrintableText(data string) (ok bool) {
|
||||
for _, c := range []byte(data) {
|
||||
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
|
||||
continue
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
|
||||
func (d *DNSFilter) parseFilterContents(file io.Reader) (int, uint32, string) {
|
||||
rulesCount := 0
|
||||
name := ""
|
||||
seenTitle := false
|
||||
r := bufio.NewReader(file)
|
||||
checksum := uint32(0)
|
||||
// scanLinesWithBreak is essentially a [bufio.ScanLines] which keeps trailing
|
||||
// line breaks.
|
||||
func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
return i + 1, data[0 : i+1], nil
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
//
|
||||
} else if line[0] == '!' {
|
||||
m := d.filterTitleRegexp.FindAllStringSubmatch(line, -1)
|
||||
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
|
||||
name = m[0][1]
|
||||
seenTitle = true
|
||||
}
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
|
||||
} else if line[0] == '#' {
|
||||
//
|
||||
} else {
|
||||
rulesCount++
|
||||
// Request more data.
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// parseFilter copies filter's content from src to dst and returns the number of
|
||||
// rules, name, number of bytes written, checksum, and title of the parsed list.
|
||||
// dst must not be nil.
|
||||
func (d *DNSFilter) parseFilter(
|
||||
src io.Reader,
|
||||
dst io.Writer,
|
||||
) (rulesNum, written int, checksum uint32, title string, err error) {
|
||||
scanner := bufio.NewScanner(src)
|
||||
scanner.Split(scanLinesWithBreak)
|
||||
|
||||
titleFound := false
|
||||
for n := 0; scanner.Scan(); written += n {
|
||||
line := scanner.Text()
|
||||
var isRule bool
|
||||
var likelyTitle string
|
||||
isRule, likelyTitle, err = d.parseFilterLine(line, !titleFound, written == 0)
|
||||
if err != nil {
|
||||
return 0, written, 0, "", err
|
||||
}
|
||||
|
||||
if isRule {
|
||||
rulesNum++
|
||||
} else if likelyTitle != "" {
|
||||
title, titleFound = likelyTitle, true
|
||||
}
|
||||
|
||||
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
|
||||
|
||||
n, err = dst.Write([]byte(line))
|
||||
if err != nil {
|
||||
break
|
||||
return 0, written, 0, "", fmt.Errorf("writing filter line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return rulesCount, checksum, name
|
||||
if err = scanner.Err(); err != nil {
|
||||
return 0, written, 0, "", fmt.Errorf("scanning filter contents: %w", err)
|
||||
}
|
||||
|
||||
return rulesNum, written, checksum, title, nil
|
||||
}
|
||||
|
||||
// parseFilterLine returns true if the passed line is a rule. line is
|
||||
// considered a rule if it's not a comment and contains no title.
|
||||
func (d *DNSFilter) parseFilterLine(
|
||||
line string,
|
||||
lookForTitle bool,
|
||||
testHTML bool,
|
||||
) (isRule bool, title string, err error) {
|
||||
if !isPrintableText(line) {
|
||||
return false, "", errors.Error("filter contains non-printable characters")
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
if testHTML && isHTML(line) {
|
||||
return false, "", errors.Error("data is HTML, not plain text")
|
||||
}
|
||||
|
||||
if line[0] == '!' && lookForTitle {
|
||||
match := d.filterTitleRegexp.FindStringSubmatch(line)
|
||||
if len(match) > 1 {
|
||||
title = match[1]
|
||||
}
|
||||
|
||||
return false, title, nil
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
// isHTML returns true if the line contains HTML tags instead of plain text.
|
||||
// line shouldn have no leading space symbols.
|
||||
//
|
||||
// TODO(ameshkov): It actually gives too much false-positives. Perhaps, just
|
||||
// check if trimmed string begins with angle bracket.
|
||||
func isHTML(line string) (ok bool) {
|
||||
line = strings.ToLower(line)
|
||||
|
||||
return strings.HasPrefix(line, "<html") || strings.HasPrefix(line, "<!doctype")
|
||||
}
|
||||
|
||||
// Perform upgrade on a filter and update LastUpdated value
|
||||
@@ -485,57 +560,10 @@ func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
|
||||
log.Error("os.Chtimes(): %v", e)
|
||||
}
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (d *DNSFilter) read(reader io.Reader, tmpFile *os.File, filter *FilterYAML) (int, error) {
|
||||
htmlTest := true
|
||||
firstChunk := make([]byte, 4*1024)
|
||||
firstChunkLen := 0
|
||||
buf := make([]byte, 64*1024)
|
||||
total := 0
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
total += n
|
||||
|
||||
if htmlTest {
|
||||
num := len(firstChunk) - firstChunkLen
|
||||
if n < num {
|
||||
num = n
|
||||
}
|
||||
copied := copy(firstChunk[firstChunkLen:], buf[:num])
|
||||
firstChunkLen += copied
|
||||
|
||||
if firstChunkLen == len(firstChunk) || err == io.EOF {
|
||||
if !isPrintableText(firstChunk, firstChunkLen) {
|
||||
return total, fmt.Errorf("data contains non-printable characters")
|
||||
}
|
||||
|
||||
s := strings.ToLower(string(firstChunk))
|
||||
if strings.Contains(s, "<html") || strings.Contains(s, "<!doctype") {
|
||||
return total, fmt.Errorf("data is HTML, not plain text")
|
||||
}
|
||||
|
||||
htmlTest = false
|
||||
firstChunk = nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err2 := tmpFile.Write(buf[:n])
|
||||
if err2 != nil {
|
||||
return total, err2
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return total, nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Couldn't fetch filter contents from URL %s, skipping: %s", filter.URL, err)
|
||||
return total, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finalizeUpdate closes and gets rid of temporary file f with filter's content
|
||||
// according to updated. It also saves new values of flt's name, rules number
|
||||
// and checksum if sucсeeded.
|
||||
@@ -552,7 +580,8 @@ func (d *DNSFilter) finalizeUpdate(
|
||||
// Close the file before renaming it because it's required on Windows.
|
||||
//
|
||||
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
|
||||
if err = file.Close(); err != nil {
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing temporary file: %w", err)
|
||||
}
|
||||
|
||||
@@ -564,38 +593,18 @@ func (d *DNSFilter) finalizeUpdate(
|
||||
|
||||
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
|
||||
|
||||
if err = os.Rename(tmpFileName, flt.Path(d.DataDir)); err != nil {
|
||||
// Don't use renamio or maybe packages, since those will require loading the
|
||||
// whole filter content to the memory on Windows.
|
||||
err = os.Rename(tmpFileName, flt.Path(d.DataDir))
|
||||
if err != nil {
|
||||
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
||||
}
|
||||
|
||||
flt.Name = stringutil.Coalesce(flt.Name, name)
|
||||
flt.checksum = cs
|
||||
flt.RulesCount = rnum
|
||||
flt.Name, flt.checksum, flt.RulesCount = aghalg.Coalesce(flt.Name, name), cs, rnum
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processUpdate copies filter's content from src to dst and returns the name,
|
||||
// rules number, and checksum for it. It also returns the number of bytes read
|
||||
// from src.
|
||||
func (d *DNSFilter) processUpdate(
|
||||
src io.Reader,
|
||||
dst *os.File,
|
||||
flt *FilterYAML,
|
||||
) (name string, rnum int, cs uint32, n int, err error) {
|
||||
if n, err = d.read(src, dst, flt); err != nil {
|
||||
return "", 0, 0, 0, err
|
||||
}
|
||||
|
||||
if _, err = dst.Seek(0, io.SeekStart); err != nil {
|
||||
return "", 0, 0, 0, err
|
||||
}
|
||||
|
||||
rnum, cs, name = d.parseFilterContents(dst)
|
||||
|
||||
return name, rnum, cs, n, nil
|
||||
}
|
||||
|
||||
// updateIntl updates the flt rewriting it's actual file. It returns true if
|
||||
// the actual update has been performed.
|
||||
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
@@ -612,31 +621,21 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
}
|
||||
defer func() {
|
||||
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
||||
ok = ok && err == nil
|
||||
if ok {
|
||||
if ok && err == nil {
|
||||
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
||||
}
|
||||
}()
|
||||
|
||||
// Change the default 0o600 permission to something more acceptable by
|
||||
// end users.
|
||||
// Change the default 0o600 permission to something more acceptable by end
|
||||
// users.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
|
||||
if err = tmpFile.Chmod(0o644); err != nil {
|
||||
return false, fmt.Errorf("changing file mode: %w", err)
|
||||
}
|
||||
|
||||
var r io.Reader
|
||||
if filepath.IsAbs(flt.URL) {
|
||||
var file io.ReadCloser
|
||||
file, err = os.Open(flt.URL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, file.Close()) }()
|
||||
|
||||
r = file
|
||||
} else {
|
||||
var rc io.ReadCloser
|
||||
if !filepath.IsAbs(flt.URL) {
|
||||
var resp *http.Response
|
||||
resp, err = d.HTTPClient.Get(flt.URL)
|
||||
if err != nil {
|
||||
@@ -649,24 +648,30 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
|
||||
|
||||
return false, fmt.Errorf("got status code != 200: %d", resp.StatusCode)
|
||||
return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
r = resp.Body
|
||||
rc = resp.Body
|
||||
} else {
|
||||
rc, err = os.Open(flt.URL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, rc.Close()) }()
|
||||
}
|
||||
|
||||
name, rnum, cs, n, err = d.processUpdate(r, tmpFile, flt)
|
||||
rnum, n, cs, name, err = d.parseFilter(rc, tmpFile)
|
||||
|
||||
return cs != flt.checksum, err
|
||||
return cs != flt.checksum && err == nil, err
|
||||
}
|
||||
|
||||
// loads filter contents from the file in dataDir
|
||||
func (d *DNSFilter) load(filter *FilterYAML) (err error) {
|
||||
filterFilePath := filter.Path(d.DataDir)
|
||||
func (d *DNSFilter) load(flt *FilterYAML) (err error) {
|
||||
fileName := flt.Path(d.DataDir)
|
||||
|
||||
log.Tracef("filtering: loading filter %d from %s", filter.ID, filterFilePath)
|
||||
log.Debug("filtering: loading filter %d from %s", flt.ID, fileName)
|
||||
|
||||
file, err := os.Open(filterFilePath)
|
||||
file, err := os.Open(fileName)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Do nothing, file doesn't exist.
|
||||
return nil
|
||||
@@ -680,13 +685,14 @@ func (d *DNSFilter) load(filter *FilterYAML) (err error) {
|
||||
return fmt.Errorf("getting filter file stat: %w", err)
|
||||
}
|
||||
|
||||
log.Tracef("filtering: File %s, id %d, length %d", filterFilePath, filter.ID, st.Size())
|
||||
log.Debug("filtering: file %s, id %d, length %d", fileName, flt.ID, st.Size())
|
||||
|
||||
rulesCount, checksum, _ := d.parseFilterContents(file)
|
||||
rulesCount, _, checksum, _, err := d.parseFilter(file, io.Discard)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing filter file: %w", err)
|
||||
}
|
||||
|
||||
filter.RulesCount = rulesCount
|
||||
filter.checksum = checksum
|
||||
filter.LastUpdated = st.ModTime()
|
||||
flt.RulesCount, flt.checksum, flt.LastUpdated = rulesCount, checksum, st.ModTime()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,33 +4,23 @@ import (
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// serveFiltersLocally is a helper that concurrently listens on a free port to
|
||||
// respond with fltContent. It also gracefully closes the listener when the
|
||||
// test under t finishes.
|
||||
func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
|
||||
// serveHTTPLocally starts a new HTTP server, that handles its index with h. It
|
||||
// also gracefully closes the listener when the test under t finishes.
|
||||
func serveHTTPLocally(t *testing.T, h http.Handler) (urlStr string) {
|
||||
t.Helper()
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
pt := testutil.PanicT{}
|
||||
|
||||
n, werr := w.Write(fltContent)
|
||||
require.NoError(pt, werr)
|
||||
require.Equal(pt, len(fltContent), n)
|
||||
})
|
||||
|
||||
l, err := net.Listen("tcp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -38,9 +28,26 @@ func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
|
||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||
|
||||
addr := l.Addr()
|
||||
require.IsType(t, new(net.TCPAddr), addr)
|
||||
require.IsType(t, (*net.TCPAddr)(nil), addr)
|
||||
|
||||
return netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(addr.(*net.TCPAddr).Port))
|
||||
return (&url.URL{
|
||||
Scheme: aghhttp.SchemeHTTP,
|
||||
Host: addr.String(),
|
||||
}).String()
|
||||
}
|
||||
|
||||
// serveFiltersLocally is a helper that concurrently listens on a free port to
|
||||
// respond with fltContent.
|
||||
func serveFiltersLocally(t *testing.T, fltContent []byte) (urlStr string) {
|
||||
t.Helper()
|
||||
|
||||
return serveHTTPLocally(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
pt := testutil.PanicT{}
|
||||
|
||||
n, werr := w.Write(fltContent)
|
||||
require.NoError(pt, werr)
|
||||
require.Equal(pt, len(fltContent), n)
|
||||
}))
|
||||
}
|
||||
|
||||
func TestFilters(t *testing.T) {
|
||||
@@ -65,10 +72,7 @@ func TestFilters(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
f := &FilterYAML{
|
||||
URL: (&url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
}).String(),
|
||||
URL: addr,
|
||||
}
|
||||
|
||||
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
|
||||
@@ -103,11 +107,7 @@ func TestFilters(t *testing.T) {
|
||||
anotherContent := []byte(`||example.com^`)
|
||||
oldURL := f.URL
|
||||
|
||||
ipp := serveFiltersLocally(t, anotherContent)
|
||||
f.URL = (&url.URL{
|
||||
Scheme: "http",
|
||||
Host: ipp.String(),
|
||||
}).String()
|
||||
f.URL = serveFiltersLocally(t, anotherContent)
|
||||
t.Cleanup(func() { f.URL = oldURL })
|
||||
|
||||
updateAndAssert(t, require.True, 1)
|
||||
|
||||
@@ -190,6 +190,8 @@ type DNSFilter struct {
|
||||
|
||||
// filterTitleRegexp is the regular expression to retrieve a name of a
|
||||
// filter list.
|
||||
//
|
||||
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
|
||||
filterTitleRegexp *regexp.Regexp
|
||||
|
||||
hostCheckers []hostChecker
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -30,11 +29,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
|
||||
endpoint: &badRulesEndpoint,
|
||||
content: []byte(`<html></html>`),
|
||||
}} {
|
||||
ipp := serveFiltersLocally(t, rulesSource.content)
|
||||
*rulesSource.endpoint = (&url.URL{
|
||||
Scheme: "http",
|
||||
Host: ipp.String(),
|
||||
}).String()
|
||||
*rulesSource.endpoint = serveFiltersLocally(t, rulesSource.content)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
|
||||
@@ -253,7 +253,6 @@ var blockedServices = []blockedService{{
|
||||
Rules: []string{
|
||||
"||aus.social^",
|
||||
"||awscommunity.social^",
|
||||
"||dju.social^",
|
||||
"||dresden.network^",
|
||||
"||fedibird.com^",
|
||||
"||fosstodon.org^",
|
||||
@@ -261,11 +260,11 @@ var blockedServices = []blockedService{{
|
||||
"||h4.io^",
|
||||
"||hachyderm.io^",
|
||||
"||hessen.social^",
|
||||
"||hispagatos.space^",
|
||||
"||home.social^",
|
||||
"||hostux.social^",
|
||||
"||ieji.de^",
|
||||
"||indieweb.social^",
|
||||
"||infosec.exchange^",
|
||||
"||ioc.exchange^",
|
||||
"||kolektiva.social^",
|
||||
"||livellosegreto.it^",
|
||||
@@ -287,9 +286,11 @@ var blockedServices = []blockedService{{
|
||||
"||mastodon.nu^",
|
||||
"||mastodon.nz^",
|
||||
"||mastodon.online^",
|
||||
"||mastodon.online^",
|
||||
"||mastodon.scot^",
|
||||
"||mastodon.sdf.org^",
|
||||
"||mastodon.social^",
|
||||
"||mastodon.social^",
|
||||
"||mastodon.top^",
|
||||
"||mastodon.uno^",
|
||||
"||mastodon.world^",
|
||||
@@ -309,7 +310,6 @@ var blockedServices = []blockedService{{
|
||||
"||mstdn.social^",
|
||||
"||muenchen.social^",
|
||||
"||muenster.im^",
|
||||
"||nerdculture.de^",
|
||||
"||newsie.social^",
|
||||
"||noc.social^",
|
||||
"||norden.social^",
|
||||
@@ -335,21 +335,21 @@ var blockedServices = []blockedService{{
|
||||
"||social.vivaldi.net^",
|
||||
"||sself.co^",
|
||||
"||sueden.social^",
|
||||
"||tech.lgbt^",
|
||||
"||techhub.social^",
|
||||
"||theblower.au^",
|
||||
"||tkz.one^",
|
||||
"||todon.eu^",
|
||||
"||toot.aquilenet.fr^",
|
||||
"||toot.community^",
|
||||
"||toot.funami.tech^",
|
||||
"||toot.wales^",
|
||||
"||troet.cafe^",
|
||||
"||uiuxdev.social^",
|
||||
"||twingyeo.kr^",
|
||||
"||union.place^",
|
||||
"||universeodon.com^",
|
||||
"||urbanists.social^",
|
||||
"||vocalodon.net^",
|
||||
"||wxw.moe^",
|
||||
"||xarxa.cloud^",
|
||||
},
|
||||
}, {
|
||||
ID: "minecraft",
|
||||
@@ -540,6 +540,7 @@ var blockedServices = []blockedService{{
|
||||
Name: "Twitter",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M22.398 5.55a8.583 8.583 0 0 1-2.449.673 4.252 4.252 0 0 0 1.875-2.364 8.66 8.66 0 0 1-2.71 1.04A4.251 4.251 0 0 0 16 3.546a4.27 4.27 0 0 0-4.266 4.27c0 .335.036.66.11.972a12.126 12.126 0 0 1-8.797-4.46 4.259 4.259 0 0 0-.578 2.148c0 1.48.754 2.785 1.898 3.55a4.273 4.273 0 0 1-1.933-.535v.055a4.27 4.27 0 0 0 3.425 4.183c-.359.098-.734.149-1.125.149-.273 0-.543-.027-.804-.074a4.276 4.276 0 0 0 3.988 2.965 8.562 8.562 0 0 1-5.3 1.824 8.82 8.82 0 0 1-1.02-.059 12.088 12.088 0 0 0 6.543 1.918c7.851 0 12.14-6.504 12.14-12.144 0-.184-.004-.368-.011-.551a8.599 8.599 0 0 0 2.128-2.207zm0 0\" /></svg>"),
|
||||
Rules: []string{
|
||||
"||pscp.tv^",
|
||||
"||t.co^",
|
||||
"||twimg.com^",
|
||||
"||twitter.com^",
|
||||
|
||||
@@ -106,6 +106,8 @@ type configuration struct {
|
||||
ProxyURL string `yaml:"http_proxy"`
|
||||
// Language is a two-letter ISO 639-1 language code.
|
||||
Language string `yaml:"language"`
|
||||
// Theme is a UI theme for current user.
|
||||
Theme Theme `yaml:"theme"`
|
||||
// DebugPProf defines if the profiling HTTP handler will listen on :6060.
|
||||
DebugPProf bool `yaml:"debug_pprof"`
|
||||
|
||||
@@ -278,15 +280,20 @@ var config = &configuration{
|
||||
PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy
|
||||
PortDNSOverQUIC: defaultPortQUIC,
|
||||
},
|
||||
// NOTE: Keep these parameters in sync with the one put into
|
||||
// client/src/helpers/filters/filters.js by scripts/vetted-filters.
|
||||
//
|
||||
// TODO(a.garipov): Think of a way to make scripts/vetted-filters update
|
||||
// these as well if necessary.
|
||||
Filters: []filtering.FilterYAML{{
|
||||
Filter: filtering.Filter{ID: 1},
|
||||
Enabled: true,
|
||||
URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt",
|
||||
URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt",
|
||||
Name: "AdGuard DNS filter",
|
||||
}, {
|
||||
Filter: filtering.Filter{ID: 2},
|
||||
Enabled: false,
|
||||
URL: "https://adaway.org/hosts.txt",
|
||||
URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt",
|
||||
Name: "AdAway Default Blocklist",
|
||||
}},
|
||||
DHCP: &dhcpd.ServerConfig{
|
||||
@@ -317,6 +324,7 @@ var config = &configuration{
|
||||
},
|
||||
OSConfig: &osConfig{},
|
||||
SchemaVersion: currentSchemaVersion,
|
||||
Theme: ThemeAuto,
|
||||
}
|
||||
|
||||
// getConfigFilename returns path to the current config file
|
||||
|
||||
@@ -149,19 +149,6 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
type profileJSON struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
u := Context.auth.getCurrentUser(r)
|
||||
resp := &profileJSON{
|
||||
Name: u.Name,
|
||||
}
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// registration of handlers
|
||||
// ------------------------
|
||||
@@ -172,6 +159,7 @@ func registerControlHandlers() {
|
||||
Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
||||
httpRegister(http.MethodPost, "/control/update", handleUpdate)
|
||||
httpRegister(http.MethodGet, "/control/profile", handleGetProfile)
|
||||
httpRegister(http.MethodPut, "/control/profile/update", handlePutProfile)
|
||||
|
||||
// No auth is necessary for DoH/DoT configurations
|
||||
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH))
|
||||
|
||||
@@ -123,7 +123,7 @@ func handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = Context.updater.Update()
|
||||
err = Context.updater.Update(false)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||
@@ -39,17 +41,13 @@ func onConfigModified() {
|
||||
}
|
||||
}
|
||||
|
||||
// initDNSServer creates an instance of the dnsforward.Server
|
||||
// Please note that we must do it even if we don't start it
|
||||
// so that we had access to the query log and the stats
|
||||
func initDNSServer() (err error) {
|
||||
// initDNS updates all the fields of the [Context] needed to initialize the DNS
|
||||
// server and initializes it at last. It also must not be called unless
|
||||
// [config] and [Context] are initialized.
|
||||
func initDNS() (err error) {
|
||||
baseDir := Context.getDataDir()
|
||||
|
||||
var anonFunc aghnet.IPMutFunc
|
||||
if config.DNS.AnonymizeClientIP {
|
||||
anonFunc = querylog.AnonymizeIP
|
||||
}
|
||||
anonymizer := aghnet.NewIPMut(anonFunc)
|
||||
anonymizer := config.anonymizer()
|
||||
|
||||
statsConf := stats.Config{
|
||||
Filename: filepath.Join(baseDir, "stats.db"),
|
||||
@@ -82,34 +80,46 @@ func initDNSServer() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
var privateNets netutil.SubnetSet
|
||||
switch len(config.DNS.PrivateNets) {
|
||||
case 0:
|
||||
// Use an optimized locally-served matcher.
|
||||
privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
||||
case 1:
|
||||
privateNets, err = netutil.ParseSubnet(config.DNS.PrivateNets[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing the set of private subnets: %w", err)
|
||||
}
|
||||
default:
|
||||
var nets []*net.IPNet
|
||||
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing the set of private subnets: %w", err)
|
||||
}
|
||||
tlsConf := &tlsConfigSettings{}
|
||||
Context.tls.WriteDiskConfig(tlsConf)
|
||||
|
||||
privateNets = netutil.SliceSubnetSet(nets)
|
||||
return initDNSServer(
|
||||
Context.filters,
|
||||
Context.stats,
|
||||
Context.queryLog,
|
||||
Context.dhcpServer,
|
||||
anonymizer,
|
||||
httpRegister,
|
||||
tlsConf,
|
||||
)
|
||||
}
|
||||
|
||||
// initDNSServer initializes the [context.dnsServer]. To only use the internal
|
||||
// proxy, none of the arguments are required, but tlsConf still must not be nil,
|
||||
// in other cases all the arguments also must not be nil. It also must not be
|
||||
// called unless [config] and [Context] are initialized.
|
||||
func initDNSServer(
|
||||
filters *filtering.DNSFilter,
|
||||
sts stats.Interface,
|
||||
qlog querylog.QueryLog,
|
||||
dhcpSrv dhcpd.Interface,
|
||||
anonymizer *aghnet.IPMut,
|
||||
httpReg aghhttp.RegisterFunc,
|
||||
tlsConf *tlsConfigSettings,
|
||||
) (err error) {
|
||||
privateNets, err := parseSubnetSet(config.DNS.PrivateNets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing set of private subnets: %w", err)
|
||||
}
|
||||
|
||||
p := dnsforward.DNSCreateParams{
|
||||
DNSFilter: Context.filters,
|
||||
Stats: Context.stats,
|
||||
QueryLog: Context.queryLog,
|
||||
DNSFilter: filters,
|
||||
Stats: sts,
|
||||
QueryLog: qlog,
|
||||
PrivateNets: privateNets,
|
||||
Anonymizer: anonymizer,
|
||||
LocalDomain: config.DHCP.LocalDomainName,
|
||||
DHCPServer: Context.dhcpServer,
|
||||
DHCPServer: dhcpSrv,
|
||||
}
|
||||
|
||||
Context.dnsServer, err = dnsforward.NewServer(p)
|
||||
@@ -120,15 +130,15 @@ func initDNSServer() (err error) {
|
||||
}
|
||||
|
||||
Context.clients.dnsServer = Context.dnsServer
|
||||
var dnsConfig dnsforward.ServerConfig
|
||||
dnsConfig, err = generateServerConfig()
|
||||
|
||||
dnsConf, err := generateServerConfig(tlsConf, httpReg)
|
||||
if err != nil {
|
||||
closeDNSServer()
|
||||
|
||||
return fmt.Errorf("generateServerConfig: %w", err)
|
||||
}
|
||||
|
||||
err = Context.dnsServer.Prepare(&dnsConfig)
|
||||
err = Context.dnsServer.Prepare(&dnsConf)
|
||||
if err != nil {
|
||||
closeDNSServer()
|
||||
|
||||
@@ -146,6 +156,32 @@ func initDNSServer() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseSubnetSet parses a slice of subnets. If the slice is empty, it returns
|
||||
// a subnet set that matches all locally served networks, see
|
||||
// [netutil.IsLocallyServed].
|
||||
func parseSubnetSet(nets []string) (s netutil.SubnetSet, err error) {
|
||||
switch len(nets) {
|
||||
case 0:
|
||||
// Use an optimized function-based matcher.
|
||||
return netutil.SubnetSetFunc(netutil.IsLocallyServed), nil
|
||||
case 1:
|
||||
s, err = netutil.ParseSubnet(nets[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
default:
|
||||
var nets []*net.IPNet
|
||||
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return netutil.SliceSubnetSet(nets), nil
|
||||
}
|
||||
}
|
||||
|
||||
func isRunning() bool {
|
||||
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
|
||||
}
|
||||
@@ -193,7 +229,10 @@ func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
|
||||
return udpAddrs
|
||||
}
|
||||
|
||||
func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||
func generateServerConfig(
|
||||
tlsConf *tlsConfigSettings,
|
||||
httpReg aghhttp.RegisterFunc,
|
||||
) (newConf dnsforward.ServerConfig, err error) {
|
||||
dnsConf := config.DNS
|
||||
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
|
||||
newConf = dnsforward.ServerConfig{
|
||||
@@ -201,12 +240,10 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
|
||||
FilteringConfig: dnsConf.FilteringConfig,
|
||||
ConfigModified: onConfigModified,
|
||||
HTTPRegister: httpRegister,
|
||||
HTTPRegister: httpReg,
|
||||
OnDNSRequest: onDNSRequest,
|
||||
}
|
||||
|
||||
tlsConf := tlsConfigSettings{}
|
||||
Context.tls.WriteDiskConfig(&tlsConf)
|
||||
if tlsConf.Enabled {
|
||||
newConf.TLSConfig = tlsConf.TLSConfig
|
||||
newConf.TLSConfig.ServerName = tlsConf.ServerName
|
||||
@@ -224,7 +261,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||
}
|
||||
|
||||
if tlsConf.PortDNSCrypt != 0 {
|
||||
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, tlsConf)
|
||||
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, *tlsConf)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's already
|
||||
// wrapped by newDNSCrypt.
|
||||
@@ -413,7 +450,11 @@ func startDNSServer() error {
|
||||
|
||||
func reconfigureDNSServer() (err error) {
|
||||
var newConf dnsforward.ServerConfig
|
||||
newConf, err = generateServerConfig()
|
||||
|
||||
tlsConf := &tlsConfigSettings{}
|
||||
Context.tls.WriteDiskConfig(tlsConf)
|
||||
|
||||
newConf, err = generateServerConfig(tlsConf, httpRegister)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating forwarding dns server config: %w", err)
|
||||
}
|
||||
|
||||
@@ -455,6 +455,10 @@ func run(opts options, clientBuildFS fs.FS) {
|
||||
err = setupConfig(opts)
|
||||
fatalOnError(err)
|
||||
|
||||
// TODO(e.burkov): This could be made earlier, probably as the option's
|
||||
// effect.
|
||||
cmdlineUpdate(opts)
|
||||
|
||||
if !Context.firstRun {
|
||||
// Save the updated config
|
||||
err = config.write()
|
||||
@@ -522,7 +526,7 @@ func run(opts options, clientBuildFS fs.FS) {
|
||||
fatalOnError(err)
|
||||
|
||||
if !Context.firstRun {
|
||||
err = initDNSServer()
|
||||
err = initDNS()
|
||||
fatalOnError(err)
|
||||
|
||||
Context.tls.start()
|
||||
@@ -543,20 +547,24 @@ func run(opts options, clientBuildFS fs.FS) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(a.garipov): This could be made much earlier and could be done on
|
||||
// the first run as well, but to achieve this we need to bypass requests
|
||||
// over dnsforward resolver.
|
||||
cmdlineUpdate(opts)
|
||||
|
||||
Context.web.Start()
|
||||
|
||||
// wait indefinitely for other go-routines to complete their job
|
||||
select {}
|
||||
}
|
||||
|
||||
func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
|
||||
var anonFunc aghnet.IPMutFunc
|
||||
if c.DNS.AnonymizeClientIP {
|
||||
anonFunc = querylog.AnonymizeIP
|
||||
}
|
||||
|
||||
return aghnet.NewIPMut(anonFunc)
|
||||
}
|
||||
|
||||
// startMods initializes and starts the DNS server after installation.
|
||||
func startMods() error {
|
||||
err := initDNSServer()
|
||||
func startMods() (err error) {
|
||||
err = initDNS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -927,8 +935,8 @@ func getHTTPProxy(_ *http.Request) (*url.URL, error) {
|
||||
|
||||
// jsonError is a generic JSON error response.
|
||||
//
|
||||
// TODO(a.garipov): Merge together with the implementations in .../dhcpd and
|
||||
// other packages after refactoring the web handler registering.
|
||||
// TODO(a.garipov): Merge together with the implementations in [dhcpd] and other
|
||||
// packages after refactoring the web handler registering.
|
||||
type jsonError struct {
|
||||
// Message is the error message, an opaque string.
|
||||
Message string `json:"message"`
|
||||
@@ -940,30 +948,40 @@ func cmdlineUpdate(opts options) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("starting update")
|
||||
// Initialize the DNS server to use the internal resolver which the updater
|
||||
// needs to be able to resolve the update source hostname.
|
||||
//
|
||||
// TODO(e.burkov): We could probably initialize the internal resolver
|
||||
// separately.
|
||||
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{})
|
||||
fatalOnError(err)
|
||||
|
||||
if Context.firstRun {
|
||||
log.Info("update not allowed on first run")
|
||||
log.Info("cmdline update: performing update")
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
_, err := Context.updater.VersionInfo(true)
|
||||
updater := Context.updater
|
||||
info, err := updater.VersionInfo(true)
|
||||
if err != nil {
|
||||
vcu := Context.updater.VersionCheckURL()
|
||||
vcu := updater.VersionCheckURL()
|
||||
log.Error("getting version info from %s: %s", vcu, err)
|
||||
|
||||
os.Exit(0)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if Context.updater.NewVersion() == "" {
|
||||
if info.NewVersion == version.Version() {
|
||||
log.Info("no updates available")
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err = Context.updater.Update()
|
||||
err = updater.Update(Context.firstRun)
|
||||
fatalOnError(err)
|
||||
|
||||
err = restartService()
|
||||
if err != nil {
|
||||
log.Debug("restarting service: %s", err)
|
||||
log.Info("AdGuard Home was not installed as a service. " +
|
||||
"Please restart running instances of AdGuardHome manually.")
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ type languageJSON struct {
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
// TODO(d.kolyshev): Deprecated, remove it later.
|
||||
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("home: language is %s", config.Language)
|
||||
|
||||
@@ -62,6 +63,7 @@ func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(d.kolyshev): Deprecated, remove it later.
|
||||
func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
|
||||
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
||||
return
|
||||
|
||||
@@ -229,7 +229,7 @@ var cmdLineOpts = []cmdLineOpt{{
|
||||
updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil },
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return "", o.performUpdate },
|
||||
description: "Update application and exit.",
|
||||
description: "Update the current binary and restart the service in case it's installed.",
|
||||
longName: "update",
|
||||
shortName: "",
|
||||
}, {
|
||||
|
||||
102
internal/home/profilehttp.go
Normal file
102
internal/home/profilehttp.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Theme is an enum of all allowed UI themes.
|
||||
type Theme string
|
||||
|
||||
// Allowed [Theme] values.
|
||||
//
|
||||
// Keep in sync with client/src/helpers/constants.js.
|
||||
const (
|
||||
ThemeAuto Theme = "auto"
|
||||
ThemeLight Theme = "light"
|
||||
ThemeDark Theme = "dark"
|
||||
)
|
||||
|
||||
// UnmarshalText implements [encoding.TextUnmarshaler] interface for *Theme.
|
||||
func (t *Theme) UnmarshalText(b []byte) (err error) {
|
||||
switch string(b) {
|
||||
case "auto":
|
||||
*t = ThemeAuto
|
||||
case "dark":
|
||||
*t = ThemeDark
|
||||
case "light":
|
||||
*t = ThemeLight
|
||||
default:
|
||||
return fmt.Errorf("invalid theme %q, supported: %q, %q, %q", b, ThemeAuto, ThemeDark, ThemeLight)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// profileJSON is an object for /control/profile and /control/profile/update
|
||||
// endpoints.
|
||||
type profileJSON struct {
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Theme Theme `json:"theme"`
|
||||
}
|
||||
|
||||
// handleGetProfile is the handler for GET /control/profile endpoint.
|
||||
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
u := Context.auth.getCurrentUser(r)
|
||||
|
||||
var resp profileJSON
|
||||
func() {
|
||||
config.RLock()
|
||||
defer config.RUnlock()
|
||||
|
||||
resp = profileJSON{
|
||||
Name: u.Name,
|
||||
Language: config.Language,
|
||||
Theme: config.Theme,
|
||||
}
|
||||
}()
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// handlePutProfile is the handler for PUT /control/profile/update endpoint.
|
||||
func handlePutProfile(w http.ResponseWriter, r *http.Request) {
|
||||
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
profileReq := &profileJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(profileReq)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
lang := profileReq.Language
|
||||
if !allowedLanguages.Has(lang) {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "unknown language: %q", lang)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
theme := profileReq.Theme
|
||||
|
||||
func() {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
|
||||
config.Language = lang
|
||||
config.Theme = theme
|
||||
log.Printf("home: language is set to %s", lang)
|
||||
log.Printf("home: theme is set to %s", theme)
|
||||
}()
|
||||
|
||||
onConfigModified()
|
||||
aghhttp.OK(w)
|
||||
}
|
||||
@@ -159,6 +159,38 @@ func sendSigReload() {
|
||||
log.Debug("service: sent signal to pid %d", pid)
|
||||
}
|
||||
|
||||
// restartService restarts the service. It returns error if the service is not
|
||||
// running.
|
||||
func restartService() (err error) {
|
||||
// Call chooseSystem explicitly to introduce OpenBSD support for service
|
||||
// package. It's a noop for other GOOS values.
|
||||
chooseSystem()
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
svcConfig := &service.Config{
|
||||
Name: serviceName,
|
||||
DisplayName: serviceDisplayName,
|
||||
Description: serviceDescription,
|
||||
WorkingDirectory: pwd,
|
||||
}
|
||||
configureService(svcConfig)
|
||||
|
||||
var s service.Service
|
||||
if s, err = service.New(&program{}, svcConfig); err != nil {
|
||||
return fmt.Errorf("initializing service: %w", err)
|
||||
}
|
||||
|
||||
if err = svcAction(s, "restart"); err != nil {
|
||||
return fmt.Errorf("restarting service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleServiceControlAction one of the possible control actions:
|
||||
//
|
||||
// - install: Installs a service/daemon.
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
// chooseSystem checks the current system detected and substitutes it with local
|
||||
// implementation if needed.
|
||||
func chooseSystem() {
|
||||
sys := service.ChosenSystem()
|
||||
// By default, package service uses the SysV system if it cannot detect
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
// sysVersion is the version of local service.System interface implementation.
|
||||
const sysVersion = "openbsd-runcom"
|
||||
|
||||
// chooseSystem checks the current system detected and substitutes it with local
|
||||
// implementation if needed.
|
||||
func chooseSystem() {
|
||||
service.ChooseSystem(openbsdSystem{})
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ func withRecovered(orig *error) {
|
||||
// type check
|
||||
var _ Interface = (*StatsCtx)(nil)
|
||||
|
||||
// Start implements the Interface interface for *StatsCtx.
|
||||
// Start implements the [Interface] interface for *StatsCtx.
|
||||
func (s *StatsCtx) Start() {
|
||||
s.initWeb()
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func (u *Updater) VersionInfo(forceRecheck bool) (vi VersionInfo, err error) {
|
||||
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
|
||||
}
|
||||
|
||||
u.prevCheckTime = time.Now()
|
||||
u.prevCheckTime = now
|
||||
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
|
||||
|
||||
return u.prevCheckResult, u.prevCheckError
|
||||
@@ -92,7 +92,11 @@ func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
|
||||
info.AnnouncementURL = versionJSON["announcement_url"]
|
||||
|
||||
packageURL, ok := u.downloadURL(versionJSON)
|
||||
info.CanAutoUpdate = aghalg.BoolToNullBool(ok && info.NewVersion != u.version)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("version.json: packageURL not found")
|
||||
}
|
||||
|
||||
info.CanAutoUpdate = aghalg.BoolToNullBool(info.NewVersion != u.version)
|
||||
|
||||
u.newVersion = info.NewVersion
|
||||
u.packageURL = packageURL
|
||||
|
||||
@@ -104,49 +104,58 @@ func NewUpdater(conf *Config) *Updater {
|
||||
}
|
||||
}
|
||||
|
||||
// Update performs the auto-update.
|
||||
func (u *Updater) Update() (err error) {
|
||||
// Update performs the auto-update. It returns an error if the update failed.
|
||||
// If firstRun is true, it assumes the configuration file doesn't exist.
|
||||
func (u *Updater) Update(firstRun bool) (err error) {
|
||||
u.mu.Lock()
|
||||
defer u.mu.Unlock()
|
||||
|
||||
log.Info("updater: updating")
|
||||
defer func() { log.Info("updater: finished; errors: %v", err) }()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Error("updater: failed: %v", err)
|
||||
} else {
|
||||
log.Info("updater: finished")
|
||||
}
|
||||
}()
|
||||
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting executable path: %w", err)
|
||||
}
|
||||
|
||||
err = u.prepare(execPath)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("preparing: %w", err)
|
||||
}
|
||||
|
||||
defer u.clean()
|
||||
|
||||
err = u.downloadPackageFile(u.packageURL, u.packageName)
|
||||
err = u.downloadPackageFile()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("downloading package file: %w", err)
|
||||
}
|
||||
|
||||
err = u.unpack()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unpacking: %w", err)
|
||||
}
|
||||
|
||||
err = u.check()
|
||||
if err != nil {
|
||||
return err
|
||||
if !firstRun {
|
||||
err = u.check()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = u.backup()
|
||||
err = u.backup(firstRun)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("making backup: %w", err)
|
||||
}
|
||||
|
||||
err = u.replace()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("replacing: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -174,7 +183,7 @@ func (u *Updater) prepare(exePath string) (err error) {
|
||||
|
||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||
if pkgNameOnly == "" {
|
||||
return fmt.Errorf("invalid PackageURL")
|
||||
return fmt.Errorf("invalid PackageURL: %q", u.packageURL)
|
||||
}
|
||||
|
||||
u.packageName = filepath.Join(u.updateDir, pkgNameOnly)
|
||||
@@ -204,6 +213,7 @@ func (u *Updater) prepare(exePath string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unpack extracts the files from the downloaded archive.
|
||||
func (u *Updater) unpack() error {
|
||||
var err error
|
||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||
@@ -228,38 +238,48 @@ func (u *Updater) unpack() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check returns an error if the configuration file couldn't be used with the
|
||||
// version of AdGuard Home just downloaded.
|
||||
func (u *Updater) check() error {
|
||||
log.Debug("updater: checking configuration")
|
||||
|
||||
err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyFile() failed: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(u.updateExeName, "--check-config")
|
||||
err = cmd.Run()
|
||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updater) backup() error {
|
||||
// backup makes a backup of the current configuration and supporting files. It
|
||||
// ignores the configuration file if firstRun is true.
|
||||
func (u *Updater) backup(firstRun bool) (err error) {
|
||||
log.Debug("updater: backing up current configuration")
|
||||
_ = os.Mkdir(u.backupDir, 0o755)
|
||||
err := copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyFile() failed: %w", err)
|
||||
if !firstRun {
|
||||
err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyFile() failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
wd := u.workDir
|
||||
err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
|
||||
wd, u.backupDir, err)
|
||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", wd, u.backupDir, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// replace moves the current executable with the updated one and also copies the
|
||||
// supporting files.
|
||||
func (u *Updater) replace() error {
|
||||
err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
|
||||
if err != nil {
|
||||
@@ -287,6 +307,7 @@ func (u *Updater) replace() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// clean removes the temporary directory itself and all it's contents.
|
||||
func (u *Updater) clean() {
|
||||
_ = os.RemoveAll(u.updateDir)
|
||||
}
|
||||
@@ -297,9 +318,9 @@ func (u *Updater) clean() {
|
||||
const MaxPackageFileSize = 32 * 1024 * 1024
|
||||
|
||||
// Download package file and save it to disk
|
||||
func (u *Updater) downloadPackageFile(url, filename string) (err error) {
|
||||
func (u *Updater) downloadPackageFile() (err error) {
|
||||
var resp *http.Response
|
||||
resp, err = u.client.Get(url)
|
||||
resp, err = u.client.Get(u.packageURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http request failed: %w", err)
|
||||
}
|
||||
@@ -321,7 +342,7 @@ func (u *Updater) downloadPackageFile(url, filename string) (err error) {
|
||||
_ = os.Mkdir(u.updateDir, 0o755)
|
||||
|
||||
log.Debug("updater: saving package to file")
|
||||
err = os.WriteFile(filename, body, 0o644)
|
||||
err = os.WriteFile(u.packageName, body, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.WriteFile() failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ func TestUpdate(t *testing.T) {
|
||||
u.packageURL = fakeURL.String()
|
||||
|
||||
require.NoError(t, u.prepare(exePath))
|
||||
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName))
|
||||
require.NoError(t, u.downloadPackageFile())
|
||||
require.NoError(t, u.unpack())
|
||||
// require.NoError(t, u.check())
|
||||
require.NoError(t, u.backup())
|
||||
require.NoError(t, u.backup(false))
|
||||
require.NoError(t, u.replace())
|
||||
|
||||
u.clean()
|
||||
@@ -215,10 +215,10 @@ func TestUpdateWindows(t *testing.T) {
|
||||
u.packageURL = fakeURL.String()
|
||||
|
||||
require.NoError(t, u.prepare(exePath))
|
||||
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName))
|
||||
require.NoError(t, u.downloadPackageFile())
|
||||
require.NoError(t, u.unpack())
|
||||
// assert.Nil(t, u.check())
|
||||
require.NoError(t, u.backup())
|
||||
require.NoError(t, u.backup(false))
|
||||
require.NoError(t, u.replace())
|
||||
|
||||
u.clean()
|
||||
|
||||
@@ -6,6 +6,33 @@
|
||||
|
||||
|
||||
|
||||
## v0.107.22: API changes
|
||||
|
||||
### `POST /control/i18n/change_language` is deprecated
|
||||
|
||||
Use `PUT /control/profile/update`.
|
||||
|
||||
### `GET /control/i18n/current_language` is deprecated
|
||||
|
||||
Use `GET /control/profile`.
|
||||
|
||||
* The `/control/profile` HTTP API has been changed.
|
||||
|
||||
* The new `PUT /control/profile/update` HTTP API allows user info updates.
|
||||
|
||||
These `control/profile/update` and `control/profile` APIs accept and return a
|
||||
JSON object with the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"name":"user name",
|
||||
"language": "en",
|
||||
"theme": "auto"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## v0.107.20: API Changes
|
||||
|
||||
### `POST /control/cache_clear`
|
||||
|
||||
@@ -962,6 +962,9 @@
|
||||
'description': 'OK.'
|
||||
'/i18n/change_language':
|
||||
'post':
|
||||
'deprecated': true
|
||||
'description': >
|
||||
Deprecated: Use `PUT /control/profile` instead.
|
||||
'tags':
|
||||
- 'i18n'
|
||||
'operationId': 'changeLanguage'
|
||||
@@ -980,6 +983,9 @@
|
||||
'description': 'OK.'
|
||||
'/i18n/current_language':
|
||||
'get':
|
||||
'deprecated': true
|
||||
'description': >
|
||||
Deprecated: Use `GET /control/profile` instead.
|
||||
'tags':
|
||||
- 'i18n'
|
||||
'operationId': 'currentLanguage'
|
||||
@@ -1145,6 +1151,20 @@
|
||||
'responses':
|
||||
'302':
|
||||
'description': 'OK.'
|
||||
'/profile/update':
|
||||
'put':
|
||||
'tags':
|
||||
- 'global'
|
||||
'operationId': 'updateProfile'
|
||||
'summary': 'Updates current user info'
|
||||
'requestBody':
|
||||
'content':
|
||||
'application/json':
|
||||
'schema':
|
||||
'$ref': '#/components/schemas/ProfileInfo'
|
||||
'responses':
|
||||
'200':
|
||||
'description': 'OK'
|
||||
'/profile':
|
||||
'get':
|
||||
'tags':
|
||||
@@ -2335,6 +2355,19 @@
|
||||
'properties':
|
||||
'name':
|
||||
'type': 'string'
|
||||
'language':
|
||||
'type': 'string'
|
||||
'theme':
|
||||
'type': 'string'
|
||||
'description': 'Interface theme'
|
||||
'enum':
|
||||
- 'auto'
|
||||
- 'dark'
|
||||
- 'light'
|
||||
'required':
|
||||
- 'name'
|
||||
- 'language'
|
||||
- 'theme'
|
||||
'Client':
|
||||
'type': 'object'
|
||||
'description': 'Client information.'
|
||||
|
||||
@@ -85,7 +85,7 @@ in
|
||||
esac
|
||||
readonly docker_image_full_name docker_tags
|
||||
|
||||
# Copy the binaries into a new directory under new names, so that it's eaiser to
|
||||
# Copy the binaries into a new directory under new names, so that it's easier to
|
||||
# COPY them later. DO NOT remove the trailing underscores. See file
|
||||
# scripts/make/Dockerfile.
|
||||
dist_docker="${dist_dir}/docker"
|
||||
|
||||
@@ -7,22 +7,18 @@
|
||||
# Experienced readers may find it overly verbose.
|
||||
|
||||
# The default verbosity level is 0. Show log messages if the caller requested
|
||||
# verbosity level greater than 0. Show every command that is run if the
|
||||
# verbosity level is greater than 1. Show the environment if the verbosity
|
||||
# level is greater than 2. Otherwise, print nothing.
|
||||
# verbosity level greater than 0. Show the environment and every command that
|
||||
# is run if the verbosity level is greater than 1. Otherwise, print nothing.
|
||||
#
|
||||
# The level of verbosity for the build script is the same minus one level. See
|
||||
# below in build().
|
||||
verbose="${VERBOSE:-0}"
|
||||
readonly verbose
|
||||
|
||||
if [ "$verbose" -gt '2' ]
|
||||
if [ "$verbose" -gt '1' ]
|
||||
then
|
||||
env
|
||||
set -x
|
||||
elif [ "$verbose" -gt '1' ]
|
||||
then
|
||||
set -x
|
||||
fi
|
||||
|
||||
# By default, sign the packages, but allow users to skip that step.
|
||||
@@ -188,9 +184,6 @@ build() {
|
||||
#
|
||||
# Set GOARM and GOMIPS to an empty string if $build_arm and $build_mips are
|
||||
# the zero value by removing the hyphen as if it's a prefix.
|
||||
#
|
||||
# Don't use quotes with $build_par because we want an empty space if
|
||||
# parallelism wasn't set.
|
||||
env\
|
||||
GOARCH="$build_arch"\
|
||||
GOARM="${build_arm#-}"\
|
||||
|
||||
Reference in New Issue
Block a user