Compare commits

...

4 Commits

Author SHA1 Message Date
Ainar Garipov
c69639c013 all: imp chlog 2023-01-19 15:29:10 +03:00
Ainar Garipov
5f6fbe8e08 all: sync with master; upd chlog 2023-01-19 15:04:46 +03:00
Ainar Garipov
b40bbf0260 all: upd chlog 2023-01-19 15:00:14 +03:00
Ainar Garipov
a11c8e91ab all: sync with master 2022-12-15 17:50:08 +03:00
84 changed files with 1020 additions and 511 deletions

View File

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

View File

@@ -1,7 +1,7 @@
'name': 'build'
'env':
'GO_VERSION': '1.18.8'
'GO_VERSION': '1.18.9'
'NODE_VERSION': '14'
'on':

View File

@@ -1,7 +1,7 @@
'name': 'lint'
'env':
'GO_VERSION': '1.18.8'
'GO_VERSION': '1.18.9'
'on':
'push':

View File

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

View File

@@ -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':

View File

@@ -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-адрас шлюза",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "차단 해제",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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-адрес шлюза",

View File

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

View File

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

View File

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

View File

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

View File

@@ -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ığı",

View File

@@ -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-адреса шлюзу",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,7 @@
width: 250px;
height: 100vh;
transition: transform 0.3s ease;
background-color: #fff;
background-color: var(--header-bgcolor);
overflow-y: auto;
}

View File

@@ -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%;

View File

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

View File

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

View File

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

View File

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

View File

@@ -107,5 +107,5 @@
.checkbox__label-subtitle {
display: block;
line-height: 1.5;
color: rgba(74, 74, 74, 0.7);
color: var(--scolor);
}

View File

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

View File

@@ -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')} &copy; {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"

View File

@@ -11,6 +11,7 @@
.ReactModal__Overlay--after-open {
opacity: 1;
transition: opacity 150ms ease-out;
background-color: var(--modal-overlay-bgcolor) !important;
}
.ReactModal__Content {

View File

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

View File

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

View File

@@ -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: "";
}

View File

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

View File

@@ -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',

View File

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

View File

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

View File

@@ -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
View File

@@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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^",

View File

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

View 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))

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: "",
}, {

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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#-}"\