Compare commits
28 Commits
v0.108.0-b
...
6399-fix-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24a62d0638 | ||
|
|
f81a94eb94 | ||
|
|
ca898fe74e | ||
|
|
366ec81621 | ||
|
|
f9ee511094 | ||
|
|
deedc490e1 | ||
|
|
f8fe9bfc8b | ||
|
|
6cff5865d2 | ||
|
|
cbcc17a58b | ||
|
|
6a3906aa95 | ||
|
|
ffdebc7b2d | ||
|
|
f3817e4411 | ||
|
|
52713a2600 | ||
|
|
62ec0d5adc | ||
|
|
2a56c78f26 | ||
|
|
c0588146e7 | ||
|
|
f6e34adee7 | ||
|
|
e3cc3b0642 | ||
|
|
cd09ba63b6 | ||
|
|
1d1de1bfb5 | ||
|
|
763bbb5e6b | ||
|
|
9e9d0206b8 | ||
|
|
76899cc31e | ||
|
|
f514f365ab | ||
|
|
413f484810 | ||
|
|
733d6c1fca | ||
|
|
506d71310c | ||
|
|
bb652cd9a1 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.20.10'
|
||||
'GO_VERSION': '1.20.11'
|
||||
'NODE_VERSION': '16'
|
||||
|
||||
'on':
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.20.10'
|
||||
'GO_VERSION': '1.20.11'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -14,21 +14,76 @@ and this project adheres to
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.40] - 2023-10-25 (APPROX.)
|
||||
## [v0.107.41] - 2023-11-01 (APPROX.)
|
||||
|
||||
See also the [v0.107.40 GitHub milestone][ms-v0.107.40].
|
||||
See also the [v0.107.41 GitHub milestone][ms-v0.107.41].
|
||||
|
||||
[ms-v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/milestone/75?closed=1
|
||||
[ms-v0.107.41]: https://github.com/AdguardTeam/AdGuardHome/milestone/76?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to specify multiple domain specific upstreams per line, e.g.
|
||||
`[/domain1/../domain2/]upstream1 upstream2 .. upstreamN` ([#4977]).
|
||||
|
||||
### Changed
|
||||
|
||||
- The height of ready-to-use filter lists has been increased ([#6358]).
|
||||
- Improved authentication failure logging ([#6357]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Redundant shortening long client names in the Top Clients table ([#6338]).
|
||||
- Scrolling column headers in the tables ([#6337]).
|
||||
- `$important,dnsrewrite` rules do not take precedence over allowlist rules
|
||||
([#6204]).
|
||||
- Dark mode DNS rewrite background ([#6329]).
|
||||
- Issues with QUIC and HTTP/3 upstreams on Linux ([#6335]).
|
||||
|
||||
[#4977]: https://github.com/AdguardTeam/AdGuardHome/issues/4977
|
||||
[#6204]: https://github.com/AdguardTeam/AdGuardHome/issues/6204
|
||||
[#6329]: https://github.com/AdguardTeam/AdGuardHome/issues/6329
|
||||
[#6335]: https://github.com/AdguardTeam/AdGuardHome/issues/6335
|
||||
[#6337]: https://github.com/AdguardTeam/AdGuardHome/issues/6337
|
||||
[#6338]: https://github.com/AdguardTeam/AdGuardHome/issues/6338
|
||||
[#6357]: https://github.com/AdguardTeam/AdGuardHome/issues/6357
|
||||
[#6358]: https://github.com/AdguardTeam/AdGuardHome/issues/6358
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [v0.107.40] - 2023-10-18
|
||||
|
||||
See also the [v0.107.40 GitHub milestone][ms-v0.107.40].
|
||||
|
||||
### Changed
|
||||
|
||||
- *Block* and *Unblock* buttons of the query log moved to the tooltip menu
|
||||
([#684]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Dashboard tables scroll issue ([#6180]).
|
||||
- The time shown in the statistics is one hour less than the current time
|
||||
([#6296]).
|
||||
- Issues with QUIC and HTTP/3 upstreams on FreeBSD ([#6301]).
|
||||
- Panic on clearing the query log ([#6304]).
|
||||
|
||||
[#684]: https://github.com/AdguardTeam/AdGuardHome/issues/684
|
||||
[#6180]: https://github.com/AdguardTeam/AdGuardHome/issues/6180
|
||||
[#6296]: https://github.com/AdguardTeam/AdGuardHome/issues/6296
|
||||
[#6301]: https://github.com/AdguardTeam/AdGuardHome/issues/6301
|
||||
[#6304]: https://github.com/AdguardTeam/AdGuardHome/issues/6304
|
||||
|
||||
[ms-v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/milestone/75?closed=1
|
||||
|
||||
|
||||
|
||||
## [v0.107.39] - 2023-10-11
|
||||
|
||||
See also the [v0.107.39 GitHub milestone][ms-v0.107.39].
|
||||
@@ -66,9 +121,8 @@ See also the [v0.107.39 GitHub milestone][ms-v0.107.39].
|
||||
[#6233]: https://github.com/AdguardTeam/AdGuardHome/issues/6233
|
||||
[#6280]: https://github.com/AdguardTeam/AdGuardHome/issues/6280
|
||||
|
||||
[go-1.20.9]: https://groups.google.com/g/golang-announce/c/XBa1oHDevAo/m/desYyx3qAgAJ
|
||||
[go-1.20.10]: https://groups.google.com/g/golang-announce/c/iNNxDTCjZvo/m/UDd7VKQuAAAJ
|
||||
|
||||
[go-1.20.10]: https://groups.google.com/g/golang-announce/c/iNNxDTCjZvo/m/UDd7VKQuAAAJ
|
||||
[go-1.20.9]: https://groups.google.com/g/golang-announce/c/XBa1oHDevAo/m/desYyx3qAgAJ
|
||||
[ms-v0.107.39]: https://github.com/AdguardTeam/AdGuardHome/milestone/74?closed=1
|
||||
|
||||
|
||||
@@ -2531,11 +2585,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...HEAD
|
||||
[v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...v0.107.40
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.41...HEAD
|
||||
[v0.107.41]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...v0.107.41
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...HEAD
|
||||
[v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...v0.107.40
|
||||
[v0.107.39]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.38...v0.107.39
|
||||
[v0.107.38]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.37...v0.107.38
|
||||
[v0.107.37]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.36...v0.107.37
|
||||
|
||||
@@ -201,7 +201,7 @@ opinion, this cannot be legitimately counted as a Pi-Hole's feature.
|
||||
| Cross-platform | ✅ | ❌ (not natively, only via Docker) |
|
||||
| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) |
|
||||
| Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) |
|
||||
| Parental control (blocking adult domains) | ✅ | ❌ |
|
||||
| Parental control (blocking adult domains) | ✅ | ❌ (requires non-default blocklists) |
|
||||
| Force Safe search on search engines | ✅ | ❌ |
|
||||
| Per-client (device) configuration | ✅ | ✅ |
|
||||
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:7.4'
|
||||
'dockerGo': 'adguard/golang-ubuntu:7.5'
|
||||
|
||||
'stages':
|
||||
- 'Build frontend':
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:7.4'
|
||||
'dockerGo': 'adguard/golang-ubuntu:7.5'
|
||||
'snapcraftChannel': 'edge'
|
||||
|
||||
'stages':
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerGo': 'adguard/golang-ubuntu:7.4'
|
||||
'dockerGo': 'adguard/golang-ubuntu:7.5'
|
||||
|
||||
'stages':
|
||||
- 'Tests':
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Заблакаваныя шкодныя і фішынгавыя сайты",
|
||||
"stats_adult": "Заблакаваныя «дарослыя» сайты",
|
||||
"stats_query_domain": "Часта запытаныя дамены",
|
||||
"for_last_24_hours": "за 24 гадзіны",
|
||||
"for_last_hours": "за апошнюю {{count}} гадзіну",
|
||||
"for_last_hours_plural": "за апошнія {{count}} гадзін",
|
||||
"for_last_days": "за апошні {{count}} дзень",
|
||||
"for_last_days_plural": "за апошнія {{count}} дзён",
|
||||
"stats_disabled": "Статыстыка была адключаная. Вы можаце ўключыць яго <0>на старонцы налад </0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Няма дадзеных аб upstream серверах",
|
||||
"number_of_dns_query_days": "Колькасць DNS-запытаў за апошні {{count}} дзень",
|
||||
"number_of_dns_query_days_plural": "Колькасць DNS запытаў, апрацаваных за апошнія {{count}} дзён",
|
||||
"number_of_dns_query_24_hours": "Колькасць DNS-запытаў за 24 гадзіны",
|
||||
"number_of_dns_query_hours": "Колькасць DNS-запытаў, апрацаваных за апошнюю {{count}} гадзіну",
|
||||
"number_of_dns_query_hours_plural": "Колькасць DNS-запытаў, апрацаваных за апошнія {{count}} гадзін",
|
||||
"number_of_dns_query_blocked_24_hours": "Колькасць DNS-запытаў, заблакаваных фільтрамі і блок-спісамі",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Колькасць DNS-запытаў, заблакаваных модулем Антыфішынгу AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Колькасць заблакаваных «сайтаў для дарослых»",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Blokovaný malware/podvody",
|
||||
"stats_adult": "Blokované stránky pro dospělé",
|
||||
"stats_query_domain": "Nejčastěji dotazované domény",
|
||||
"for_last_24_hours": "za posledních 24 hodin",
|
||||
"for_last_hours": "za poslední {{count}} hodinu",
|
||||
"for_last_hours_plural": "za posledních {{count}} hodin",
|
||||
"for_last_days": "za posledních {{count}} dní",
|
||||
"for_last_days_plural": "za posledních {{count}} dní",
|
||||
"stats_disabled": "Statistiky byly vypnuty. Můžete je zapnout ze <0>stránky nastavení</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nebyla nalezena žádná data odchozích připojení",
|
||||
"number_of_dns_query_days": "Počet DNS dotazů zpracovaných za posledních {{count}} den",
|
||||
"number_of_dns_query_days_plural": "Počet DNS dotazů zpracovaných za posledních {{count}} dní",
|
||||
"number_of_dns_query_24_hours": "Počet DNS dotazů zpracovaných za posledních 24 hodin",
|
||||
"number_of_dns_query_hours": "Počet DNS dotazů zpracovaných za poslední {{count}} hodinu",
|
||||
"number_of_dns_query_hours_plural": "Počet DNS dotazů zpracovaných za posledních {{count}} hodin",
|
||||
"number_of_dns_query_blocked_24_hours": "Počet požadavků DNS zablokovaných filtrem reklam a seznamy blokování hostitelů",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Počet požadavků DNS zablokovaných AdGuard modulem Bezpečné prohlížení",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Počet zablokovaných stránek pro dospělé",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Blokeret malware/phishing",
|
||||
"stats_adult": "Blokerede voksne websteder",
|
||||
"stats_query_domain": "Mest forespurgte domæner",
|
||||
"for_last_24_hours": "de seneste 24 timer",
|
||||
"for_last_hours": "den seneste {{count}} time",
|
||||
"for_last_hours_plural": "de seneste {{count}} timer",
|
||||
"for_last_days": "den seneste {{count}} dag",
|
||||
"for_last_days_plural": "de seneste {{count}} dage",
|
||||
"stats_disabled": "Statistikker er deaktiveret. De kan aktiveres via <0>indstillingssiden</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Ingen upstreams-data fundet",
|
||||
"number_of_dns_query_days": "Antallet af DNS-forespørgsler behandlet den seneste {{count}} dag",
|
||||
"number_of_dns_query_days_plural": "Antallet af DNS-forespørgsler behandlet de seneste {{count}} dage",
|
||||
"number_of_dns_query_24_hours": "Antallet af DNS-forespørgsler behandlet de seneste 24 timer",
|
||||
"number_of_dns_query_hours": "Antallet af DNS-forespørgsler behandlet den seneste {{count}} time",
|
||||
"number_of_dns_query_hours_plural": "Antallet af DNS-forespørgsler behandlet de seneste {{count}} timer",
|
||||
"number_of_dns_query_blocked_24_hours": "Antallet af DNS-forespørgsler blokeret af adblockfiltre og værtssortlister",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Antallet af DNS-forespørgsler blokeret af AdGuards browsingsikkerhedsmodul",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Antallet af blokerede voksenwebsteder",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Gesperrte Schädliche/Phishing-Websites",
|
||||
"stats_adult": "Gesperrte jugendgefährdende Websites",
|
||||
"stats_query_domain": "Am häufigsten angefragte Domains",
|
||||
"for_last_24_hours": "für die letzten 24 Stunden",
|
||||
"for_last_hours": "in die letzte {{count}} Stunde",
|
||||
"for_last_hours_plural": "in die letzten {{count}} Stunden",
|
||||
"for_last_days": "am letzten {{count}} Tag",
|
||||
"for_last_days_plural": "in den letzten {{count}} Tage",
|
||||
"stats_disabled": "Die Statistik wurde deaktiviert. Sie können diese in den <0>Einstellungen</0> erneut aktivieren.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Keine Upstream-Daten gefunden",
|
||||
"number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen",
|
||||
"number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}} Tagen verarbeitet wurden",
|
||||
"number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen",
|
||||
"number_of_dns_query_hours": "Die Anzahl der DNS-Anfragen, die in der letzten {{count}} Stunde verarbeitet wurden",
|
||||
"number_of_dns_query_hours_plural": "Die Anzahl der DNS-Anfragen, die in den letzten {{count}} Stunden verarbeitet wurden",
|
||||
"number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Sperrlisten abgelehnte DNS-Anfragen",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul „Internetsicherheit“ gesperrten DNS-Anfragen",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Anzahl der gesperrten Websites mit jugendgefährdenden Inhalten",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"client_settings": "Client settings",
|
||||
"example_upstream_reserved": "an upstream <0>for specific domains</0>;",
|
||||
"example_multiple_upstreams_reserved": "multiple upstreams <0>for specific domains</0>;",
|
||||
"example_upstream_comment": "a comment.",
|
||||
"upstream_parallel": "Use parallel queries to speed up resolving by querying all upstream servers simultaneously.",
|
||||
"parallel_requests": "Parallel requests",
|
||||
@@ -119,7 +120,8 @@
|
||||
"stats_malware_phishing": "Blocked malware/phishing",
|
||||
"stats_adult": "Blocked adult websites",
|
||||
"stats_query_domain": "Top queried domains",
|
||||
"for_last_24_hours": "for the last 24 hours",
|
||||
"for_last_hours": "for the last {{count}} hour",
|
||||
"for_last_hours_plural": "for the last {{count}} hours",
|
||||
"for_last_days": "for the last {{count}} day",
|
||||
"for_last_days_plural": "for the last {{count}} days",
|
||||
"stats_disabled": "The statistics have been disabled. You can turn it on from the <0>settings page</0>.",
|
||||
@@ -134,7 +136,8 @@
|
||||
"no_upstreams_data_found": "No upstreams data found",
|
||||
"number_of_dns_query_days": "The number of DNS queries processed for the last {{count}} day",
|
||||
"number_of_dns_query_days_plural": "The number of DNS queries processed for the last {{count}} days",
|
||||
"number_of_dns_query_24_hours": "The number of DNS queries processed for the last 24 hours",
|
||||
"number_of_dns_query_hours": "The number of DNS queries processed for the last {{count}} hour",
|
||||
"number_of_dns_query_hours_plural": "The number of DNS queries processed for the last {{count}} hours",
|
||||
"number_of_dns_query_blocked_24_hours": "The number of DNS requests blocked by adblock filters and hosts blocklists",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "The number of DNS requests blocked by the AdGuard browsing security module",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "The number of adult websites blocked",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Malware/phishing bloqueado",
|
||||
"stats_adult": "Sitios web para adultos bloqueado",
|
||||
"stats_query_domain": "Dominios más consultados",
|
||||
"for_last_24_hours": "en las últimas 24 horas",
|
||||
"for_last_hours": "de la última {{count}} hora",
|
||||
"for_last_hours_plural": "de las últimas {{count}} horas",
|
||||
"for_last_days": "durante los últimos {{count}} días",
|
||||
"for_last_days_plural": "durante los últimos {{count}} días",
|
||||
"stats_disabled": "Las estadísticas se han deshabilitado. Puedes habilitarlas desde la <0>página de configuración</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "No se han encontrado datos de upstreams",
|
||||
"number_of_dns_query_days": "Número de consultas DNS procesadas durante el último {{count}} día",
|
||||
"number_of_dns_query_days_plural": "Número de consultas DNS procesadas durante los últimos {{count}} días",
|
||||
"number_of_dns_query_24_hours": "Número de consultas DNS procesadas durante las últimas 24 horas",
|
||||
"number_of_dns_query_hours": "Número de consultas DNS procesadas durante la última {{count}} hora",
|
||||
"number_of_dns_query_hours_plural": "Número de consultas DNS procesadas durante las últimas {{count}} horas",
|
||||
"number_of_dns_query_blocked_24_hours": "Número de peticiones DNS bloqueadas por los filtros y listas de bloqueo de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Número de peticiones DNS bloqueadas por el módulo de seguridad de navegación de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Número de sitios web para adultos bloqueado",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Estetyt haittaohjelmat/tietojenkalastelut",
|
||||
"stats_adult": "Estetyt aikuisille tarkoitetut sivustot",
|
||||
"stats_query_domain": "Kysytyimmät verkkotunnukset",
|
||||
"for_last_24_hours": "viimeisten 24 tunnin ajalta",
|
||||
"for_last_hours": "viimeisen {{count}} tunnin ajalta",
|
||||
"for_last_hours_plural": "viimeisen {{count}} tunnin ajalta",
|
||||
"for_last_days": "viimeisten {{count}} päivän ajalta",
|
||||
"for_last_days_plural": "viimeisten {{count}} päivän ajalta",
|
||||
"stats_disabled": "Tilastointi ei ole käytössä. Voit ottaa sen käyttöön <0>asetuksista</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Ylävirtatietoja ei löytynyt",
|
||||
"number_of_dns_query_days": "Käsiteltyjen DNS-pyyntöjen määrä viimeisten {{count}} päivän ajalta",
|
||||
"number_of_dns_query_days_plural": "Käsiteltyjen DNS-pyyntöjen määrä viimeisten {{count}} päivän ajalta",
|
||||
"number_of_dns_query_24_hours": "Käsiteltyjen DNS-pyyntöjen määrä viimeisten 24 tunnin ajalta",
|
||||
"number_of_dns_query_hours": "Viimeisen {{count}} tunnin aikana käsiteltyjen DNS-kyselyiden määrä",
|
||||
"number_of_dns_query_hours_plural": "Viimeisen {{count}} tunnin aikana käsiteltyjen DNS-kyselyiden määrä",
|
||||
"number_of_dns_query_blocked_24_hours": "Mainoseston suodattimien ja hosts-estolistojen estämien DNS-pyyntöjen määrä",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuardin Turvallinen selaus -moduulin estämien DNS-pyyntöjen määrä",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Estettyjen aikuisille tarkoitettujen sivustojen määrä",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Tentative de malware/hameçonnage bloquée",
|
||||
"stats_adult": "Sites à contenu adulte bloqués",
|
||||
"stats_query_domain": "Domaines les plus recherchés",
|
||||
"for_last_24_hours": "pendant les dernières 24 heures",
|
||||
"for_last_hours": "pendant la dernière {{count}} heure",
|
||||
"for_last_hours_plural": "pendant les dernières {{count}} heures",
|
||||
"for_last_days": "pour les {{count}} derniers jours",
|
||||
"for_last_days_plural": "pour les {{count}} derniers jours",
|
||||
"stats_disabled": "Les statistiques ont été désactivées. Vous pouvez l'activer à partir de la <0>page des paramètres</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Aucune donnée en amont trouvée",
|
||||
"number_of_dns_query_days": "Le nombre de requêtes DNS traitées pour les {{count}} derniers jours",
|
||||
"number_of_dns_query_days_plural": "Le nombre de requêtes DNS traitées ces {{count}} derniers jours",
|
||||
"number_of_dns_query_24_hours": "Le nombre de requêtes DNS traitées au cours des 24 dernières heures",
|
||||
"number_of_dns_query_hours": "Le nombre de requêtes DNS traitées pendant la dernière {{count}} heure",
|
||||
"number_of_dns_query_hours_plural": "Le nombre de requêtes DNS traitées pendant les dernières {{count}} heures",
|
||||
"number_of_dns_query_blocked_24_hours": "Le nombre de requêtes DNS bloquées par les filtres adblock et les listes de blocage des hôtes",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Le nombre de requêtes DNS bloquées par le module Sécurité de navigation d'AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Le nombre de sites à contenu adulte bloqués",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Blokiran zločudni program/krađe identiteta",
|
||||
"stats_adult": "Blokirane web stranice za odrasle",
|
||||
"stats_query_domain": "Top tražene domene",
|
||||
"for_last_24_hours": "u zadnja 24 sata",
|
||||
"for_last_hours": "za posljednji {{count}} sat",
|
||||
"for_last_hours_plural": "za posljednjih {{count}} sati",
|
||||
"for_last_days": "zadnjih {{count}} dana",
|
||||
"for_last_days_plural": "zadnjih {{count}} dana",
|
||||
"stats_disabled": "Statistika je onemogućena. Možete ga uključiti sa <0>stranice s postavkama</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nema podataka o upstream poslužiteljima",
|
||||
"number_of_dns_query_days": "Broj DNS upita obrađenih u posljednja {{count}} dan",
|
||||
"number_of_dns_query_days_plural": "Broj DNS upita obrađenih u posljednja {{count}} dana",
|
||||
"number_of_dns_query_24_hours": "Broj DNS upita obrađenih u posljednja 24 sata",
|
||||
"number_of_dns_query_hours": "Broj DNS upita obrađenih za posljednji {{count}} sat",
|
||||
"number_of_dns_query_hours_plural": "Broj DNS upita obrađenih za posljednjih {{count}} sati",
|
||||
"number_of_dns_query_blocked_24_hours": "Broj DNS zahtjeva koji blokiraju filtri za blokiranje oglasa i popisi blokova hostova",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Broj DNS zahtjeva koje je blokirao modul AdGuard zaštita pregledavanja",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Broj blokiranih stranica s sadržajem za odrasle",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Blokkolt kártevő/adathalászat",
|
||||
"stats_adult": "Blokkolt felnőtt tartalom",
|
||||
"stats_query_domain": "Leglátogatottabb domainek",
|
||||
"for_last_24_hours": "az utóbbi 24 órában",
|
||||
"for_last_hours": "az utolsó {{count}} órában",
|
||||
"for_last_hours_plural": "az utolsó {{count}} órában",
|
||||
"for_last_days": "az utóbbi {{count}} napban",
|
||||
"for_last_days_plural": "az utóbbi {{count}} napban",
|
||||
"stats_disabled": "Ezek a statisztikák ki lettek kapcsolva. Be tudja kapcsolni őket a <0>beállítások oldalon</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nem található upstream szerver adat",
|
||||
"number_of_dns_query_days": "Lekérdezések száma az utolsó {{count}} napban",
|
||||
"number_of_dns_query_days_plural": "Feldolgozott DNS lekérdezések száma az utolsó {{count}} napban",
|
||||
"number_of_dns_query_24_hours": "Az elmúlt 24 órában feldolgozott DNS lekérdezések száma",
|
||||
"number_of_dns_query_hours": "Feldolgozott DNS lekérdezések száma az utolsó {{count}} órában",
|
||||
"number_of_dns_query_hours_plural": "Feldolgozott DNS lekérdezések száma az utolsó {{count}} órában",
|
||||
"number_of_dns_query_blocked_24_hours": "A hirdetésblokkoló szűrők és a hosztfájlok által letiltott DNS kérések száma",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Az AdGuard böngészési biztonság modulja által letiltott DNS kérések száma",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Blokkolt felnőtt tartalmak száma",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Malware/phishing diblokir",
|
||||
"stats_adult": "Situs dewasa diblokir",
|
||||
"stats_query_domain": "Kueri domain teratas",
|
||||
"for_last_24_hours": "untuk 24 jam terakhir",
|
||||
"for_last_hours": "selama {{count}} jam terakhir",
|
||||
"for_last_hours_plural": "selama {{count}} jam terakhir",
|
||||
"for_last_days": "untuk {{count}} hari terakhir",
|
||||
"for_last_days_plural": "selama {{count}} hari terakhir",
|
||||
"stats_disabled": "Statistik telah dinonaktifkan. Anda dapat mengaktifkannya dari <0>halaman setelan</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Tidak ada data server upstream yang ditemukan",
|
||||
"number_of_dns_query_days": "Jumlah kueri DNS diproses selama {{value}} hari terakhir",
|
||||
"number_of_dns_query_days_plural": "Jumlah kueri DNS yang diproses selama {{count}} hari terakhir",
|
||||
"number_of_dns_query_24_hours": "Jumlah kueri DNS diproses selama 24 jam terakhir",
|
||||
"number_of_dns_query_hours": "Jumlah kueri DNS diproses selama {{{count}} jam terakhir",
|
||||
"number_of_dns_query_hours_plural": "Jumlah kueri DNS diproses selama {{count}} jam terakhir",
|
||||
"number_of_dns_query_blocked_24_hours": "Julah DNS diblokir oleh penyaring adblock dan daftar blokir hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Jumlah perminataan DNS diblokir oleh modul Kemanan Penjelajahan AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Jumlah website dewasa diblokir",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Malware/phishing bloccati",
|
||||
"stats_adult": "Siti per adulti bloccati",
|
||||
"stats_query_domain": "Domini maggiormente richiesti",
|
||||
"for_last_24_hours": "nelle ultime 24 ore",
|
||||
"for_last_hours": "per l'ultima {{count}} ora",
|
||||
"for_last_hours_plural": "per le ultime {{count}} ore",
|
||||
"for_last_days": "per gli ultimi {{count}} giorni",
|
||||
"for_last_days_plural": "per gli ultimi {{count}} giorni",
|
||||
"stats_disabled": "Le statistiche sono state disattivate. Puoi attivarle dalla <0>pagina delle impostazioni</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nessun dato upstream trovato",
|
||||
"number_of_dns_query_days": "Numero di richieste DNS elaborate negli ultimi {{count}} giorni",
|
||||
"number_of_dns_query_days_plural": "Numero di richieste DNS elaborate negli ultimi {{count}} giorni",
|
||||
"number_of_dns_query_24_hours": "Numero di richieste DNS elaborate nelle ultime 24 ore",
|
||||
"number_of_dns_query_hours": "Numero di richieste DNS processate nell'ultima {{count}} ora",
|
||||
"number_of_dns_query_hours_plural": "Numero di richieste DNS processate nelle ultime {{count}} ore",
|
||||
"number_of_dns_query_blocked_24_hours": "Numero di richieste DNS bloccate dai filtri per annunci e dagli elenchi di blocco host",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Numero di richieste DNS bloccate dal modulo sicurezza di navigazione di AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Numero di siti web per adulti bloccati",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "ブロックされたマルウェア/フィッシング",
|
||||
"stats_adult": "ブロックされたアダルトウェブサイト",
|
||||
"stats_query_domain": "最も問合せされたドメイン",
|
||||
"for_last_24_hours": "過去24時間以内",
|
||||
"for_last_hours": "過去{{count}}時間",
|
||||
"for_last_hours_plural": "過去{{count}}時間",
|
||||
"for_last_days": "過去{{count}}日間以内",
|
||||
"for_last_days_plural": "過去{{count}}日間以内",
|
||||
"stats_disabled": "統計は無効化されています。<0>設定ページ</0>でオンにすることができます。",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "アップストリームのデータが見つかりません",
|
||||
"number_of_dns_query_days": "過去{{count}}日間に処理されたDNSクエリの数",
|
||||
"number_of_dns_query_days_plural": "過去{{count}}日間に処理されたDNSクエリの数",
|
||||
"number_of_dns_query_24_hours": "過去24時間に処理されたDNSクエリの数",
|
||||
"number_of_dns_query_hours": "過去{{count}}時間に処理されたDNSクエリの数",
|
||||
"number_of_dns_query_hours_plural": "過去{{count}}時間に処理されたDNSクエリの数",
|
||||
"number_of_dns_query_blocked_24_hours": "広告ブロックフィルタとhostsブロックリストによってブロックされたDNSリクエストの数",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuardブラウジングセキュリティモジュールによってブロックされたDNSリクエストの数",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "ブロックされたアダルトウェブサイトの数",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "차단된 멀웨어/피싱",
|
||||
"stats_adult": "차단된 성인 웹사이트",
|
||||
"stats_query_domain": "쿼리 도메인",
|
||||
"for_last_24_hours": "지난 24시간 동안",
|
||||
"for_last_hours": "마지막 {{count}} 시간",
|
||||
"for_last_hours_plural": "마지막 {{count}} 시간의 기록",
|
||||
"for_last_days": "마지막 {{count}} 일",
|
||||
"for_last_days_plural": "마지막 {{count}} 일의 기록",
|
||||
"stats_disabled": "통계 기능이 꺼졌습니다. <0>설정 페이지</0>에서 켤 수 있습니다.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "업스트림 데이터 없음",
|
||||
"number_of_dns_query_days": "최근 {{count}}일 동안 처리된 DNS 쿼리의 수",
|
||||
"number_of_dns_query_days_plural": "최근 {{count}}일 동안 처리된 DNS 쿼리의 수",
|
||||
"number_of_dns_query_24_hours": "최근 24시간 동안 처리된 DNS 쿼리의 수",
|
||||
"number_of_dns_query_hours": "최근 {{count}}시간 동안 처리된 DNS 쿼리의 수",
|
||||
"number_of_dns_query_hours_plural": "최근 {{count}}시간 동안 처리된 DNS 쿼리의 수",
|
||||
"number_of_dns_query_blocked_24_hours": "광고 차단 필터 및 호스트 차단 목록에 의해 차단된 DNS 요청 수",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard 브라우징 보안 모듈에 의해 차단된 DNS 요청 수",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "차단된 성인 웹 사이트의 수",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Geblokkeerde malware/phishing",
|
||||
"stats_adult": "Geblokkeerde 18+ websites",
|
||||
"stats_query_domain": "Meest bezochte domeinen",
|
||||
"for_last_24_hours": "van de laatste 24-uur",
|
||||
"for_last_hours": "voor het afgelopen {{count}} uur",
|
||||
"for_last_hours_plural": "voor de afgelopen {{count}} uren",
|
||||
"for_last_days": "sinds de laatste {{count}} dagen",
|
||||
"for_last_days_plural": "sinds de laatste {{count}} dagen",
|
||||
"stats_disabled": "Statistieken zijn uitgeschakeld. Je kunt ze inschakelen op de <0>instellingen pagina</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Geen upstreams-gegevens gevonden",
|
||||
"number_of_dns_query_days": "Aantal verwerkte DNS aanvragen van de laatste {{count}} dag",
|
||||
"number_of_dns_query_days_plural": "Aantal verwerkte DNS aanvragen van de laatste {{count}} dagen",
|
||||
"number_of_dns_query_24_hours": "Aantal verwerkte DNS aanvragen van de laatste 24 uur",
|
||||
"number_of_dns_query_hours": "Het aantal DNS-verzoeken dat het afgelopen {{count}} uur is verwerkt",
|
||||
"number_of_dns_query_hours_plural": "Het aantal DNS-verzoeken dat de afgelopen {{count}} uren is verwerkt",
|
||||
"number_of_dns_query_blocked_24_hours": "Aantal geblokkeerde DNS aanvragen door advertentie blokkering en hosts blokkeerlijsten",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Aantal geblokkeerde DNS aanvragen door AdGuard browsing beveiligingsmodule",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Aantal geblokkeerde 18+ websites",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Zablokowane złośliwe oprogramowanie/phishing",
|
||||
"stats_adult": "Zablokowane witryny dla dorosłych",
|
||||
"stats_query_domain": "Najczęściej wyszukiwane domeny",
|
||||
"for_last_24_hours": "przez ostatnie 24 godziny",
|
||||
"for_last_hours": "w ciągu ostatniej {{count}} godziny",
|
||||
"for_last_hours_plural": "w ciągu ostatnich {{count}} godzin",
|
||||
"for_last_days": "za ostatni dzień {{count}}",
|
||||
"for_last_days_plural": "z ostatnich {{count}} dni",
|
||||
"stats_disabled": "Statystyki zostały wyłączone. Można je włączyć na <0>stronie ustawień</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Brak danych dotyczących serwerów nadrzędnych",
|
||||
"number_of_dns_query_days": "Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} dni",
|
||||
"number_of_dns_query_days_plural": "Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} dni",
|
||||
"number_of_dns_query_24_hours": "Liczba zapytań DNS przetworzonych w ciągu ostatnich 24 godzin",
|
||||
"number_of_dns_query_hours": "Liczba przetworzonych zapytań DNS w ciągu ostatniej {{count}} godziny",
|
||||
"number_of_dns_query_hours_plural": "Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} godzin",
|
||||
"number_of_dns_query_blocked_24_hours": "Liczba żądań DNS zablokowanych przez filtry blokowania reklam i listy zablokowanych hostów",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Liczba żądań DNS zablokowanych przez moduł Bezpiecznego przeglądania AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Liczba zablokowanych witryn dla dorosłych",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Bloqueado malware/phishing",
|
||||
"stats_adult": "Bloqueado sites adultos",
|
||||
"stats_query_domain": "Principais domínios consultados",
|
||||
"for_last_24_hours": "nas últimas 24 horas",
|
||||
"for_last_hours": "na última {{count}} hora",
|
||||
"for_last_hours_plural": "nas últimas {{count}} horas",
|
||||
"for_last_days": "nos últimos {{count}} dias",
|
||||
"for_last_days_plural": "nos últimos {{count}} dias",
|
||||
"stats_disabled": "As estatísticas foram desativadas. Você pode ligá-las através da <0>página de configurações</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nenhum dado de servidor DNS primário encontrado",
|
||||
"number_of_dns_query_days": "O número de consultas DNS processadas nos últimos {{count}} dias",
|
||||
"number_of_dns_query_days_plural": "Número de consultas DNS processadas nos últimos {{count}} dias",
|
||||
"number_of_dns_query_24_hours": "O número de consultas DNS processadas nas últimas 24 horas",
|
||||
"number_of_dns_query_hours": "Número de consultas DNS processadas durante a última {{count}} hora",
|
||||
"number_of_dns_query_hours_plural": "Número de consultas DNS processadas durante as últimas {{count}} horas",
|
||||
"number_of_dns_query_blocked_24_hours": "Várias solicitações DNS bloqueadas por filtros de bloqueio de anúncios e listas de bloqueio de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Várias solicitações de DNS bloqueadas pelo módulo de segurança da navegação do AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "O número de sites adultos bloqueados",
|
||||
@@ -689,7 +691,7 @@
|
||||
"log_and_stats_section_label": "Registro de consultas e estatísticas",
|
||||
"ignore_query_log": "Ignorar este cliente no registo de consultas",
|
||||
"ignore_statistics": "Ignorar este cliente nas estatísticas",
|
||||
"schedule_services": "Pausa o bloqueio de serviço",
|
||||
"schedule_services": "Pausar bloqueio de serviço",
|
||||
"schedule_services_desc": "Configura o agendamento de pausa do filtro de bloqueio de serviço",
|
||||
"schedule_services_desc_client": "Configura o agendamento de pausa do filtro de bloqueio de serviço para este cliente",
|
||||
"schedule_desc": "Define períodos de inatividade para serviços bloqueados",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Malware/phishing bloqueados",
|
||||
"stats_adult": "Sítios adultos bloqueados",
|
||||
"stats_query_domain": "Principais domínios consultados",
|
||||
"for_last_24_hours": "nas últimas 24 horas",
|
||||
"for_last_hours": "na última {{count}} hora",
|
||||
"for_last_hours_plural": "nas últimas {{count}} horas",
|
||||
"for_last_days": "nos últimos {{count}} dias",
|
||||
"for_last_days_plural": "nos últimos {{count}} dias",
|
||||
"stats_disabled": "As estatísticas foram desativadas. Você pode ligá-las através da <0>página de definições</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nenhum dado de servidor DNS primário encontrado",
|
||||
"number_of_dns_query_days": "Número de consultas DNS processadas durante los últimos {{count}} días",
|
||||
"number_of_dns_query_days_plural": "Número de consultas DNS processadas durante os últimos {{count}} dias",
|
||||
"number_of_dns_query_24_hours": "O número de consultas DNS processadas nas últimas 24 horas",
|
||||
"number_of_dns_query_hours": "Número de consultas DNS processadas durante a última {{count}} hora",
|
||||
"number_of_dns_query_hours_plural": "Número de consultas DNS processadas durante as últimas {{count}} horas",
|
||||
"number_of_dns_query_blocked_24_hours": "Várias solicitações DNS bloqueadas por filtros de bloqueio de anúncios e listas de bloqueio de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Várias solicitações de DNS bloqueadas pelo módulo de segurança da navegação do AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "O número de sítios adultos bloqueados",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Malware/phishing blocate",
|
||||
"stats_adult": "Site-uri cu conținut adult blocate",
|
||||
"stats_query_domain": "Domeniile cele mai căutate",
|
||||
"for_last_24_hours": "în ultimele 24 ore",
|
||||
"for_last_hours": "în ultima {{count}} oră",
|
||||
"for_last_hours_plural": "în ultimele {{count}} ore",
|
||||
"for_last_days": "în ultima {{count}} zi",
|
||||
"for_last_days_plural": "pentru ultimele {{count}} zile",
|
||||
"stats_disabled": "Statisticile au fost dezactivate. Puteți să le porniți din <0>pagina de setări</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nu există date despre serverele din amonte",
|
||||
"number_of_dns_query_days": "Numărul de interogări DNS procesate în ultima {{count}} zi",
|
||||
"number_of_dns_query_days_plural": "Numărul de interogări DNS procesate în ultimele {{count}} zile",
|
||||
"number_of_dns_query_24_hours": "Numărul de interogări DNS procesate în ultimele 24 de ore",
|
||||
"number_of_dns_query_hours": "Numărul de interogări DNS procesate în ultima {{count}} oră",
|
||||
"number_of_dns_query_hours_plural": "Numărul de interogări DNS procesate în ultimele {{count}} ore",
|
||||
"number_of_dns_query_blocked_24_hours": "Numărul de interogări DNS blocate de filtrele adblock și lista de blocări din hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Numărul de interogări DNS blocate de modulul de securitate de navigare AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Numărul de site-uri pentru adulți blocate",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты",
|
||||
"stats_adult": "Заблокированные «взрослые» сайты",
|
||||
"stats_query_domain": "Часто запрашиваемые домены",
|
||||
"for_last_24_hours": "за 24 часа",
|
||||
"for_last_hours": "за последний {{count}} час",
|
||||
"for_last_hours_plural": "за последние {{count}} часов",
|
||||
"for_last_days": "за последний {{count}} день",
|
||||
"for_last_days_plural": "за последние {{count}} дней",
|
||||
"stats_disabled": "Статистика отключена. Вы можете включить её на <0>странице настроек</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Нет данных об upstream-серверах",
|
||||
"number_of_dns_query_days": "Количество DNS-запросов за последний {{count}} день",
|
||||
"number_of_dns_query_days_plural": "Количество DNS запросов, обработанных за последние {{count}} дней",
|
||||
"number_of_dns_query_24_hours": "Количество DNS-запросов за последние 24 часа",
|
||||
"number_of_dns_query_hours": "Количество DNS-запросов, обработанных за последний {{count}} час",
|
||||
"number_of_dns_query_hours_plural": "Количество DNS-запросов, обработанных за последние {{count}} часов",
|
||||
"number_of_dns_query_blocked_24_hours": "Количество DNS-запросов, заблокированных фильтрами и блок-списками",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Количество заблокированных «сайтов для взрослых»",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Blokovaný škodlivý kód/pokus o podvod",
|
||||
"stats_adult": "Blokovaná stránka pre dospelých",
|
||||
"stats_query_domain": "Najčastejšie dopytované domény",
|
||||
"for_last_24_hours": "za posledných 24 hodín",
|
||||
"for_last_hours": "za poslednú {{count}} hodinu",
|
||||
"for_last_hours_plural": "za posledné {{count}} hodiny|za posledných {{count}} hodín",
|
||||
"for_last_days": "za posledný {{count}} deň",
|
||||
"for_last_days_plural": "za posledných {{count}} dní",
|
||||
"stats_disabled": "Štatistiky boli vypnuté. Môžete ich zapnúť na <0>stránke nastavení</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nenašli sa žiadne údaje o upstream serveroch",
|
||||
"number_of_dns_query_days": "Počet DNS dopytov spracovaných za posledný {{count}} deň",
|
||||
"number_of_dns_query_days_plural": "Počet DNS dopytov spracovaných za posledných {{count}} dní",
|
||||
"number_of_dns_query_24_hours": "Počet DNS dopytov spracovaných za posledných 24 hodín",
|
||||
"number_of_dns_query_hours": "Počet DNS dopytov spracovaných za poslednú {{count}} hodinu",
|
||||
"number_of_dns_query_hours_plural": "Počet DNS dopytov spracovaných za posledné {{count}} hodiny)|Počet DNS dopytov spracovaných za posledných {{count}} hodín",
|
||||
"number_of_dns_query_blocked_24_hours": "Počet DNS dopytov zablokovaných filtrami reklamy a zoznamami blokovaných hostov",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Počet DNS dopytov zablokovaných AdGuard modulom Bezpečné prehliadanie",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Počet zablokovaných stránok pre dospelých",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Onemogočeno zlonamernih programov/lažnih predstavljanj",
|
||||
"stats_adult": "Onemogočeno spletnih strani za odrasle",
|
||||
"stats_query_domain": "Najbolj poizvedovane domene",
|
||||
"for_last_24_hours": "v zadnjih 24 urah",
|
||||
"for_last_hours": "za zadnjo {{count}} uro",
|
||||
"for_last_hours_plural": "za zadnjih {{count}} ur",
|
||||
"for_last_days": "zadnjega {{count}} dne",
|
||||
"for_last_days_plural": "zadnjih {{count}} dni",
|
||||
"stats_disabled": "Statistika je onemogočena. Vklopite ga lahko na <0>strani z nastavitvami</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Ni podatkov o gorvodnih strežnikih",
|
||||
"number_of_dns_query_days": "Število obdelanih poizvedb DNS v zadnjem {{count}} dnevu",
|
||||
"number_of_dns_query_days_plural": "Število obdelanih poizvedb DNS v zadnjih {{count}} dneh",
|
||||
"number_of_dns_query_24_hours": "Število obdelanih poizvedb DNS v zadnjih 24 urah",
|
||||
"number_of_dns_query_hours": "Število poizvedb DNS, obdelanih v zadnji {{count}} uri",
|
||||
"number_of_dns_query_hours_plural": "Število poizvedb DNS, obdelanih v zadnjih {{count}} urah",
|
||||
"number_of_dns_query_blocked_24_hours": "Število zahtev DNS, ki so jih onemogočili filtri za zaviranje oglasov in seznami nedovoljenih, gostiteljev",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Število zahtev DNS, ki jih je blokiral AdGuard zaščitni modul brskanja",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Število onemogočenih spletnih strani za odrasle",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Blokiraj štetan softver i fišing",
|
||||
"stats_adult": "Blokiraj sajtove za odrasle",
|
||||
"stats_query_domain": "Najčešće unošeni domeni",
|
||||
"for_last_24_hours": "u poslednja 24 časa",
|
||||
"for_last_hours": "u poslednjih {{count}} sat",
|
||||
"for_last_hours_plural": "u poslednjih {{count}} sati",
|
||||
"for_last_days": "u poslednjih {{count}} dana",
|
||||
"for_last_days_plural": "u poslednjih {{count}} dana",
|
||||
"stats_disabled": "Statistika je isključena. Možete ga uključiti sa stranice <0>sa postavkama</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Nema podataka o upstream serverima",
|
||||
"number_of_dns_query_days": "Broj obrađenih DNS unosa u poslednjih {{count}} dan",
|
||||
"number_of_dns_query_days_plural": "Broj obrađenih DNS unosa u poslednjih {{count}} dana",
|
||||
"number_of_dns_query_24_hours": "Broj obrađenih DNS unosa u poslednja 24 časa",
|
||||
"number_of_dns_query_hours": "Broj obrađenih DNS unosa u poslednji {{count}} sat",
|
||||
"number_of_dns_query_hours_plural": "Broj obrađenih DNS unosa u poslednjih {{count}} sati",
|
||||
"number_of_dns_query_blocked_24_hours": "Broj DNS zahteva blokiranih od filtera blokatora reklama i blok liste hostova",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Broj DNS zahteva blokiranih od AdGuard-ovog podprograma za bezbedno pregledanje",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Broj blokiranih sajtova za odrasle",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Blockerad skadekod/phishing",
|
||||
"stats_adult": "Blockerade vuxensajter",
|
||||
"stats_query_domain": "Mest eftersökta domäner",
|
||||
"for_last_24_hours": "under de senaste 24 timmarna",
|
||||
"for_last_hours": "för den senaste {{count}} timme",
|
||||
"for_last_hours_plural": "för de senaste {{count}} timmar",
|
||||
"for_last_days": "för den senaste {{count}} dagen",
|
||||
"for_last_days_plural": "för de senaste {{count}} dagarna",
|
||||
"stats_disabled": "Statistiken har inaktiverats. Du kan aktivera det från <0>inställningssidan</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Inga uppströmsdata hittades",
|
||||
"number_of_dns_query_days": "Antalet DNS-förfrågningar som utfördes under senaste {{count}} dagen",
|
||||
"number_of_dns_query_days_plural": "Ett antal DNS förfrågningar utfördes under de senaste {{count}} dagarna",
|
||||
"number_of_dns_query_24_hours": "Antalet DNS-förfrågningar som utfördes under de senaste 24 timmarna",
|
||||
"number_of_dns_query_hours": "Ett antal DNS förfrågningar utfördes för den senaste {{count}} timme",
|
||||
"number_of_dns_query_hours_plural": "Ett antal DNS förfrågningar utfördes för den senaste {{count}} timmar",
|
||||
"number_of_dns_query_blocked_24_hours": "Antalet DNS-förfrågningar som blockerades av annonsfilter och värdens blockeringsklistor",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Antalet DNS-förfrågningar som blockerades av AdGuards modul för surfsäkerhet",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Antalet vuxensajter som blockerats",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Engellenen kötü amaçlı yazılım ve kimlik avı",
|
||||
"stats_adult": "Engellenen yetişkin içerikli siteler",
|
||||
"stats_query_domain": "Başlıca sorgulanan alan adları",
|
||||
"for_last_24_hours": "son 24 saat içindekiler",
|
||||
"for_last_hours": "son {{count}} saat için",
|
||||
"for_last_hours_plural": "son {{count}} saat için",
|
||||
"for_last_days": "son {{count}} gün boyunca",
|
||||
"for_last_days_plural": "son {{count}} gün boyunca",
|
||||
"stats_disabled": "İstatistikler devre dışı bırakıldı. Bunu, <0>ayarlar sayfasından</0> etkinleştirebilirsiniz.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Üst kaynak verisi bulunamadı",
|
||||
"number_of_dns_query_days": "Son {{count}} gün boyunca işlenen DNS sorgularının sayısı",
|
||||
"number_of_dns_query_days_plural": "Son {{count}} gün boyunca işlenen DNS sorgularının sayısı",
|
||||
"number_of_dns_query_24_hours": "Son 24 saat içinde işlenen DNS sorgularının sayısı",
|
||||
"number_of_dns_query_hours": "Son {{count}} saat için işlenen DNS sorgularının sayısı",
|
||||
"number_of_dns_query_hours_plural": "Son {{count}} saatiçin işlenen DNS sorgularının sayısı",
|
||||
"number_of_dns_query_blocked_24_hours": "Reklam engelleme filtreleri ve hosts engel listeleri tarafından engellenen DNS isteklerinin sayısı",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard gezinti koruması modülü tarafından engellenen DNS isteklerinin sayısı",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Engellenen yetişkin içerikli sitelerin sayısı",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Заблоковано зловмисних/шахрайських програм",
|
||||
"stats_adult": "Заблоковано вебсайтів для дорослих",
|
||||
"stats_query_domain": "Найчастіші запити доменів",
|
||||
"for_last_24_hours": "за останні 24 години",
|
||||
"for_last_hours": "за останню {{count}} годину",
|
||||
"for_last_hours_plural": "за останні {{count}} годин",
|
||||
"for_last_days": "за останній {{count}} день",
|
||||
"for_last_days_plural": "за останні {{count}} днів",
|
||||
"stats_disabled": "Статистику вимкнено. Ви можете увімкнути її на <0>сторінці налаштувань</0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Немає даних про upstream-сервери",
|
||||
"number_of_dns_query_days": "Кількість DNS-запитів, оброблених за останні {{count}} дні",
|
||||
"number_of_dns_query_days_plural": "Кількість DNS-запитів, оброблених за останні {{count}} днів",
|
||||
"number_of_dns_query_24_hours": "Кількість DNS-запитів, оброблених за останні 24 години",
|
||||
"number_of_dns_query_hours": "Кількість DNS-запитів, оброблених за останню {{count}} годину",
|
||||
"number_of_dns_query_hours_plural": "Кількість DNS-запитів, оброблених за останні {{count}} годин",
|
||||
"number_of_dns_query_blocked_24_hours": "Кількість DNS-запитів, заблокованих фільтрами і списками блокування hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Кількість DNS-запитів, заблокованих модулем «Безпека перегляду» AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Кількість заблокованих вебсайтів для дорослих",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "Mã độc/lừa đảo đã chặn",
|
||||
"stats_adult": "Website người lớn đã chặn",
|
||||
"stats_query_domain": "Tên miền truy vấn nhiều",
|
||||
"for_last_24_hours": "trong 24 giờ qua",
|
||||
"for_last_hours": "trong {{count}} giờ qua",
|
||||
"for_last_hours_plural": "trong {{count}} giờ qua",
|
||||
"for_last_days": "trong {{count}} ngày qua",
|
||||
"for_last_days_plural": "trong {{count}} ngày qua",
|
||||
"stats_disabled": "Số liệu thống kê đã bị vô hiệu hóa. Bạn có thể bật nó từ <0> trang cài đặt </0>.",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "Không tìm thấy dữ liệu máy chủ ngược dòng",
|
||||
"number_of_dns_query_days": "Một số truy vấn DNS được xử lý trong {{count}} ngày qua",
|
||||
"number_of_dns_query_days_plural": "Một số truy vấn DNS được xử lý trong {{count}} ngày qua",
|
||||
"number_of_dns_query_24_hours": "Số yêu cầu DNS đã xử lý trong 24 giờ qua",
|
||||
"number_of_dns_query_hours": "Một số truy vấn DNS được xử lý trong {{count}} giờ qua",
|
||||
"number_of_dns_query_hours_plural": "Một số truy vấn DNS được xử lý trong {{count}} giờ qua",
|
||||
"number_of_dns_query_blocked_24_hours": "Số yêu cầu DNS bị chặn bởi bộ lọc quảng cáo và danh sách chặn host",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Số yêu cầu DNS bị chặn bởi chế độ bảo vệ duyệt web AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Số trang web người lớn đã chặn",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "被拦截的恶意/钓鱼网站",
|
||||
"stats_adult": "被拦截的成人网站",
|
||||
"stats_query_domain": "请求域名排行",
|
||||
"for_last_24_hours": "在过去 24 小时",
|
||||
"for_last_hours": "最近 {{count}} 小时",
|
||||
"for_last_hours_plural": "最近 {{count}} 小时",
|
||||
"for_last_days": "最近 {{count}} 天",
|
||||
"for_last_days_plural": "最近 {{count}} 天",
|
||||
"stats_disabled": "已禁用统计数据。您可以从<0>设置页面</0>打开它。",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "未找到上游服务器数据",
|
||||
"number_of_dns_query_days": "过去 {{count}} 天内处理的 DNS 查询总数",
|
||||
"number_of_dns_query_days_plural": "在过去的 {{count}} 天内处理了多少个 DNS 查询",
|
||||
"number_of_dns_query_24_hours": "过去 24 小时内处理的 DNS 请求总数",
|
||||
"number_of_dns_query_hours": "最近 {{count}} 小时内处理的 DNS 查询次数",
|
||||
"number_of_dns_query_hours_plural": "最近 {{count}} 小时内处理的 DNS 查询次数",
|
||||
"number_of_dns_query_blocked_24_hours": "被广告过滤器和 Hosts 黑名单阻止的 DNS 请求总数",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "被 AdGuard 安全浏览模块阻止的 DNS 请求总数",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "被阻止的成人网站总数",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"stats_malware_phishing": "已封鎖的惡意軟體/網路釣魚",
|
||||
"stats_adult": "已封鎖的成人網站",
|
||||
"stats_query_domain": "熱門已查詢的網域",
|
||||
"for_last_24_hours": "在最近的 24 小時內",
|
||||
"for_last_hours": "在過去的 {{count}} 小時內",
|
||||
"for_last_hours_plural": "在過去的 {{count}} 小時內",
|
||||
"for_last_days": "在最近的 {{count}} 日內",
|
||||
"for_last_days_plural": "在最近的 {{count}} 日內",
|
||||
"stats_disabled": "該統計資料已被禁用。您可從<0>設定頁面</0>中打開它。",
|
||||
@@ -134,7 +135,8 @@
|
||||
"no_upstreams_data_found": "找不到上游伺服器資料",
|
||||
"number_of_dns_query_days": "在最近的 {{count}} 日內已處理的 DNS 查詢之數量",
|
||||
"number_of_dns_query_days_plural": "在最近的 {{count}} 日內已處理的 DNS 查詢之數量",
|
||||
"number_of_dns_query_24_hours": "在最近的 24 小時內已處理的 DNS 查詢之數量",
|
||||
"number_of_dns_query_hours": "過去 {{count}} 小時內處理的 DNS 查詢次數",
|
||||
"number_of_dns_query_hours_plural": "過去 {{count}} 小時內處理的 DNS 查詢次數",
|
||||
"number_of_dns_query_blocked_24_hours": "被廣告封鎖過濾器和主機封鎖清單阻擋的 DNS 請求之數量",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "被 AdGuard 瀏覽安全模組封鎖的 DNS 請求之數量",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "已封鎖的成人網站之數量",
|
||||
|
||||
@@ -118,6 +118,11 @@ body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-body--filters {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-body__item:not(:first-child) {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
@@ -134,15 +139,6 @@ body {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button-action {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.logs__row:hover .button-action,
|
||||
.button-action--active {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.ReactModal__Body--open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Cell from '../ui/Cell';
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
|
||||
const CountCell = (totalBlocked) => function cell(row) {
|
||||
const { value } = row;
|
||||
@@ -62,8 +62,8 @@ const BlockedDomains = ({
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_domains_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
|
||||
className="-highlight card-table-overflow--limited stats__table"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
@@ -9,10 +9,16 @@ import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
import { getPercent, sortIp } from '../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
DASHBOARD_TABLES_DEFAULT_PAGE_SIZE,
|
||||
STATUS_COLORS,
|
||||
TABLES_MIN_ROWS,
|
||||
} from '../../helpers/constants';
|
||||
import { toggleClientBlock } from '../../actions/access';
|
||||
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
|
||||
import { getStats } from '../../actions/stats';
|
||||
import IconTooltip from '../Logs/Cells/IconTooltip';
|
||||
|
||||
const getClientsPercentColor = (percent) => {
|
||||
if (percent > 50) {
|
||||
@@ -40,9 +46,7 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
const processingSet = useSelector((state) => state.access.processingSet);
|
||||
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
|
||||
|
||||
const buttonClass = classNames('button-action button-action--main', {
|
||||
'button-action--unblock': disallowed,
|
||||
});
|
||||
const [isOptionsOpened, setOptionsOpened] = useState(false);
|
||||
|
||||
const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
|
||||
let confirmMessage;
|
||||
@@ -62,23 +66,49 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onClick = () => toggleClientStatus(ip, disallowed, disallowed_rule);
|
||||
const onClick = () => {
|
||||
toggleClientStatus(ip, disallowed, disallowed_rule);
|
||||
setOptionsOpened(false);
|
||||
};
|
||||
|
||||
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
|
||||
const disabled = processingSet || lastRuleInAllowlist;
|
||||
return (
|
||||
<div className="table__action pl-4">
|
||||
<div className="table__action">
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
|
||||
className="btn btn-icon btn-sm px-0"
|
||||
onClick={() => setOptionsOpened(true)}
|
||||
>
|
||||
<Trans>{text}</Trans>
|
||||
<svg className="icon24 icon--lightgray button-action__icon">
|
||||
<use xlinkHref="#bullets" />
|
||||
</svg>
|
||||
</button>
|
||||
{isOptionsOpened && (
|
||||
<IconTooltip
|
||||
className="icon24"
|
||||
tooltipClass="button-action--arrow-option-container"
|
||||
xlinkHref="bullets"
|
||||
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
|
||||
content={(
|
||||
<button
|
||||
className={classNames('button-action--arrow-option px-4 py-1', disallowed ? 'bg--green' : 'bg--danger')}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
|
||||
>
|
||||
<Trans>{text}</Trans>
|
||||
</button>
|
||||
)}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
onVisibilityChange={setOptionsOpened}
|
||||
defaultTooltipShown={true}
|
||||
delayHide={0}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -134,8 +164,8 @@ const Clients = ({
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_clients_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
|
||||
className="-highlight card-table-overflow--limited clients__table"
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import round from 'lodash/round';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import Card from '../ui/Card';
|
||||
import { formatNumber } from '../../helpers/helpers';
|
||||
import { formatNumber, msToDays, msToHours } from '../../helpers/helpers';
|
||||
import LogsSearchLink from '../ui/LogsSearchLink';
|
||||
import { RESPONSE_FILTER, DAY } from '../../helpers/constants';
|
||||
import { RESPONSE_FILTER, TIME_UNITS } from '../../helpers/constants';
|
||||
import Tooltip from '../ui/Tooltip';
|
||||
|
||||
const Row = ({
|
||||
@@ -52,14 +52,19 @@ const Counters = ({ refreshButton, subtitle }) => {
|
||||
numReplacedParental,
|
||||
numReplacedSafesearch,
|
||||
avgProcessingTime,
|
||||
timeUnits,
|
||||
} = useSelector((state) => state.stats, shallowEqual);
|
||||
const { t } = useTranslation();
|
||||
const days = interval / DAY;
|
||||
|
||||
const dnsQueryTooltip = timeUnits === TIME_UNITS.HOURS
|
||||
? t('number_of_dns_query_hours', { count: msToHours(interval) })
|
||||
: t('number_of_dns_query_days', { count: msToDays(interval) });
|
||||
|
||||
const rows = [
|
||||
{
|
||||
label: 'dns_query',
|
||||
count: numDnsQueries,
|
||||
tooltipTitle: days === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: days }),
|
||||
tooltipTitle: dnsQueryTooltip,
|
||||
response_status: RESPONSE_FILTER.ALL.QUERY,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,11 +28,14 @@
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
|
||||
@media (max-width: 1279.98px) {
|
||||
.table__action {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.table__action {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.table__action .btn-icon {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.page-title--dashboard {
|
||||
|
||||
@@ -7,7 +7,7 @@ import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
|
||||
const getQueriedPercentColor = (percent) => {
|
||||
@@ -58,8 +58,8 @@ const QueriedDomains = ({
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_domains_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
|
||||
className="-highlight card-table-overflow--limited stats__table"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import DomainCell from './DomainCell';
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
|
||||
const TimeCell = ({ value }) => {
|
||||
if (!value) {
|
||||
@@ -62,8 +63,8 @@ const UpstreamAvgTime = ({
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_upstreams_data_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
|
||||
className="-highlight card-table-overflow--limited stats__table"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -8,7 +8,7 @@ import Cell from '../ui/Cell';
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
|
||||
const CountCell = (totalBlocked) => (
|
||||
function cell(row) {
|
||||
@@ -64,8 +64,8 @@ const UpstreamResponses = ({
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_upstreams_data_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
|
||||
className="-highlight card-table-overflow--limited stats__table"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -9,7 +9,12 @@ import Counters from './Counters';
|
||||
import Clients from './Clients';
|
||||
import QueriedDomains from './QueriedDomains';
|
||||
import BlockedDomains from './BlockedDomains';
|
||||
import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS } from '../../helpers/constants';
|
||||
import {
|
||||
DISABLE_PROTECTION_TIMINGS,
|
||||
ONE_SECOND_IN_MS,
|
||||
SETTINGS_URLS,
|
||||
TIME_UNITS,
|
||||
} from '../../helpers/constants';
|
||||
import {
|
||||
msToSeconds,
|
||||
msToMinutes,
|
||||
@@ -46,15 +51,12 @@ const Dashboard = ({
|
||||
getAllStats();
|
||||
}, []);
|
||||
const getSubtitle = () => {
|
||||
const ONE_DAY = 1;
|
||||
const intervalInDays = msToDays(stats.interval);
|
||||
|
||||
if (intervalInDays < ONE_DAY) {
|
||||
if (!stats.enabled) {
|
||||
return t('stats_disabled_short');
|
||||
}
|
||||
|
||||
return intervalInDays === ONE_DAY
|
||||
? t('for_last_24_hours')
|
||||
return stats.timeUnits === TIME_UNITS.HOURS
|
||||
? t('for_last_hours', { count: msToHours(stats.interval) })
|
||||
: t('for_last_days', { count: msToDays(stats.interval) });
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const renderIcons = (iconsData) => iconsData.map(({
|
||||
}) => <a key={iconName} href={href} target="_blank" rel="noopener noreferrer"
|
||||
className={classNames('d-flex align-items-center', className)}
|
||||
>
|
||||
<svg className="nav-icon nav-icon--gray">
|
||||
<svg className="icon icon--15 mr-1 icon--gray">
|
||||
<use xlinkHref={`#${iconName}`} />
|
||||
</svg>
|
||||
</a>);
|
||||
@@ -110,7 +110,7 @@ const Form = (props) => {
|
||||
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="modal-body modal-body--medium">
|
||||
<div className="modal-body modal-body--filters">
|
||||
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE
|
||||
&& <div className="d-flex justify-content-around">
|
||||
<button onClick={openFilteringListModal}
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { sortIp } from '../../../helpers/helpers';
|
||||
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||
import { MODAL_TYPE, TABLES_MIN_ROWS } from '../../../helpers/constants';
|
||||
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
|
||||
|
||||
class Table extends Component {
|
||||
@@ -88,7 +88,7 @@ class Table extends Component {
|
||||
showPagination
|
||||
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE) || 10}
|
||||
onPageSizeChange={(size) => LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE, size)}
|
||||
minRows={5}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
|
||||
@@ -26,9 +26,7 @@ const ClientCell = ({
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||
const processingRules = useSelector((state) => state.filtering.processingRules);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
const processingSet = useSelector((state) => state.access.processingSet);
|
||||
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
|
||||
const [isOptionsOpened, setOptionsOpened] = useState(false);
|
||||
|
||||
@@ -84,11 +82,23 @@ const ClientCell = ({
|
||||
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
|
||||
const clientNameBlockingFor = getBlockingClientName(clients, client);
|
||||
|
||||
const onClick = async () => {
|
||||
await dispatch(toggleBlocking(buttonType, domain));
|
||||
await dispatch(getStats());
|
||||
setOptionsOpened(false);
|
||||
};
|
||||
|
||||
const BUTTON_OPTIONS = [
|
||||
{
|
||||
name: buttonType,
|
||||
onClick,
|
||||
className: isFiltered ? 'bg--green' : 'bg--danger',
|
||||
},
|
||||
{
|
||||
name: blockingForClientKey,
|
||||
onClick: () => {
|
||||
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
|
||||
setOptionsOpened(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -101,27 +111,25 @@ const ClientCell = ({
|
||||
client_info?.disallowed_rule || '',
|
||||
));
|
||||
await dispatch(updateLogs());
|
||||
setOptionsOpened(false);
|
||||
}
|
||||
},
|
||||
disabled: processingSet || lastRuleInAllowlist,
|
||||
disabled: lastRuleInAllowlist,
|
||||
},
|
||||
];
|
||||
|
||||
const onClick = async () => {
|
||||
await dispatch(toggleBlocking(buttonType, domain));
|
||||
await dispatch(getStats());
|
||||
};
|
||||
|
||||
const getOptions = (options) => {
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{options.map(({ name, onClick, disabled }) => (
|
||||
{options.map(({
|
||||
name, onClick, disabled, className,
|
||||
}) => (
|
||||
<button
|
||||
key={name}
|
||||
className="button-action--arrow-option px-4 py-1"
|
||||
className={classNames('button-action--arrow-option px-4 py-1', className)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -134,17 +142,6 @@ const ClientCell = ({
|
||||
|
||||
const content = getOptions(BUTTON_OPTIONS);
|
||||
|
||||
const buttonClass = classNames('button-action button-action--main', {
|
||||
'button-action--unblock': isFiltered,
|
||||
'button-action--with-options': content,
|
||||
'button-action--active': isOptionsOpened,
|
||||
});
|
||||
|
||||
const buttonArrowClass = classNames('button-action button-action--arrow', {
|
||||
'button-action--unblock': isFiltered,
|
||||
'button-action--active': isOptionsOpened,
|
||||
});
|
||||
|
||||
const containerClass = classNames('button-action__container', {
|
||||
'button-action__container--detailed': isDetailed,
|
||||
});
|
||||
@@ -153,25 +150,26 @@ const ClientCell = ({
|
||||
<div className={containerClass}>
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
className="btn btn-icon btn-sm px-0"
|
||||
onClick={() => setOptionsOpened(true)}
|
||||
>
|
||||
{t(buttonType)}
|
||||
<svg className="icon24 icon--lightgray button-action__icon">
|
||||
<use xlinkHref="#bullets" />
|
||||
</svg>
|
||||
</button>
|
||||
{content && (
|
||||
<button className={buttonArrowClass} disabled={processingRules}>
|
||||
<IconTooltip
|
||||
className="icon24"
|
||||
tooltipClass="button-action--arrow-option-container"
|
||||
xlinkHref="chevron-down"
|
||||
triggerClass="button-action--icon"
|
||||
content={content}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
onVisibilityChange={setOptionsOpened}
|
||||
/>
|
||||
</button>
|
||||
{isOptionsOpened && (
|
||||
<IconTooltip
|
||||
className="icon24"
|
||||
tooltipClass="button-action--arrow-option-container"
|
||||
xlinkHref="bullets"
|
||||
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
|
||||
content={content}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
onVisibilityChange={setOptionsOpened}
|
||||
defaultTooltipShown={true}
|
||||
delayHide={0}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -198,7 +196,7 @@ const ClientCell = ({
|
||||
</div>
|
||||
{isDetailed && clientName && !whoisAvailable && (
|
||||
<Link
|
||||
className="detailed-info d-none d-sm-block logs__text logs__text--link"
|
||||
className="detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client"
|
||||
to={`logs?search="${encodeURIComponent(clientName)}"`}
|
||||
title={clientName}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.tooltip-custom__container {
|
||||
min-width: 150px;
|
||||
padding: 1rem 1.5rem 1.25rem 1.5rem;
|
||||
font-size: 16px !important;
|
||||
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
@@ -21,6 +21,8 @@ const IconTooltip = ({
|
||||
content,
|
||||
trigger,
|
||||
onVisibilityChange,
|
||||
defaultTooltipShown,
|
||||
delayHide,
|
||||
renderContent = content ? React.Children.map(
|
||||
processContent(content),
|
||||
(item, idx) => <div key={idx} className={contentItemClass}>
|
||||
@@ -44,6 +46,8 @@ const IconTooltip = ({
|
||||
trigger={trigger}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
|
||||
delayHide={delayHide}
|
||||
defaultTooltipShown={defaultTooltipShown}
|
||||
>
|
||||
{xlinkHref && <svg className={className}>
|
||||
<use xlinkHref={`#${xlinkHref}`} />
|
||||
@@ -65,6 +69,8 @@ IconTooltip.propTypes = {
|
||||
content: PropTypes.node,
|
||||
renderContent: PropTypes.arrayOf(PropTypes.element),
|
||||
onVisibilityChange: PropTypes.func,
|
||||
defaultTooltipShown: PropTypes.bool,
|
||||
delayHide: PropTypes.number,
|
||||
};
|
||||
|
||||
export default IconTooltip;
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
||||
.logs__table .logs__text--client {
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.icon--selected {
|
||||
background-color: var(--gray-f3);
|
||||
border: solid 1px var(--gray-d8);
|
||||
@@ -261,9 +265,8 @@
|
||||
.button-action__container {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: 2px;
|
||||
bottom: 0.5rem;
|
||||
height: 1.6rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
@@ -307,45 +310,10 @@
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.button-action--arrow {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 1px solid var(--white);
|
||||
width: 1.5625rem;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-action:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-action--arrow .button-action--icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-action:active {
|
||||
background: var(--btn-block-active);
|
||||
}
|
||||
|
||||
.button-action--unblock:active {
|
||||
background: var(--btn-unblock-active);
|
||||
}
|
||||
|
||||
.button-action:disabled {
|
||||
background: var(--btn-block-disabled);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.button-action--unblock:disabled {
|
||||
background: var(--btn-unblock-disabled);
|
||||
}
|
||||
|
||||
.button-action--arrow-option {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
@@ -551,3 +519,20 @@
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--card-bgcolor);
|
||||
}
|
||||
|
||||
.button-action__hidden-trigger {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 33px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .button-action__icon {
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import whoisCell from './whoisCell';
|
||||
import LogsSearchLink from '../../ui/LogsSearchLink';
|
||||
import { sortIp } from '../../../helpers/helpers';
|
||||
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
|
||||
import { TABLES_MIN_ROWS } from '../../../helpers/constants';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 200;
|
||||
|
||||
@@ -90,7 +91,7 @@ class AutoClients extends Component {
|
||||
onPageSizeChange={(size) => (
|
||||
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE, size)
|
||||
)}
|
||||
minRows={5}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
sortIp,
|
||||
getService,
|
||||
} from '../../../../helpers/helpers';
|
||||
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE, TABLES_MIN_ROWS } from '../../../../helpers/constants';
|
||||
import Card from '../../../ui/Card';
|
||||
import CellWrap from '../../../ui/CellWrap';
|
||||
import LogsSearchLink from '../../../ui/LogsSearchLink';
|
||||
@@ -347,7 +347,7 @@ const ClientsTable = ({
|
||||
onPageSizeChange={(size) => (
|
||||
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE, size)
|
||||
)}
|
||||
minRows={5}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
|
||||
@@ -137,6 +137,22 @@ const Examples = (props) => (
|
||||
example_upstream_reserved
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<code>[/example.local/]94.140.14.140 2a10:50c0::1:ff</code>: <Trans
|
||||
components={[
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
Link
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
example_multiple_upstreams_reserved
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<code>{COMMENT_LINE_DEFAULT_TOKEN} comment</code>: <Trans>
|
||||
example_upstream_comment
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
@@ -19,8 +20,16 @@
|
||||
max-height: 17.5rem;
|
||||
}
|
||||
|
||||
.dashboard .card-table {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard .card-table-overflow--limited {
|
||||
max-height: 18rem;
|
||||
max-height: 292px;
|
||||
}
|
||||
|
||||
.dashboard .ReactTable .rt-tr-group {
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
@@ -125,7 +134,7 @@
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.dashboard .card:not(.card--full) {
|
||||
height: 22rem;
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,3 +149,7 @@
|
||||
.card .logs__row--blue {
|
||||
background-color: #ecf7ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .card .logs__row--blue {
|
||||
background-color: var(--logs__row--blue-bgcolor);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.icon--15 {
|
||||
--size: 0.95rem;
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.icon--gray {
|
||||
color: var(--gray-a5);
|
||||
}
|
||||
|
||||
@@ -239,6 +239,12 @@ const Icons = () => (
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M16.1215 12.1213H11.8789V7.87866" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="bullets" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 7C11.1716 7 10.5 6.32843 10.5 5.5C10.5 4.67157 11.1716 4 12 4C12.8284 4 13.5 4.67157 13.5 5.5C13.5 6.32843 12.8284 7 12 7Z" fill="currentColor" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 13.5C11.1716 13.5 10.5 12.8284 10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5Z" fill="currentColor" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 20C11.1716 20 10.5 19.3284 10.5 18.5C10.5 17.6716 11.1716 17 12 17C12.8284 17 13.5 17.6716 13.5 18.5C13.5 19.3284 12.8284 20 12 20Z" fill="currentColor" />
|
||||
</symbol>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import addDays from 'date-fns/add_days';
|
||||
import addHours from 'date-fns/add_hours';
|
||||
import subDays from 'date-fns/sub_days';
|
||||
import subHours from 'date-fns/sub_hours';
|
||||
import dateFormat from 'date-fns/format';
|
||||
@@ -9,12 +8,14 @@ import round from 'lodash/round';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Line.css';
|
||||
import { msToDays } from '../../helpers/helpers';
|
||||
import { msToDays, msToHours } from '../../helpers/helpers';
|
||||
import { TIME_UNITS } from '../../helpers/constants';
|
||||
|
||||
const Line = ({
|
||||
data, color = 'black',
|
||||
}) => {
|
||||
const interval = msToDays(useSelector((state) => state.stats.interval));
|
||||
const interval = useSelector((state) => state.stats.interval);
|
||||
const timeUnits = useSelector((state) => state.stats.timeUnits);
|
||||
|
||||
return <ResponsiveLine
|
||||
enableArea
|
||||
@@ -44,12 +45,12 @@ const Line = ({
|
||||
enableGridY={false}
|
||||
enablePoints={false}
|
||||
xFormat={(x) => {
|
||||
if (interval >= 0 && interval <= 7) {
|
||||
const hoursAgo = subHours(Date.now(), 24 * interval);
|
||||
return dateFormat(addHours(hoursAgo, x), 'D MMM HH:00');
|
||||
if (timeUnits === TIME_UNITS.HOURS) {
|
||||
const hoursAgo = msToHours(interval) - x - 1;
|
||||
return dateFormat(subHours(Date.now(), hoursAgo), 'D MMM HH:00');
|
||||
}
|
||||
|
||||
const daysAgo = subDays(Date.now(), interval - 1);
|
||||
const daysAgo = subDays(Date.now(), msToDays(interval) - 1);
|
||||
return dateFormat(addDays(daysAgo, x), 'D MMM YYYY');
|
||||
}}
|
||||
yFormat={(y) => round(y, 2)}
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ReactTable .rt-tbody {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ReactTable .rt-noData {
|
||||
color: var(--rt-nodata-color);
|
||||
background-color: var(--rt-nodata-bgcolor);
|
||||
|
||||
@@ -21,6 +21,7 @@ const Tooltip = ({
|
||||
delayShow = SHOW_TOOLTIP_DELAY,
|
||||
delayHide = HIDE_TOOLTIP_DELAY,
|
||||
onVisibilityChange,
|
||||
defaultTooltipShown,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const touchEventsAvailable = 'ontouchstart' in window;
|
||||
@@ -75,6 +76,7 @@ const Tooltip = ({
|
||||
delayShow={delayShowValue}
|
||||
tooltip={renderTooltip}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
defaultTooltipShown={defaultTooltipShown}
|
||||
>
|
||||
{renderTrigger}
|
||||
</TooltipTrigger>
|
||||
@@ -97,6 +99,7 @@ Tooltip.propTypes = {
|
||||
className: propTypes.string,
|
||||
triggerClass: propTypes.string,
|
||||
onVisibilityChange: propTypes.func,
|
||||
defaultTooltipShown: propTypes.bool,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
@@ -554,3 +554,12 @@ export const DISABLE_PROTECTION_TIMINGS = {
|
||||
};
|
||||
|
||||
export const LOCAL_TIMEZONE_VALUE = 'Local';
|
||||
|
||||
export const TABLES_MIN_ROWS = 5;
|
||||
|
||||
export const DASHBOARD_TABLES_DEFAULT_PAGE_SIZE = 100;
|
||||
|
||||
export const TIME_UNITS = {
|
||||
HOURS: 'hours',
|
||||
DAYS: 'days',
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export const renderFormattedClientCell = (value, info, isDetailed = false, isLog
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
if (name) {
|
||||
const nameValue = <div className="logs__text logs__text--link logs__text--nowrap" title={`${name} (${value})`}>
|
||||
const nameValue = <div className="logs__text logs__text--link logs__text--nowrap logs__text--client" title={`${name} (${value})`}>
|
||||
{name} <small>{`(${value})`}</small>
|
||||
</div>;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"timeUpdated": "2023-10-08T00:09:45.691Z",
|
||||
"timeUpdated": "2023-10-15T12:13:01.838Z",
|
||||
"categories": {
|
||||
"0": "audio_video_player",
|
||||
"1": "comments",
|
||||
@@ -243,7 +243,7 @@
|
||||
"name": "Australian Broadcasting Corporation",
|
||||
"categoryId": 8,
|
||||
"url": "https://www.abc.net.au/",
|
||||
"companyId": "abc",
|
||||
"companyId": "australian_government",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"ablida": {
|
||||
@@ -3593,6 +3593,13 @@
|
||||
"url": null,
|
||||
"companyId": null
|
||||
},
|
||||
"bom": {
|
||||
"name": "Bureau of Meteorology",
|
||||
"categoryId": 9,
|
||||
"url": "http://bom.gov.au/",
|
||||
"companyId": "australian_government",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"bombora": {
|
||||
"name": "Bombora",
|
||||
"categoryId": 6,
|
||||
@@ -5943,6 +5950,20 @@
|
||||
"url": "https://discordapp.com/",
|
||||
"companyId": null
|
||||
},
|
||||
"disneyplus": {
|
||||
"name": "Disney+",
|
||||
"categoryId": 0,
|
||||
"url": "https://www.disneyplus.com/",
|
||||
"companyId": "disney",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"disneystreaming": {
|
||||
"name": "Disney Streaming",
|
||||
"categoryId": 0,
|
||||
"url": "https://press.disneyplus.com",
|
||||
"companyId": "disney",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"display_block": {
|
||||
"name": "display block",
|
||||
"categoryId": 4,
|
||||
@@ -6160,6 +6181,13 @@
|
||||
"url": "http://www.drawbrid.ge/",
|
||||
"companyId": "drawbridge"
|
||||
},
|
||||
"dreame_tech": {
|
||||
"name": "Dreame Technology",
|
||||
"categoryId": 8,
|
||||
"url": "https://www.dreame.tech/",
|
||||
"companyId": "xiaomi",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"dreamlab.pl": {
|
||||
"name": "DreamLab.pl",
|
||||
"categoryId": 4,
|
||||
@@ -10372,7 +10400,7 @@
|
||||
"name": "Let's Encrypt",
|
||||
"categoryId": 5,
|
||||
"url": "https://letsencrypt.org/",
|
||||
"companyId": "lets_encrypt",
|
||||
"companyId": "isrg",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"letv": {
|
||||
@@ -19444,6 +19472,13 @@
|
||||
"companyId": "xhamster",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"xiaomi": {
|
||||
"name": "Xiaomi",
|
||||
"categoryId": 8,
|
||||
"url": "https://www.mi.com/",
|
||||
"companyId": "xiaomi",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"xing": {
|
||||
"name": "Xing",
|
||||
"categoryId": 6,
|
||||
@@ -20047,8 +20082,8 @@
|
||||
"2leep.com": "2leep",
|
||||
"33across.com": "33across",
|
||||
"3dstats.com": "3dstats",
|
||||
"3gppnetwork.org": "3gpp",
|
||||
"3gpp.org": "3gpp",
|
||||
"3gppnetwork.org": "3gpp",
|
||||
"4cdn.org": "4chan",
|
||||
"4finance.com": "4finance_com",
|
||||
"4wnet.com": "4w_marketplace",
|
||||
@@ -20069,7 +20104,6 @@
|
||||
"aaxads.com": "aaxads.com",
|
||||
"abtasty.com": "ab_tasty",
|
||||
"d1447tq2m68ekg.cloudfront.net": "ab_tasty",
|
||||
"abc.net.au": "abc",
|
||||
"ab.co": "abc",
|
||||
"abc-cdn.net.au": "abc",
|
||||
"abc-host.net": "abc",
|
||||
@@ -20077,6 +20111,7 @@
|
||||
"abc-prod.net.au": "abc",
|
||||
"abc-stage.net.au": "abc",
|
||||
"abc-test.net.au": "abc",
|
||||
"abc.net.au": "abc",
|
||||
"abcaustralia.net.au": "abc",
|
||||
"abcradio.net.au": "abc",
|
||||
"ablida.de": "ablida",
|
||||
@@ -20238,17 +20273,17 @@
|
||||
"adgorithms.com": "adgorithms",
|
||||
"adgoto.com": "adgoto",
|
||||
"adguard.com": "adguard",
|
||||
"adtidy.org": "adguard",
|
||||
"agrd.io": "adguard",
|
||||
"adguard.app": "adguard",
|
||||
"adguard.info": "adguard",
|
||||
"adguard.io": "adguard",
|
||||
"adguard.org": "adguard",
|
||||
"adtidy.org": "adguard",
|
||||
"agrd.io": "adguard",
|
||||
"adguard-dns.com": "adguard_dns",
|
||||
"adguard-dns.io": "adguard_dns",
|
||||
"adguardvpn.com": "adguard_vpn",
|
||||
"adguard-vpn.com": "adguard_vpn",
|
||||
"adguard-vpn.online": "adguard_vpn",
|
||||
"adguardvpn.com": "adguard_vpn",
|
||||
"adhands.ru": "adhands",
|
||||
"adhese.be": "adhese",
|
||||
"adhese.com": "adhese",
|
||||
@@ -20264,9 +20299,9 @@
|
||||
"cdn.adjs.net": "adjs",
|
||||
"adjug.com": "adjug",
|
||||
"adjust.com": "adjust",
|
||||
"adjust.net.in": "adjust",
|
||||
"adj.st": "adjust",
|
||||
"adjust.io": "adjust",
|
||||
"adjust.net.in": "adjust",
|
||||
"adjust.world": "adjust",
|
||||
"apptrace.com": "adjust",
|
||||
"adk2.com": "adk2",
|
||||
@@ -20616,17 +20651,17 @@
|
||||
"amazon.fr": "amazon",
|
||||
"amazon.it": "amazon",
|
||||
"d3io1k5o0zdpqr.cloudfront.net": "amazon",
|
||||
"amazoncrl.com": "amazon",
|
||||
"aamazoncognito.com": "amazon",
|
||||
"amazonbrowserapp.es": "amazon",
|
||||
"amazonbrowserapp.co.uk": "amazon",
|
||||
"amazon.sa": "amazon",
|
||||
"amazon.nl": "amazon",
|
||||
"amazon.in": "amazon",
|
||||
"amazon.com.mx": "amazon",
|
||||
"amazon.com.au": "amazon",
|
||||
"amazon-corp.com": "amazon",
|
||||
"a2z.com": "amazon",
|
||||
"aamazoncognito.com": "amazon",
|
||||
"amazon-corp.com": "amazon",
|
||||
"amazon.com.au": "amazon",
|
||||
"amazon.com.mx": "amazon",
|
||||
"amazon.in": "amazon",
|
||||
"amazon.nl": "amazon",
|
||||
"amazon.sa": "amazon",
|
||||
"amazonbrowserapp.co.uk": "amazon",
|
||||
"amazonbrowserapp.es": "amazon",
|
||||
"amazoncrl.com": "amazon",
|
||||
"firetvcaptiveportal.com": "amazon",
|
||||
"amazon-adsystem.com": "amazon_adsystem",
|
||||
"serving-sys.com": "amazon_adsystem",
|
||||
@@ -20692,31 +20727,32 @@
|
||||
"eum-appdynamics.com": "appdynamics",
|
||||
"jscdn.appier.net": "appier",
|
||||
"apple.com": "apple",
|
||||
"me.com": "apple",
|
||||
"apple.news": "apple",
|
||||
"apple-dns.net": "apple",
|
||||
"aaplimg.com": "apple",
|
||||
"icloud.com": "apple",
|
||||
"itunes.com": "apple",
|
||||
"icloud-content.com": "apple",
|
||||
"mzstatic.com": "apple",
|
||||
"cdn-apple.com": "apple",
|
||||
"apple-mapkit.com": "apple",
|
||||
"icons.axm-usercontent-apple.com": "apple",
|
||||
"apple-cloudkit.com": "apple",
|
||||
"apzones.com": "apple",
|
||||
"apple-dns.net": "apple",
|
||||
"apple-livephotoskit.com": "apple",
|
||||
"apple-mapkit.com": "apple",
|
||||
"apple.news": "apple",
|
||||
"apzones.com": "apple",
|
||||
"cdn-apple.com": "apple",
|
||||
"icloud-content.com": "apple",
|
||||
"icloud.com": "apple",
|
||||
"icons.axm-usercontent-apple.com": "apple",
|
||||
"itunes.com": "apple",
|
||||
"me.com": "apple",
|
||||
"mzstatic.com": "apple",
|
||||
"safebrowsing.apple": "apple",
|
||||
"safebrowsing.g.applimg.com": "apple",
|
||||
"iadsdk.apple.com": "apple_ads",
|
||||
"applifier.com": "applifier",
|
||||
"assets.applovin.com": "applovin",
|
||||
"applvn.com": "applovin",
|
||||
"applovin.com": "applovin",
|
||||
"applvn.com": "applovin",
|
||||
"appmetrx.com": "appmetrx",
|
||||
"adnxs.com": "appnexus",
|
||||
"adnxs.net": "appnexus",
|
||||
"appsflyer.com": "appsflyer",
|
||||
"appsflyersdk.com": "appsflyer",
|
||||
"adne.tv": "apptv",
|
||||
"readserver.net": "apptv",
|
||||
"www.apture.com": "apture",
|
||||
@@ -20926,6 +20962,7 @@
|
||||
"secure.apps.shappify.com": "bold",
|
||||
"boldchat.com": "boldchat",
|
||||
"boltdns.net": "boltdns.net",
|
||||
"bom.gov.au": "bom",
|
||||
"ml314.com": "bombora",
|
||||
"bongacams.com": "bongacams.com",
|
||||
"bonial.com": "bonial",
|
||||
@@ -21170,8 +21207,8 @@
|
||||
"cloud-media.fr": "cloud-media.fr",
|
||||
"cloudflare.com": "cloudflare",
|
||||
"cloudflare.net": "cloudflare",
|
||||
"cloudflare-dns.com": "cloudflare",
|
||||
"cloudflare-dm-cmpimg.com": "cloudflare",
|
||||
"cloudflare-dns.com": "cloudflare",
|
||||
"cloudflare-ipfs.com": "cloudflare",
|
||||
"cloudflare-quic.com": "cloudflare",
|
||||
"cloudflare-terms-of-service-abuse.com": "cloudflare",
|
||||
@@ -21184,8 +21221,10 @@
|
||||
"cloudflareresolve.com": "cloudflare",
|
||||
"cloudflaressl.com": "cloudflare",
|
||||
"cloudflarestatus.com": "cloudflare",
|
||||
"cloudflarestream.com": "cloudflare",
|
||||
"pacloudflare.com": "cloudflare",
|
||||
"sn-cloudflare.com": "cloudflare",
|
||||
"videodelivery.net": "cloudflare",
|
||||
"cloudimg.io": "cloudimage.io",
|
||||
"cloudinary.com": "cloudinary",
|
||||
"clovenetwork.com": "clove_network",
|
||||
@@ -21454,6 +21493,10 @@
|
||||
"directadvert.ru": "directadvert",
|
||||
"directrev.com": "directrev",
|
||||
"discordapp.com": "discord",
|
||||
"disneyplus.com": "disneyplus",
|
||||
"bamgrid.com": "disneystreaming",
|
||||
"dssedge.com": "disneystreaming",
|
||||
"dssott.com": "disneystreaming",
|
||||
"d81mfvml8p5ml.cloudfront.net": "display_block",
|
||||
"disqus.com": "disqus",
|
||||
"disquscdn.com": "disqus",
|
||||
@@ -21497,6 +21540,8 @@
|
||||
"doubleverify.com": "doubleverify",
|
||||
"wrating.com": "dratio",
|
||||
"adsymptotic.com": "drawbridge",
|
||||
"dreame.tech": "dreame_tech",
|
||||
"dreametech.com": "dreame_tech",
|
||||
"dreamlab.pl": "dreamlab.pl",
|
||||
"drift.com": "drift",
|
||||
"js.driftt.com": "drift",
|
||||
@@ -21717,18 +21762,18 @@
|
||||
"findologic.com": "findologic.com",
|
||||
"app-measurement.com": "firebase",
|
||||
"fcm.googleapis.com": "firebase",
|
||||
"firebaseappcheck.googleapis.com": "firebase",
|
||||
"firebaseapp.com": "firebase",
|
||||
"firebase.com": "firebase",
|
||||
"firebasedynamiclinks.googleapis.com": "firebase",
|
||||
"firebase.google.com": "firebase",
|
||||
"firebase.googleapis.com": "firebase",
|
||||
"firebaseapp.com": "firebase",
|
||||
"firebaseappcheck.googleapis.com": "firebase",
|
||||
"firebasedynamiclinks-ipv4.googleapis.com": "firebase",
|
||||
"firebasedynamiclinks-ipv6.googleapis.com": "firebase",
|
||||
"firebase.googleapis.com": "firebase",
|
||||
"firebase.google.com": "firebase",
|
||||
"firebasedynamiclinks.googleapis.com": "firebase",
|
||||
"firebaseinappmessaging.googleapis.com": "firebase",
|
||||
"firebaseinstallations.googleapis.com": "firebase",
|
||||
"firebaselogging.googleapis.com": "firebase",
|
||||
"firebaselogging-pa.googleapis.com": "firebase",
|
||||
"firebaselogging.googleapis.com": "firebase",
|
||||
"firebaseperusertopics-pa.googleapis.com": "firebase",
|
||||
"firebaseremoteconfig.googleapis.com": "firebase",
|
||||
"firebaseio.com": "firebaseio.com",
|
||||
@@ -21806,9 +21851,9 @@
|
||||
"freegeoip.net": "freegeoip_net",
|
||||
"freenet.de": "freenet_de",
|
||||
"freent.de": "freenet_de",
|
||||
"freeviewaustralia.tv": "freeview",
|
||||
"freeview.com.au": "freeview",
|
||||
"freeview.com": "freeview",
|
||||
"freeview.com.au": "freeview",
|
||||
"freeviewaustralia.tv": "freeview",
|
||||
"fwmrm.net": "freewheel",
|
||||
"heimdall.fresh8.co": "fresh8",
|
||||
"d36mpcpuzc4ztk.cloudfront.net": "freshdesk",
|
||||
@@ -21958,7 +22003,6 @@
|
||||
"google.ae": "google",
|
||||
"google.al": "google",
|
||||
"google.am": "google",
|
||||
"googleapis.cn": "google",
|
||||
"google.as": "google",
|
||||
"google.az": "google",
|
||||
"google.ba": "google",
|
||||
@@ -21981,11 +22025,20 @@
|
||||
"google.co.bw": "google",
|
||||
"google.co.ck": "google",
|
||||
"google.co.cr": "google",
|
||||
"googlecode.com": "google",
|
||||
"google.co.il": "google",
|
||||
"google.co.ke": "google",
|
||||
"google.co.kr": "google",
|
||||
"google.co.ls": "google",
|
||||
"google.co.mz": "google",
|
||||
"google.co.nz": "google",
|
||||
"google.co.tz": "google",
|
||||
"google.co.ug": "google",
|
||||
"google.co.uz": "google",
|
||||
"google.co.ve": "google",
|
||||
"google.co.vi": "google",
|
||||
"google.co.za": "google",
|
||||
"google.co.zm": "google",
|
||||
"google.co.zw": "google",
|
||||
"google.com.af": "google",
|
||||
"google.com.ag": "google",
|
||||
"google.com.ai": "google",
|
||||
@@ -22033,20 +22086,9 @@
|
||||
"google.com.uy": "google",
|
||||
"google.com.vc": "google",
|
||||
"google.com.vn": "google",
|
||||
"google.co.mz": "google",
|
||||
"google.co.nz": "google",
|
||||
"google.co.tz": "google",
|
||||
"google.co.ug": "google",
|
||||
"google.co.uz": "google",
|
||||
"google.co.ve": "google",
|
||||
"google.co.vi": "google",
|
||||
"google.co.za": "google",
|
||||
"google.co.zm": "google",
|
||||
"google.co.zw": "google",
|
||||
"google.cv": "google",
|
||||
"google.dj": "google",
|
||||
"google.dm": "google",
|
||||
"googledownloads.cn": "google",
|
||||
"google.ee": "google",
|
||||
"google.fm": "google",
|
||||
"google.ga": "google",
|
||||
@@ -22088,7 +22130,6 @@
|
||||
"google.net": "google",
|
||||
"google.nr": "google",
|
||||
"google.nu": "google",
|
||||
"googleoptimize.com": "google",
|
||||
"google.org": "google",
|
||||
"google.pn": "google",
|
||||
"google.ps": "google",
|
||||
@@ -22112,8 +22153,12 @@
|
||||
"google.us": "google",
|
||||
"google.vg": "google",
|
||||
"google.vu": "google",
|
||||
"googleweblight.in": "google",
|
||||
"google.ws": "google",
|
||||
"googleapis.cn": "google",
|
||||
"googlecode.com": "google",
|
||||
"googledownloads.cn": "google",
|
||||
"googleoptimize.com": "google",
|
||||
"googleweblight.in": "google",
|
||||
"googlezip.net": "google",
|
||||
"gstatic.cn": "google",
|
||||
"news.google.com": "google",
|
||||
@@ -22140,10 +22185,10 @@
|
||||
"alt7-mtalk.google.com": "google_chat",
|
||||
"alt8-mtalk.google.com": "google_chat",
|
||||
"chat.google.com": "google_chat",
|
||||
"mobile-gtalk4.l.google.com": "google_chat",
|
||||
"mobile-gtalk.l.google.com": "google_chat",
|
||||
"mtalk4.google.com": "google_chat",
|
||||
"mobile-gtalk4.l.google.com": "google_chat",
|
||||
"mtalk.google.com": "google_chat",
|
||||
"mtalk4.google.com": "google_chat",
|
||||
"talk.google.com": "google_chat",
|
||||
"talk.l.google.com": "google_chat",
|
||||
"talkx.l.google.com": "google_chat",
|
||||
@@ -22163,10 +22208,10 @@
|
||||
"mail-ads.google.com": "google_email",
|
||||
"fonts.googleapis.com": "google_fonts",
|
||||
"cloudfunctions.net": "google_hosted",
|
||||
"ghs46.googlehosted.com": "google_hosted",
|
||||
"ghs4.googlehosted.com": "google_hosted",
|
||||
"ghs6.googlehosted.com": "google_hosted",
|
||||
"ghs.googlehosted.com": "google_hosted",
|
||||
"ghs4.googlehosted.com": "google_hosted",
|
||||
"ghs46.googlehosted.com": "google_hosted",
|
||||
"ghs6.googlehosted.com": "google_hosted",
|
||||
"googlehosted.l.googleusercontent.com": "google_hosted",
|
||||
"run.app": "google_hosted",
|
||||
"supl.google.com": "google_location",
|
||||
@@ -22180,9 +22225,9 @@
|
||||
"maps.google.ca": "google_maps",
|
||||
"maps.google.ch": "google_maps",
|
||||
"maps.google.co.jp": "google_maps",
|
||||
"maps.google.co.uk": "google_maps",
|
||||
"maps.google.com": "google_maps",
|
||||
"maps.google.com.mx": "google_maps",
|
||||
"maps.google.co.uk": "google_maps",
|
||||
"maps.google.es": "google_maps",
|
||||
"maps.google.se": "google_maps",
|
||||
"maps.gstatic.com": "google_maps",
|
||||
@@ -22190,6 +22235,8 @@
|
||||
"adservice.google.ca": "google_marketing",
|
||||
"adservice.google.co.in": "google_marketing",
|
||||
"adservice.google.co.kr": "google_marketing",
|
||||
"adservice.google.co.uk": "google_marketing",
|
||||
"adservice.google.co.za": "google_marketing",
|
||||
"adservice.google.com": "google_marketing",
|
||||
"adservice.google.com.ar": "google_marketing",
|
||||
"adservice.google.com.au": "google_marketing",
|
||||
@@ -22203,8 +22250,6 @@
|
||||
"adservice.google.com.tr": "google_marketing",
|
||||
"adservice.google.com.tw": "google_marketing",
|
||||
"adservice.google.com.vn": "google_marketing",
|
||||
"adservice.google.co.uk": "google_marketing",
|
||||
"adservice.google.co.za": "google_marketing",
|
||||
"adservice.google.de": "google_marketing",
|
||||
"adservice.google.dk": "google_marketing",
|
||||
"adservice.google.es": "google_marketing",
|
||||
@@ -22219,17 +22264,17 @@
|
||||
"googlesyndication-cn.com": "google_marketing",
|
||||
"duo.google.com": "google_meet",
|
||||
"hangouts.clients6.google.com": "google_meet",
|
||||
"hangouts.googleapis.com": "google_meet",
|
||||
"hangouts.google.com": "google_meet",
|
||||
"hangouts.googleapis.com": "google_meet",
|
||||
"meet.google.com": "google_meet",
|
||||
"meetings.googleapis.com": "google_meet",
|
||||
"stun1.l.google.com": "google_meet",
|
||||
"stun.l.google.com": "google_meet",
|
||||
"stun1.l.google.com": "google_meet",
|
||||
"ggpht.com": "google_photos",
|
||||
"play-fe.googleapis.com": "google_play",
|
||||
"play.googleapis.com": "google_play",
|
||||
"play.google.com": "google_play",
|
||||
"play-lh.googleusercontent.com": "google_play",
|
||||
"play.google.com": "google_play",
|
||||
"play.googleapis.com": "google_play",
|
||||
"1e100cdn.net": "google_servers",
|
||||
"gvt1.com": "google_servers",
|
||||
"gvt2.com": "google_servers",
|
||||
@@ -22531,9 +22576,9 @@
|
||||
"iprom.net": "iprom",
|
||||
"ipromote.com": "ipromote",
|
||||
"clickmanage.com": "iprospect",
|
||||
"qy.net": "iqiyi",
|
||||
"iqiyi.com": "iqiyi",
|
||||
"iq.com": "iqiyi",
|
||||
"iqiyi.com": "iqiyi",
|
||||
"qy.net": "iqiyi",
|
||||
"addelive.com": "ironsource",
|
||||
"afdads.com": "ironsource",
|
||||
"delivery47.com": "ironsource",
|
||||
@@ -22772,11 +22817,11 @@
|
||||
"footprint.net": "level3_communications",
|
||||
"alphonso.tv": "lgads",
|
||||
"lgads.tv": "lgads",
|
||||
"lg.com": "lgtv",
|
||||
"lge.com": "lgtv",
|
||||
"lgsmartad.com": "lgtv",
|
||||
"lgtvcommon.com": "lgtv",
|
||||
"lgtvsdp.com": "lgtv",
|
||||
"lge.com": "lgtv",
|
||||
"lg.com": "lgtv",
|
||||
"licensebuttons.net": "licensebuttons.net",
|
||||
"lfstmedia.com": "lifestreet_media",
|
||||
"content-recommendation.net": "ligatus",
|
||||
@@ -22971,8 +23016,8 @@
|
||||
"s1.mediaad.org": "mediaad",
|
||||
"mlnadvertising.com": "mediaglu",
|
||||
"fhserve.com": "mediahub",
|
||||
"medialab.la": "medialab",
|
||||
"media-lab.ai": "medialab",
|
||||
"medialab.la": "medialab",
|
||||
"adnet.ru": "medialand",
|
||||
"medialand.ru": "medialand",
|
||||
"medialead.de": "medialead",
|
||||
@@ -23352,12 +23397,12 @@
|
||||
"opinary.com": "opinary",
|
||||
"opinionbar.com": "opinionbar",
|
||||
"emagazines.com": "oplytic",
|
||||
"oppomobile.com": "oppo",
|
||||
"heytapmobi.com": "oppo",
|
||||
"heytapmobile.com": "oppo",
|
||||
"heytapdl.com": "oppo",
|
||||
"allawnos.com": "oppo",
|
||||
"allawntech.com": "oppo",
|
||||
"heytapdl.com": "oppo",
|
||||
"heytapmobi.com": "oppo",
|
||||
"heytapmobile.com": "oppo",
|
||||
"oppomobile.com": "oppo",
|
||||
"opta.net": "opta.net",
|
||||
"optaim.com": "optaim",
|
||||
"cookielaw.org": "optanaon",
|
||||
@@ -23484,8 +23529,9 @@
|
||||
"loveadvert.ru": "play_by_mamba",
|
||||
"playbuzz.com": "playbuzz.com",
|
||||
"pof.com": "plenty_of_fish",
|
||||
"plex.tv": "plex",
|
||||
"plex.bz": "plex",
|
||||
"plex.direct": "plex",
|
||||
"plex.tv": "plex",
|
||||
"analytics.plex.tv": "plex_metrics",
|
||||
"metrics.plex.tv": "plex_metrics",
|
||||
"plista.com": "plista",
|
||||
@@ -23896,6 +23942,7 @@
|
||||
"samsungsds.com": "samsungsds",
|
||||
"internetat.tv": "samsungtv",
|
||||
"samsungcloud.tv": "samsungtv",
|
||||
"tizenservice.com": "samsungtv",
|
||||
"ilsemedia.nl": "sanoma.fi",
|
||||
"sanoma.fi": "sanoma.fi",
|
||||
"d13im3ek7neeqp.cloudfront.net": "sap_crm",
|
||||
@@ -24000,12 +24047,12 @@
|
||||
"cdn.shopify.com": "shopify",
|
||||
"myshopify.com": "shopify",
|
||||
"shop.app": "shopify",
|
||||
"shopifyapps.com": "shopify",
|
||||
"shopifycdn.net": "shopify",
|
||||
"shopify.co.za": "shopify",
|
||||
"shopify.com.au": "shopify",
|
||||
"shopify.com.mx": "shopify",
|
||||
"shopify.co.za": "shopify",
|
||||
"shopify.dev": "shopify",
|
||||
"shopifyapps.com": "shopify",
|
||||
"shopifycdn.net": "shopify",
|
||||
"shopifynetwork.com": "shopify",
|
||||
"shopifypreview.com": "shopify",
|
||||
"shopifysvc.com": "shopify_stats",
|
||||
@@ -24042,8 +24089,8 @@
|
||||
"pages04.net": "silverpop",
|
||||
"pages05.net": "silverpop",
|
||||
"similardeals.net": "similardeals.net",
|
||||
"similarweb.io": "similarweb",
|
||||
"similarweb.com": "similarweb",
|
||||
"similarweb.io": "similarweb",
|
||||
"d8rk54i4mohrb.cloudfront.net": "simplereach",
|
||||
"simplereach.com": "simplereach",
|
||||
"simpli.fi": "simpli.fi",
|
||||
@@ -24082,10 +24129,10 @@
|
||||
"skypeassets.com": "skype",
|
||||
"skysa.com": "skysa",
|
||||
"skyscnr.com": "skyscnr.com",
|
||||
"slack.com": "slack",
|
||||
"slackb.com": "slack",
|
||||
"slack-edge.com": "slack",
|
||||
"slack-imgs.com": "slack",
|
||||
"slack.com": "slack",
|
||||
"slackb.com": "slack",
|
||||
"slashdot.org": "slashdot_widget",
|
||||
"sleeknotestaticcontent.sleeknote.com": "sleeknote",
|
||||
"resultspage.com": "sli_systems",
|
||||
@@ -24356,8 +24403,8 @@
|
||||
"teaser.cc": "teaser.cc",
|
||||
"emailretargeting.com": "tedemis",
|
||||
"tracking.dsmmadvantage.com": "teletech",
|
||||
"telstra.com.au": "telstra",
|
||||
"telstra.com": "telstra",
|
||||
"telstra.com.au": "telstra",
|
||||
"tenderapp.com": "tender",
|
||||
"tensitionschoo.club": "tensitionschoo.club",
|
||||
"watch.teroti.com": "teroti",
|
||||
@@ -24728,9 +24775,9 @@
|
||||
"tools.vpscash.nl": "vpscash",
|
||||
"vsassets.io": "vs",
|
||||
"exp-tas.com": "vscode",
|
||||
"vscode-unpkg.net": "vscode",
|
||||
"v0cdn.net": "vscode",
|
||||
"vscode-cdn.net": "vscode",
|
||||
"vscode-unpkg.net": "vscode",
|
||||
"vtracy.de": "vtracy.de",
|
||||
"liftoff.io": "vungle",
|
||||
"vungle.com": "vungle",
|
||||
@@ -24803,8 +24850,8 @@
|
||||
"wetter.com": "wetter_com",
|
||||
"wettercomassets.com": "wetter_com",
|
||||
"whatsbroadcast.com": "whatbroadcast",
|
||||
"whatsapp.net": "whatsapp",
|
||||
"whatsapp.com": "whatsapp",
|
||||
"whatsapp.net": "whatsapp",
|
||||
"whisper.onelink.me": "whisper",
|
||||
"whisper.sh": "whisper",
|
||||
"amung.us": "whos.amung.us",
|
||||
@@ -24876,6 +24923,13 @@
|
||||
"xhamsterlive.com": "xhamster",
|
||||
"xhamsterpremium.com": "xhamster",
|
||||
"xhcdn.com": "xhamster",
|
||||
"huami.com": "xiaomi",
|
||||
"mi-img.com": "xiaomi",
|
||||
"mi.com": "xiaomi",
|
||||
"miui.com": "xiaomi",
|
||||
"xiaomi.com": "xiaomi",
|
||||
"xiaomi.net": "xiaomi",
|
||||
"xiaomiyoupin.com": "xiaomi",
|
||||
"xing-share.com": "xing",
|
||||
"xing.com": "xing",
|
||||
"xmediaclicks.com": "xmediaclicks",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { normalizeTopClients } from '../helpers/helpers';
|
||||
import { DAY, HOUR, STATS_INTERVALS_DAYS } from '../helpers/constants';
|
||||
import {
|
||||
DAY,
|
||||
HOUR,
|
||||
STATS_INTERVALS_DAYS,
|
||||
TIME_UNITS,
|
||||
} from '../helpers/constants';
|
||||
|
||||
import * as actions from '../actions/stats';
|
||||
|
||||
@@ -18,6 +23,7 @@ const defaultStats = {
|
||||
numReplacedSafebrowsing: 0,
|
||||
numReplacedSafesearch: 0,
|
||||
avgProcessingTime: 0,
|
||||
timeUnits: TIME_UNITS.HOURS,
|
||||
};
|
||||
|
||||
const stats = handleActions(
|
||||
@@ -60,6 +66,7 @@ const stats = handleActions(
|
||||
avg_processing_time: avgProcessingTime,
|
||||
top_upstreams_responses: topUpstreamsResponses,
|
||||
top_upstrems_avg_time: topUpstreamsAvgTime,
|
||||
time_units: timeUnits,
|
||||
} = payload;
|
||||
|
||||
const newState = {
|
||||
@@ -81,6 +88,7 @@ const stats = handleActions(
|
||||
avgProcessingTime,
|
||||
topUpstreamsResponses,
|
||||
topUpstreamsAvgTime,
|
||||
timeUnits,
|
||||
};
|
||||
|
||||
return newState;
|
||||
|
||||
12
go.mod
12
go.mod
@@ -3,9 +3,9 @@ module github.com/AdguardTeam/AdGuardHome
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.56.1
|
||||
github.com/AdguardTeam/golibs v0.17.1
|
||||
github.com/AdguardTeam/urlfilter v0.17.0
|
||||
github.com/AdguardTeam/dnsproxy v0.56.3
|
||||
github.com/AdguardTeam/golibs v0.17.2
|
||||
github.com/AdguardTeam/urlfilter v0.17.3
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.7
|
||||
github.com/bluele/gcache v0.0.2
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/renameio/v2 v2.0.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||
github.com/kardianos/service v1.2.2
|
||||
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
||||
@@ -27,9 +27,9 @@ require (
|
||||
// own code for that. Perhaps, use gopacket.
|
||||
github.com/mdlayher/raw v0.1.0
|
||||
github.com/miekg/dns v1.1.56
|
||||
github.com/quic-go/quic-go v0.39.0
|
||||
github.com/quic-go/quic-go v0.39.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/ti-mo/netfilter v0.5.0
|
||||
github.com/ti-mo/netfilter v0.5.1
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,9 +1,9 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.56.1 h1:QltfyWO7k4mxWERCEYDzkQnKzvZX/zkneWjbuJ0TU6o=
|
||||
github.com/AdguardTeam/dnsproxy v0.56.1/go.mod h1:fqmehcE3cHFNqKbWQpIjGk7GqBy7ur1v5At499lFjRc=
|
||||
github.com/AdguardTeam/golibs v0.17.1 h1:j3Ehhld5GI/amcHYG+CF0sJ4OOzAQ06BY3N/iBYJZ1M=
|
||||
github.com/AdguardTeam/golibs v0.17.1/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U=
|
||||
github.com/AdguardTeam/urlfilter v0.17.0 h1:tUzhtR9wMx704GIP3cibsDQJrixlMHfwoQbYJfPdFow=
|
||||
github.com/AdguardTeam/urlfilter v0.17.0/go.mod h1:bbuZjPUzm/Ip+nz5qPPbwIP+9rZyQbQad8Lt/0fCulU=
|
||||
github.com/AdguardTeam/dnsproxy v0.56.3 h1:WP1FooLfZQPHEH2SuwMtJsOurDt32rubGx0OddcsKT0=
|
||||
github.com/AdguardTeam/dnsproxy v0.56.3/go.mod h1:ZvkbM71HwpilgkCnTubDiR4Ba6x5Qvnhy2iasMWaTDM=
|
||||
github.com/AdguardTeam/golibs v0.17.2 h1:vg6wHMjUKscnyPGRvxS5kAt7Uw4YxcJiITZliZ476W8=
|
||||
github.com/AdguardTeam/golibs v0.17.2/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U=
|
||||
github.com/AdguardTeam/urlfilter v0.17.3 h1:fg/ObbnO0Cv6aw0tW6N/ETDMhhNvmcUUOZ7HlmKC3rw=
|
||||
github.com/AdguardTeam/urlfilter v0.17.3/go.mod h1:Jru7jFfeH2CoDf150uDs+rRYcZBzHHBz05r9REyDKyE=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
@@ -49,8 +49,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a h1:S33o3djA1nPRd+d/bf7jbbXytXuK/EoXow7+aa76grQ=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
@@ -94,8 +94,8 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So=
|
||||
github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
|
||||
github.com/quic-go/quic-go v0.39.2 h1:hmwAf8zAHlvan0Y5PXxeeBFZEW17IW99sXLry8I2kjk=
|
||||
github.com/quic-go/quic-go v0.39.2/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -105,8 +105,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
|
||||
github.com/ti-mo/netfilter v0.5.0 h1:MZmsUw5bFRecOb0AeyjOPxTHg4UxYzyEs0Ek/6Lxoy8=
|
||||
github.com/ti-mo/netfilter v0.5.0/go.mod h1:nt+8B9hx/QpqHr7Hazq+2qMCCA8u2OTkyc/7+U9ARz8=
|
||||
github.com/ti-mo/netfilter v0.5.1 h1:cqamEd1c1zmpfpqvInLOro0Znq/RAfw2QL5wL2rAR/8=
|
||||
github.com/ti-mo/netfilter v0.5.1/go.mod h1:h9UPQ3ZrTZGBitay+LETMxZvNgWGK/efTUcqES2YiLw=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
|
||||
|
||||
@@ -182,6 +182,7 @@ func (s *Server) accessListJSON() (j accessListJSON) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleAccessList handles requests to the GET /control/access/list endpoint.
|
||||
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
|
||||
aghhttp.WriteJSONResponseOK(w, r, s.accessListJSON())
|
||||
}
|
||||
@@ -224,6 +225,7 @@ func validateStrUniq(clients []string) (uc aghalg.UniqChecker[string], err error
|
||||
return uc, uc.Validate()
|
||||
}
|
||||
|
||||
// handleAccessSet handles requests to the POST /control/access/set endpoint.
|
||||
func (s *Server) handleAccessSet(w http.ResponseWriter, r *http.Request) {
|
||||
list := &accessListJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&list)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
@@ -151,6 +152,8 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||
// DNS-over-HTTPS requests, it will return the hostname part of the Host header
|
||||
// if there is one.
|
||||
func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string, err error) {
|
||||
from := "tls conn"
|
||||
|
||||
switch proto {
|
||||
case proxy.ProtoHTTPS:
|
||||
r := pctx.HTTPRequest
|
||||
@@ -164,6 +167,7 @@ func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string
|
||||
}
|
||||
|
||||
srvName = host
|
||||
from = "host header"
|
||||
}
|
||||
case proxy.ProtoQUIC:
|
||||
qConn := pctx.QUICConnection
|
||||
@@ -183,5 +187,7 @@ func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string
|
||||
srvName = tc.ConnectionState().ServerName
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: got client server name %q from %s", srvName, from)
|
||||
|
||||
return srvName, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
@@ -444,19 +446,10 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, u := range upstreams {
|
||||
var ups string
|
||||
var domains []string
|
||||
ups, domains, err = separateUpstream(u)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = validateUpstream(ups, domains)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating upstream %q: %w", u, err)
|
||||
}
|
||||
err = validateUpstreamConfig(upstreams)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf, err = proxy.ParseUpstreamsConfig(
|
||||
@@ -467,6 +460,7 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
} else if len(conf.Upstreams) == 0 {
|
||||
return nil, errors.Error("no default upstreams specified")
|
||||
@@ -475,6 +469,31 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// validateUpstreamConfig validates each upstream from the upstream
|
||||
// configuration and returns an error if any upstream is invalid.
|
||||
//
|
||||
// TODO(e.burkov): Move into aghnet or even into dnsproxy.
|
||||
func validateUpstreamConfig(conf []string) (err error) {
|
||||
for _, u := range conf {
|
||||
var ups []string
|
||||
var domains []string
|
||||
ups, domains, err = separateUpstream(u)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range ups {
|
||||
_, err = validateUpstream(addr, len(domains) > 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating upstream %q: %w", addr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpstreams validates each upstream and returns an error if any
|
||||
// upstream is invalid or if there are no default upstreams specified.
|
||||
//
|
||||
@@ -534,14 +553,14 @@ var protocols = []string{
|
||||
}
|
||||
|
||||
// validateUpstream returns an error if u alongside with domains is not a valid
|
||||
// upstream configuration. useDefault is true if the upstream is
|
||||
// upstream configuration. usesDefault is true if the upstream is
|
||||
// domain-specific and is configured to point at the default upstream server
|
||||
// which is validated separately. The upstream is considered domain-specific
|
||||
// only if domains is at least not nil.
|
||||
func validateUpstream(u string, domains []string) (useDefault bool, err error) {
|
||||
// which is validated separately. specific reflects if the upstream is
|
||||
// domain-specific.
|
||||
func validateUpstream(u string, specific bool) (usesDefault bool, err error) {
|
||||
// The special server address '#' means that default server must be used.
|
||||
if useDefault = u == "#" && domains != nil; useDefault {
|
||||
return useDefault, nil
|
||||
if u == "#" && specific {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check if the upstream has a valid protocol prefix.
|
||||
@@ -567,12 +586,12 @@ func validateUpstream(u string, domains []string) (useDefault bool, err error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// separateUpstream returns the upstream and the specified domains. domains is
|
||||
// nil when the upstream is not domains-specific. Otherwise it may also be
|
||||
// separateUpstream returns the upstreams and the specified domains. domains
|
||||
// is nil when the upstream is not domains-specific. Otherwise it may also be
|
||||
// empty.
|
||||
func separateUpstream(upstreamStr string) (ups string, domains []string, err error) {
|
||||
func separateUpstream(upstreamStr string) (upstreams, domains []string, err error) {
|
||||
if !strings.HasPrefix(upstreamStr, "[/") {
|
||||
return upstreamStr, nil, nil
|
||||
return []string{upstreamStr}, nil, nil
|
||||
}
|
||||
|
||||
defer func() { err = errors.Annotate(err, "bad upstream for domain %q: %w", upstreamStr) }()
|
||||
@@ -582,9 +601,9 @@ func separateUpstream(upstreamStr string) (ups string, domains []string, err err
|
||||
case 2:
|
||||
// Go on.
|
||||
case 1:
|
||||
return "", nil, errors.Error("missing separator")
|
||||
return nil, nil, errors.Error("missing separator")
|
||||
default:
|
||||
return "", []string{}, errors.Error("duplicated separator")
|
||||
return nil, nil, errors.Error("duplicated separator")
|
||||
}
|
||||
|
||||
for i, host := range strings.Split(parts[0], "/") {
|
||||
@@ -594,21 +613,22 @@ func separateUpstream(upstreamStr string) (ups string, domains []string, err err
|
||||
|
||||
err = netutil.ValidateDomainName(strings.TrimPrefix(host, "*."))
|
||||
if err != nil {
|
||||
return "", domains, fmt.Errorf("domain at index %d: %w", i, err)
|
||||
return nil, nil, fmt.Errorf("domain at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
domains = append(domains, host)
|
||||
}
|
||||
|
||||
return parts[1], domains, nil
|
||||
return strings.Fields(parts[1]), domains, nil
|
||||
}
|
||||
|
||||
// healthCheckFunc is a signature of function to check if upstream exchanges
|
||||
// properly.
|
||||
type healthCheckFunc func(u upstream.Upstream) (err error)
|
||||
|
||||
// checkDNSUpstreamExc checks if the DNS upstream exchanges correctly.
|
||||
func checkDNSUpstreamExc(u upstream.Upstream) (err error) {
|
||||
// checkExchange is a [healthCheckFunc] that checks if the DNS upstream
|
||||
// exchanges correctly.
|
||||
func checkExchange(u upstream.Upstream) (err error) {
|
||||
// testTLD is the special-use fully-qualified domain name for testing the
|
||||
// DNS server reachability.
|
||||
//
|
||||
@@ -638,11 +658,11 @@ func checkDNSUpstreamExc(u upstream.Upstream) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPrivateUpstreamExc checks if the upstream for resolving private
|
||||
// addresses exchanges correctly.
|
||||
// checkPrivateExchange is a [healthCheckFunc] that checks if the upstream for
|
||||
// resolving private addresses exchanges correctly.
|
||||
//
|
||||
// TODO(e.burkov): Think about testing the ip6.arpa. as well.
|
||||
func checkPrivateUpstreamExc(u upstream.Upstream) (err error) {
|
||||
func checkPrivateExchange(u upstream.Upstream) (err error) {
|
||||
// inAddrArpaTLD is the special-use fully-qualified domain name for PTR IP
|
||||
// address resolution.
|
||||
//
|
||||
@@ -683,75 +703,153 @@ func (err domainSpecificTestError) Error() (msg string) {
|
||||
return fmt.Sprintf("WARNING: %s", err.error)
|
||||
}
|
||||
|
||||
// parseUpstreamLine parses line and creates the [upstream.Upstream] using opts
|
||||
// and information from [s.dnsFilter.EtcHosts]. It returns an error if the line
|
||||
// is not a valid upstream line, see [upstream.AddressToUpstream]. It's a
|
||||
// caller's responsibility to close u.
|
||||
func (s *Server) parseUpstreamLine(
|
||||
line string,
|
||||
opts *upstream.Options,
|
||||
) (u upstream.Upstream, specific bool, err error) {
|
||||
// Separate upstream from domains list.
|
||||
upstreamAddr, domains, err := separateUpstream(line)
|
||||
// checkUpstreamAddr creates the upstream using opts and, possibly, information
|
||||
// from system hosts files, then checks if the DNS upstream exchanges correctly.
|
||||
// It returns an error if addr is not valid DNS upstream address or the upstream
|
||||
// is not exchanging correctly.
|
||||
//
|
||||
// TODO(e.burkov): Remove the receiver.
|
||||
func (s *Server) checkUpstreamAddr(
|
||||
addr string,
|
||||
specific bool,
|
||||
basicOpts *upstream.Options,
|
||||
check healthCheckFunc,
|
||||
) (err error) {
|
||||
usesDefault, err := validateUpstream(addr, specific)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("wrong upstream format: %w", err)
|
||||
}
|
||||
|
||||
specific = len(domains) > 0
|
||||
|
||||
useDefault, err := validateUpstream(upstreamAddr, domains)
|
||||
if err != nil {
|
||||
return nil, specific, fmt.Errorf("wrong upstream format: %w", err)
|
||||
} else if useDefault {
|
||||
return nil, specific, nil
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: checking if upstream %q works", upstreamAddr)
|
||||
|
||||
opts = &upstream.Options{
|
||||
Bootstrap: opts.Bootstrap,
|
||||
Timeout: opts.Timeout,
|
||||
PreferIPv6: opts.PreferIPv6,
|
||||
}
|
||||
|
||||
// dnsFilter can be nil during application update.
|
||||
if s.dnsFilter != nil {
|
||||
recs := s.dnsFilter.EtcHostsRecords(extractUpstreamHost(upstreamAddr))
|
||||
for _, rec := range recs {
|
||||
opts.ServerIPAddrs = append(opts.ServerIPAddrs, rec.Addr.AsSlice())
|
||||
}
|
||||
sortNetIPAddrs(opts.ServerIPAddrs, opts.PreferIPv6)
|
||||
}
|
||||
u, err = upstream.AddressToUpstream(upstreamAddr, opts)
|
||||
if err != nil {
|
||||
return nil, specific, fmt.Errorf("creating upstream for %q: %w", upstreamAddr, err)
|
||||
}
|
||||
|
||||
return u, specific, nil
|
||||
}
|
||||
|
||||
func (s *Server) checkDNS(line string, opts *upstream.Options, check healthCheckFunc) (err error) {
|
||||
if IsCommentOrEmpty(line) {
|
||||
return fmt.Errorf("wrong upstream format: %w", err)
|
||||
} else if usesDefault {
|
||||
return nil
|
||||
}
|
||||
|
||||
var u upstream.Upstream
|
||||
var specific bool
|
||||
log.Debug("dnsforward: checking if upstream %q works", addr)
|
||||
|
||||
defer func() {
|
||||
if err != nil && specific {
|
||||
err = domainSpecificTestError{error: err}
|
||||
}
|
||||
}()
|
||||
|
||||
u, specific, err = s.parseUpstreamLine(line, opts)
|
||||
if err != nil || u == nil {
|
||||
return err
|
||||
opts := &upstream.Options{
|
||||
Bootstrap: basicOpts.Bootstrap,
|
||||
Timeout: basicOpts.Timeout,
|
||||
PreferIPv6: basicOpts.PreferIPv6,
|
||||
}
|
||||
|
||||
// dnsFilter can be nil during application update.
|
||||
//
|
||||
// TODO(e.burkov): Remove when update dnsproxy.
|
||||
if s.dnsFilter != nil {
|
||||
recs := s.dnsFilter.EtcHostsRecords(extractUpstreamHost(addr))
|
||||
for _, rec := range recs {
|
||||
opts.ServerIPAddrs = append(opts.ServerIPAddrs, rec.Addr.AsSlice())
|
||||
}
|
||||
sortNetIPAddrs(opts.ServerIPAddrs, opts.PreferIPv6)
|
||||
}
|
||||
|
||||
u, err := upstream.AddressToUpstream(addr, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating upstream for %q: %w", addr, err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, u.Close()) }()
|
||||
|
||||
return check(u)
|
||||
}
|
||||
|
||||
// checkResult is a result of checking an upstream server.
|
||||
type checkResult = struct {
|
||||
// status is an error message if the upstream server is not working. It's
|
||||
// nil for working upstreams.
|
||||
status error
|
||||
|
||||
// address is the upstream server address as given in the request. It may
|
||||
// appear to be a whole line if it's incorrect itself.
|
||||
address string
|
||||
}
|
||||
|
||||
// checkDNS parses an upstream configuration line using opts and checks if the
|
||||
// specified upstreams are working using check. countWG is decremented when the
|
||||
// expected number of results added to resNum, then results are sent to resCh.
|
||||
//
|
||||
// TODO(e.burkov): Remove the receiver.
|
||||
func (s *Server) checkDNS(
|
||||
line string,
|
||||
opts *upstream.Options,
|
||||
check healthCheckFunc,
|
||||
countWG *sync.WaitGroup,
|
||||
resNum *atomic.Int32,
|
||||
resCh chan<- checkResult,
|
||||
) {
|
||||
defer log.OnPanic("dnsforward: checking upstreams")
|
||||
|
||||
addrs, domains, err := separateUpstream(line)
|
||||
if err != nil {
|
||||
resNum.Add(1)
|
||||
countWG.Done()
|
||||
|
||||
resCh <- checkResult{
|
||||
address: line,
|
||||
status: fmt.Errorf("wrong upstream format: %w", err),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resNum.Add(int32(len(addrs)))
|
||||
countWG.Done()
|
||||
|
||||
specific := len(domains) > 0
|
||||
for _, addr := range addrs {
|
||||
resCh <- checkResult{
|
||||
address: addr,
|
||||
status: s.checkUpstreamAddr(addr, specific, opts, check),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check returns the mapping of upstream addresses to their check results.
|
||||
func (s *Server) check(req *upstreamJSON, opts *upstream.Options) (result map[string]string) {
|
||||
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
|
||||
req.FallbackDNS = stringutil.FilterOut(req.FallbackDNS, IsCommentOrEmpty)
|
||||
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
|
||||
|
||||
countWG := &sync.WaitGroup{}
|
||||
countWG.Add(len(req.Upstreams) + len(req.FallbackDNS) + len(req.PrivateUpstreams))
|
||||
|
||||
resNum := &atomic.Int32{}
|
||||
resCh := make(chan checkResult)
|
||||
|
||||
for _, addr := range req.Upstreams {
|
||||
go s.checkDNS(addr, opts, checkExchange, countWG, resNum, resCh)
|
||||
}
|
||||
for _, addr := range req.FallbackDNS {
|
||||
go s.checkDNS(addr, opts, checkExchange, countWG, resNum, resCh)
|
||||
}
|
||||
for _, addr := range req.PrivateUpstreams {
|
||||
go s.checkDNS(addr, opts, checkPrivateExchange, countWG, resNum, resCh)
|
||||
}
|
||||
|
||||
// Wait until all the servers are counted and enqueued.
|
||||
countWG.Wait()
|
||||
n := resNum.Load()
|
||||
|
||||
result = make(map[string]string, n)
|
||||
for i := int32(0); i < n; i++ {
|
||||
// TODO(e.burkov): Upstreams intended for different purposes should
|
||||
// be distinguished in the result, even if specified equally.
|
||||
res := <-resCh
|
||||
if res.status != nil {
|
||||
result[res.address] = res.status.Error()
|
||||
} else {
|
||||
result[res.address] = "OK"
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// handleTestUpstreamDNS handles requests to the POST /control/test_upstream_dns
|
||||
// endpoint.
|
||||
func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||
req := &upstreamJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
@@ -761,65 +859,18 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
bootstrapAddrs := stringutil.FilterOut(req.BootstrapDNS, IsCommentOrEmpty)
|
||||
if len(bootstrapAddrs) == 0 {
|
||||
bootstrapAddrs = defaultBootstrap
|
||||
}
|
||||
|
||||
opts := &upstream.Options{
|
||||
Bootstrap: req.BootstrapDNS,
|
||||
Bootstrap: bootstrapAddrs,
|
||||
Timeout: s.conf.UpstreamTimeout,
|
||||
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||
}
|
||||
if len(opts.Bootstrap) == 0 {
|
||||
opts.Bootstrap = defaultBootstrap
|
||||
}
|
||||
|
||||
type upsCheckResult = struct {
|
||||
err error
|
||||
host string
|
||||
}
|
||||
|
||||
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
|
||||
req.FallbackDNS = stringutil.FilterOut(req.FallbackDNS, IsCommentOrEmpty)
|
||||
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
|
||||
|
||||
upsNum := len(req.Upstreams) + len(req.FallbackDNS) + len(req.PrivateUpstreams)
|
||||
result := make(map[string]string, upsNum)
|
||||
resCh := make(chan upsCheckResult, upsNum)
|
||||
|
||||
for _, ups := range req.Upstreams {
|
||||
go func(ups string) {
|
||||
resCh <- upsCheckResult{
|
||||
host: ups,
|
||||
err: s.checkDNS(ups, opts, checkDNSUpstreamExc),
|
||||
}
|
||||
}(ups)
|
||||
}
|
||||
for _, ups := range req.FallbackDNS {
|
||||
go func(ups string) {
|
||||
resCh <- upsCheckResult{
|
||||
host: ups,
|
||||
err: s.checkDNS(ups, opts, checkDNSUpstreamExc),
|
||||
}
|
||||
}(ups)
|
||||
}
|
||||
for _, ups := range req.PrivateUpstreams {
|
||||
go func(ups string) {
|
||||
resCh <- upsCheckResult{
|
||||
host: ups,
|
||||
err: s.checkDNS(ups, opts, checkPrivateUpstreamExc),
|
||||
}
|
||||
}(ups)
|
||||
}
|
||||
|
||||
for i := 0; i < upsNum; i++ {
|
||||
// TODO(e.burkov): The upstreams used for both common and private
|
||||
// resolving should be reported separately.
|
||||
pair := <-resCh
|
||||
if pair.err != nil {
|
||||
result[pair.host] = pair.err.Error()
|
||||
} else {
|
||||
result[pair.host] = "OK"
|
||||
}
|
||||
}
|
||||
|
||||
aghhttp.WriteJSONResponseOK(w, r, result)
|
||||
aghhttp.WriteJSONResponseOK(w, r, s.check(req, opts))
|
||||
}
|
||||
|
||||
// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.
|
||||
|
||||
@@ -49,13 +49,18 @@ func loadTestData(t *testing.T, casesFileName string, cases any) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
const jsonExt = ".json"
|
||||
const (
|
||||
jsonExt = ".json"
|
||||
|
||||
// testBlockedRespTTL is the TTL for blocked responses to use in tests.
|
||||
testBlockedRespTTL = 10
|
||||
)
|
||||
|
||||
func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
||||
filterConf := &filtering.Config{
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
BlockedResponseTTL: 10,
|
||||
BlockedResponseTTL: testBlockedRespTTL,
|
||||
SafeBrowsingEnabled: true,
|
||||
SafeBrowsingCacheSize: 1000,
|
||||
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
|
||||
@@ -133,7 +138,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
filterConf := &filtering.Config{
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
BlockedResponseTTL: 10,
|
||||
BlockedResponseTTL: testBlockedRespTTL,
|
||||
SafeBrowsingEnabled: true,
|
||||
SafeBrowsingCacheSize: 1000,
|
||||
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
|
||||
@@ -229,6 +234,9 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
}, {
|
||||
name: "blocked_response_ttl",
|
||||
wantSet: "",
|
||||
}, {
|
||||
name: "multiple_domain_specific_upstreams",
|
||||
wantSet: "",
|
||||
}}
|
||||
|
||||
var data map[string]struct {
|
||||
@@ -250,6 +258,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
s.dnsFilter.SetBlockingMode(filtering.BlockingModeDefault, netip.Addr{}, netip.Addr{})
|
||||
s.conf = defaultConf
|
||||
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{}
|
||||
s.dnsFilter.SetBlockedResponseTTL(testBlockedRespTTL)
|
||||
})
|
||||
|
||||
rBody := io.NopCloser(bytes.NewReader(caseData.Req))
|
||||
@@ -470,6 +479,8 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
||||
Host: newLocalUpstreamListener(t, 0, badHandler).String(),
|
||||
}).String()
|
||||
|
||||
goodAndBadUps := strings.Join([]string{goodUps, badUps}, " ")
|
||||
|
||||
const (
|
||||
upsTimeout = 100 * time.Millisecond
|
||||
|
||||
@@ -547,7 +558,7 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
||||
"upstream_dns": []string{"[/domain.example/]" + badUps},
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
"[/domain.example/]" + badUps: `WARNING: couldn't communicate ` +
|
||||
badUps: `WARNING: couldn't communicate ` +
|
||||
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
|
||||
`dns: id mismatch`,
|
||||
},
|
||||
@@ -585,6 +596,40 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
||||
goodUps: "OK",
|
||||
},
|
||||
name: "fallback_comment_mix",
|
||||
}, {
|
||||
body: map[string]any{
|
||||
"upstream_dns": []string{"[/domain.example/]" + goodUps + " " + badUps},
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
goodUps: "OK",
|
||||
badUps: `WARNING: couldn't communicate ` +
|
||||
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
|
||||
`dns: id mismatch`,
|
||||
},
|
||||
name: "multiple_domain_specific_upstreams",
|
||||
}, {
|
||||
body: map[string]any{
|
||||
"upstream_dns": []string{"[/domain.example/]/]1.2.3.4"},
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
"[/domain.example/]/]1.2.3.4": `wrong upstream format: ` +
|
||||
`bad upstream for domain "[/domain.example/]/]1.2.3.4": ` +
|
||||
`duplicated separator`,
|
||||
},
|
||||
name: "bad_specification",
|
||||
}, {
|
||||
body: map[string]any{
|
||||
"upstream_dns": []string{"[/domain.example/]" + goodAndBadUps},
|
||||
"fallback_dns": []string{"[/domain.example/]" + goodAndBadUps},
|
||||
"private_upstream": []string{"[/domain.example/]" + goodAndBadUps},
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
goodUps: "OK",
|
||||
badUps: `WARNING: couldn't communicate ` +
|
||||
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
|
||||
`dns: id mismatch`,
|
||||
},
|
||||
name: "all_different",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -839,5 +839,47 @@
|
||||
"edns_cs_use_custom": false,
|
||||
"edns_cs_custom_ip": ""
|
||||
}
|
||||
},
|
||||
"multiple_domain_specific_upstreams": {
|
||||
"req": {
|
||||
"upstream_dns": [
|
||||
"8.8.8.8:77",
|
||||
"[/example.com/]8.8.4.4:77 9.9.9.10 https://1.1.1.1"
|
||||
]
|
||||
},
|
||||
"want": {
|
||||
"upstream_dns": [
|
||||
"8.8.8.8:77",
|
||||
"[/example.com/]8.8.4.4:77 9.9.9.10 https://1.1.1.1"
|
||||
],
|
||||
"upstream_dns_file": "",
|
||||
"bootstrap_dns": [
|
||||
"9.9.9.10",
|
||||
"149.112.112.10",
|
||||
"2620:fe::10",
|
||||
"2620:fe::fe:10"
|
||||
],
|
||||
"fallback_dns": [],
|
||||
"protection_enabled": true,
|
||||
"protection_disabled_until": null,
|
||||
"ratelimit": 0,
|
||||
"blocking_mode": "default",
|
||||
"blocking_ipv4": "",
|
||||
"blocking_ipv6": "",
|
||||
"blocked_response_ttl": 10,
|
||||
"edns_cs_enabled": false,
|
||||
"dnssec_enabled": false,
|
||||
"disable_ipv6": false,
|
||||
"upstream_mode": "",
|
||||
"cache_size": 0,
|
||||
"cache_ttl_min": 0,
|
||||
"cache_ttl_max": 0,
|
||||
"cache_optimistic": false,
|
||||
"resolve_clients": false,
|
||||
"use_private_ptr_resolvers": false,
|
||||
"local_ptr_upstreams": [],
|
||||
"edns_cs_use_custom": false,
|
||||
"edns_cs_custom_ip": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,30 +263,6 @@ func assignUniqueFilterID() int64 {
|
||||
return value
|
||||
}
|
||||
|
||||
// Sets up a timer that will be checking for filters updates periodically
|
||||
func (d *DNSFilter) periodicallyRefreshFilters() {
|
||||
const maxInterval = 1 * 60 * 60
|
||||
ivl := 5 // use a dynamically increasing time interval
|
||||
for {
|
||||
isNetErr, ok := false, false
|
||||
if d.conf.FiltersUpdateIntervalHours != 0 {
|
||||
_, isNetErr, ok = d.tryRefreshFilters(true, true, false)
|
||||
if ok && !isNetErr {
|
||||
ivl = maxInterval
|
||||
}
|
||||
}
|
||||
|
||||
if isNetErr {
|
||||
ivl *= 2
|
||||
if ivl > maxInterval {
|
||||
ivl = maxInterval
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(ivl) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// tryRefreshFilters is like [refreshFilters], but backs down if the update is
|
||||
// already going on.
|
||||
//
|
||||
|
||||
@@ -257,6 +257,9 @@ type DNSFilter struct {
|
||||
// conf contains filtering parameters.
|
||||
conf *Config
|
||||
|
||||
// done is the channel to signal to stop running filters updates loop.
|
||||
done chan struct{}
|
||||
|
||||
// Channel for passing data to filters-initializer goroutine
|
||||
filtersInitializerChan chan filtersInitializerParams
|
||||
filtersInitializerLock sync.Mutex
|
||||
@@ -424,24 +427,15 @@ func (d *DNSFilter) setFilters(blockFilters, allowFilters []Filter, async bool)
|
||||
return d.initFiltering(allowFilters, blockFilters)
|
||||
}
|
||||
|
||||
// Starts initializing new filters by signal from channel
|
||||
func (d *DNSFilter) filtersInitializer() {
|
||||
for {
|
||||
params := <-d.filtersInitializerChan
|
||||
err := d.initFiltering(params.allowFilters, params.blockFilters)
|
||||
if err != nil {
|
||||
log.Error("filtering: initializing: %s", err)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close - close the object
|
||||
func (d *DNSFilter) Close() {
|
||||
d.engineLock.Lock()
|
||||
defer d.engineLock.Unlock()
|
||||
|
||||
if d.done != nil {
|
||||
d.done <- struct{}{}
|
||||
}
|
||||
|
||||
d.reset()
|
||||
}
|
||||
|
||||
@@ -1131,19 +1125,64 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Start - start the module:
|
||||
// . start async filtering initializer goroutine
|
||||
// . register web handlers
|
||||
// Start registers web handlers and starts filters updates loop.
|
||||
func (d *DNSFilter) Start() {
|
||||
d.filtersInitializerChan = make(chan filtersInitializerParams, 1)
|
||||
go d.filtersInitializer()
|
||||
d.done = make(chan struct{}, 1)
|
||||
|
||||
d.RegisterFilteringHandlers()
|
||||
|
||||
// Here we should start updating filters,
|
||||
// but currently we can't wake up the periodic task to do so.
|
||||
// So for now we just start this periodic task from here.
|
||||
go d.periodicallyRefreshFilters()
|
||||
go d.updatesLoop()
|
||||
}
|
||||
|
||||
// updatesLoop initializes new filters and checks for filters updates in a loop.
|
||||
func (d *DNSFilter) updatesLoop() {
|
||||
defer log.OnPanic("filtering: updates loop")
|
||||
|
||||
ivl := time.Second * 5
|
||||
t := time.NewTimer(ivl)
|
||||
|
||||
for {
|
||||
select {
|
||||
case params := <-d.filtersInitializerChan:
|
||||
err := d.initFiltering(params.allowFilters, params.blockFilters)
|
||||
if err != nil {
|
||||
log.Error("filtering: initializing: %s", err)
|
||||
|
||||
continue
|
||||
}
|
||||
case <-t.C:
|
||||
ivl = d.periodicallyRefreshFilters(ivl)
|
||||
t.Reset(ivl)
|
||||
case <-d.done:
|
||||
t.Stop()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// periodicallyRefreshFilters checks for filters updates and returns time
|
||||
// interval for the next update.
|
||||
func (d *DNSFilter) periodicallyRefreshFilters(ivl time.Duration) (nextIvl time.Duration) {
|
||||
const maxInterval = time.Hour
|
||||
|
||||
if d.conf.FiltersUpdateIntervalHours == 0 {
|
||||
return ivl
|
||||
}
|
||||
|
||||
isNetErr, ok := false, false
|
||||
_, isNetErr, ok = d.tryRefreshFilters(true, true, false)
|
||||
|
||||
if ok && !isNetErr {
|
||||
ivl = maxInterval
|
||||
} else if isNetErr {
|
||||
ivl *= 2
|
||||
// TODO(s.chzhen): Use built-in function max in Go 1.21.
|
||||
ivl = mathutil.Max(ivl, maxInterval)
|
||||
}
|
||||
|
||||
return ivl
|
||||
}
|
||||
|
||||
// Safe browsing and parental control methods.
|
||||
|
||||
@@ -256,6 +256,13 @@ var blockedServices = []blockedService{{
|
||||
"||z.cn^",
|
||||
"||zappos^",
|
||||
},
|
||||
}, {
|
||||
ID: "amino",
|
||||
Name: "Amino",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M9 4C6.24 4 4 6.24 4 9v32c0 2.76 2.24 5 5 5h32c2.76 0 5-2.24 5-5V9c0-2.76-2.24-5-5-5H9zm8.17 3.78a1.001 1.001 0 0 1 1.01 1c0 .37-.21.73-.57.9-.14.07-.29.1-.43.1a.987.987 0 0 1-.9-.56 1 1 0 0 1 .89-1.44zm15.66 0c.14 0 .29.03.43.1.36.17.56.53.56.9a.987.987 0 0 1-1 1c-.14 0-.29-.03-.43-.1a.998.998 0 0 1-.57-.9c0-.14.03-.29.1-.43a1 1 0 0 1 .91-.57zM13.77 9.93c.29 0 .58.13.78.38.15.18.22.4.22.62 0 .3-.13.59-.38.78a.963.963 0 0 1-.62.22c-.29 0-.58-.13-.78-.37a1.001 1.001 0 0 1 .78-1.63zm22.46 0c.22 0 .44.07.63.22a1.01 1.01 0 0 1 .15 1.41c-.2.24-.49.37-.78.37a.963.963 0 0 1-.62-.22.972.972 0 0 1-.38-.78c0-.22.07-.44.22-.62.2-.25.49-.38.78-.38zM25 10c8.27 0 15 6.73 15 15s-6.73 15-15 15-15-6.73-15-15 6.73-15 15-15zm-14.08 2.78a1.01 1.01 0 0 1 1.01 1 1.005 1.005 0 0 1-1.629.781 1 1 0 0 1 .619-1.782zm28.16 0c.29 0 .58.13.78.38.14.18.22.4.22.62 0 .29-.13.58-.38.78a1.005 1.005 0 0 1-1.629-.781 1.01 1.01 0 0 1 1.01-1zm-14.434 3.222a2.185 2.185 0 0 0-2.175 1.398l-5.09 13.59a.75.75 0 0 0 .7 1.01h1.458a.973.973 0 0 0 .75-.35c1.75-2 4.35-3.58 6.64-4.7l1.821 4.43a1.003 1.003 0 0 0 .92.62h1.25a.75.75 0 0 0 .7-1.01l-1.97-5.24c1.3-.52 2.19-.79 2.22-.79.8-.21 1.29-1.02 1.08-1.83a1.502 1.502 0 0 0-1.81-1.09c-.12.03-1.13.3-2.57.82l-2.01-5.35c-.25-.69-.79-1.25-1.5-1.44a2.223 2.223 0 0 0-.414-.068zm-15.867.187a1 1 0 0 1 1 1.01c0 .14-.03.292-.1.432a1.004 1.004 0 0 1-1.34.459.991.991 0 0 1-.458-1.33c.17-.36.528-.57.898-.57zm32.442 0c.37 0 .728.21.898.57a.985.985 0 0 1-.459 1.33 1.004 1.004 0 0 1-1.34-.459.971.971 0 0 1-.1-.43 1 1 0 0 1 1-1.01zM24.5 22.04c.24 0 .48.13.59.39l.66 1.622a27.9 27.9 0 0 0-1.61.83c-.543.304-1.09.633-1.63.988l1.4-3.44c.11-.26.35-.39.59-.39zM8.78 31.811c.37 0 .73.208.9.558a1 1 0 1 1-1.9.432c0-.36.2-.72.56-.89.14-.07.29-.1.44-.1zm32.44 0a.991.991 0 0 1 .898 1.43.995.995 0 0 1-1.329.47 1 1 0 0 1-.568-.91c0-.14.03-.292.1-.432.17-.35.53-.558.9-.558zm-30.3 3.41a1.005 1.005 0 0 1 1.01 1 1.01 1.01 0 0 1-1.01 1 1 1 0 0 1-.78-.381c-.14-.18-.22-.4-.22-.62 0-.29.13-.58.38-.78.18-.15.4-.22.62-.22zm28.16 0a1 1 0 1 1-.63 1.78.996.996 0 0 1-.38-.78 1.005 1.005 0 0 1 1.01-1zm-25.31 2.85c.22 0 .44.068.62.218.25.19.38.481.38.781 0 .22-.07.44-.22.62a1.002 1.002 0 0 1-1.41.16 1.01 1.01 0 0 1-.15-1.41c.2-.24.49-.37.78-.37zm22.46 0c.29 0 .58.128.78.368a1.001 1.001 0 0 1-.78 1.631c-.29 0-.58-.13-.78-.38a.958.958 0 0 1-.22-.62c0-.3.13-.59.38-.78a.963.963 0 0 1 .62-.22zm-19.05 2.15c.14 0 .29.03.43.1.36.17.57.53.57.9 0 .14-.03.29-.1.43a1.001 1.001 0 0 1-1.34.468.986.986 0 0 1-.56-.898.987.987 0 0 1 1-1zm15.64 0c.37 0 .73.198.9.558a1 1 0 1 1-1.9.442c0-.37.21-.73.57-.9.14-.07.29-.1.43-.1z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||aminoapps.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "apple_streaming",
|
||||
Name: "Apple Streaming",
|
||||
@@ -337,6 +344,16 @@ var blockedServices = []blockedService{{
|
||||
"||betwaysatta.com^",
|
||||
"||vietnambetway88.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "bigo_live",
|
||||
Name: "Bigo Live",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"3 3.5 41 41\" fill=\"currentColor\"><g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M29.18 26.13c5.84-2.13 10.03-4.81 5.7-10.34.3-11.15-18.48-13.27-23.18-.61-.54 2.44-.36 7.7 1.48 11.92.68 1.6.15 8.1-2.23 5.16-1.12-1.52-2.16-1.58-2.6-1.42-2.14 3.95 3.39 7.32 4.6 7.34 3.02 3.5 5.59 3.07 7.77 3.23\"/><path d=\"M32.03 24.95c5 7.69 2.45 12.6-3.85 14.2M18.16 25.72c.97.92 2.4.74 3.38.06.19-1.83 1.67-1.76 1.92.11 1.2-.02 2.44-.16 2.67.7.14.9-.63 1.44-1.8 1.28.06 1.84-.86 1.78-2.93 1.6-.68.3-1.3.42-1.93.55m3.33-8.6c5.01 1.97 9.5 1.55 12.19-2.67\"/><ellipse cx=\"25.08\" cy=\"18.77\" rx=\"1.12\" ry=\"1.28\"/><ellipse cx=\"32.83\" cy=\"16.88\" rx=\".93\" ry=\"1.22\"/><path d=\"M20.52 40c6.87 13.72 14.2-18.17.53-6.2m13.52-4.57c11.28-7.01 2.68 13.07-1.86 8.09M25.83 6.57c-4.11-3.58-6.75-2.2-8.56 1.68-7.73-1.63-7.5 4.7-5.57 6.93-3.37 2.43-4.1 4.87.12 7.3-1.62.75-3.3 3.43 1.36 5.35-1.18.95-2.77 1.83-.12 3.4m24.07-15.36h2.63m-1.31-1.5v2.92\"/></g></svg>"),
|
||||
Rules: []string{
|
||||
"||bigo.sg^",
|
||||
"||bigo.tv^",
|
||||
"||bigolive.tv^",
|
||||
"||bigovideo.tv^",
|
||||
},
|
||||
}, {
|
||||
ID: "bilibili",
|
||||
Name: "Bilibili",
|
||||
@@ -508,6 +525,13 @@ var blockedServices = []blockedService{{
|
||||
"||deezer.com^",
|
||||
"||dzcdn.net^",
|
||||
},
|
||||
}, {
|
||||
ID: "directvgo",
|
||||
Name: "DirecTV Go",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"-2 0 20 20\"><path d=\"M17 9.97A10.12 10.12 0 0 1 6.82 20H.46a.44.44 0 0 1-.34-.16.42.42 0 0 1-.11-.36A5.03 5.03 0 0 1 5 15.32h1.82a5.16 5.16 0 0 0 5.17-5.35v-.36a5.2 5.2 0 0 0-5.25-4.93H5a5.1 5.1 0 0 1-3.57-1.46A4.98 4.98 0 0 1 0 .54.45.45 0 0 1 .1.16.45.45 0 0 1 .47 0h6.26C12.36 0 16.95 4.44 17 9.97z\"/><path d=\"M12 9.97a9.95 9.95 0 0 1-2.9 7.09A9.85 9.85 0 0 1 2.04 20H.45a.43.43 0 0 1-.34-.16.43.43 0 0 1-.1-.36 4.94 4.94 0 0 1 4.86-4.16h1.77a5.36 5.36 0 0 0 5.27-4.89 4.32 4.32 0 0 0 0-.49v-.3a5.36 5.36 0 0 0-5.34-4.96h-1.7A4.92 4.92 0 0 1 1.4 3.22 5.02 5.02 0 0 1 .01.54.46.46 0 0 1 .45 0h1.5c5.51-.02 10 4.44 10.05 9.97z\"/><path d=\"M4 10a3 3 0 1 0 6 0 3 3 0 0 0-6 0z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||directvgo.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "discord",
|
||||
Name: "Discord",
|
||||
@@ -1975,6 +1999,23 @@ var blockedServices = []blockedService{{
|
||||
"||sonyentertainmentnetwork.com",
|
||||
"||station.sony.com",
|
||||
},
|
||||
}, {
|
||||
ID: "plenty_of_fish",
|
||||
Name: "Plenty of Fish",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"-8 -3 30 30\"><path d=\"M13.96 4.88C11.3.68 7.04 1.25.22 1.25v.09c.52.2.95.59 1.21 1.08.26.47.38 1.04.38 1.72v16.44c.01.61-.11 1.21-.38 1.76-.25.5-.68.9-1.2 1.1v.09h7.13v-.09c-.5-.22-.92-.6-1.17-1.1a3.78 3.78 0 0 1-.4-1.76V16c.54.13 1.08.2 1.62.19a7.66 7.66 0 0 0 6.84-4.3c.47-.97.7-2.1.7-3.41a6.6 6.6 0 0 0-1-3.6ZM9.45 13.6a3.33 3.33 0 0 1-2.96 1.65c-.24 0-.49-.02-.73-.05V7.56l.01-4.63a3 3 0 0 0-.1-.88c1.62 0 2.43.5 3.16 1.19.74.68 1.86 2.68 1.86 5.52a9.4 9.4 0 0 1-1.24 4.85Z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||pof.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "plex",
|
||||
Name: "Plex",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 48 48\"><path d=\"M11.5 6A5.5 5.5 0 0 0 6 11.5v25a5.5 5.5 0 0 0 5.5 5.5h25a5.5 5.5 0 0 0 5.5-5.5v-25A5.5 5.5 0 0 0 36.5 6h-25zm6.67 7.08h6.47L31.75 24l-7.1 10.92h-6.48L25.2 24l-7.03-10.92z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||plex.bz^",
|
||||
"||plex.direct^",
|
||||
"||plex.tv^",
|
||||
"||plexapp.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "pluto_tv",
|
||||
Name: "Pluto TV",
|
||||
@@ -2086,6 +2127,14 @@ var blockedServices = []blockedService{{
|
||||
"||shopeemobile.com^",
|
||||
"||shp.ee^",
|
||||
},
|
||||
}, {
|
||||
ID: "signal",
|
||||
Name: "Signal",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 32 32\"><path d=\"M26.6 16c-.2 4-1.9 7.2-5.5 9.2a10.6 10.6 0 0 1-10.5-.2h-.5l-3.8.9c-.2.1-.3 0-.2-.2l.9-3.8-.1-.5a10.6 10.6 0 1 1 19.5-7.2l.2 1.8z\"/><path d=\"M4.6 28.6c-.8 0-1.4-.7-1.2-1.4l.6-2.5.2-.1.9.2c.2 0 .1.1.1.2l-.5 2c-.1.3-.1.3.3.2l2-.5c.2 0 .2 0 .3.2l.2.8-.1.2-2.8.7c.1 0 .1 0 0 0zm5.1-1.2-1 .2c-.2.1-.3 0-.3-.2l-.2-.8.1-.2 1.5-.3h.2c.9.5 1.9.9 2.9 1.2.1 0 .2.1.1.2l-.2.7c0 .2-.1.2-.3.2-.9-.2-1.8-.6-2.6-1 0 .1 0 0-.2 0zm-3.8-5.3-.3 1.3c-.1.5 0 .4-.5.3l-.6-.1c-.1 0-.2-.1-.1-.2l.2-.9v-.4c-.4-.8-.8-1.7-1-2.6-.1-.3-.1-.3.2-.3l.7-.2.2.1c.3 1 .7 2 1.2 2.9-.1 0 0 0 0 .1zM26.4 8.3l-.1.1-.7.5c-.1.1-.2.1-.2 0-.7-.9-1.4-1.6-2.3-2.3-.1-.1-.1-.1 0-.2l.5-.7c.1-.1.1-.1.2 0 1 .7 1.8 1.5 2.6 2.6 0-.1 0-.1 0 0zm-7.2-4.9h.1c1.1.3 2.2.7 3.2 1.3.2.1.2.2.1.3l-.4.7-.2.1a10 10 0 0 0-3-1.2c-.1 0-.2-.1-.1-.2l.2-.9.1-.1zM6.5 9s-.1-.1 0 0l-.8-.6v-.3l1-1.3 1.4-1.2h.3l.5.7v.3C8 7.3 7.3 8 6.6 8.9l-.1.1zm21.1 9.9.9.2.1.2-.8 2.2-.5 1-.3.1-.7-.4-.1-.2c.5-.9 1-1.9 1.2-3 0-.1.1-.2.2-.1zM4.4 13.1l-.9-.2-.1-.2c.2-.9.6-1.8 1-2.6l.3-.6c.2-.2.2-.2.3-.1l.8.5c.1.1.1.1 0 .2-.5.9-1 1.9-1.2 3 0 0 0 .1-.2 0zM3 16l.1-1.7c0-.2.1-.2.3-.2l.8.1.2.2c-.1 1.1-.1 2.1 0 3.2l-.1.2-.9.1c-.2 0-.2-.1-.2-.2C3 17.2 3 16.6 3 16zm25.6-3.2-.1.1-.9.2c-.2 0-.2-.1-.2-.2-.2-.9-.6-1.7-1-2.5l-.2-.4c-.1-.1 0-.1 0-.2l.8-.5h.2a15 15 0 0 1 1.4 3.5zm-2.2 10.9-.1.1c-.7.9-1.6 1.8-2.5 2.5-.1.1-.2.1-.2 0l-.5-.7v-.3c.8-.6 1.6-1.4 2.2-2.2h.3l.7.5.1.1zM16 3l1.7.1c.2 0 .2.1.2.3l-.1.7-.2.2a16 16 0 0 0-3.1 0c-.2 0-.2 0-.2-.2l-.1-.8c0-.1 0-.2.2-.2L16 3zm0 26-1.8-.1c-.2 0-.2-.1-.2-.3l.1-.8.2-.2c1.1.1 2.1.1 3.2 0 .1 0 .2 0 .2.2l.1.8c0 .2-.1.2-.2.2-.4.2-1 .2-1.6.2zM12.8 3.4l.1.1.2.9c0 .2-.1.2-.2.2-.9.2-1.8.6-2.6 1-.3.2-.5.2-.7-.1l-.2-.4c-.1-.2-.1-.2.1-.3.8-.5 1.7-.9 2.6-1.2l.7-.2zM29 16l-.1 1.7c0 .2-.1.2-.3.2l-.7-.1c-.3 0-.3 0-.2-.3v-3.1c0-.1 0-.2.2-.2l.8-.1c.2 0 .2 0 .2.2L29 16zm-9.8 12.6-.1-.1-.2-.9.2-.2c.9-.2 1.7-.6 2.5-1l.4-.2c.1-.1.1 0 .2 0l.5.8v.2c-1.1.6-2.2 1.1-3.5 1.4z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||signal.org^",
|
||||
"||whispersystems.org^",
|
||||
},
|
||||
}, {
|
||||
ID: "skype",
|
||||
Name: "Skype",
|
||||
@@ -2210,6 +2259,14 @@ var blockedServices = []blockedService{{
|
||||
"||tx.me^",
|
||||
"||usercontent.dev^",
|
||||
},
|
||||
}, {
|
||||
ID: "temu",
|
||||
Name: "Temu",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 256 256\"><path d=\"M199.1 0C230.525 0 256 25.475 256 56.9v142.2c0 31.425-25.475 56.9-56.9 56.9H56.9C25.475 256 0 230.525 0 199.1V56.9C0 25.475 25.475 0 56.9 0zm-64 132.98h-3.4c-3.025 0-5.5 2.45-5.475 5.475v37.625c0 3.025 2.45 5.5 5.475 5.5s5.5-2.45 5.5-5.5v-24.7l9.25 13.05c1.925 2.7 5.925 2.7 7.875 0l9.25-13.05v24.7a5.5 5.5 0 0 0 5.5 5.5c3.025 0 5.5-2.45 5.475-5.5v-37.625c0-3.025-2.45-5.5-5.475-5.475h-3.4c-1.3 0-2.55.625-3.3 1.7l-11.975 18-12-18a3.997 3.997 0 0 0-3.3-1.7zm85.05 0c-3.025 0-5.5 2.45-5.5 5.475v22.975c0 7.225-4.075 10.925-10.775 10.9-6.7 0-10.775-3.825-10.75-11.225v-22.65c0-3.025-2.45-5.5-5.5-5.475-3.025 0-5.5 2.45-5.475 5.475v22.9c0 13.4 8.2 20.225 21.6 20.225s21.9-6.75 21.875-20.55v-22.575c0-3.025-2.45-5.5-5.475-5.475zm-154.22 0H33.855c-3.025 0-5.5 2.45-5.5 5.475s2.45 5.5 5.5 5.5h10.55v32.075c0 3.025 2.45 5.5 5.475 5.5s5.5-2.45 5.5-5.5v-32.075h10.55a5.5 5.5 0 0 0 5.5-5.5c0-3.025-2.45-5.5-5.5-5.475zm47.475 0H83.68c-3.025 0-5.5 2.45-5.5 5.475v37.575a5.5 5.5 0 0 0 5.5 5.5h29.725c3.025 0 5.5-2.45 5.475-5.5 0-3.025-2.45-5.5-5.475-5.5h-24.25v-7.8h21.1c3.025 0 5.5-2.45 5.5-5.475s-2.45-5.5-5.5-5.5h-21.1v-7.8h24.25c3.025 0 5.5-2.45 5.475-5.5 0-3.025-2.45-5.5-5.475-5.475zM59.78 75.63l-1.025.025c-4.275.275-7.2 2.125-8.85 4.625-1.925-2.875-5.525-4.9-10.95-4.6l-.125.175c-.625 1-2.975 5.475.825 10.35.775.825 2.675 3.15 1.9 6.125L30.53 110.155c-.9 1.45-.5 3.325.875 4.3 2.85 2 8.575 4.75 18.5 4.75 9.9 0 15.625-2.75 18.475-4.75l.375-.325a3.179 3.179 0 0 0 .5-3.975l-11-17.825.075.325-.125-.5c-.6-2.675.9-4.8 1.725-5.75l.2-.2c3.825-4.875 1.45-9.325.825-10.35l-.1-.175zm35.7 8.35c-3.775-7.5-8.675-8.775-11.125-6.825-1.875 1.5-6.2 7.425-6.5 7.825-4.775 6.775-4.5 8.425 1.625 12.275 3.45 2.175 6.225-.625 7.425-1.45-.575 3.575-2.325 9.2-4.95 13.15-1.425-1.075-2.475-1.9-3.125-2.5-.825-.75-2.075-.7-2.875.075a1.865 1.865 0 0 0-.55 1.425c.025.525.25 1.025.625 1.375 6.375 5.825 14.75 9.125 23.675 9.15 8.95 0 17.375-3.3 23.75-9.15.825-.75.85-2 .1-2.8a2.07 2.07 0 0 0-2.875-.075c-.5.45-1 .875-1.525 1.3l-2.8-6.25c-.45-1.075-.95-2.425-1.5-4.05.275-.675.85-1.325 1.675-2.175.6-.6 1.1-1.2 1.475-1.775 1.85-2.925.8-4.65.225-5.8-1.325-2.7-3.4-1.825-4.9-.225-1.85 1.95-3.65 2.8-6.55 3.45-2.425.55-4.3.275-5.85-.7-2.15-1.325-5.45-6.25-5.45-6.25zm69.325-7.625c-8 7.6-.325 24.125-14.875 31.15-1.6.775-2.925-1.775-5.075-1.775-6.075.05-17.675 5.4-18.125 8.1-.375 2.225 4.575 4 19.175 4.025 12.7 0 16.8-19.325 21.25-19.35 4.45 0 2.375 17.525 1.9 19.35h4.65c-.4-1.825-.7-7.325-.675-15.1 0-7.775 1.4-9.5 2.525-15.375.975-5.1-6.575-9.525-10.75-11.025zm45.6.625H197.38c-8.425 0-15.425 6.525-16 14.925l-.95 13.475c-.45 6.4 4.625 11.825 11.025 11.85h24.85c6.425 0 11.475-5.425 11.05-11.85l-.95-13.475c-.6-8.4-7.575-14.925-16-14.925zm-110.65 31c3.925 0 6.925 1.925 8.025 5.5-2.675.7-5.35 1.05-8.075 1.025-4.1 0-5.55-.375-8.175-1.075 1.05-3.15 4.525-5.45 8.225-5.45zm98.225-19.825v.375c0 3.25 2.65 5.925 5.9 5.925s5.925-2.65 5.925-5.925v-.375c0-1.45 5.25-1.45 5.25 0v.375c0 6.15-5 11.15-11.175 11.15-6.15 0-11.15-5-11.15-11.15v-.375c0-1.45 5.225-1.45 5.25 0z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||kwcdn.com^",
|
||||
"||temu.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "tidal",
|
||||
Name: "Tidal",
|
||||
@@ -2256,6 +2313,13 @@ var blockedServices = []blockedService{{
|
||||
"||tinder.com^",
|
||||
"||tindersparks.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "tumblr",
|
||||
Name: "Tumblr",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M40 0H10A10 10 0 0 0 0 10v30a10 10 0 0 0 10 10h30a10 10 0 0 0 10-10V10A10 10 0 0 0 40 0Zm-6 40.24c0 .12-.05.24-.14.32-.12.1-2.85 2.44-9.12 2.44-7.51 0-7.74-8.38-7.74-9.34V23.01L13.43 23a.42.42 0 0 1-.43-.42V18.8c0-.18.1-.34.27-.4.07-.03 6.79-2.64 6.79-8.98 0-.24.2-.43.43-.43h4.09c.24 0 .43.2.43.43L25 17h6.56c.24 0 .43.2.43.45v5.1c0 .24-.19.45-.43.45H25v10.5c0 .25.23 3.27 3.43 3.27a10.3 10.3 0 0 0 4.91-1.39c.14-.08.3-.09.44 0 .13.07.22.21.22.37Z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||tumblr.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "twitch",
|
||||
Name: "Twitch",
|
||||
@@ -2434,6 +2498,15 @@ var blockedServices = []blockedService{{
|
||||
"||whatsapp.tv^",
|
||||
"||whatsappbrand.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "wizz",
|
||||
Name: "Wizz",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 -297 867 867\"><path d=\"M94.66 31.7c0-11.49-7.72-21.78-17.23-23.05Q47.26 4.74 17.18.16C7.7-1.34 0 7.77 0 20.57v207.77c0 25.69 15.38 43.88 34.38 40.93q44.73-6.52 89.6-11.57c17.66-2.09 32.49-18.81 34.34-38.88q4.86-52.95 9.73-105.04c.54-5.82 8.04-5.63 8.59.16q4.86 51.9 9.73 103.01c1.85 19.55 16.69 33.01 34.4 31.58q44.97-3.47 90.02-5.46a36.76 36.76 0 0 0 34.66-36.57V47.86a18.27 18.27 0 0 0-17.33-18.23q-30.32-1.15-60.63-2.97c-9.56-.59-17.31 7.13-17.31 17.32v112.25c0 5.52-7.5 6.48-8.57 1.06q-10.13-51.32-20.26-104c-3.38-17.49-17.5-31.07-33.85-32.59q-15.16-1.35-30.32-2.87c-16.34-1.7-30.42 10.04-33.79 28.63q-10.1 55.6-20.2 113.34c-1.06 6.1-8.52 5.4-8.52-.87Zm297.01-.38c-12.77-.2-23.11 7.4-23.12 17.07V223.6c0 9.69 10.35 17.28 23.12 17.08q41.4-.63 82.8 0c12.77.2 23.11-7.4 23.11-17.08V48.4c0-9.69-10.34-17.28-23.11-17.08q-41.4.62-82.8 0ZM520.7 47.87a18.28 18.28 0 0 1 17.33-18.23q64.98-2.47 129.85-8c9.55-.84 17.3 7.09 17.3 17.78v65.23c0 18.33-11.46 34.12-27.47 37.67q-27.56 6.23-55.16 12.04c-5.08 1.07-4.31 8.95.9 9.04q32.23.5 64.44 1.2c9.55.17 17.29 9 17.29 19.7v48.28c0 10.7-7.74 18.62-17.3 17.78q-64.84-5.53-129.84-8a18.27 18.27 0 0 1-17.33-18.23V164.9a35.26 35.26 0 0 1 27.86-34.62q29.6-6.15 59.18-12.68c5.12-1.14 4.38-9.15-.85-9.06q-34.42.55-68.86.87a17.3 17.3 0 0 1-17.33-17.48Zm181.75-9.81c0-10.8 7.74-20.32 17.28-21.34Q784.49 9.97 848.97.16c9.5-1.49 17.18 7.62 17.18 20.41v77.96c0 21.94-11.38 40.83-27.3 44.95q-27.4 7.28-54.87 13.76c-5.06 1.2-4.29 10.3.9 10.47q32.08 1.03 64.1 2.25c9.49.31 17.18 10.96 17.18 23.75v57.72c0 12.8-7.7 21.9-17.19 20.41q-64.46-9.8-129.24-16.56c-9.54-1.03-17.27-10.55-17.27-21.34v-65.82c0-18.61 11.63-34.66 27.77-38.57q29.49-7.02 58.92-14.88c5.1-1.37 4.36-10.67-.85-10.49q-34.25 1.11-68.57 2c-9.54.22-17.27-8.36-17.27-19.15Z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||getwizz.io^",
|
||||
"||wizz.chat^",
|
||||
"||wizzapp.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "xboxlive",
|
||||
Name: "Xbox Live",
|
||||
@@ -2448,6 +2521,14 @@ var blockedServices = []blockedService{{
|
||||
"||xboxlive.com^",
|
||||
"||xboxservices.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "xiaohongshu",
|
||||
Name: "Xiaohongshu",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M35 22v2h1v-2h-1zm0 0v2h1v-2h-1zm9-18H6c-1.09 0-2 .91-2 2v38c0 1.09.91 2 2 2h38c1.09 0 2-.91 2-2V6c0-1.09-.91-2-2-2zM12 24c0 1.38-.19 5.89-2.61 6.24l-.28-1.98c.39-.19.89-2.14.89-4.26v-2h2v2zm3 6h-2V19h2v11zm2.29-.29c-1.2-1.2-1.29-4.73-1.29-5.78V22h2v1.93c0 1.91.34 3.99.71 4.36l-1.42 1.42zM22 31h-3l1-2h3l-1 2zm9 0h-7l1-2h2v-7h-2l-2.1 4.38h1.72l-1 2H21a1 1 0 0 1-.82-1.57L22 24h-2a1 1 0 0 1-.86-1.51l3-5 1.72 1.02L21.77 22H25v-2h6v2h-2v7h2v2zm9-2.5a2.5 2.5 0 0 1-2.5 2.5c-1.21 0-1.22-.86-1.45-2H38v-3h-3v5h-2v-5h-2v-2h2v-2h-1v-2h1v-1h2v1h1a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2.5zm0-6.5h-1v-1c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1zm-5 2h1v-2h-1v2zm0-2v2h1v-2h-1z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||xhscdn.com^",
|
||||
"||xiaohongshu.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "youtube",
|
||||
Name: "YouTube",
|
||||
|
||||
@@ -4,32 +4,17 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// cookieTTL is the time-to-live of the session cookie.
|
||||
const cookieTTL = 365 * timeutil.Day
|
||||
|
||||
// sessionCookieName is the name of the session cookie.
|
||||
const sessionCookieName = "agh_session"
|
||||
|
||||
// sessionTokenSize is the length of session token in bytes.
|
||||
const sessionTokenSize = 16
|
||||
|
||||
@@ -69,7 +54,7 @@ func (s *session) deserialize(data []byte) bool {
|
||||
// Auth - global object
|
||||
type Auth struct {
|
||||
db *bbolt.DB
|
||||
raleLimiter *authRateLimiter
|
||||
rateLimiter *authRateLimiter
|
||||
sessions map[string]*session
|
||||
users []webUser
|
||||
lock sync.Mutex
|
||||
@@ -77,6 +62,8 @@ type Auth struct {
|
||||
}
|
||||
|
||||
// webUser represents a user of the Web UI.
|
||||
//
|
||||
// TODO(s.chzhen): Improve naming.
|
||||
type webUser struct {
|
||||
Name string `yaml:"name"`
|
||||
PasswordHash string `yaml:"password"`
|
||||
@@ -88,7 +75,7 @@ func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter
|
||||
|
||||
a := &Auth{
|
||||
sessionTTL: sessionTTL,
|
||||
raleLimiter: rateLimiter,
|
||||
rateLimiter: rateLimiter,
|
||||
sessions: make(map[string]*session),
|
||||
users: users,
|
||||
}
|
||||
@@ -216,8 +203,8 @@ func (a *Auth) storeSession(data []byte, s *session) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// remove session from file
|
||||
func (a *Auth) removeSession(sess []byte) {
|
||||
// removeSessionFromFile removes a stored session from the DB file on disk.
|
||||
func (a *Auth) removeSessionFromFile(sess []byte) {
|
||||
tx, err := a.db.Begin(true)
|
||||
if err != nil {
|
||||
log.Error("auth: bbolt.Begin: %s", err)
|
||||
@@ -279,7 +266,7 @@ func (a *Auth) checkSession(sess string) (res checkSessionResult) {
|
||||
if s.expire <= now {
|
||||
delete(a.sessions, sess)
|
||||
key, _ := hex.DecodeString(sess)
|
||||
a.removeSession(key)
|
||||
a.removeSessionFromFile(key)
|
||||
|
||||
return checkSessionExpired
|
||||
}
|
||||
@@ -301,351 +288,17 @@ func (a *Auth) checkSession(sess string) (res checkSessionResult) {
|
||||
return checkSessionOK
|
||||
}
|
||||
|
||||
// RemoveSession - remove session
|
||||
func (a *Auth) RemoveSession(sess string) {
|
||||
// removeSession removes the session from the active sessions and the disk.
|
||||
func (a *Auth) removeSession(sess string) {
|
||||
key, _ := hex.DecodeString(sess)
|
||||
a.lock.Lock()
|
||||
delete(a.sessions, sess)
|
||||
a.lock.Unlock()
|
||||
a.removeSession(key)
|
||||
a.removeSessionFromFile(key)
|
||||
}
|
||||
|
||||
type loginJSON struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// newSessionToken returns cryptographically secure randomly generated slice of
|
||||
// bytes of sessionTokenSize length.
|
||||
//
|
||||
// TODO(e.burkov): Think about using byte array instead of byte slice.
|
||||
func newSessionToken() (data []byte, err error) {
|
||||
randData := make([]byte, sessionTokenSize)
|
||||
|
||||
_, err = rand.Read(randData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return randData, nil
|
||||
}
|
||||
|
||||
// newCookie creates a new authentication cookie.
|
||||
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) {
|
||||
rateLimiter := a.raleLimiter
|
||||
u, ok := a.findUser(req.Name, req.Password)
|
||||
if !ok {
|
||||
if rateLimiter != nil {
|
||||
rateLimiter.inc(addr)
|
||||
}
|
||||
|
||||
return nil, errors.Error("invalid username or password")
|
||||
}
|
||||
|
||||
if rateLimiter != nil {
|
||||
rateLimiter.remove(addr)
|
||||
}
|
||||
|
||||
sess, err := newSessionToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating token: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
a.addSession(sess, &session{
|
||||
userName: u.Name,
|
||||
expire: uint32(now.Unix()) + a.sessionTTL,
|
||||
})
|
||||
|
||||
return &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: hex.EncodeToString(sess),
|
||||
Path: "/",
|
||||
Expires: now.Add(cookieTTL),
|
||||
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// realIP extracts the real IP address of the client from an HTTP request using
|
||||
// the known HTTP headers.
|
||||
//
|
||||
// TODO(a.garipov): Currently, this is basically a copy of a similar function in
|
||||
// module dnsproxy. This should really become a part of module golibs and be
|
||||
// replaced both here and there. Or be replaced in both places by
|
||||
// a well-maintained third-party module.
|
||||
//
|
||||
// TODO(a.garipov): Support header Forwarded from RFC 7329.
|
||||
func realIP(r *http.Request) (ip net.IP, err error) {
|
||||
proxyHeaders := []string{
|
||||
httphdr.CFConnectingIP,
|
||||
httphdr.TrueClientIP,
|
||||
httphdr.XRealIP,
|
||||
}
|
||||
|
||||
for _, h := range proxyHeaders {
|
||||
v := r.Header.Get(h)
|
||||
ip = net.ParseIP(v)
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above yielded any results, get the leftmost IP address
|
||||
// from the X-Forwarded-For header.
|
||||
s := r.Header.Get(httphdr.XForwardedFor)
|
||||
ipStrs := strings.SplitN(s, ", ", 2)
|
||||
ip = net.ParseIP(ipStrs[0])
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// When everything else fails, just return the remote address as understood
|
||||
// by the stdlib.
|
||||
ipStr, err := netutil.SplitHost(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting ip from client addr: %w", err)
|
||||
}
|
||||
|
||||
return net.ParseIP(ipStr), nil
|
||||
}
|
||||
|
||||
// writeErrorWithIP is like [aghhttp.Error], but includes the remote IP address
|
||||
// when it writes to the log.
|
||||
func writeErrorWithIP(
|
||||
r *http.Request,
|
||||
w http.ResponseWriter,
|
||||
code int,
|
||||
remoteIP string,
|
||||
format string,
|
||||
args ...any,
|
||||
) {
|
||||
text := fmt.Sprintf(format, args...)
|
||||
log.Error("%s %s %s: from ip %s: %s", r.Method, r.Host, r.URL, remoteIP, text)
|
||||
http.Error(w, text, code)
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
req := loginJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var remoteIP string
|
||||
// realIP cannot be used here without taking TrustedProxies into account due
|
||||
// to security issues.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
|
||||
//
|
||||
// TODO(e.burkov): Use realIP when the issue will be fixed.
|
||||
if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
|
||||
writeErrorWithIP(
|
||||
r,
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
r.RemoteAddr,
|
||||
"auth: getting remote address: %s",
|
||||
err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
|
||||
if left := rateLimiter.check(remoteIP); left > 0 {
|
||||
w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
|
||||
writeErrorWithIP(
|
||||
r,
|
||||
w,
|
||||
http.StatusTooManyRequests,
|
||||
remoteIP,
|
||||
"auth: blocked for %s",
|
||||
left,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cookie, err := Context.auth.newCookie(req, remoteIP)
|
||||
if err != nil {
|
||||
writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Use realIP here, since this IP address is only used for logging.
|
||||
ip, err := realIP(r)
|
||||
if err != nil {
|
||||
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
|
||||
}
|
||||
|
||||
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
h := w.Header()
|
||||
h.Set(httphdr.CacheControl, "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
h.Set(httphdr.Pragma, "no-cache")
|
||||
h.Set(httphdr.Expires, "0")
|
||||
|
||||
aghhttp.OK(w)
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
respHdr := w.Header()
|
||||
c, err := r.Cookie(sessionCookieName)
|
||||
if err != nil {
|
||||
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
|
||||
// The user is already logged out.
|
||||
respHdr.Set(httphdr.Location, "/login.html")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Context.auth.RemoveSession(c.Value)
|
||||
|
||||
c = &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
respHdr.Set(httphdr.Location, "/login.html")
|
||||
respHdr.Set(httphdr.SetCookie, c.String())
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
// RegisterAuthHandlers - register handlers
|
||||
func RegisterAuthHandlers() {
|
||||
Context.mux.Handle("/control/login", postInstallHandler(ensureHandler(http.MethodPost, handleLogin)))
|
||||
httpRegister(http.MethodGet, "/control/logout", handleLogout)
|
||||
}
|
||||
|
||||
// optionalAuthThird return true if user should authenticate first.
|
||||
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
|
||||
if glProcessCookie(r) {
|
||||
log.Debug("auth: authentication is handled by GL-Inet submodule")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// redirect to login page if not authenticated
|
||||
isAuthenticated := false
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if err != nil {
|
||||
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
|
||||
// Check Basic authentication.
|
||||
user, pass, hasBasic := r.BasicAuth()
|
||||
if hasBasic {
|
||||
_, isAuthenticated = Context.auth.findUser(user, pass)
|
||||
if !isAuthenticated {
|
||||
log.Info("auth: invalid Basic Authorization value")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res := Context.auth.checkSession(cookie.Value)
|
||||
isAuthenticated = res == checkSessionOK
|
||||
if !isAuthenticated {
|
||||
log.Debug("auth: invalid cookie value: %s", cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if isAuthenticated {
|
||||
return false
|
||||
}
|
||||
|
||||
if p := r.URL.Path; p == "/" || p == "/index.html" {
|
||||
if glProcessRedirect(w, r) {
|
||||
log.Debug("auth: redirected to login page by GL-Inet submodule")
|
||||
} else {
|
||||
log.Debug("auth: redirected to login page")
|
||||
http.Redirect(w, r, "login.html", http.StatusFound)
|
||||
}
|
||||
} else {
|
||||
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte("Forbidden"))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the
|
||||
// project.
|
||||
func optionalAuth(
|
||||
h func(http.ResponseWriter, *http.Request),
|
||||
) (wrapped func(http.ResponseWriter, *http.Request)) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Path
|
||||
authRequired := Context.auth != nil && Context.auth.AuthRequired()
|
||||
if p == "/login.html" {
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if authRequired && err == nil {
|
||||
// Redirect to the dashboard if already authenticated.
|
||||
res := Context.auth.checkSession(cookie.Value)
|
||||
if res == checkSessionOK {
|
||||
http.Redirect(w, r, "", http.StatusFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("auth: invalid cookie value: %s", cookie)
|
||||
}
|
||||
} else if isPublicResource(p) {
|
||||
// Process as usual, no additional auth requirements.
|
||||
} else if authRequired {
|
||||
if optionalAuthThird(w, r) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// isPublicResource returns true if p is a path to a public resource.
|
||||
func isPublicResource(p string) (ok bool) {
|
||||
isAsset, err := path.Match("/assets/*", p)
|
||||
if err != nil {
|
||||
// The only error that is returned from path.Match is
|
||||
// [path.ErrBadPattern]. This is a programmer error.
|
||||
panic(fmt.Errorf("bad asset pattern: %w", err))
|
||||
}
|
||||
|
||||
isLogin, err := path.Match("/login.*", p)
|
||||
if err != nil {
|
||||
// Same as above.
|
||||
panic(fmt.Errorf("bad login pattern: %w", err))
|
||||
}
|
||||
|
||||
return isAsset || isLogin
|
||||
}
|
||||
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
optionalAuth(a.handler.ServeHTTP)(w, r)
|
||||
}
|
||||
|
||||
func optionalAuthHandler(handler http.Handler) http.Handler {
|
||||
return &authHandler{handler}
|
||||
}
|
||||
|
||||
// Add adds a new user with the given password.
|
||||
func (a *Auth) Add(u *webUser, password string) (err error) {
|
||||
// addUser adds a new user with the given password.
|
||||
func (a *Auth) addUser(u *webUser, password string) (err error) {
|
||||
if len(password) == 0 {
|
||||
return errors.Error("empty password")
|
||||
}
|
||||
@@ -715,22 +368,40 @@ func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
|
||||
return webUser{}
|
||||
}
|
||||
|
||||
// GetUsers - get users
|
||||
func (a *Auth) GetUsers() []webUser {
|
||||
// usersList returns a copy of a users list.
|
||||
func (a *Auth) usersList() (users []webUser) {
|
||||
a.lock.Lock()
|
||||
users := a.users
|
||||
a.lock.Unlock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
users = make([]webUser, len(a.users))
|
||||
copy(users, a.users)
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// AuthRequired - if authentication is required
|
||||
func (a *Auth) AuthRequired() bool {
|
||||
// authRequired returns true if a authentication is required.
|
||||
func (a *Auth) authRequired() bool {
|
||||
if GLMode {
|
||||
return true
|
||||
}
|
||||
|
||||
a.lock.Lock()
|
||||
r := (len(a.users) != 0)
|
||||
a.lock.Unlock()
|
||||
return r
|
||||
defer a.lock.Unlock()
|
||||
|
||||
return len(a.users) != 0
|
||||
}
|
||||
|
||||
// newSessionToken returns cryptographically secure randomly generated slice of
|
||||
// bytes of sessionTokenSize length.
|
||||
//
|
||||
// TODO(e.burkov): Think about using byte array instead of byte slice.
|
||||
func newSessionToken() (data []byte, err error) {
|
||||
randData := make([]byte, sessionTokenSize)
|
||||
|
||||
_, err = rand.Read(randData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return randData, nil
|
||||
}
|
||||
|
||||
89
internal/home/auth_internal_test.go
Normal file
89
internal/home/auth_internal_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewSessionToken(t *testing.T) {
|
||||
// Successful case.
|
||||
token, err := newSessionToken()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, token, sessionTokenSize)
|
||||
|
||||
// Break the rand.Reader.
|
||||
prevReader := rand.Reader
|
||||
t.Cleanup(func() { rand.Reader = prevReader })
|
||||
rand.Reader = &bytes.Buffer{}
|
||||
|
||||
// Unsuccessful case.
|
||||
token, err = newSessionToken()
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, token)
|
||||
}
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fn := filepath.Join(dir, "sessions.db")
|
||||
|
||||
users := []webUser{{
|
||||
Name: "name",
|
||||
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
|
||||
}}
|
||||
a := InitAuth(fn, nil, 60, nil)
|
||||
s := session{}
|
||||
|
||||
user := webUser{Name: "name"}
|
||||
err := a.addUser(&user, "password")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
|
||||
a.removeSession("notfound")
|
||||
|
||||
sess, err := newSessionToken()
|
||||
require.NoError(t, err)
|
||||
sessStr := hex.EncodeToString(sess)
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
// check expiration
|
||||
s.expire = uint32(now)
|
||||
a.addSession(sess, &s)
|
||||
assert.Equal(t, checkSessionExpired, a.checkSession(sessStr))
|
||||
|
||||
// add session with TTL = 2 sec
|
||||
s = session{}
|
||||
s.expire = uint32(time.Now().UTC().Unix() + 2)
|
||||
a.addSession(sess, &s)
|
||||
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
|
||||
|
||||
a.Close()
|
||||
|
||||
// load saved session
|
||||
a = InitAuth(fn, users, 60, nil)
|
||||
|
||||
// the session is still alive
|
||||
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
|
||||
// reset our expiration time because checkSession() has just updated it
|
||||
s.expire = uint32(time.Now().UTC().Unix() + 2)
|
||||
a.storeSession(sess, &s)
|
||||
a.Close()
|
||||
|
||||
u, ok := a.findUser("name", "password")
|
||||
assert.True(t, ok)
|
||||
assert.NotEmpty(t, u.Name)
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// load and remove expired sessions
|
||||
a = InitAuth(fn, users, 60, nil)
|
||||
assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
|
||||
|
||||
a.Close()
|
||||
}
|
||||
352
internal/home/authhttp.go
Normal file
352
internal/home/authhttp.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
)
|
||||
|
||||
// cookieTTL is the time-to-live of the session cookie.
|
||||
const cookieTTL = 365 * timeutil.Day
|
||||
|
||||
// sessionCookieName is the name of the session cookie.
|
||||
const sessionCookieName = "agh_session"
|
||||
|
||||
// loginJSON is the JSON structure for authentication.
|
||||
type loginJSON struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// newCookie creates a new authentication cookie.
|
||||
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) {
|
||||
rateLimiter := a.rateLimiter
|
||||
u, ok := a.findUser(req.Name, req.Password)
|
||||
if !ok {
|
||||
if rateLimiter != nil {
|
||||
rateLimiter.inc(addr)
|
||||
}
|
||||
|
||||
return nil, errors.Error("invalid username or password")
|
||||
}
|
||||
|
||||
if rateLimiter != nil {
|
||||
rateLimiter.remove(addr)
|
||||
}
|
||||
|
||||
sess, err := newSessionToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating token: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
a.addSession(sess, &session{
|
||||
userName: u.Name,
|
||||
expire: uint32(now.Unix()) + a.sessionTTL,
|
||||
})
|
||||
|
||||
return &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: hex.EncodeToString(sess),
|
||||
Path: "/",
|
||||
Expires: now.Add(cookieTTL),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// realIP extracts the real IP address of the client from an HTTP request using
|
||||
// the known HTTP headers.
|
||||
//
|
||||
// TODO(a.garipov): Currently, this is basically a copy of a similar function in
|
||||
// module dnsproxy. This should really become a part of module golibs and be
|
||||
// replaced both here and there. Or be replaced in both places by
|
||||
// a well-maintained third-party module.
|
||||
//
|
||||
// TODO(a.garipov): Support header Forwarded from RFC 7329.
|
||||
func realIP(r *http.Request) (ip net.IP, err error) {
|
||||
proxyHeaders := []string{
|
||||
httphdr.CFConnectingIP,
|
||||
httphdr.TrueClientIP,
|
||||
httphdr.XRealIP,
|
||||
}
|
||||
|
||||
for _, h := range proxyHeaders {
|
||||
v := r.Header.Get(h)
|
||||
ip = net.ParseIP(v)
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above yielded any results, get the leftmost IP address
|
||||
// from the X-Forwarded-For header.
|
||||
s := r.Header.Get(httphdr.XForwardedFor)
|
||||
ipStrs := strings.SplitN(s, ", ", 2)
|
||||
ip = net.ParseIP(ipStrs[0])
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// When everything else fails, just return the remote address as understood
|
||||
// by the stdlib.
|
||||
ipStr, err := netutil.SplitHost(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting ip from client addr: %w", err)
|
||||
}
|
||||
|
||||
return net.ParseIP(ipStr), nil
|
||||
}
|
||||
|
||||
// writeErrorWithIP is like [aghhttp.Error], but includes the remote IP address
|
||||
// when it writes to the log.
|
||||
func writeErrorWithIP(
|
||||
r *http.Request,
|
||||
w http.ResponseWriter,
|
||||
code int,
|
||||
remoteIP string,
|
||||
format string,
|
||||
args ...any,
|
||||
) {
|
||||
text := fmt.Sprintf(format, args...)
|
||||
log.Error("%s %s %s: from ip %s: %s", r.Method, r.Host, r.URL, remoteIP, text)
|
||||
http.Error(w, text, code)
|
||||
}
|
||||
|
||||
// handleLogin is the handler for the POST /control/login HTTP API.
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
req := loginJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var remoteIP string
|
||||
// realIP cannot be used here without taking TrustedProxies into account due
|
||||
// to security issues.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
|
||||
//
|
||||
// TODO(e.burkov): Use realIP when the issue will be fixed.
|
||||
if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
|
||||
writeErrorWithIP(
|
||||
r,
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
r.RemoteAddr,
|
||||
"auth: getting remote address: %s",
|
||||
err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if rateLimiter := Context.auth.rateLimiter; rateLimiter != nil {
|
||||
if left := rateLimiter.check(remoteIP); left > 0 {
|
||||
w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
|
||||
writeErrorWithIP(
|
||||
r,
|
||||
w,
|
||||
http.StatusTooManyRequests,
|
||||
remoteIP,
|
||||
"auth: blocked for %s",
|
||||
left,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cookie, err := Context.auth.newCookie(req, remoteIP)
|
||||
if err != nil {
|
||||
writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Use realIP here, since this IP address is only used for logging.
|
||||
ip, err := realIP(r)
|
||||
if err != nil {
|
||||
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
|
||||
}
|
||||
|
||||
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
h := w.Header()
|
||||
h.Set(httphdr.CacheControl, "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
h.Set(httphdr.Pragma, "no-cache")
|
||||
h.Set(httphdr.Expires, "0")
|
||||
|
||||
aghhttp.OK(w)
|
||||
}
|
||||
|
||||
// handleLogout is the handler for the GET /control/logout HTTP API.
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
respHdr := w.Header()
|
||||
c, err := r.Cookie(sessionCookieName)
|
||||
if err != nil {
|
||||
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
|
||||
// The user is already logged out.
|
||||
respHdr.Set(httphdr.Location, "/login.html")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Context.auth.removeSession(c.Value)
|
||||
|
||||
c = &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
respHdr.Set(httphdr.Location, "/login.html")
|
||||
respHdr.Set(httphdr.SetCookie, c.String())
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
// RegisterAuthHandlers - register handlers
|
||||
func RegisterAuthHandlers() {
|
||||
Context.mux.Handle("/control/login", postInstallHandler(ensureHandler(http.MethodPost, handleLogin)))
|
||||
httpRegister(http.MethodGet, "/control/logout", handleLogout)
|
||||
}
|
||||
|
||||
// optionalAuthThird returns true if a user should authenticate first.
|
||||
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
|
||||
pref := fmt.Sprintf("auth: raddr %s", r.RemoteAddr)
|
||||
|
||||
if glProcessCookie(r) {
|
||||
log.Debug("%s: authentication is handled by gl-inet submodule", pref)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// redirect to login page if not authenticated
|
||||
isAuthenticated := false
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if err != nil {
|
||||
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
|
||||
// Check Basic authentication.
|
||||
user, pass, hasBasic := r.BasicAuth()
|
||||
if hasBasic {
|
||||
_, isAuthenticated = Context.auth.findUser(user, pass)
|
||||
if !isAuthenticated {
|
||||
log.Info("%s: invalid basic authorization value", pref)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res := Context.auth.checkSession(cookie.Value)
|
||||
isAuthenticated = res == checkSessionOK
|
||||
if !isAuthenticated {
|
||||
log.Debug("%s: invalid cookie value: %q", pref, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
if isAuthenticated {
|
||||
return false
|
||||
}
|
||||
|
||||
if p := r.URL.Path; p == "/" || p == "/index.html" {
|
||||
if glProcessRedirect(w, r) {
|
||||
log.Debug("%s: redirected to login page by gl-inet submodule", pref)
|
||||
} else {
|
||||
log.Debug("%s: redirected to login page", pref)
|
||||
http.Redirect(w, r, "login.html", http.StatusFound)
|
||||
}
|
||||
} else {
|
||||
log.Debug("%s: responded with forbidden to %s %s", pref, r.Method, p)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte("Forbidden"))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the
|
||||
// project.
|
||||
func optionalAuth(
|
||||
h func(http.ResponseWriter, *http.Request),
|
||||
) (wrapped func(http.ResponseWriter, *http.Request)) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Path
|
||||
authRequired := Context.auth != nil && Context.auth.authRequired()
|
||||
if p == "/login.html" {
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if authRequired && err == nil {
|
||||
// Redirect to the dashboard if already authenticated.
|
||||
res := Context.auth.checkSession(cookie.Value)
|
||||
if res == checkSessionOK {
|
||||
http.Redirect(w, r, "", http.StatusFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("auth: raddr %s: invalid cookie value: %q", r.RemoteAddr, cookie)
|
||||
}
|
||||
} else if isPublicResource(p) {
|
||||
// Process as usual, no additional auth requirements.
|
||||
} else if authRequired {
|
||||
if optionalAuthThird(w, r) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// isPublicResource returns true if p is a path to a public resource.
|
||||
func isPublicResource(p string) (ok bool) {
|
||||
isAsset, err := path.Match("/assets/*", p)
|
||||
if err != nil {
|
||||
// The only error that is returned from path.Match is
|
||||
// [path.ErrBadPattern]. This is a programmer error.
|
||||
panic(fmt.Errorf("bad asset pattern: %w", err))
|
||||
}
|
||||
|
||||
isLogin, err := path.Match("/login.*", p)
|
||||
if err != nil {
|
||||
// Same as above.
|
||||
panic(fmt.Errorf("bad login pattern: %w", err))
|
||||
}
|
||||
|
||||
return isAsset || isLogin
|
||||
}
|
||||
|
||||
// authHandler is a helper structure that implements [http.Handler].
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// ServeHTTP implements the [http.Handler] interface for *authHandler.
|
||||
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
optionalAuth(a.handler.ServeHTTP)(w, r)
|
||||
}
|
||||
|
||||
// optionalAuthHandler returns a authentication handler.
|
||||
func optionalAuthHandler(handler http.Handler) http.Handler {
|
||||
return &authHandler{handler}
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
@@ -18,82 +14,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewSessionToken(t *testing.T) {
|
||||
// Successful case.
|
||||
token, err := newSessionToken()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, token, sessionTokenSize)
|
||||
|
||||
// Break the rand.Reader.
|
||||
prevReader := rand.Reader
|
||||
t.Cleanup(func() { rand.Reader = prevReader })
|
||||
rand.Reader = &bytes.Buffer{}
|
||||
|
||||
// Unsuccessful case.
|
||||
token, err = newSessionToken()
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, token)
|
||||
}
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fn := filepath.Join(dir, "sessions.db")
|
||||
|
||||
users := []webUser{{
|
||||
Name: "name",
|
||||
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
|
||||
}}
|
||||
a := InitAuth(fn, nil, 60, nil)
|
||||
s := session{}
|
||||
|
||||
user := webUser{Name: "name"}
|
||||
err := a.Add(&user, "password")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
|
||||
a.RemoveSession("notfound")
|
||||
|
||||
sess, err := newSessionToken()
|
||||
require.NoError(t, err)
|
||||
sessStr := hex.EncodeToString(sess)
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
// check expiration
|
||||
s.expire = uint32(now)
|
||||
a.addSession(sess, &s)
|
||||
assert.Equal(t, checkSessionExpired, a.checkSession(sessStr))
|
||||
|
||||
// add session with TTL = 2 sec
|
||||
s = session{}
|
||||
s.expire = uint32(time.Now().UTC().Unix() + 2)
|
||||
a.addSession(sess, &s)
|
||||
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
|
||||
|
||||
a.Close()
|
||||
|
||||
// load saved session
|
||||
a = InitAuth(fn, users, 60, nil)
|
||||
|
||||
// the session is still alive
|
||||
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
|
||||
// reset our expiration time because checkSession() has just updated it
|
||||
s.expire = uint32(time.Now().UTC().Unix() + 2)
|
||||
a.storeSession(sess, &s)
|
||||
a.Close()
|
||||
|
||||
u, ok := a.findUser("name", "password")
|
||||
assert.True(t, ok)
|
||||
assert.NotEmpty(t, u.Name)
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// load and remove expired sessions
|
||||
a = InitAuth(fn, users, 60, nil)
|
||||
assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
|
||||
|
||||
a.Close()
|
||||
}
|
||||
|
||||
// implements http.ResponseWriter
|
||||
type testResponseWriter struct {
|
||||
hdr http.Header
|
||||
@@ -587,7 +587,7 @@ func (c *configuration) write() (err error) {
|
||||
defer c.Unlock()
|
||||
|
||||
if Context.auth != nil {
|
||||
config.Users = Context.auth.GetUsers()
|
||||
config.Users = Context.auth.usersList()
|
||||
}
|
||||
|
||||
if Context.tls != nil {
|
||||
|
||||
@@ -420,7 +420,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
|
||||
u := &webUser{
|
||||
Name: req.Username,
|
||||
}
|
||||
err = Context.auth.Add(u, req.Password)
|
||||
err = Context.auth.addUser(u, req.Password)
|
||||
if err != nil {
|
||||
Context.firstRun = true
|
||||
copyInstallSettings(config, curConfig)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package ipset
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -38,19 +39,69 @@ func newManager(ipsetConf []string) (set Manager, err error) {
|
||||
|
||||
// defaultDial is the default netfilter dialing function.
|
||||
func defaultDial(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn, err error) {
|
||||
conn, err = ipset.Dial(pf, conf)
|
||||
c, err := ipset.Dial(pf, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
return &queryConn{c}, nil
|
||||
}
|
||||
|
||||
// queryConn is the [ipsetConn] implementation with listAll method, which
|
||||
// returns the list of properties of all available ipsets.
|
||||
type queryConn struct {
|
||||
*ipset.Conn
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ipsetConn = (*queryConn)(nil)
|
||||
|
||||
// listAll returns the list of properties of all available ipsets.
|
||||
//
|
||||
// TODO(s.chzhen): Use https://github.com/vishvananda/netlink.
|
||||
func (qc *queryConn) listAll() (sets []props, err error) {
|
||||
msg, err := netfilter.MarshalNetlink(
|
||||
netfilter.Header{
|
||||
// The family doesn't seem to matter. See TODO on parseIpsetConfig.
|
||||
Family: qc.Conn.Family,
|
||||
SubsystemID: netfilter.NFSubsysIPSet,
|
||||
MessageType: netfilter.MessageType(ipset.CmdList),
|
||||
Flags: netlink.Request | netlink.Dump,
|
||||
},
|
||||
[]netfilter.Attribute{{
|
||||
Type: uint16(ipset.AttrProtocol),
|
||||
Data: []byte{ipset.Protocol},
|
||||
}},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling netlink msg: %w", err)
|
||||
}
|
||||
|
||||
// We assume it's OK to call a method of an unexported type
|
||||
// [ipset.connector], since there is no negative effects.
|
||||
ms, err := qc.Conn.Conn.Query(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying netlink msg: %w", err)
|
||||
}
|
||||
|
||||
for i, s := range ms {
|
||||
p := props{}
|
||||
err = p.unmarshalMessage(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling netlink msg at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
sets = append(sets, p)
|
||||
}
|
||||
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
// ipsetConn is the ipset conn interface.
|
||||
type ipsetConn interface {
|
||||
Add(name string, entries ...*ipset.Entry) (err error)
|
||||
Close() (err error)
|
||||
Header(name string) (p *ipset.HeaderPolicy, err error)
|
||||
listAll() (sets []props, err error)
|
||||
}
|
||||
|
||||
// dialer creates an ipsetConn.
|
||||
@@ -58,8 +109,75 @@ type dialer func(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn
|
||||
|
||||
// props contains one Linux Netfilter ipset properties.
|
||||
type props struct {
|
||||
name string
|
||||
// name of the ipset.
|
||||
name string
|
||||
|
||||
// family of the IP addresses in the ipset.
|
||||
family netfilter.ProtoFamily
|
||||
|
||||
// isPersistent indicates that ipset has no timeout parameter and all
|
||||
// entries are added permanently.
|
||||
isPersistent bool
|
||||
}
|
||||
|
||||
// unmarshalMessage unmarshals netlink message and sets the properties of the
|
||||
// ipset.
|
||||
func (p *props) unmarshalMessage(msg netlink.Message) (err error) {
|
||||
_, attrs, err := netfilter.UnmarshalNetlink(msg)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
// By default ipset has no timeout parameter.
|
||||
p.isPersistent = true
|
||||
|
||||
for _, a := range attrs {
|
||||
p.parseAttribute(a)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseAttribute parses netfilter attribute and sets the name and family of
|
||||
// the ipset.
|
||||
func (p *props) parseAttribute(a netfilter.Attribute) {
|
||||
switch ipset.AttributeType(a.Type) {
|
||||
case ipset.AttrData:
|
||||
p.parseAttrData(a)
|
||||
case ipset.AttrSetName:
|
||||
// Trim the null character.
|
||||
p.name = string(bytes.Trim(a.Data, "\x00"))
|
||||
case ipset.AttrFamily:
|
||||
p.family = netfilter.ProtoFamily(a.Data[0])
|
||||
default:
|
||||
// Go on.
|
||||
}
|
||||
}
|
||||
|
||||
// parseAttrData parses attribute data and sets the timeout of the ipset.
|
||||
func (p *props) parseAttrData(a netfilter.Attribute) {
|
||||
for _, a := range a.Children {
|
||||
switch ipset.AttributeType(a.Type) {
|
||||
case ipset.AttrTimeout:
|
||||
timeout := a.Uint32()
|
||||
p.isPersistent = timeout == 0
|
||||
default:
|
||||
// Go on.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unit is a convenient alias for struct{}.
|
||||
type unit = struct{}
|
||||
|
||||
// ipsInIpset is the type of a set of IP-address-to-ipset mappings.
|
||||
type ipsInIpset map[ipInIpsetEntry]unit
|
||||
|
||||
// ipInIpsetEntry is the type for entries in an ipsInIpset set.
|
||||
type ipInIpsetEntry struct {
|
||||
ipsetName string
|
||||
ipArr [net.IPv6len]byte
|
||||
}
|
||||
|
||||
// manager is the Linux Netfilter ipset manager.
|
||||
@@ -72,6 +190,13 @@ type manager struct {
|
||||
// mu protects all properties below.
|
||||
mu *sync.Mutex
|
||||
|
||||
// TODO(a.garipov): Currently, the ipset list is static, and we don't
|
||||
// read the IPs already in sets, so we can assume that all incoming IPs
|
||||
// are either added to all corresponding ipsets or not. When that stops
|
||||
// being the case, for example if we add dynamic reconfiguration of
|
||||
// ipsets, this map will need to become a per-ipset-name one.
|
||||
addedIPs ipsInIpset
|
||||
|
||||
ipv4Conn ipsetConn
|
||||
ipv6Conn ipsetConn
|
||||
}
|
||||
@@ -96,8 +221,8 @@ func (m *manager) dialNetfilter(conf *netlink.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseIpsetConfig parses one ipset configuration string.
|
||||
func parseIpsetConfig(confStr string) (hosts, ipsetNames []string, err error) {
|
||||
// parseIpsetConfigLine parses one ipset configuration line.
|
||||
func parseIpsetConfigLine(confStr string) (hosts, ipsetNames []string, err error) {
|
||||
confStr = strings.TrimSpace(confStr)
|
||||
hostsAndNames := strings.Split(confStr, "/")
|
||||
if len(hostsAndNames) != 2 {
|
||||
@@ -125,50 +250,53 @@ func parseIpsetConfig(confStr string) (hosts, ipsetNames []string, err error) {
|
||||
return hosts, ipsetNames, nil
|
||||
}
|
||||
|
||||
// ipsetProps returns the properties of an ipset with the given name.
|
||||
func (m *manager) ipsetProps(name string) (set props, err error) {
|
||||
// The family doesn't seem to matter when we use a header query, so
|
||||
// query only the IPv4 one.
|
||||
// parseIpsetConfig parses the ipset configuration and stores ipsets. It
|
||||
// returns an error if the configuration can't be used.
|
||||
func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) {
|
||||
// The family doesn't seem to matter when we use a header query, so query
|
||||
// only the IPv4 one.
|
||||
//
|
||||
// TODO(a.garipov): Find out if this is a bug or a feature.
|
||||
var res *ipset.HeaderPolicy
|
||||
res, err = m.ipv4Conn.Header(name)
|
||||
all, err := m.ipv4Conn.listAll()
|
||||
if err != nil {
|
||||
return set, err
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
if res == nil || res.Family == nil {
|
||||
return set, errors.Error("empty response or no family data")
|
||||
for _, p := range all {
|
||||
m.nameToIpset[p.name] = p
|
||||
}
|
||||
|
||||
family := netfilter.ProtoFamily(res.Family.Value)
|
||||
if family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 {
|
||||
return set, fmt.Errorf("unexpected ipset family %d", family)
|
||||
for i, confStr := range ipsetConf {
|
||||
var hosts, ipsetNames []string
|
||||
hosts, ipsetNames, err = parseIpsetConfigLine(confStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config line at idx %d: %w", i, err)
|
||||
}
|
||||
|
||||
var ipsets []props
|
||||
ipsets, err = m.ipsets(ipsetNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting ipsets from config line at idx %d: %w", i, err)
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
m.domainToIpsets[host] = append(m.domainToIpsets[host], ipsets...)
|
||||
}
|
||||
}
|
||||
|
||||
return props{
|
||||
name: name,
|
||||
family: family,
|
||||
}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// ipsets returns currently known ipsets.
|
||||
func (m *manager) ipsets(names []string) (sets []props, err error) {
|
||||
for _, name := range names {
|
||||
set, ok := m.nameToIpset[name]
|
||||
if ok {
|
||||
sets = append(sets, set)
|
||||
|
||||
continue
|
||||
for _, n := range names {
|
||||
p, ok := m.nameToIpset[n]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown ipset %q", n)
|
||||
}
|
||||
|
||||
set, err = m.ipsetProps(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying ipset %q: %w", name, err)
|
||||
}
|
||||
|
||||
m.nameToIpset[name] = set
|
||||
sets = append(sets, set)
|
||||
sets = append(sets, p)
|
||||
}
|
||||
|
||||
return sets, nil
|
||||
@@ -186,6 +314,8 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
|
||||
domainToIpsets: make(map[string][]props),
|
||||
|
||||
dial: dial,
|
||||
|
||||
addedIPs: make(ipsInIpset),
|
||||
}
|
||||
|
||||
err = m.dialNetfilter(&netlink.Config{})
|
||||
@@ -201,26 +331,9 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
|
||||
return nil, fmt.Errorf("dialing netfilter: %w", err)
|
||||
}
|
||||
|
||||
for i, confStr := range ipsetConf {
|
||||
var hosts, ipsetNames []string
|
||||
hosts, ipsetNames, err = parseIpsetConfig(confStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config line at idx %d: %w", i, err)
|
||||
}
|
||||
|
||||
var ipsets []props
|
||||
ipsets, err = m.ipsets(ipsetNames)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"getting ipsets from config line at idx %d: %w",
|
||||
i,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
m.domainToIpsets[host] = append(m.domainToIpsets[host], ipsets...)
|
||||
}
|
||||
err = m.parseIpsetConfig(ipsetConf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting ipsets: %w", err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -259,8 +372,19 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
|
||||
}
|
||||
|
||||
var entries []*ipset.Entry
|
||||
var newAddedEntries []ipInIpsetEntry
|
||||
for _, ip := range ips {
|
||||
e := ipInIpsetEntry{
|
||||
ipsetName: set.name,
|
||||
}
|
||||
copy(e.ipArr[:], ip.To16())
|
||||
|
||||
if _, added := m.addedIPs[e]; added {
|
||||
continue
|
||||
}
|
||||
|
||||
entries = append(entries, ipset.NewEntry(ipset.EntryIP(ip)))
|
||||
newAddedEntries = append(newAddedEntries, e)
|
||||
}
|
||||
|
||||
n = len(entries)
|
||||
@@ -283,6 +407,15 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
|
||||
return 0, fmt.Errorf("adding %q%s to ipset %q: %w", host, ips, set.name, err)
|
||||
}
|
||||
|
||||
// Only add these to the cache once we're sure that all of them were
|
||||
// actually sent to the ipset.
|
||||
for _, e := range newAddedEntries {
|
||||
s := m.nameToIpset[e.ipsetName]
|
||||
if s.isPersistent {
|
||||
m.addedIPs[e] = unit{}
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,12 @@ type fakeConn struct {
|
||||
ipv4Entries *[]*ipset.Entry
|
||||
ipv6Header *ipset.HeaderPolicy
|
||||
ipv6Entries *[]*ipset.Entry
|
||||
sets []props
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ipsetConn = (*fakeConn)(nil)
|
||||
|
||||
// Add implements the [ipsetConn] interface for *fakeConn.
|
||||
func (c *fakeConn) Add(name string, entries ...*ipset.Entry) (err error) {
|
||||
if strings.Contains(name, "ipv4") {
|
||||
@@ -43,15 +47,9 @@ func (c *fakeConn) Close() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Header implements the [ipsetConn] interface for *fakeConn.
|
||||
func (c *fakeConn) Header(name string) (p *ipset.HeaderPolicy, err error) {
|
||||
if strings.Contains(name, "ipv4") {
|
||||
return c.ipv4Header, nil
|
||||
} else if strings.Contains(name, "ipv6") {
|
||||
return c.ipv6Header, nil
|
||||
}
|
||||
|
||||
return nil, errors.Error("test: ipset not found")
|
||||
// listAll implements the [ipsetConn] interface for *fakeConn.
|
||||
func (c *fakeConn) listAll() (sets []props, err error) {
|
||||
return c.sets, nil
|
||||
}
|
||||
|
||||
func TestManager_Add(t *testing.T) {
|
||||
@@ -76,6 +74,13 @@ func TestManager_Add(t *testing.T) {
|
||||
Family: ipset.NewUInt8Box(uint8(netfilter.ProtoIPv6)),
|
||||
},
|
||||
ipv6Entries: &ipv6Entries,
|
||||
sets: []props{{
|
||||
name: "ipv4set",
|
||||
family: netfilter.ProtoIPv4,
|
||||
}, {
|
||||
name: "ipv6set",
|
||||
family: netfilter.ProtoIPv6,
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ func (l *queryLog) clear() {
|
||||
l.bufferLock.Lock()
|
||||
defer l.bufferLock.Unlock()
|
||||
|
||||
l.buffer = nil
|
||||
l.buffer.Clear()
|
||||
l.flushPending = false
|
||||
}()
|
||||
|
||||
|
||||
@@ -8,10 +8,8 @@ require (
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601
|
||||
github.com/kisielk/errcheck v1.6.3
|
||||
github.com/kyoh86/looppointer v0.2.1
|
||||
github.com/securego/gosec/v2 v2.18.0
|
||||
// TODO(a.garipov): Return to latest once the release is tagged
|
||||
// correctly. See uudashr/gocognit#31.
|
||||
github.com/uudashr/gocognit v1.0.8-0.20230906062305-bc9ca12659bf
|
||||
github.com/securego/gosec/v2 v2.18.1
|
||||
github.com/uudashr/gocognit v1.1.1
|
||||
golang.org/x/tools v0.14.0
|
||||
golang.org/x/vuln v1.0.1
|
||||
honnef.co/go/tools v0.4.6
|
||||
|
||||
@@ -33,11 +33,11 @@ github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA
|
||||
github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/securego/gosec/v2 v2.18.0 h1:PEQsNSe8NjGrQ6oF3LrmMhZTm2iqYKUOYf+OWXzo+XQ=
|
||||
github.com/securego/gosec/v2 v2.18.0/go.mod h1:06rgh4+5IrlRpi573DJRZ6y/tlIE+a0rFgMlJDxFIyQ=
|
||||
github.com/securego/gosec/v2 v2.18.1 h1:xnnehWg7dIW8qrRPGm8ykY21zp2MueKyC99Vlcuj96I=
|
||||
github.com/securego/gosec/v2 v2.18.1/go.mod h1:ZUTcKD9gAFip1lLGHWCjkoBQJyaEzePTNzjwlL2HHoE=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/uudashr/gocognit v1.0.8-0.20230906062305-bc9ca12659bf h1:LA5CHw6L5BvI0RjqvBYa9+3hXAL2rhuAJPtS5rS2a2k=
|
||||
github.com/uudashr/gocognit v1.0.8-0.20230906062305-bc9ca12659bf/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
|
||||
github.com/uudashr/gocognit v1.1.1 h1:qIj6KhmcGQGBiWtaKH6ZlIyDGa6br2febZNZ6MDzqMw=
|
||||
github.com/uudashr/gocognit v1.1.1/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -63,7 +63,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -35,7 +35,7 @@ set -f -u
|
||||
go_version="$( "${GO:-go}" version )"
|
||||
readonly go_version
|
||||
|
||||
go_min_version='go1.20.10'
|
||||
go_min_version='go1.20.11'
|
||||
go_version_msg="
|
||||
warning: your go version (${go_version}) is different from the recommended minimal one (${go_min_version}).
|
||||
if you have the version installed, please set the GO environment variable.
|
||||
|
||||
Reference in New Issue
Block a user