Compare commits
39 Commits
v0.105.0-b
...
fix-client
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46698078a0 | ||
|
|
59a3045615 | ||
|
|
1453c27d87 | ||
|
|
e31c0c456a | ||
|
|
836d0db563 | ||
|
|
a0abad6644 | ||
|
|
1122e71cf3 | ||
|
|
e32c18faab | ||
|
|
f68f6c9b37 | ||
|
|
dbcc55f528 | ||
|
|
66b549a565 | ||
|
|
a1c9e9d36b | ||
|
|
ab81ff03f6 | ||
|
|
d295621352 | ||
|
|
aebcd74efe | ||
|
|
7d1ca48ae4 | ||
|
|
8a0bc5468b | ||
|
|
e272e19516 | ||
|
|
10f03b7527 | ||
|
|
0d44822c43 | ||
|
|
e83b919dbd | ||
|
|
2eb21ef409 | ||
|
|
7b014082ab | ||
|
|
6b8a46ef3b | ||
|
|
a623ac694b | ||
|
|
7dd2d0af96 | ||
|
|
7e08565212 | ||
|
|
841bb9bc35 | ||
|
|
f016ae172c | ||
|
|
44168292d5 | ||
|
|
e64df20e74 | ||
|
|
890f0322d8 | ||
|
|
2bf2d5a19d | ||
|
|
771a32cc9d | ||
|
|
9df0935781 | ||
|
|
a3dddd72c1 | ||
|
|
6471504555 | ||
|
|
1fa4d55ae3 | ||
|
|
63e4adc0e7 |
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1,4 +1 @@
|
||||
client/* linguist-vendored
|
||||
client/src/__locales/*.json binary
|
||||
# Make an exception for the english locale, as it is the base one.
|
||||
client/src/__locales/en.json -binary
|
||||
client/* linguist-vendored
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,11 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve AdGuard Home
|
||||
|
||||
---
|
||||
|
||||
<!-- As an open-source project with a dedicated but small maintainer team, it can sometimes take a long time for issues to be addressed so please be patient and we will get back to you as soon as we can.
|
||||
-->
|
||||
Have a question or an idea? Please search it [on our forum](https://github.com/AdguardTeam/AdGuardHome/discussions) to make sure it was not yet asked. If you cannot find what you had in mind, please [submit it here](https://github.com/AdguardTeam/AdGuardHome/discussions/new).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -17,16 +15,18 @@ Please answer the following questions for yourself before submitting an issue. *
|
||||
|
||||
### Issue Details
|
||||
|
||||
<!--- Please include all relevant details about the environment you experienced the bug in -->
|
||||
<!-- Please include all relevant details about the environment you experienced the bug in. -->
|
||||
|
||||
* **Version of AdGuard Home server:**
|
||||
* <!-- (e.g. v1.0) -->
|
||||
* <!-- (e.g. v0.123.4) -->
|
||||
* **How did you install AdGuard Home:**
|
||||
* <!-- (e.g. Snapcraft, Docker, Github releases) -->
|
||||
* <!-- (e.g. Built from source, Snapcraft, Docker, Github releases, etc.) -->
|
||||
* **How did you setup DNS configuration:**
|
||||
* <!-- (System/Router/IoT) -->
|
||||
* **If it's a router or IoT, please write device model:**
|
||||
* <!-- (e.g. Raspberry Pi 3 Model B) -->
|
||||
* **CPU architecture:**
|
||||
* <!-- (e.g. AMD64, MIPS, etc.) -->
|
||||
* **Operating system and version:**
|
||||
* <!-- (e.g. Ubuntu 18.04.1) -->
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -1,11 +1,9 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for AdGuard Home
|
||||
|
||||
about: Suggest a feature request for AdGuard Home
|
||||
---
|
||||
|
||||
<!-- As an open-source project with a dedicated but small maintainer team, it can sometimes take a long time for issues to be addressed so please be patient and we will get back to you as soon as we can.
|
||||
-->
|
||||
Have a question or an idea? Please search it [on our forum](https://github.com/AdguardTeam/AdGuardHome/discussions) to make sure it was not yet asked. If you cannot find what you had in mind, please [submit it here](https://github.com/AdguardTeam/AdGuardHome/discussions/new).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
|
||||
8
.github/stale.yml
vendored
8
.github/stale.yml
vendored
@@ -1,20 +1,22 @@
|
||||
# Number of days of inactivity before an issue becomes stale.
|
||||
'daysUntilStale': 60
|
||||
'daysUntilStale': 90
|
||||
# Number of days of inactivity before a stale issue is closed.
|
||||
'daysUntilClose': 7
|
||||
'daysUntilClose': 15
|
||||
# Issues with these labels will never be considered stale.
|
||||
'exemptLabels':
|
||||
- 'bug'
|
||||
- 'enhancement'
|
||||
- 'feature request'
|
||||
- 'localization'
|
||||
- 'needs investigation'
|
||||
- 'recurrent'
|
||||
- 'research'
|
||||
# Label to use when marking an issue as stale.
|
||||
'staleLabel': 'wontfix'
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable.
|
||||
'markComment': >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable.
|
||||
'closeComment': false
|
||||
|
||||
76
CHANGELOG.md
76
CHANGELOG.md
@@ -10,9 +10,70 @@ and this project adheres to
|
||||
## [Unreleased]
|
||||
|
||||
<!--
|
||||
## [v0.105.0] - 2021-02-08
|
||||
## [v0.106.0] - 2021-04-26
|
||||
-->
|
||||
|
||||
<!--
|
||||
## [v0.105.2] - 2021-02-24
|
||||
-->
|
||||
|
||||
### Fixed
|
||||
|
||||
- DHCP lease's `expired` field incorrect time format ([#2692]).
|
||||
- Incomplete DNS upstreams validation ([#2674]).
|
||||
- Wrong parsing of DHCP options of the `ip` type ([#2688]).
|
||||
|
||||
[#2674]: https://github.com/AdguardTeam/AdGuardHome/issues/2674
|
||||
[#2688]: https://github.com/AdguardTeam/AdGuardHome/issues/2688
|
||||
[#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692
|
||||
|
||||
|
||||
|
||||
## [v0.105.1] - 2021-02-15
|
||||
|
||||
### Changed
|
||||
|
||||
- Increased HTTP API timeouts ([#2671], [#2682]).
|
||||
- "Permission denied" errors when checking if the machine has a static IP no
|
||||
longer prevent the DHCP server from starting ([#2667]).
|
||||
- The server name sent by clients of TLS APIs is not only checked when
|
||||
`strict_sni_check` is enabled ([#2664]).
|
||||
- HTTP API request body size limit for the `POST /control/access/set` and `POST
|
||||
/control/filtering/set_rules` HTTP APIs is increased ([#2666], [#2675]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Error when enabling the DHCP server when AdGuard Home couldn't determine if
|
||||
the machine has a static IP.
|
||||
- Optical issue on custom rules ([#2641]).
|
||||
- Occasional crashes during startup.
|
||||
- The field `"range_start"` in the `GET /control/dhcp/status` HTTP API response
|
||||
is now correctly named again ([#2678]).
|
||||
- DHCPv6 server's `ra_slaac_only` and `ra_allow_slaac` settings aren't reset to
|
||||
`false` on update any more ([#2653]).
|
||||
- The `Vary` header is now added along with `Access-Control-Allow-Origin` to
|
||||
prevent cache-related and other issues in browsers ([#2658]).
|
||||
- The request body size limit is now set for HTTPS requests as well.
|
||||
- Incorrect version tag in the Docker release ([#2663]).
|
||||
- DNSCrypt queries weren't marked as such in logs ([#2662]).
|
||||
|
||||
[#2641]: https://github.com/AdguardTeam/AdGuardHome/issues/2641
|
||||
[#2653]: https://github.com/AdguardTeam/AdGuardHome/issues/2653
|
||||
[#2658]: https://github.com/AdguardTeam/AdGuardHome/issues/2658
|
||||
[#2662]: https://github.com/AdguardTeam/AdGuardHome/issues/2662
|
||||
[#2663]: https://github.com/AdguardTeam/AdGuardHome/issues/2663
|
||||
[#2664]: https://github.com/AdguardTeam/AdGuardHome/issues/2664
|
||||
[#2666]: https://github.com/AdguardTeam/AdGuardHome/issues/2666
|
||||
[#2667]: https://github.com/AdguardTeam/AdGuardHome/issues/2667
|
||||
[#2671]: https://github.com/AdguardTeam/AdGuardHome/issues/2671
|
||||
[#2675]: https://github.com/AdguardTeam/AdGuardHome/issues/2675
|
||||
[#2678]: https://github.com/AdguardTeam/AdGuardHome/issues/2678
|
||||
[#2682]: https://github.com/AdguardTeam/AdGuardHome/issues/2682
|
||||
|
||||
|
||||
|
||||
## [v0.105.0] - 2021-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added more services to the "Blocked services" list ([#2224], [#2401]).
|
||||
@@ -47,7 +108,7 @@ and this project adheres to
|
||||
- Improved HTTP requests handling and timeouts ([#2343]).
|
||||
- Our snap package now uses the `core20` image as its base ([#2306]).
|
||||
- New build system and various internal improvements ([#2271], [#2276], [#2297],
|
||||
[#2509], [#2552]).
|
||||
[#2509], [#2552], [#2639], [#2646]).
|
||||
|
||||
### Deprecated
|
||||
|
||||
@@ -107,6 +168,8 @@ and this project adheres to
|
||||
[#2552]: https://github.com/AdguardTeam/AdGuardHome/issues/2552
|
||||
[#2589]: https://github.com/AdguardTeam/AdGuardHome/issues/2589
|
||||
[#2630]: https://github.com/AdguardTeam/AdGuardHome/issues/2630
|
||||
[#2639]: https://github.com/AdguardTeam/AdGuardHome/issues/2639
|
||||
[#2646]: https://github.com/AdguardTeam/AdGuardHome/issues/2646
|
||||
|
||||
## [v0.104.3] - 2020-11-19
|
||||
|
||||
@@ -147,9 +210,12 @@ and this project adheres to
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...HEAD
|
||||
[v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.2...HEAD
|
||||
[v0.105.2]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.1...v0.105.2
|
||||
-->
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...HEAD
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.1...HEAD
|
||||
[v0.105.1]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...v0.105.1
|
||||
[v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0
|
||||
[v0.104.3]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.2...v0.104.3
|
||||
[v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.1...v0.104.2
|
||||
|
||||
@@ -155,7 +155,7 @@ It depends.
|
||||
|
||||
"DNS sinkholing" is capable of blocking a big percentage of ads, but it lacks flexibility and power of traditional ad blockers. You can get a good impression about the difference between these methods by reading [this article](https://adguard.com/en/blog/adguard-vs-adaway-dns66/). It compares AdGuard for Android (a traditional ad blocker) to hosts-level ad blockers (which are almost identical to DNS-based blockers in their capabilities).
|
||||
|
||||
However, this level of protection is enough for some users. Additionally, using a DNS-based blocker can help to block ads, tracking and analytics requests on other types of devices, such as SmartTVs, smart speakers or other kinds of IoT devices (on which you can't install tradtional ad blockers).
|
||||
However, this level of protection is enough for some users. Additionally, using a DNS-based blocker can help to block ads, tracking and analytics requests on other types of devices, such as SmartTVs, smart speakers or other kinds of IoT devices (on which you can't install traditional ad blockers).
|
||||
|
||||
**Known limitations**
|
||||
|
||||
@@ -192,6 +192,12 @@ cd AdGuardHome
|
||||
make
|
||||
```
|
||||
|
||||
Please note, that the non-standard `-j` flag is currently not supported, so
|
||||
building with `make -j 4` or setting your `MAKEFLAGS` to include, for example,
|
||||
`-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to
|
||||
that, and you don't want to change it, you can override it by running
|
||||
`make -j 1`.
|
||||
|
||||
Check the [`Makefile`](https://github.com/AdguardTeam/AdGuardHome/blob/master/Makefile) to learn about other commands.
|
||||
|
||||
**Building for a different platform.** You can build AdGuard for any OS/ARCH just like any other Go project.
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Няслушны фармат IP-адраса",
|
||||
"form_error_mac_format": "Некарэктны фармат MAC",
|
||||
"form_error_client_id_format": "Няслушны фармат ID кліента",
|
||||
"form_error_server_name": "Няслушнае імя сервера",
|
||||
"form_error_positive": "Павінна быць больш 0",
|
||||
"form_error_negative": "Павінна быць не менш 0",
|
||||
"range_end_error": "Павінен перавышаць пачатак дыяпазону",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Свой IP",
|
||||
"blocking_ipv4": "Блакаванне IPv4",
|
||||
"blocking_ipv6": "Блакаванне IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "Ідэнтыфікатар кліента",
|
||||
"client_id_placeholder": "Увядзіце ідэнтыфікатар кліента",
|
||||
"client_id_desc": "Розныя кліенты могуць ідэнтыфікавацца па адмысловым ідэнтыфікатары кліента. <a>Тут</a> вы можаце даведацца больш пра ідэнтыфікацыю кліентаў.",
|
||||
"download_mobileconfig_doh": "Спампаваць .mobileconfig для DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Спампаваць .mobileconfig для DNS-over-TLS",
|
||||
"download_mobileconfig": "Загрузіць файл канфігурацыі",
|
||||
"plain_dns": "Нешыфраваны DNS",
|
||||
"form_enter_rate_limit": "Увядзіце rate limit",
|
||||
"rate_limit": "Ограничение скорости",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Крыніца",
|
||||
"found_in_known_domain_db": "Знойдзены ў базе вядомых даменаў.",
|
||||
"category_label": "Катэгорыя",
|
||||
"rule_label": "Правіла",
|
||||
"rule_label": "Правіла(ы)",
|
||||
"list_label": "Спіс",
|
||||
"unknown_filter": "Невядомы фільтр {{filterId}}",
|
||||
"known_tracker": "Вядомы трэкер",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> падтрымвае <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> падтрымвае <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Вы можаце знайсці яшчэ варыянты <0>тут</0> і <1>тут</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Канфігурацыя для iOS і macOS",
|
||||
"setup_dns_notice": "Каб выкарыстоўваць <1>DNS-over-HTTPS</1> ці <1>DNS-over-TLS</1>, вам патрэбна <0>наладзіць шыфраванне</0> у наладах AdGuard Home.",
|
||||
"rewrite_added": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова дададзена",
|
||||
"rewrite_deleted": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова выдалена",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP-адрасы: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Прычына: {{reason}}",
|
||||
"check_rule": "Правіла: {{rule}}",
|
||||
"check_service": "Назва сэрвісу: {{service}}",
|
||||
"service_name": "Назва сэрвіса",
|
||||
"check_not_found": "Не знойдзена ў вашым спісе фільтраў",
|
||||
|
||||
@@ -144,7 +144,6 @@
|
||||
"source_label": "Източник",
|
||||
"found_in_known_domain_db": "Намерен в списъците с домейни.",
|
||||
"category_label": "Категория",
|
||||
"rule_label": "Правило",
|
||||
"unknown_filter": "Непознат филтър {{filterId}}",
|
||||
"install_welcome_title": "Добре дошли в AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home e мрежово решение за блокиране на реклами и тракери на DNS ниво. Създадено е за да ви даде пълен контрол над мрежата и всичките ви устройства, без да е необходимо допълнително инсталиране на друг софтуер.",
|
||||
@@ -202,7 +201,6 @@
|
||||
"encryption_config_saved": "Конфигурацията е успешно записана",
|
||||
"encryption_server": "Име на сървъра",
|
||||
"encryption_server_enter": "Въведете име на домейна",
|
||||
"encryption_server_desc": "За да използвате HTTPS, трябва името на сървъра да съвпада с това на SSL сертификата.",
|
||||
"encryption_redirect": "Автоматично пренасочване към HTTPS",
|
||||
"encryption_redirect_desc": "Служи за автоматично пренасочване от HTTP към HTTPS на страницата за Администрация в AdGuard Home.",
|
||||
"encryption_https": "HTTPS порт",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Neplatný formát IP",
|
||||
"form_error_mac_format": "Neplatný formát MAC",
|
||||
"form_error_client_id_format": "Neplatný formát ID klienta",
|
||||
"form_error_server_name": "Neplatný název serveru",
|
||||
"form_error_positive": "Musí být větší než 0",
|
||||
"form_error_negative": "Musí být rovno nebo větší než 0",
|
||||
"range_end_error": "Musí být větší než začátek rozsahu",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Vlastní IP",
|
||||
"blocking_ipv4": "Blokování IPv4",
|
||||
"blocking_ipv6": "Blokování IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS přes HTTPS",
|
||||
"dns_over_tls": "DNS přes TLS",
|
||||
"dns_over_quic": "DNS skrze QUIC",
|
||||
"client_id": "ID klienta",
|
||||
"client_id_placeholder": "Zadejte ID klienta",
|
||||
"client_id_desc": "Různé klienty lze identifikovat pomocí speciálního ID klienta. <a>Zde</a> se můžete dozvědět více o tom, jak klienty identifikovat.",
|
||||
"download_mobileconfig_doh": "Stáhnout .mobileconfig pro DNS skrze HTTPS",
|
||||
"download_mobileconfig_dot": "Stáhnout .mobileconfig pro DNS skrze TLS",
|
||||
"download_mobileconfig": "Stáhnout konfigurační soubor",
|
||||
"plain_dns": "Čisté DNS",
|
||||
"form_enter_rate_limit": "Zadejte rychlostní limit",
|
||||
"rate_limit": "Rychlostní limit",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Zdroj",
|
||||
"found_in_known_domain_db": "Nalezeno v databázi známých domén",
|
||||
"category_label": "Kategorie",
|
||||
"rule_label": "Pravidlo",
|
||||
"rule_label": "Pravidla",
|
||||
"list_label": "Seznam",
|
||||
"unknown_filter": "Neznámý filtr {{filterId}}",
|
||||
"known_tracker": "Známý slídič",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Konfigurace šifrování byla uložena",
|
||||
"encryption_server": "Název serveru",
|
||||
"encryption_server_enter": "Zadejte název domény",
|
||||
"encryption_server_desc": "Abyste mohli používat protokol HTTPS, musíte zadat název serveru, který odpovídá vašemu certifikátu SSL.",
|
||||
"encryption_server_desc": "Abyste mohli používat HTTPS, musíte zadat název serveru, který odpovídá vašemu certifikátu SSL nebo zástupnému certifikátu. Pokud není pole nastaveno, bude přijímat připojení TLS pro libovolnou doménu.",
|
||||
"encryption_redirect": "Automaticky přesměrovat na HTTPS",
|
||||
"encryption_redirect_desc": "Pokud je zaškrtnuto, AdGuard Home vás automaticky přesměruje z adres HTTP na HTTPS.",
|
||||
"encryption_https": "HTTPS port",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Upravit klienta",
|
||||
"client_identifier": "Identifikátor",
|
||||
"ip_address": "IP adresa",
|
||||
"client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy, CIDR nebo MAC adresy. Upozorňujeme, že použití MAC jako identifikátoru je možné pouze v případě, že je AdGuard Home také <0>DHCP server</0>",
|
||||
"client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy, CIDR, MAC adresy nebo speciálního ID klienta (může být použito pro DoT/DoH/DoQ). <0>Zde</0> se můžete dozvědět více o tom, jak klienty identifikovat.",
|
||||
"form_enter_ip": "Zadejte IP",
|
||||
"form_enter_mac": "Zadejte MAC",
|
||||
"form_enter_id": "Zadejte identifikátor",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podporuje <1>DNS-přes-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podporuje <1>DNS-přes-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Další implementace naleznete <0>zde</0> a <1>zde</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Konfigurace pro iOS a macOS",
|
||||
"setup_dns_notice": "Pro použití <1>DNS-přes-HTTPS</1> nebo <1>DNS-přes-TLS</1> potřebujete v nastaveních AdGuard Home <0>nakonfigurovat šifrování</0>.",
|
||||
"rewrite_added": "Přesměrování DNS pro „{{key}}“ úspěšně přidáno",
|
||||
"rewrite_deleted": "Přesměrování DNS pro „{{key}}“ úspěšně smazáno",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP adresy: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Důvod: {{reason}}",
|
||||
"check_rule": "Pravidlo: {{rule}}",
|
||||
"check_service": "Název služby: {{service}}",
|
||||
"service_name": "Název služby",
|
||||
"check_not_found": "Nenalezeno ve Vašich seznamech filtrů",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Ugyldigt IP-format",
|
||||
"form_error_mac_format": "Ugyldigt MAC-format",
|
||||
"form_error_client_id_format": "Ugyldigt klient-ID-format",
|
||||
"form_error_server_name": "Ugyldigt servernavn",
|
||||
"form_error_positive": "Skal være større end 0",
|
||||
"form_error_negative": "Skal være lig med 0 eller større",
|
||||
"range_end_error": "Skal være større end starten af intervallet",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Tilpasset IP",
|
||||
"blocking_ipv4": "IPv4-blokering",
|
||||
"blocking_ipv6": "IPv6-blokering",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-Quic",
|
||||
"client_id": "Klient-ID",
|
||||
"client_id_placeholder": "Indtast klient-ID",
|
||||
"client_id_desc": "Forskellige klienter kan identificeres ved hjælp af et specielt klient-ID. <a>Her</a> kan du lære mere om, hvordan du identificerer klienter.",
|
||||
"download_mobileconfig_doh": "Download .mobileconfig til DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Download .mobileconfig til DNS-over-TLS",
|
||||
"download_mobileconfig": "Download konfigurationsfil",
|
||||
"plain_dns": "Almindelig DNS",
|
||||
"form_enter_rate_limit": "Indtast hyppighedsgrænse",
|
||||
"rate_limit": "Hyppighedsgrænse",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Kilde",
|
||||
"found_in_known_domain_db": "Fundet i databasen med kendte domæner.",
|
||||
"category_label": "Kategori",
|
||||
"rule_label": "Regel",
|
||||
"rule_label": "Regel(regler)",
|
||||
"list_label": "Liste",
|
||||
"unknown_filter": "Ukendt filter {{filterId}}",
|
||||
"known_tracker": "Kendt tracker",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Krypteringskonfiguration gemt",
|
||||
"encryption_server": "Servernavn",
|
||||
"encryption_server_enter": "Indtast dit domænenavn",
|
||||
"encryption_server_desc": "For at kunne bruge HTTPS skal du indtaste servernavnet, der matcher dit SSL-certifikat.",
|
||||
"encryption_server_desc": "For at kunne bruge HTTPS skal du indtaste det servernavn, der matcher dit SSL-certifikat eller wildcard-certifikat. Hvis feltet ikke er indstillet, accepterer det TLS-forbindelser til ethvert domæne.",
|
||||
"encryption_redirect": "Omdiriger automatisk til HTTPS",
|
||||
"encryption_redirect_desc": "Hvis afkrydset, vil AdGuard Home automatisk omdirigere dig fra HTTP til HTTPS-adresser.",
|
||||
"encryption_https": "HTTPS-port",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Rediger Klient",
|
||||
"client_identifier": "Identifikator",
|
||||
"ip_address": "IP-adresse",
|
||||
"client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen, CIDR eller MAC-adressen. Bemærk, at det kun er muligt at bruge MAC som identifikator, hvis AdGuard Home også er en <0>DHCP-server</0>",
|
||||
"client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen, CIDR eller MAC-adressen eller et specielt klient-ID (kan bruges til DoT/DoH/DoQ). <0>Her</0> kan du lære mere om, hvordan du identificerer klienter.",
|
||||
"form_enter_ip": "Indtast IP",
|
||||
"form_enter_mac": "Indtast MAC",
|
||||
"form_enter_id": "Indtast identifikator",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> understøtter <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> understøtter <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Du kan finde flere implementeringer <0>her</0> og <1>her</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "iOS- og macOS-konfiguration",
|
||||
"setup_dns_notice": "For at kunne bruge <1>DNS-over-HTTPS</1> eller <1>DNS-over-TLS</1>, skal du <0>konfigurere Krypteringen</0> i indstillingerne i AdGuard Home.",
|
||||
"rewrite_added": "DNS-omskrivning for \"{{key}}\" blev tilføjet",
|
||||
"rewrite_deleted": "DNS-omskrivning for \"{{key}}\" blev slettet",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP-adresser: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Årsag: {{reason}}",
|
||||
"check_rule": "Regel: {{rule}}",
|
||||
"check_service": "Servicenavn: {{service}}",
|
||||
"service_name": "Navn på tjeneste",
|
||||
"check_not_found": "Ikke fundet i dine filterlister",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Ungültiges IPv4-Format",
|
||||
"form_error_mac_format": "Ungültiges MAC-Format",
|
||||
"form_error_client_id_format": "Ungültiges Client-ID-Format",
|
||||
"form_error_server_name": "Ungültiger Servername",
|
||||
"form_error_positive": "Muss größer als 0 sein.",
|
||||
"form_error_negative": "Muss gleich oder größer als 0 (Null) sein",
|
||||
"range_end_error": "Muss größer als der Bereichsbeginn sein",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Benutzerdefinierte IP",
|
||||
"blocking_ipv4": "IPv4-Sperren",
|
||||
"blocking_ipv6": "IPv6-Sperren",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS (DNS-Abrage über HTTPS)",
|
||||
"dns_over_tls": "DNS-over-TLS (DNS-Abrage über TLS)",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "Client-ID",
|
||||
"client_id_placeholder": "Client-ID eingeben",
|
||||
"client_id_desc": "Verschiedene Clients können durch eine spezielle Client-ID identifiziert werden. <a>Hier</a> können Sie mehr darüber erfahren, wie Sie Clients identifizieren können.",
|
||||
"download_mobileconfig_doh": ".mobileconfig für DNS-über-HTTPS herunterladen",
|
||||
"download_mobileconfig_dot": ".mobileconfig für DNS-über-TLS herunterladen",
|
||||
"download_mobileconfig": "Konfigurationsdatei herunterladen",
|
||||
"plain_dns": "Einfaches DNS",
|
||||
"form_enter_rate_limit": "Begrenzungswert eingeben",
|
||||
"rate_limit": "Begrenzungswert",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Quelle",
|
||||
"found_in_known_domain_db": "In der Datenbank der bekannten Domains gefunden.",
|
||||
"category_label": "Kategorie",
|
||||
"rule_label": "Regel",
|
||||
"rule_label": "Regel(n)",
|
||||
"list_label": "Liste",
|
||||
"unknown_filter": "Unbekannter Filter {{filterId}}",
|
||||
"known_tracker": "Bekannte Tracker",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> unterstützt <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> unterstützt <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Weitere Umsetzungen finden Sie <0>hier</0> und <1>hier</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Konfiguration für iOS und macOS",
|
||||
"setup_dns_notice": "Um <1>DNS-over-HTTTPS</1> oder <1>DNS-over-TLS</1> verwenden zu können, müssen Sie in den AdGuard Home Einstellungen die <0>Verschlüsselung konfigurieren</0>.",
|
||||
"rewrite_added": "DNS-Umschreibung für „{{key}}” erfolgreich hinzugefügt",
|
||||
"rewrite_deleted": "DNS-Umschreibung für „{{key}}” erfolgreich entfernt",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP-Adressen: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Grund: {{reason}}",
|
||||
"check_rule": "Regel: {{rule}}",
|
||||
"check_service": "Dienstname: {{service}}",
|
||||
"service_name": "Name des Dienstes",
|
||||
"check_not_found": "Nicht in Ihren Filterlisten enthalten",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Invalid IP format",
|
||||
"form_error_mac_format": "Invalid MAC format",
|
||||
"form_error_client_id_format": "Invalid client ID format",
|
||||
"form_error_server_name": "Invalid server name",
|
||||
"form_error_positive": "Must be greater than 0",
|
||||
"form_error_negative": "Must be equal to 0 or greater",
|
||||
"range_end_error": "Must be greater than range start",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Custom IP",
|
||||
"blocking_ipv4": "Blocking IPv4",
|
||||
"blocking_ipv6": "Blocking IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "Client ID",
|
||||
"client_id_placeholder": "Enter client ID",
|
||||
"client_id_desc": "Different clients can be identified by a special client ID. <a>Here</a> you can learn more about how to identify clients.",
|
||||
"download_mobileconfig_doh": "Download .mobileconfig for DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Download .mobileconfig for DNS-over-TLS",
|
||||
"download_mobileconfig": "Download configuration file",
|
||||
"plain_dns": "Plain DNS",
|
||||
"form_enter_rate_limit": "Enter rate limit",
|
||||
"rate_limit": "Rate limit",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Source",
|
||||
"found_in_known_domain_db": "Found in the known domains database.",
|
||||
"category_label": "Category",
|
||||
"rule_label": "Rule",
|
||||
"rule_label": "Rule(s)",
|
||||
"list_label": "List",
|
||||
"unknown_filter": "Unknown filter {{filterId}}",
|
||||
"known_tracker": "Known tracker",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Encryption config saved",
|
||||
"encryption_server": "Server name",
|
||||
"encryption_server_enter": "Enter your domain name",
|
||||
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
|
||||
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate or wildcard certificate. If the field is not set, it will accept TLS connections for any domain.",
|
||||
"encryption_redirect": "Redirect to HTTPS automatically",
|
||||
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
|
||||
"encryption_https": "HTTPS port",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Edit Client",
|
||||
"client_identifier": "Identifier",
|
||||
"ip_address": "IP address",
|
||||
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address. Please note that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
|
||||
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address or a special client ID (can be used for DoT/DoH/DoQ). <0>Here</0> you can learn more about how to identify clients.",
|
||||
"form_enter_ip": "Enter IP",
|
||||
"form_enter_mac": "Enter MAC",
|
||||
"form_enter_id": "Enter identifier",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "iOS and macOS configuration",
|
||||
"setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings.",
|
||||
"rewrite_added": "DNS rewrite for \"{{key}}\" successfully added",
|
||||
"rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP addresses: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Reason: {{reason}}",
|
||||
"check_rule": "Rule: {{rule}}",
|
||||
"check_service": "Service name: {{service}}",
|
||||
"service_name": "Service name",
|
||||
"check_not_found": "Not found in your filter lists",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Formato IP no válido",
|
||||
"form_error_mac_format": "Formato MAC no válido",
|
||||
"form_error_client_id_format": "Formato de ID de cliente no válido",
|
||||
"form_error_server_name": "Nombre de servidor no válido",
|
||||
"form_error_positive": "Debe ser mayor que 0",
|
||||
"form_error_negative": "Debe ser igual o mayor que 0",
|
||||
"range_end_error": "Debe ser mayor que el inicio de rango",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "IP personalizada",
|
||||
"blocking_ipv4": "Bloqueo de IPv4",
|
||||
"blocking_ipv6": "Bloqueo de IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS mediante HTTPS",
|
||||
"dns_over_tls": "DNS mediante TLS",
|
||||
"dns_over_quic": "DNS mediante QUIC",
|
||||
"client_id": "ID de cliente",
|
||||
"client_id_placeholder": "Ingresa el ID del cliente",
|
||||
"client_id_desc": "Diferentes clientes pueden ser identificados por un ID de cliente especial. <a>Aquí</a> puedes obtener más información sobre cómo identificar clientes.",
|
||||
"download_mobileconfig_doh": "Descargar .mobileconfig para DNS mediante HTTPS",
|
||||
"download_mobileconfig_dot": "Descargar .mobileconfig para DNS mediante TLS",
|
||||
"download_mobileconfig": "Descargar archivo de configuración",
|
||||
"plain_dns": "DNS simple",
|
||||
"form_enter_rate_limit": "Ingresa el límite de cantidad",
|
||||
"rate_limit": "Límite de cantidad",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Configuración de cifrado guardado",
|
||||
"encryption_server": "Nombre del servidor",
|
||||
"encryption_server_enter": "Ingresa el nombre del dominio",
|
||||
"encryption_server_desc": "Para utilizar HTTPS, debes ingresar el nombre del servidor que coincida con tu certificado SSL.",
|
||||
"encryption_server_desc": "Para utilizar HTTPS, debes ingresar el nombre del servidor que coincida con tu certificado SSL o certificado comodín. Si el campo no está establecido, el servidor aceptará conexiones TLS para cualquier dominio.",
|
||||
"encryption_redirect": "Redireccionar a HTTPS automáticamente",
|
||||
"encryption_redirect_desc": "Si está marcado, AdGuard Home redireccionará automáticamente de HTTP a las direcciones HTTPS.",
|
||||
"encryption_https": "Puerto HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Editar cliente",
|
||||
"client_identifier": "Identificador",
|
||||
"ip_address": "Dirección IP",
|
||||
"client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP, MAC y CIDR. Ten en cuenta que el uso de MAC como identificador solo es posible si AdGuard Home también es un <0>servidor DHCP</0>",
|
||||
"client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP, MAC, CIDR o un ID de cliente especial (puede ser utilizado para DoT/DoH/DoQ). <0>Aquí</0> puedes obtener más información sobre cómo identificar clientes.",
|
||||
"form_enter_ip": "Ingresa la IP",
|
||||
"form_enter_mac": "Ingresa la MAC",
|
||||
"form_enter_id": "Ingresa el identificador",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> soporta <1>DNS mediante HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> soporta <1>DNS mediante HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Encontrarás más implementaciones <0>aquí</0> y <1>aquí</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Configuración de iOS y macOS",
|
||||
"setup_dns_notice": "Para utilizar <1>DNS mediante HTTPS</1> o <1>DNS mediante TLS</1>, debes <0>configurar el cifrado</0> en la configuración de AdGuard Home.",
|
||||
"rewrite_added": "Reescritura DNS para \"{{key}}\" añadido correctamente",
|
||||
"rewrite_deleted": "Reescritura DNS para \"{{key}}\" eliminado correctamente",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "Direcciones IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Razón: {{reason}}",
|
||||
"check_rule": "Regla: {{rule}}",
|
||||
"check_service": "Nombre del servicio: {{service}}",
|
||||
"service_name": "Nombre del servicio",
|
||||
"check_not_found": "No se ha encontrado en tus listas de filtros",
|
||||
|
||||
@@ -239,7 +239,6 @@
|
||||
"source_label": "منبع",
|
||||
"found_in_known_domain_db": "در پایگاه داده دامنه های شناخته شده پیدا شد",
|
||||
"category_label": "دسته بندی",
|
||||
"rule_label": "دستور",
|
||||
"list_label": "لیست",
|
||||
"unknown_filter": "فیلتر ناشناخته {{filterId}}",
|
||||
"known_tracker": "ردیاب های شناخته شده",
|
||||
@@ -300,7 +299,6 @@
|
||||
"encryption_config_saved": "پیکربندی رمزگذاری ذخیره شد",
|
||||
"encryption_server": "نام سرور",
|
||||
"encryption_server_enter": "نام دامنه خود را وارد کنید",
|
||||
"encryption_server_desc": "به منظور استفاده از HTTPS،شما باید نام سرور مطابق با گواهینامه اِس اِس اِل را وارد کنید.",
|
||||
"encryption_redirect": "تغییر مسیر خودکار به HTTPS",
|
||||
"encryption_redirect_desc": "اگر انتخاب شده باشد،AdGuard Home خودکار شما را از آدرس HTTP به HTTPS منتقل می کند",
|
||||
"encryption_https": "پورت HTTPS",
|
||||
@@ -354,7 +352,6 @@
|
||||
"client_edit": "ویرایش کلاینت",
|
||||
"client_identifier": "احراز با",
|
||||
"ip_address": "آدرس آی پی",
|
||||
"client_identifier_desc": "کلاینت میتواند با آدرس آی پی یا آدرس مَک احراز شود. لطفا توجه کنید،که استفاده از مَک بعنوان عامل احراز زمانی امکان دارد که AdGuard Home نیز <0>سرور DHCP </0> باشد",
|
||||
"form_enter_ip": "آی پی را وارد کنید",
|
||||
"form_enter_mac": "مَک را وارد کنید",
|
||||
"form_enter_id": "خطای احرازکننده",
|
||||
@@ -487,7 +484,6 @@
|
||||
"check_ip": "آدرس آی پی: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "علت: {{reason}}",
|
||||
"check_rule": "دستور: {{rule}}",
|
||||
"check_service": "نام سرویس: {{service}}",
|
||||
"check_not_found": "در لیست فیلترهای شما یافت نشد",
|
||||
"client_confirm_block": "آیا واقعا میخواهید کلاینت \"{{ip}}\" را مسدود کنید؟",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Format IPv4 invalide",
|
||||
"form_error_mac_format": "Format MAC invalide",
|
||||
"form_error_client_id_format": "Format d'ID client non valide",
|
||||
"form_error_server_name": "Nom de serveur invalide",
|
||||
"form_error_positive": "Doit être supérieur à 0",
|
||||
"form_error_negative": "Doit être égal à 0 ou supérieur",
|
||||
"range_end_error": "Doit être supérieur au début de la gamme",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "IP personnalisée",
|
||||
"blocking_ipv4": "Blocage IPv4",
|
||||
"blocking_ipv6": "Blocage IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "ID du client",
|
||||
"client_id_placeholder": "Saisissez le ID du client",
|
||||
"client_id_desc": "Les clients différents peuvent être identifiés par aide d'un ID client spécial. Vous trouverez plus d'information sur l'identification des clients <a>ici</a> .",
|
||||
"download_mobileconfig_doh": "Télécharger .mobileconfig pour DNS-sur-HTTPS",
|
||||
"download_mobileconfig_dot": "Télécharger .mobileconfig pour DNS-sur-TLS",
|
||||
"download_mobileconfig": "Télécharger le fichier de configuration",
|
||||
"plain_dns": "DNS brut",
|
||||
"form_enter_rate_limit": "Entrez la limite de taux",
|
||||
"rate_limit": "Limite de taux",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Source",
|
||||
"found_in_known_domain_db": "Trouvé dans la base de données des domaines connus",
|
||||
"category_label": "Catégorie",
|
||||
"rule_label": "Règle",
|
||||
"rule_label": "Règle(s)",
|
||||
"list_label": "Liste",
|
||||
"unknown_filter": "Filtre inconnu {{filterId}}",
|
||||
"known_tracker": "Pisteur connu",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Configuration de chiffrement enregistrée",
|
||||
"encryption_server": "Nom du serveur",
|
||||
"encryption_server_enter": "Entrez votre nom de domaine",
|
||||
"encryption_server_desc": "Pour utiliser HTTPS, vous devez entrer le nom du serveur qui correspond à votre certificat SSL.",
|
||||
"encryption_server_desc": "Pour utiliser HTTPS, vous devez saisir le nom du serveur qui correspond à votre certificat SSL ou wildcard. Si le champ n'est pas configuré, les connexions TLS pour tous les domaines seront acceptées.",
|
||||
"encryption_redirect": "Redirection automatiquement vers HTTPS",
|
||||
"encryption_redirect_desc": "Si coché, AdGuard Home vous redirigera automatiquement d'adresses HTTP vers HTTPS.",
|
||||
"encryption_https": "Port HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Modifier le client",
|
||||
"client_identifier": "Identifiant",
|
||||
"ip_address": "Adresse IP",
|
||||
"client_identifier_desc": "Les clients peuvent être identifiés par les adresses IP ou MAC. Veuillez noter que l'utilisation de l'adresse MAC comme identifiant est possible uniquement si AdGuard Home est aussi un <0>serveur DHCP</0>",
|
||||
"client_identifier_desc": "Les clients peuvent être identifiés par les adresses IP, CIDR, MAC ou un ID client spécial (qui peut être utilisé pour DoT/DoH/DoQ). Vous trouverez plus d'information sur l'identification des clients <0>ici</0> .",
|
||||
"form_enter_ip": "Saisissez l'IP",
|
||||
"form_enter_mac": "Saisissez MAC",
|
||||
"form_enter_id": "Entrer identifiant",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supporte le <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supporte le <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Vous trouverez plus d'implémentations <0>ici</0> et <1>ici</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Configuration sur iOS et macOS",
|
||||
"setup_dns_notice": "Pour utiliser le <1>DNS-over-HTTPS</1> ou le <1>DNS-over-TLS</1>, vous devez <0>configurer le Chiffrement</0> dans les paramètres de AdGuard Home.",
|
||||
"rewrite_added": "Réécriture DNS pour \"{{key}}\" ajoutée",
|
||||
"rewrite_deleted": "Réécriture DNS pour \"{{key}}\" supprimée",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "Adresses IP : {{ip}}",
|
||||
"check_cname": "CNAME : {{cname}}",
|
||||
"check_reason": "Raison : {{reason}}",
|
||||
"check_rule": "Règle : {{rule}}",
|
||||
"check_service": "Nom du service : {{service}}",
|
||||
"service_name": "Nom du service",
|
||||
"check_not_found": "Introuvable dans vos listes de filtres",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Nevažeći format IP adrese",
|
||||
"form_error_mac_format": "Nevažeći MAC format",
|
||||
"form_error_client_id_format": "Nevažeći format ID-a klijenta",
|
||||
"form_error_server_name": "Nevažeće ime poslužitelja",
|
||||
"form_error_positive": "Mora biti veće od 0",
|
||||
"form_error_negative": "Mora biti jednako ili veće od 0",
|
||||
"range_end_error": "Mora biti veće od početne vrijednosti raspona",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Prilagođen IP",
|
||||
"blocking_ipv4": "Blokiranje IPv4",
|
||||
"blocking_ipv6": "Blokiranje IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-Quic",
|
||||
"client_id": "ID klijenta",
|
||||
"client_id_placeholder": "Unesite ID klijenta",
|
||||
"client_id_desc": "Razni klijenti mogu biti prepoznati po specijalnom identifikatoru. <a>Ovdje</a> možete saznati više kako možete identificirati klijente.",
|
||||
"download_mobileconfig_doh": "Preuzmi .mobileconfig za DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Preuzmi .mobileconfig za DNS-over-TLS",
|
||||
"download_mobileconfig": "Preuzmite konfiguracijsku datoteku",
|
||||
"plain_dns": "Obični DNS",
|
||||
"form_enter_rate_limit": "Unesite ograničenje",
|
||||
"rate_limit": "Ograničenje",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podržava <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podržava <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Možete pronaći više implementacija <0>ovdje</0> i <1>ovdje</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "konfiguracija za iOS i macOS",
|
||||
"setup_dns_notice": "Da biste koristili <1>DNS-over-HTTPS</1> ili <1>DNS-over-TLS</1>, morate <0>postaviti šifriranje</0> u AdGuard Home postavkama.",
|
||||
"rewrite_added": "DNS prijepis za \"{{key}}\" je uspješno dodan",
|
||||
"rewrite_deleted": "DNS prijepis za \"{{key}}\" je uspješno uklonjen",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP adrese: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Razlog: {{reason}}",
|
||||
"check_rule": "Pravilo: {{rule}}",
|
||||
"check_service": "Naziv usluge: {{service}}",
|
||||
"service_name": "Naziv usluge",
|
||||
"check_not_found": "Nije pronađeno na vašoj listi filtara",
|
||||
|
||||
@@ -269,7 +269,6 @@
|
||||
"source_label": "Forrás",
|
||||
"found_in_known_domain_db": "Benne van az ismert domainek listájában.",
|
||||
"category_label": "Kategória",
|
||||
"rule_label": "Szabály",
|
||||
"list_label": "Lista",
|
||||
"unknown_filter": "Ismeretlen szűrő: {{filterId}}",
|
||||
"known_tracker": "Ismert követő",
|
||||
@@ -330,7 +329,6 @@
|
||||
"encryption_config_saved": "Titkosítási beállítások mentve",
|
||||
"encryption_server": "Szerver neve",
|
||||
"encryption_server_enter": "Adja meg az Ön domain címét",
|
||||
"encryption_server_desc": "A HTTPS használatához be kell írnia egy, az SSL-tanúsítvánnyal megegyező kiszolgálónevet.",
|
||||
"encryption_redirect": "Automatikus átirányítás HTTPS kapcsolatra",
|
||||
"encryption_redirect_desc": "Ha be van jelölve, az AdGuard Home automatikusan átirányítja a HTTP kapcsolatokat a biztonságos HTTPS protokollra.",
|
||||
"encryption_https": "HTTPS port",
|
||||
@@ -386,7 +384,6 @@
|
||||
"client_edit": "Kliens módosítása",
|
||||
"client_identifier": "Azonosító",
|
||||
"ip_address": "IP cím",
|
||||
"client_identifier_desc": "A klienseket be lehet azonosítani IP-cím, CIDR, valamint MAC-cím alapján. Kérjük, vegye figyelembe, hogy a MAC-cím alapján történő azonosítás csak akkor működik, ha az AdGuard Home egyben <0>DHCP szerverként</0> is funkcionál",
|
||||
"form_enter_ip": "IP-cím megadása",
|
||||
"form_enter_mac": "MAC-cím megadása",
|
||||
"form_enter_id": "Azonosító megadása",
|
||||
@@ -529,7 +526,6 @@
|
||||
"check_ip": "IP-címek: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Indok: {{reason}}",
|
||||
"check_rule": "Szabály: {{rule}}",
|
||||
"check_service": "Szolgáltatás neve: {{service}}",
|
||||
"service_name": "Szolgáltatás neve",
|
||||
"check_not_found": "Nem található az Ön szűrőlistái között",
|
||||
|
||||
@@ -269,7 +269,6 @@
|
||||
"source_label": "Sumber",
|
||||
"found_in_known_domain_db": "Ditemukan di database domain dikenal",
|
||||
"category_label": "Kategori",
|
||||
"rule_label": "Aturan",
|
||||
"list_label": "Daftar",
|
||||
"unknown_filter": "Penyaringan {{filterId}} tidak dikenal",
|
||||
"known_tracker": "Pelacak yang dikenal",
|
||||
@@ -330,7 +329,6 @@
|
||||
"encryption_config_saved": "Pengaturan enkripsi telah tersimpan",
|
||||
"encryption_server": "Nama server",
|
||||
"encryption_server_enter": "Masukkan nama domain anda",
|
||||
"encryption_server_desc": "Untuk menggunakan HTTPS, Anda harus memasukkan nama server yang cocok dengan sertifikat SSL Anda.",
|
||||
"encryption_redirect": "Alihkan ke HTTPS secara otomatis",
|
||||
"encryption_redirect_desc": "Jika dicentang, AdGuard Home akan secara otomatis mengarahkan anda dari HTTP ke alamat HTTPS.",
|
||||
"encryption_https": "Port HTTPS",
|
||||
@@ -386,7 +384,6 @@
|
||||
"client_edit": "Ubah Klien",
|
||||
"client_identifier": "Identifikasi",
|
||||
"ip_address": "Alamat IP",
|
||||
"client_identifier_desc": "Klien dapat diidentifikasi dengan alamat IP atau alamat MAC. Harap dicatat bahwa menggunakan MAC sebagai pengidentifikasi hanya dimungkinkan jika AdGuard Home juga merupakan <0>server DHCP</0>",
|
||||
"form_enter_ip": "Masukkan IP",
|
||||
"form_enter_mac": "Masukkan MAC",
|
||||
"form_enter_id": "Masukkan pengenal",
|
||||
@@ -529,7 +526,6 @@
|
||||
"check_ip": "Alamat IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Alasan: {{reason}}",
|
||||
"check_rule": "Aturan: {{rule}}",
|
||||
"check_service": "Nama layanan: {{service}}",
|
||||
"service_name": "Nama layanan",
|
||||
"check_not_found": "Tidak di temukan di daftar penyaringan anda",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Formato IPv4 non valido",
|
||||
"form_error_mac_format": "Formato MAC non valido",
|
||||
"form_error_client_id_format": "Formato ID cliente non valido",
|
||||
"form_error_server_name": "Nome server non valido",
|
||||
"form_error_positive": "Deve essere maggiore di 0",
|
||||
"form_error_negative": "Deve essere maggiore o uguale a 0 (zero)",
|
||||
"range_end_error": "Deve essere maggiore dell'intervallo di inizio",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "IP personalizzato",
|
||||
"blocking_ipv4": "Blocca IPv4",
|
||||
"blocking_ipv6": "Blocca IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS su HTTPS",
|
||||
"dns_over_tls": "DNS su TLS",
|
||||
"dns_over_quic": "DNS su Quic",
|
||||
"client_id": "ID client",
|
||||
"client_id_placeholder": "Inserisci ID client",
|
||||
"client_id_desc": "Client differenti possono essere identificati da uno speciale ID. <a>Qui</a> potrai saperne di più sui metodi per identificarli.",
|
||||
"download_mobileconfig_doh": "Scarica .mobileconfig per DNS su HTTPS",
|
||||
"download_mobileconfig_dot": "Scarica .mobileconfig per DNS su TLS",
|
||||
"download_mobileconfig": "Scarica file di configurazione",
|
||||
"plain_dns": "DNS semplice",
|
||||
"form_enter_rate_limit": "Imposta limite delle richieste",
|
||||
"rate_limit": "Limite delle richieste",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Fonte",
|
||||
"found_in_known_domain_db": "Trovato nel database dei domini conosciuti.",
|
||||
"category_label": "Categoria",
|
||||
"rule_label": "Regola",
|
||||
"rule_label": "Regola(e)",
|
||||
"list_label": "Lista",
|
||||
"unknown_filter": "Filtro sconosciuto {{filterId}}",
|
||||
"known_tracker": "Tracker conosciuto",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Configurazione della crittografia salvata",
|
||||
"encryption_server": "Nome server",
|
||||
"encryption_server_enter": "Inserisci il tuo nome di dominio",
|
||||
"encryption_server_desc": "Per utilizzare HTTPS, è necessario inserire il nome del server che corrisponde al certificato SSL.",
|
||||
"encryption_server_desc": "Per utilizzare HTTPS, è necessario immettere il nome del server che corrisponde al certificato SSL o al certificato wildcard. Se il campo risulterà vuoto, accetterà connessioni TLS per qualsiasi dominio.",
|
||||
"encryption_redirect": "Reindirizza automaticamente a HTTPS",
|
||||
"encryption_redirect_desc": "Se selezionato, AdGuard Home ti reindirizzerà automaticamente da indirizzi HTTP a HTTPS.",
|
||||
"encryption_https": "Porta HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Modifica Client",
|
||||
"client_identifier": "Identificatore",
|
||||
"ip_address": "Indirizzo IP",
|
||||
"client_identifier_desc": "I client possono essere identificati dall indirizzo IP o dall' indirizzo MAC. Nota che l' utilizzo dell' indirizzo MAC come identificatore è consentito solo se AdGuard Home è anche il <0>server DHCP</0>",
|
||||
"client_identifier_desc": "I client possono essere identificati dall'indirizzo IP, CIDR, indirizzo MAC o un ID speciale (che può essere utilizzato per DoT/DoH/DoQ). <0>Qui</0> potrai saperne di più sui metodi per identificarli.",
|
||||
"form_enter_ip": "Inserisci IP",
|
||||
"form_enter_mac": "Inserisci MAC",
|
||||
"form_enter_id": "Inserisci identificatore",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supporta <1>DNS su HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supporta <1>DNS su HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Troverai più implementazioni <0>qui</0> e <1>qui</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "configurazione iOS e macOS",
|
||||
"setup_dns_notice": "Per utilizzare <1>DNS su HTTPS</1> o <1>DNS su TLS</1>, è necessario <0>configurare la crittografia</0> nelle impostazioni di AdGuard Home.",
|
||||
"rewrite_added": "Riscrittura DNS per \"{{key}}\" aggiunta correttamente",
|
||||
"rewrite_deleted": "La riscrittura DNS per \"{{key}}\" è stata eliminata correttamente",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "Indirizzi IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Motivo: {{reason}}",
|
||||
"check_rule": "Regola: {{rule}}",
|
||||
"check_service": "Nome servizio: {{service}}",
|
||||
"service_name": "Nome servizio",
|
||||
"check_not_found": "Non trovato negli elenchi dei filtri",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "IPv4フォーマットではありません",
|
||||
"form_error_mac_format": "MACフォーマットではありません",
|
||||
"form_error_client_id_format": "Client IDの形式が無効です",
|
||||
"form_error_server_name": "サーバ名が無効です",
|
||||
"form_error_positive": "0より大きい必要があります",
|
||||
"form_error_negative": "0以上である必要があります",
|
||||
"range_end_error": "範囲開始よりも大きくなければなりません",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "カスタムIP",
|
||||
"blocking_ipv4": "ブロック中のIPv4",
|
||||
"blocking_ipv6": "ブロック中のIPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "Client ID(クライアントID)",
|
||||
"client_id_placeholder": "クライアントIDを入力してください",
|
||||
"client_id_desc": "それぞれのクライアントは、特別なクライアントIDで識別できます。 <a>ここ</a>では、クライアントを特定する方法について詳しく知ることができます。",
|
||||
"download_mobileconfig_doh": "DNS-over-HTTPS用の .mobileconfig をダウンロード",
|
||||
"download_mobileconfig_dot": "DNS-over-TLS用の .mobileconfig をダウンロード",
|
||||
"download_mobileconfig": "設定ファイルをダウンロードする",
|
||||
"plain_dns": "通常のDNS",
|
||||
"form_enter_rate_limit": "頻度制限を入力してください",
|
||||
"rate_limit": "頻度制限",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "暗号化の設定を保存しました",
|
||||
"encryption_server": "サーバ名",
|
||||
"encryption_server_enter": "ドメイン名を入力してください",
|
||||
"encryption_server_desc": "HTTPSを使用するには、SSL証明書と一致するサーバ名を入力する必要があります。",
|
||||
"encryption_server_desc": "HTTPSを使用するには、SSL証明書またはワイルドカード証明書と一致するサーバー名を入力する必要があります。このフィールドが設定されていない場合は、任意のドメインのTLS接続を受け入れます。",
|
||||
"encryption_redirect": "HTTPSに自動的にリダイレクト",
|
||||
"encryption_redirect_desc": "チェックすると、AdGuard Homeは自動的にHTTPからHTTPSアドレスへリダイレクトします。",
|
||||
"encryption_https": "HTTPS ポート",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "クライアントの編集",
|
||||
"client_identifier": "識別子",
|
||||
"ip_address": "IPアドレス",
|
||||
"client_identifier_desc": "クライアントはIPアドレスまたはMACアドレスで識別できます。AdGuard Homeが<0>DHCPサーバ</0>でもある場合にのみ、識別子としてMACを使用することが可能であることにご注意ください。",
|
||||
"client_identifier_desc": "クライアントは、IPアドレス、CIDR、MACアドレス、または特別なクライアントID(DoT/DoH/DoQで使用可能)によって識別することができます。<0>ここ</0>では、クライアントの識別方法についてより詳しく説明しています。",
|
||||
"form_enter_ip": "IPアドレスを入力してください",
|
||||
"form_enter_mac": "MACアドレスを入力してください",
|
||||
"form_enter_id": "識別子を入力してください",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0>は<1>DNS-over-HTTPS</1>をサポートします。",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0>は<1>DNS-over-HTTPS</1>をサポートしています。",
|
||||
"setup_dns_privacy_other_5": "もっと多くの実装を<0>ここ</0>や<1>ここ</1>で見つけられます。",
|
||||
"setup_dns_privacy_ioc_mac": "iOS と macOS での設定",
|
||||
"setup_dns_notice": "<1>DNS-over-HTTPS</1>または<1>DNS-over-TLS</1>を使用するには、AdGuard Home 設定の<0>暗号化設定</0>が必要です。",
|
||||
"rewrite_added": "\"{{key}}\" のためのDNS書き換え情報を追加完了しました",
|
||||
"rewrite_deleted": "\"{{key}}\" のためのDNS書き換え情報を削除完了しました",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IPアドレス: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "理由: {{reason}}",
|
||||
"check_rule": "ルール: {{rule}}",
|
||||
"check_service": "サービス名: {{service}}",
|
||||
"service_name": "サービス名",
|
||||
"check_not_found": "フィルタ一覧には見つかりません",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "잘못된 IP 형식",
|
||||
"form_error_mac_format": "잘못된 MAC 형식",
|
||||
"form_error_client_id_format": "잘못된 클라이언트 ID 형식",
|
||||
"form_error_server_name": "유효하지 않은 서버 이름입니다",
|
||||
"form_error_positive": "0보다 커야 합니다",
|
||||
"form_error_negative": "반드시 0 이상이여야 합니다",
|
||||
"range_end_error": "입력 값은 범위의 시작 지점보다 큰 값 이여야 합니다.",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "사용자 지정 IP",
|
||||
"blocking_ipv4": "IPv4 차단",
|
||||
"blocking_ipv6": "IPv6 차단",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "클라이언트 ID",
|
||||
"client_id_placeholder": "클라이언트 ID 입력",
|
||||
"client_id_desc": "클라이언트는 특별한 클라이언트 ID를 기반으로 구분됩니다. <a>여기</a>에서 클라이언트를 구분하는 방법을 자세히 알아보세요.",
|
||||
"download_mobileconfig_doh": "DNS-over-HTTPS용 .mobileconfig 다운로드",
|
||||
"download_mobileconfig_dot": "DNS-over-TLS용 .mobileconfig 다운로드",
|
||||
"download_mobileconfig": "설정 파일 내려받기",
|
||||
"plain_dns": "평문 DNS",
|
||||
"form_enter_rate_limit": "한도 제한 입력하기",
|
||||
"rate_limit": "한도 제한",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "클라이언트 수정",
|
||||
"client_identifier": "식별자",
|
||||
"ip_address": "IP 주소",
|
||||
"client_identifier_desc": "사용자는 IP 주소 또는 MAC 주소로 식별할 수 있지만 AdGuard Home이 <0>DHCP 서버인 </0> 경우에만 사용자는 MAC 주소로 식별할 수 있습니다.",
|
||||
"client_identifier_desc": "클라이언트는 IP 주소, CIDR, MAC 주소 또는 특수 클라이언트 ID로 식별할 수 있습니다 (DoT/DoH/DoQ에 사용 가능). <0>여기에서</0> 클라이언트를 식별하는 방법에 대한 자세한 내용은 확인하실 수 있습니다.",
|
||||
"form_enter_ip": "IP 입력",
|
||||
"form_enter_mac": "MAC 입력",
|
||||
"form_enter_id": "식별자 입력",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> <1>DNS-over-HTTPS</1> 지원합니다.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0><1>DNS-over-HTTPS</1>지원합니다.",
|
||||
"setup_dns_privacy_other_5": "<0>이곳이나</0> <1>이곳을</1> 클릭하여 더 많은 구현에 대한 정보를 확인하세요.",
|
||||
"setup_dns_privacy_ioc_mac": "iOS 및 macOS 설정",
|
||||
"setup_dns_notice": "<1>DNS-over-HTTPS</1> 또는 <1>DNS-over-TLS를</1> 사용하려면 AdGuard Home 설정에서 <0>암호화를 구성해야 합니다.</0>",
|
||||
"rewrite_added": "\"{{key}}\"에 대한 DNS 수정 정보를 성공적으로 추가 됩니다.",
|
||||
"rewrite_deleted": "\"{{key}}\"에 대한 DNS 수정 정보를 성공적으로 삭제 됩니다.",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP 주소: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "이유: {{reason}}",
|
||||
"check_rule": "규칙: {{rule}}",
|
||||
"check_service": "서비스 이름: {{service}}",
|
||||
"service_name": "서비스 이름",
|
||||
"check_not_found": "필터 목록에서 찾을 수 없음",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Ongeldig IPv4 formaat",
|
||||
"form_error_mac_format": "Ongeldig MAC formaat.",
|
||||
"form_error_client_id_format": "Opmaak cliënt-ID is ongeldig",
|
||||
"form_error_server_name": "Ongeldige servernaam",
|
||||
"form_error_positive": "Moet groter zijn dan 0",
|
||||
"form_error_negative": "Moet 0 of hoger dan 0 zijn",
|
||||
"range_end_error": "Moet groter zijn dan het startbereik",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Aangepast IP",
|
||||
"blocking_ipv4": "Blokkeren IP4",
|
||||
"blocking_ipv6": "Blokkeren IP6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-via-HTTPS",
|
||||
"dns_over_tls": "DNS-via-TLS",
|
||||
"dns_over_quic": "DNS-via-QUIC",
|
||||
"client_id": "Apparaat-ID",
|
||||
"client_id_placeholder": "Apparaat-ID invoeren",
|
||||
"client_id_desc": "Verschillende apparaten kunnen worden geïdentificeerd door hun specifiek apparaat-ID. <a>Hier</a> vind je meer informatie over het identificeren van apparaten.",
|
||||
"download_mobileconfig_doh": ".mobileconfig voor DNS-via-HTTPS downloaden",
|
||||
"download_mobileconfig_dot": ".mobileconfig voor DNS-via-TLS downloaden",
|
||||
"download_mobileconfig": "Configuratiebestand downloaden",
|
||||
"plain_dns": "Gewone DNS",
|
||||
"form_enter_rate_limit": "Voer ratio limiet in",
|
||||
"rate_limit": "Ratio limiet",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Bron",
|
||||
"found_in_known_domain_db": "Gevonden in de bekende domeingegevensbank.",
|
||||
"category_label": "Categorie",
|
||||
"rule_label": "Regel",
|
||||
"rule_label": "Regel(s)",
|
||||
"list_label": "Lijst",
|
||||
"unknown_filter": "Onbekend filter {{filterId}}",
|
||||
"known_tracker": "Bekende volger",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Encryptie configuratie opgeslagen",
|
||||
"encryption_server": "Server naam",
|
||||
"encryption_server_enter": "Voer domein naam in",
|
||||
"encryption_server_desc": "Om HTTPS te gebruiken, voer de naam in van de server overeenkomstig met het SSL certificaat.",
|
||||
"encryption_server_desc": "Om HTTPS te gebruiken, moet je de servernaam invoeren die overeenkomt met je SSL-certificaat of jokerteken-certificaat. Als het veld niet is ingesteld, accepteert het TLS-verbindingen voor elk domein.",
|
||||
"encryption_redirect": "Herleid automatisch naar HTTPS",
|
||||
"encryption_redirect_desc": "Indien ingeschakeld, zal AdGuard Home je automatisch herleiden van HTTP naar HTTPS.",
|
||||
"encryption_https": "HTTPS poort",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Wijzig gebruiker",
|
||||
"client_identifier": "Identificeer via",
|
||||
"ip_address": "IP adres",
|
||||
"client_identifier_desc": "Gebruikers kunnen worden geïdentificeerd door het IP-adres, CIDR of MAC-adres. Hou er rekening mee dat het gebruik van MAC als ID alleen mogelijk is als AdGuard Home ook een <0>DHCP-server</0> is",
|
||||
"client_identifier_desc": "Apparaten kunnen worden geïdentificeerd door hun IP-adres, CIDR, MAC-adres of een speciaal apparaat-ID (kan gebruikt worden voor DoT/DoH/DoQ). <0>Hier</0> kan je meer lezen over het identificeren van apparaten.",
|
||||
"form_enter_ip": "Vul IP in",
|
||||
"form_enter_mac": "Vul MAC in",
|
||||
"form_enter_id": "ID invoeren",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> ondersteunt <1>DNS-via-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> ondersteunt <1>DNS-via-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "U vindt meer implementaties <0> hier </0> en <1> hier </1>.",
|
||||
"setup_dns_privacy_ioc_mac": "iOS en macOS configuratie",
|
||||
"setup_dns_notice": "Om <1>DNS-via-HTTPS</1> of <1>DNS-via-TLS</1> te gebruiken, moet je <0>Versleuteling configureren</0> in de AdGuard Home instellingen.",
|
||||
"rewrite_added": "DNS-herschrijving voor \"{{key}}\" met succes toegevoegd",
|
||||
"rewrite_deleted": "DNS-herschrijving voor \"{{key}}\" met succes verwijderd",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP-adressen: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Reden: {{reason}}",
|
||||
"check_rule": "Regel: {{rule}}",
|
||||
"check_service": "Servicenaam: {{service}}",
|
||||
"service_name": "Naam service",
|
||||
"check_not_found": "Niet in je lijst met filters gevonden",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Ugyldig IPv4-format",
|
||||
"form_error_mac_format": "Ugyldig MAC-format",
|
||||
"form_error_client_id_format": "Ugyldig ID-klientformat",
|
||||
"form_error_server_name": "Ugyldig tjenernavn",
|
||||
"form_error_positive": "Må være høyere enn 0",
|
||||
"form_error_negative": "Må være ≥0",
|
||||
"range_end_error": "Må være høyere enn rekkeviddens start",
|
||||
@@ -247,8 +248,11 @@
|
||||
"custom_ip": "Tilpasset IP",
|
||||
"blocking_ipv4": "IPv4-blokkering",
|
||||
"blocking_ipv6": "IPv6-blokkering",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "Klient-ID",
|
||||
"download_mobileconfig_doh": "Last ned .mobileconfig for DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Last ned .mobileconfig for DNS-over-TLS",
|
||||
"plain_dns": "Ordinær DNS",
|
||||
@@ -269,7 +273,6 @@
|
||||
"source_label": "Kilde",
|
||||
"found_in_known_domain_db": "Funnet i databasen over kjente domener.",
|
||||
"category_label": "Kategori",
|
||||
"rule_label": "Oppføring",
|
||||
"list_label": "Liste",
|
||||
"unknown_filter": "Ukjent filter {{filterId}}",
|
||||
"known_tracker": "Kjent sporer",
|
||||
@@ -330,7 +333,6 @@
|
||||
"encryption_config_saved": "Krypteringsoppsettet ble lagret",
|
||||
"encryption_server": "Tjenerens navn",
|
||||
"encryption_server_enter": "Skriv inn domenenavnet ditt",
|
||||
"encryption_server_desc": "For å kunne bruke HTTPS, må du skrive inn tjenernavnet som samsvarer med ditt SSL-sertifikat.",
|
||||
"encryption_redirect": "Automatisk omdiriger til HTTPS",
|
||||
"encryption_redirect_desc": "Dersom dette er valgt, vil AdGuard Home automatisk omdirigere deg fra HTTP til HTTPS-adresser.",
|
||||
"encryption_https": "HTTPS-port",
|
||||
@@ -386,7 +388,6 @@
|
||||
"client_edit": "Rediger klienten",
|
||||
"client_identifier": "Identifikator",
|
||||
"ip_address": "IP-adresse",
|
||||
"client_identifier_desc": "Klienter kan bli identifisert gjennom IP-adressen eller MAC-adressen. Vennligst bemerk at å bruke MAC som en identifikator, bare er mulig dersom AdGuard Home også er en <0>DHCP-tjener</0>",
|
||||
"form_enter_ip": "Skriv inn IP",
|
||||
"form_enter_mac": "Skriv inn MAC",
|
||||
"form_enter_id": "Skriv inn identifikator",
|
||||
@@ -528,7 +529,6 @@
|
||||
"check_ip": "IP-adresser: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Årsak: {{reason}}",
|
||||
"check_rule": "Oppføring: {{rule}}",
|
||||
"check_service": "Tjenestenavn: {{service}}",
|
||||
"service_name": "Tjenestenavn",
|
||||
"check_not_found": "Ikke funnet i filterlistene dine",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Nieprawidłowy format IP",
|
||||
"form_error_mac_format": "Nieprawidłowy format MAC",
|
||||
"form_error_client_id_format": "Nieprawidłowy format identyfikatora klienta",
|
||||
"form_error_server_name": "Nieprawidłowa nazwa serwera",
|
||||
"form_error_positive": "Musi być większa niż 0",
|
||||
"form_error_negative": "Musi być równy 0 lub większy",
|
||||
"range_end_error": "Zakres musi być większy niż początkowy",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Niestandardowy adres IP",
|
||||
"blocking_ipv4": "Blokowanie IPv4",
|
||||
"blocking_ipv6": "Blokowanie IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "ID klienta",
|
||||
"client_id_placeholder": "Wpisz ID klienta",
|
||||
"client_id_desc": "Różnych klientów można zidentyfikować za pomocą specjalnego ID klienta. <a>Tutaj</a> możesz dowiedzieć się więcej o tym, jak identyfikować klientów.",
|
||||
"download_mobileconfig_doh": "Pobierz plik .mobileconfig dla DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Pobierz plik .mobileconfig dla DNS-over-TLS",
|
||||
"download_mobileconfig": "Pobierz plik konfiguracyjny",
|
||||
"plain_dns": "Zwykły DNS",
|
||||
"form_enter_rate_limit": "Wpisz limit ilościowy",
|
||||
"rate_limit": "Limit ilościowy",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Źródło",
|
||||
"found_in_known_domain_db": "Znaleziono w bazie danych znanych domen.",
|
||||
"category_label": "Kategoria",
|
||||
"rule_label": "Reguła",
|
||||
"rule_label": "Reguła(y)",
|
||||
"list_label": "Lista",
|
||||
"unknown_filter": "Nieznany filtr {{filterId}}",
|
||||
"known_tracker": "Znany element śledzący",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Zapisano konfigurację szyfrowania",
|
||||
"encryption_server": "Nazwa serwera",
|
||||
"encryption_server_enter": "Wpisz swoją nazwę domeny",
|
||||
"encryption_server_desc": "Aby korzystać z protokołu HTTPS, musisz wprowadzić nazwę serwera zgodną z certyfikatem SSL.",
|
||||
"encryption_server_desc": "Aby korzystać z protokołu HTTPS, musisz wprowadzić nazwę serwera, która jest zgodna z certyfikatem SSL lub certyfikatem typu wildcard. Jeśli pole nie jest ustawione, będzie akceptować połączenia TLS dla dowolnej domeny.",
|
||||
"encryption_redirect": "Przekieruj automatycznie do HTTPS",
|
||||
"encryption_redirect_desc": "Jeśli zaznaczone, AdGuard Home automatycznie przekieruje Cię z adresów HTTP na HTTPS.",
|
||||
"encryption_https": "Port HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Edytuj klienta",
|
||||
"client_identifier": "Identyfikator",
|
||||
"ip_address": "Adres IP",
|
||||
"client_identifier_desc": "Klienci mogą być identyfikowani na podstawie adresu IP, CIDR, adresu MAC. Pamiętaj, że użycie MAC jako identyfikatora jest możliwe tylko wtedy, gdy AdGuard Home jest również <0>serwerem DHCP</0>",
|
||||
"client_identifier_desc": "Klientów można zidentyfikować po adresie IP, CIDR, adresie MAC lub specjalnym identyfikatorze klienta (może służyć do DoT/DoH/DoQ). <0>Tutaj</0> możesz dowiedzieć się więcej o tym, jak identyfikować klientów.",
|
||||
"form_enter_ip": "Wpisz adres IP",
|
||||
"form_enter_mac": "Wpisz adres MAC",
|
||||
"form_enter_id": "Wpisz identyfikator",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> obsługuje <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> obsługuje <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Znajdziesz więcej implementacji <0>tutaj</0> i <1>tutaj</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Konfiguracja iOS i macOS",
|
||||
"setup_dns_notice": "Aby skorzystać z <1>DNS-over-HTTPS</1> lub <1>DNS-over-TLS</1>, musisz w ustawieniach AdGuard Home <0>skonfigurować szyfrowanie</0>.",
|
||||
"rewrite_added": "Pomyślnie dodano przepisanie DNS dla „{{key}}”",
|
||||
"rewrite_deleted": "Przepisanie DNS dla „{{key}}” zostało pomyślnie usunięte",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "Adresy IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Powód: {{reason}}",
|
||||
"check_rule": "Reguła: {{rule}}",
|
||||
"check_service": "Nazwa usługi: {{service}}",
|
||||
"service_name": "Nazwa usługi",
|
||||
"check_not_found": "Nie znaleziono na Twoich listach filtrów",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Formato de endereço IPv inválido",
|
||||
"form_error_mac_format": "Formato do endereço MAC inválido",
|
||||
"form_error_client_id_format": "Formato do ID de cliente inválido",
|
||||
"form_error_server_name": "Nome de servidor inválido",
|
||||
"form_error_positive": "Deve ser maior que 0",
|
||||
"form_error_negative": "Deve ser igual ou superior a 0",
|
||||
"range_end_error": "Deve ser maior que o início do intervalo",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "IP personalizado",
|
||||
"blocking_ipv4": "Bloqueando IPv4",
|
||||
"blocking_ipv6": "Bloqueando IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-sobre-HTTPS",
|
||||
"dns_over_tls": "DNS-sobre-TLS",
|
||||
"dns_over_quic": "DNS-sobre-QUIC",
|
||||
"client_id": "ID do cliente",
|
||||
"client_id_placeholder": "Digite o ID do cliente",
|
||||
"client_id_desc": "Diferentes clientes podem ser identificados por um ID de cliente especial. <a>Aqui</a> você pode aprender mais sobre como identificar clientes.",
|
||||
"download_mobileconfig_doh": "BAixar .mobileconfig para DNS-sobre-HTTPS",
|
||||
"download_mobileconfig_dot": "BAixar .mobileconfig para DNS-sobre-TLS",
|
||||
"download_mobileconfig": "Baixar arquivo de configuração",
|
||||
"plain_dns": "DNS simples",
|
||||
"form_enter_rate_limit": "Insira a taxa limite",
|
||||
"rate_limit": "Taxa limite",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Fonte",
|
||||
"found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecidos.",
|
||||
"category_label": "Categoria",
|
||||
"rule_label": "Regra",
|
||||
"rule_label": "Regra(s)",
|
||||
"list_label": "Lista",
|
||||
"unknown_filter": "Filtro desconhecido {{filterId}}",
|
||||
"known_tracker": "Rastreador conhecido",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Configuração de criptografia salva",
|
||||
"encryption_server": "Nome do servidor",
|
||||
"encryption_server_enter": "Digite seu nome de domínio",
|
||||
"encryption_server_desc": "Para usar o protocolo HTTPS, você precisa digitar o nome do servidor que corresponde ao seu certificado SSL.",
|
||||
"encryption_server_desc": "Para usar HTTPS, você precisa inserir o nome do servidor que corresponda ao seu certificado SSL ou certificado curinga. Se o campo não estiver definido, ele aceitará conexões TLS para qualquer domínio.",
|
||||
"encryption_redirect": "Redirecionar automaticamente para HTTPS",
|
||||
"encryption_redirect_desc": "Se marcado, o AdGuard Home irá redirecionar automaticamente os endereços HTTP para HTTPS.",
|
||||
"encryption_https": "Porta HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Editar cliente",
|
||||
"client_identifier": "Identificador",
|
||||
"ip_address": "Endereço de IP",
|
||||
"client_identifier_desc": "Clientes podem ser identificados pelo endereço de IP ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>",
|
||||
"client_identifier_desc": "Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). <0>Aqui</0> você pode aprender mais sobre como identificar clientes.",
|
||||
"form_enter_ip": "Digite o endereço de IP",
|
||||
"form_enter_mac": "Digite o endereço MAC",
|
||||
"form_enter_id": "Inserir identificador",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Você encontrará mais implementações <0>aqui</0> e <1>aqui</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "configuração para iOS e macOS",
|
||||
"setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, você precisa <0>configurar a criptografia</0> nas configurações do AdGuard Home.",
|
||||
"rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso",
|
||||
"rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "Endereços de IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Motivo: {{reason}}",
|
||||
"check_rule": "Regra: {{rule}}",
|
||||
"check_service": "Nome do serviço: {{service}}",
|
||||
"service_name": "Nome do serviço",
|
||||
"check_not_found": "Não encontrado em suas listas de filtros",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Formato de endereço IPv4 inválido",
|
||||
"form_error_mac_format": "Formato do endereço MAC inválido",
|
||||
"form_error_client_id_format": "Formato inválido",
|
||||
"form_error_server_name": "Nome de servidor inválido",
|
||||
"form_error_positive": "Deve ser maior que 0",
|
||||
"form_error_negative": "Deve ser igual ou superior a 0",
|
||||
"range_end_error": "Deve ser maior que o início do intervalo",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "IP Personalizado",
|
||||
"blocking_ipv4": "A bloquear IPv4",
|
||||
"blocking_ipv6": "A bloquear IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-sobre-HTTPS",
|
||||
"dns_over_tls": "DNS-sobre-TLS",
|
||||
"dns_over_quic": "DNS-sobre-QUIC",
|
||||
"client_id": "ID do cliente",
|
||||
"client_id_placeholder": "Insira o ID do cliente",
|
||||
"client_id_desc": "Diferentes clientes podem ser identificados por um ID de cliente especial. <a>Aqui</a> você pode aprender mais sobre como identificar clientes.",
|
||||
"download_mobileconfig_doh": "Transferir .mobileconfig para DNS-sobre-HTTPS",
|
||||
"download_mobileconfig_dot": "Transferir .mobileconfig para DNS-sobre-TLS",
|
||||
"download_mobileconfig": "Transferir arquivo de configuração",
|
||||
"plain_dns": "DNS simples",
|
||||
"form_enter_rate_limit": "Insira o limite de taxa",
|
||||
"rate_limit": "Limite de taxa",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Fonte",
|
||||
"found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecido.",
|
||||
"category_label": "Categoria",
|
||||
"rule_label": "Regra",
|
||||
"rule_label": "Regra(s)",
|
||||
"list_label": "Lista",
|
||||
"unknown_filter": "Filtro desconhecido {{filterId}}",
|
||||
"known_tracker": "Rastreador conhecido",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Definição de criptografia guardada",
|
||||
"encryption_server": "Nome do servidor",
|
||||
"encryption_server_enter": "Insira o seu nome de domínio",
|
||||
"encryption_server_desc": "Para usar o protocolo HTTPS, precisa de inserir o nome do servidor que corresponde ao seu certificado SSL.",
|
||||
"encryption_server_desc": "Para usar HTTPS, você precisa inserir o nome do servidor que corresponda ao seu certificado SSL ou certificado curinga. Se o campo não estiver definido, ele aceitará conexões TLS para qualquer domínio.",
|
||||
"encryption_redirect": "Redireccionar automaticamente para HTTPS",
|
||||
"encryption_redirect_desc": "Se marcado, o AdGuard Home irá redireccionar automaticamente os endereços HTTP para HTTPS.",
|
||||
"encryption_https": "Porta HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Editar cliente",
|
||||
"client_identifier": "Identificador",
|
||||
"ip_address": "Endereço de IP",
|
||||
"client_identifier_desc": "Os clientes podem ser identificados pelo endereço de IP, CIDR, ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>",
|
||||
"client_identifier_desc": "Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). <0>Aqui</0> você pode aprender mais sobre como identificar clientes.",
|
||||
"form_enter_ip": "Insira IP",
|
||||
"form_enter_mac": "Insira o endereço MAC",
|
||||
"form_enter_id": "Inserir identificador",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Encontrará mais implementações <0>aqui</0> e <1>aqui</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "configuração para iOS e macOS",
|
||||
"setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, precisa de <0>configurar a criptografia</0> nas configurações do AdGuard Home.",
|
||||
"rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso",
|
||||
"rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "Endereços de IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Motivo: {{reason}}",
|
||||
"check_rule": "Regra: {{rule}}",
|
||||
"check_service": "Nome do serviço: {{service}}",
|
||||
"service_name": "Nome do serviço",
|
||||
"check_not_found": "Não encontrado nas tuas listas de filtros",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Format IP invalid",
|
||||
"form_error_mac_format": "Format MAC invalid",
|
||||
"form_error_client_id_format": "Format ID de client invalid",
|
||||
"form_error_server_name": "Nume de server nevalid",
|
||||
"form_error_positive": "Trebuie să fie mai mare de 0",
|
||||
"form_error_negative": "Trebuie să fie egală cu 0 sau mai mare",
|
||||
"range_end_error": "Trebuie să fie mai mare decât începutul intervalului",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "IP personalizat",
|
||||
"blocking_ipv4": "Blocarea IPv4",
|
||||
"blocking_ipv6": "Blocarea IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "ID Client",
|
||||
"client_id_placeholder": "Introduceți ID client",
|
||||
"client_id_desc": "Diferiți clienți pot fi identificați printr-un ID special al clientului. <a>Aici</a> puteți afla mai multe despre cum să identificați clienții.",
|
||||
"download_mobileconfig_doh": "Descărcați .mobileconfig pentru DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Descărcați .mobileconfig pentru DNS-over-TLS",
|
||||
"download_mobileconfig": "Descărcați fișierul de configurare",
|
||||
"plain_dns": "DNS simplu",
|
||||
"form_enter_rate_limit": "Introduceți limita ratei",
|
||||
"rate_limit": "Limita ratei",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Sursă",
|
||||
"found_in_known_domain_db": "Găsit în baza de date de domenii cunoscută.",
|
||||
"category_label": "Categorie",
|
||||
"rule_label": "Regulă",
|
||||
"rule_label": "Regulă(reguli)",
|
||||
"list_label": "Listă",
|
||||
"unknown_filter": "Filtru necunoscut {{filterId}}",
|
||||
"known_tracker": "Tracker cunoscut",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Configurația de criptare salvată",
|
||||
"encryption_server": "Nume de server",
|
||||
"encryption_server_enter": "Introduceți numele domeniului",
|
||||
"encryption_server_desc": "Pentru a utiliza HTTPS, trebuie introdus numele serverului care corespunde certificatului SSL.",
|
||||
"encryption_server_desc": "Pentru a utiliza HTTPS, trebuie să introduceți numele serverului care se potrivește cu certificatul SSL sau certificatul wildcard al dvs. În cazul în care câmpul nu este setat, va accepta conexiuni TLS pentru orice domeniu.",
|
||||
"encryption_redirect": "Redirecționați automat la HTTPS",
|
||||
"encryption_redirect_desc": "Dacă este bifat, AdGuard Home vă va redirecționa automat de la adrese HTTP la HTTPS.",
|
||||
"encryption_https": "Port HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Editare client",
|
||||
"client_identifier": "Identificator",
|
||||
"ip_address": "Adresa IP",
|
||||
"client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC. Vă rugăm să rețineți că utilizarea MAC ca identificator este posibilă numai dacă AdGuard Home este și un <0>server DHCP</0>",
|
||||
"client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC sau un ID special al clientului (poate fi folosit pentru DoT/DoH/DoQ). <0>Aici</0> puteți afla mai multe despre cum să identificați clienții.",
|
||||
"form_enter_ip": "Introduceți IP",
|
||||
"form_enter_mac": "Introduceți MAC",
|
||||
"form_enter_id": "Introduceți identificator",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> acceptă <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> acceptă <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Veți găsi mai multe implementări <0>aici</0> și <1>aici</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Configurarea iOS și macOS",
|
||||
"setup_dns_notice": "Pentru a utiliza <1>DNS-over-HTTPS</1> sau <1>DNS-over-TLS</1>, trebuie să <0>configurați Criptarea</0> în setările AdGuard Home.",
|
||||
"rewrite_added": "Rescriere DNS pentru \"{{key}}\" adăugată cu succes",
|
||||
"rewrite_deleted": "Rescriere DNS pentru \"{{key}}\" ștearsă cu succes",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "Adrese IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Cauza: {{reason}}",
|
||||
"check_rule": "Regula: {{rule}}",
|
||||
"check_service": "Nume servici: {{service}}",
|
||||
"service_name": "Numele serviciului",
|
||||
"check_not_found": "Nu se găsește în listele de filtre",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Неверный формат IP-адреса",
|
||||
"form_error_mac_format": "Некорректный формат MAC",
|
||||
"form_error_client_id_format": "Неверный формат ID клиента",
|
||||
"form_error_server_name": "Неверное имя сервера",
|
||||
"form_error_positive": "Должно быть больше 0",
|
||||
"form_error_negative": "Должно быть не меньше 0",
|
||||
"range_end_error": "Должно превышать начало диапазона",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Свой IP",
|
||||
"blocking_ipv4": "Блокировка IPv4",
|
||||
"blocking_ipv6": "Блокировка IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "Идентификатор клиента",
|
||||
"client_id_placeholder": "Введите идентификатор клиента",
|
||||
"client_id_desc": "Различные клиенты могут идентифицироваться по специальному идентификатору клиента. <a>Здесь</a> вы можете узнать больше об идентификации клиентов.",
|
||||
"download_mobileconfig_doh": "Скачать .mobileconfig для DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Скачать .mobileconfig для DNS-over-TLS",
|
||||
"download_mobileconfig": "Загрузить файл конфигурации",
|
||||
"plain_dns": "Нешифрованный DNS",
|
||||
"form_enter_rate_limit": "Введите rate limit",
|
||||
"rate_limit": "Rate limit",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Источник",
|
||||
"found_in_known_domain_db": "Найден в базе известных доменов.",
|
||||
"category_label": "Категория",
|
||||
"rule_label": "Правило",
|
||||
"rule_label": "Правило(-а)",
|
||||
"list_label": "Список",
|
||||
"unknown_filter": "Неизвестный фильтр {{filterId}}",
|
||||
"known_tracker": "Известный трекер",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Настройки шифрования сохранены",
|
||||
"encryption_server": "Имя сервера",
|
||||
"encryption_server_enter": "Введите ваше доменное имя",
|
||||
"encryption_server_desc": "Для использования HTTPS вам необходимо ввести имя сервера, которое подходит вашему SSL-сертификату.",
|
||||
"encryption_server_desc": "Для использования HTTPS вам необходимо ввести имя сервера, которое подходит вашему SSL-сертификату или сертификату с поддержкой поддоменов. Если это поле не задано, сервер будет принимать TLS-соединения для любого домена.",
|
||||
"encryption_redirect": "Автоматически перенаправлять на HTTPS",
|
||||
"encryption_redirect_desc": "Если включено, AdGuard Home будет автоматически перенаправлять вас с HTTP на HTTPS адрес.",
|
||||
"encryption_https": "Порт HTTPS",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Редактировать клиента",
|
||||
"client_identifier": "Идентификатор",
|
||||
"ip_address": "IP-адрес",
|
||||
"client_identifier_desc": "Клиенты могут быть идентифицированы по IP-адресу, CIDR или MAC-адресу. Обратите внимание, что использование MAC как идентификатора возможно, только если AdGuard Home также является и <0>DHCP-сервером</0>",
|
||||
"client_identifier_desc": "Клиенты могут быть идентифицированы по IP-адресу, CIDR или MAC-адресу или специальному ID (можно использовать для DoT/DoH/DoQ). <0>Здесь</0> вы можете узнать больше об идентификации клиентов.",
|
||||
"form_enter_ip": "Введите IP",
|
||||
"form_enter_mac": "Введите MAC",
|
||||
"form_enter_id": "Введите идентификатор",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> поддерживает <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> поддерживает <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Вы можете найти еще варианты <0>тут</0> и <1>тут</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Конфигурация для iOS и macOS",
|
||||
"setup_dns_notice": "Чтобы использовать <1>DNS-over-HTTPS</1> или <1>DNS-over-TLS</1>, вам нужно <0>настроить шифрование</0> в настройках AdGuard Home.",
|
||||
"rewrite_added": "Правило перенаправления DNS для \"{{key}}\" успешно добавлено",
|
||||
"rewrite_deleted": "Правило перенаправления DNS для \"{{key}}\" успешно удалено",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP-адреса: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Причина: {{reason}}",
|
||||
"check_rule": "Правило: {{rule}}",
|
||||
"check_service": "Название сервиса: {{service}}",
|
||||
"service_name": "Имя сервиса",
|
||||
"check_not_found": "Не найдено в вашем списке фильтров",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"dns_query": "ව.නා.ප. (DNS) විමසුම්",
|
||||
"blocked_by": "<0>පෙරහන් මගින් අවහිර කරන ලද</0>",
|
||||
"stats_malware_phishing": "අවහිර කළ ද්වේශාංග/තතුබෑම්",
|
||||
"stats_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි",
|
||||
"stats_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි",
|
||||
"stats_query_domain": "ජනප්රිය විමසන ලද වසම්",
|
||||
"for_last_24_hours": "පසුගිය පැය 24 සඳහා",
|
||||
"for_last_days": "පසුගිය දින {{count}} සඳහා",
|
||||
@@ -83,22 +83,22 @@
|
||||
"top_clients": "ජනප්රිය අනුග්රාහකයන්",
|
||||
"general_statistics": "පොදු සංඛ්යාලේඛන",
|
||||
"number_of_dns_query_blocked_24_hours": "දැන්වීම් වාරණ පෙරහන් සහ ධාරක වාරණ ලැයිස්තු මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard browsing security ඒකකය මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි ගණන",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ ඒකකය මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි ගණන",
|
||||
"enforced_save_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන ලද",
|
||||
"number_of_dns_query_to_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්ර සඳහා ව.නා.ප. ඉල්ලීම් ගණන",
|
||||
"average_processing_time": "සාමාන්ය සැකසුම් කාලය",
|
||||
"average_processing_time_hint": "ව.නා.ප. ඉල්ලීමක් සැකසීමේ සාමාන්ය කාලය මිලි තත්පර වලින්",
|
||||
"block_domain_use_filters_and_hosts": "පෙරහන් සහ ධාරක ගොනු භාවිතා කරමින් වසම් අවහිර කරන්න",
|
||||
"filters_block_toggle_hint": "ඔබට අවහිර කිරීමේ නීති <a>පෙරහන්</a> තුළ පිහිටුවිය හැකිය.",
|
||||
"use_adguard_browsing_sec": "AdGuard browsing security වෙබ් සේවාව භාවිතා කරන්න",
|
||||
"use_adguard_parental": "AdGuard parental control වෙබ් සේවාව භාවිතා කරන්න",
|
||||
"use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය බ්රව්සින් සෙකියුරිටි වෙබ් සේවාව මෙන් රහස්යතා හිතකාමී යෙ.ක්ර. අ.මු. (API) භාවිතා කරයි.",
|
||||
"use_adguard_browsing_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ වියමන සේවාව භාවිතා කරන්න",
|
||||
"use_adguard_parental": "ඇඩ්ගාර්ඩ් දෙමාපිය පාලන වියමන සේවාව භාවිතා කරන්න",
|
||||
"use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය පිරික්සුම් ආරක්ෂණ වියමන සේවාව මෙන් රහස්යතා හිතකාමී යෙ.ක්ර. අ.මු. (API) භාවිතා කරයි.",
|
||||
"enforce_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන්න",
|
||||
"enforce_save_search_hint": "ඇඩ්ගාර්ඩ් හෝම් හට පහත සෙවුම් යන්ත්ර තුළ ආරක්ෂිත සෙවීම බලාත්මක කළ හැකිය: ගූගල්, යූටියුබ්, බින්ග්, ඩක්ඩක්ගෝ, යාන්ඩෙක්ස් සහ පික්සාබේ.",
|
||||
"no_servers_specified": "සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත",
|
||||
"general_settings": "පොදු සැකසුම්",
|
||||
"dns_settings": "DNS සැකසුම්",
|
||||
"dns_settings": "ව.නා.ප. සැකසුම්",
|
||||
"dns_blocklists": "ව.නා.ප. අවහිර කිරීමේ ලැයිස්තු",
|
||||
"dns_allowlists": "ව.නා.ප. අවසර දීමේ ලැයිස්තු",
|
||||
"dns_blocklists_desc": "ඇඩ්ගාර්ඩ් හෝම් විසින් අවහිර කිරීමේ ලැයිස්තු වලට ගැලපෙන වසම් අවහිර කරනු ඇත.",
|
||||
@@ -119,7 +119,7 @@
|
||||
"enabled_save_search_toast": "ආරක්ෂිත සෙවීම සබල කර ඇත",
|
||||
"enabled_table_header": "සබල කර ඇත",
|
||||
"name_table_header": "නම",
|
||||
"list_url_table_header": "URL ලැයිස්තුව",
|
||||
"list_url_table_header": "ඒ.ස.නි.(URL) ලැයිස්තුව",
|
||||
"rules_count_table_header": "නීති ගණන",
|
||||
"last_time_updated_table_header": "අවසන් වරට යාවත්කාලීන කරන ලද",
|
||||
"actions_table_header": "ක්රියාමාර්ග",
|
||||
@@ -134,7 +134,7 @@
|
||||
"add_allowlist": "අවසර දීමේ ලැයිස්තුවක් එකතු කරන්න",
|
||||
"cancel_btn": "අහෝසි කරන්න",
|
||||
"enter_name_hint": "නම ඇතුළත් කරන්න",
|
||||
"enter_url_or_path_hint": "ලැයිස්තුවක URL හෝ ස්ථීර මාර්ගයක් ඇතුළත් කරන්න",
|
||||
"enter_url_or_path_hint": "ලැයිස්තුවක ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයක් ඇතුළත් කරන්න",
|
||||
"check_updates_btn": "යාවත්කාල පරීක්ෂා කරන්න",
|
||||
"new_blocklist": "නව අවහිර කිරීමේ ලැයිස්තුව",
|
||||
"new_allowlist": "නව අවසර දීමේ ලැයිස්තුව",
|
||||
@@ -142,10 +142,10 @@
|
||||
"edit_allowlist": "අවසර දීමේ ලැයිස්තුව සංස්කරණය කරන්න",
|
||||
"choose_blocklist": "අවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න",
|
||||
"choose_allowlist": "අනවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න",
|
||||
"enter_valid_blocklist": "අවහිර කිරීමේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.",
|
||||
"enter_valid_allowlist": "අවසර දීමේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.",
|
||||
"form_error_url_format": "වලංගු නොවන URL ආකෘතියකි",
|
||||
"form_error_url_or_path_format": "ලැයිස්තුවක වලංගු නොවන URL හෝ ස්ථීර මාර්ගයකි",
|
||||
"enter_valid_blocklist": "අවහිර කිරීමේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුළත් කරන්න.",
|
||||
"enter_valid_allowlist": "අවසර දීමේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුළත් කරන්න.",
|
||||
"form_error_url_format": "වලංගු නොවන ඒ.ස.නි.(URL) ආකෘතියකි",
|
||||
"form_error_url_or_path_format": "ලැයිස්තුවක වලංගු නොවන ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයකි",
|
||||
"custom_filter_rules": "අභිරුචි පෙරීමේ නීති",
|
||||
"custom_filter_rules_hint": "පේළියකට එක් නීතියක් බැගින් ඇතුළත් කරන්න. ඔබට දැන්වීම් අවහිර කිරීමේ නීති හෝ ධාරක ගොනු පද ගැලපුම් භාවිතා කළ හැකිය.",
|
||||
"examples_title": "උදාහරණ",
|
||||
@@ -156,11 +156,11 @@
|
||||
"example_comment_meaning": "විස්තර කිරීමක්",
|
||||
"example_comment_hash": "# එසේම අදහස් දැක්වීමක්",
|
||||
"example_regex_meaning": "නිශ්චිතව දක්වා ඇති නිත්ය වාක්යවිධියට ගැලපෙන වසම් වෙත පිවිසීම අවහිර කරයි",
|
||||
"example_upstream_regular": "සාමාන්ය DNS (UDP හරහා)",
|
||||
"example_upstream_regular": "සාමාන්ය ව.නා.ප. (UDP හරහා)",
|
||||
"example_upstream_dot": "සංකේතාංකනය කළ <0>DNS-over-TLS</0>",
|
||||
"example_upstream_doh": "සංකේතාංකනය කළ <0>DNS-over-HTTPS</0>",
|
||||
"example_upstream_doq": "සංකේතාංකනය කළ <0>DNS-over-QUIC</0>",
|
||||
"example_upstream_tcp": "සාමාන්ය DNS (TCP හරහා)",
|
||||
"example_upstream_tcp": "සාමාන්ය ව.නා.ප. (TCP/ස.පා.කෙ. හරහා) ",
|
||||
"all_lists_up_to_date_toast": "සියලුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි",
|
||||
"dns_test_ok_toast": "සඳහන් කළ ව.නා.ප. සේවාදායකයන් නිවැරදිව ක්රියා කරයි",
|
||||
"dns_test_not_ok_toast": "සේවාදායක \"{{key}}\": භාවිතා කළ නොහැකි විය, කරුණාකර ඔබ එය නිවැරදිව ලියා ඇත්දැයි පරීක්ෂා කරන්න",
|
||||
@@ -227,22 +227,21 @@
|
||||
"source_label": "මූලාශ්රය",
|
||||
"found_in_known_domain_db": "දැනුවත් වසම් දත්ත ගබඩාවේ හමු විය.",
|
||||
"category_label": "ප්රවර්ගය",
|
||||
"rule_label": "නීතිය",
|
||||
"list_label": "ලැයිස්තුව",
|
||||
"unknown_filter": "{{filterId}} නොදන්නා පෙරහනකි",
|
||||
"known_tracker": "දැනුවත් ලුහුබැඳීමක්",
|
||||
"install_welcome_title": "ඇඩ්ගාර්ඩ් හෝම් වෙත සාදරයෙන් පිළිගනිමු!",
|
||||
"install_welcome_desc": "ඇඩ්ගාර්ඩ් හෝම් යනු ජාලය පුරා ඇති දැන්වීම් සහ ලුහුබැඳීම අවහිර කරන ව.නා.ප. සේවාදායකි. ඔබගේ මුළු ජාලය සහ සියලුම උපාංග පාලනය කිරීමට ඉඩ සලසා දීම එහි පරමාර්ථය යි, එයට අනුග්රාහක පාර්ශවීය වැඩසටහනක් භාවිතා කිරීම අවශ්ය නොවේ.",
|
||||
"install_settings_title": "පරිපාලක වෙබ් අතුරු මුහුණත",
|
||||
"install_settings_title": "පරිපාලක වියමන අතුරු මුහුණත",
|
||||
"install_settings_listen": "සවන් දෙන අතුරු මුහුණත",
|
||||
"install_settings_port": "කවුළුව",
|
||||
"install_settings_interface_link": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වෙබ් අතුරු මුහුණත පහත ලිපිනයන්ගෙන් ප්රවේශ විය හැකිය:",
|
||||
"install_settings_interface_link": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වියමන අතුරු මුහුණත පහත ලිපිනයන්ගෙන් ප්රවේශ විය හැකිය:",
|
||||
"form_error_port": "වලංගු කවුළුවක අගයක් ඇතුළත් කරන්න",
|
||||
"install_settings_dns": "ව.නා.ප. සේවාදායකය",
|
||||
"install_settings_dns_desc": "පහත ලිපිනයන්හි ව.නා.ප. සේවාදායකය භාවිතා කිරීම සඳහා ඔබගේ උපාංග හෝ මාර්ගකාරකය වින්යාසගත කිරීමට අවශ්ය වනු ඇත:",
|
||||
"install_settings_all_interfaces": "සියලුම අතුරුමුහුණත්",
|
||||
"install_auth_title": "සත්යාපනය",
|
||||
"install_auth_desc": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වෙබ් අතුරු මුහුණතට මුරපද සත්යාපනය වින්යාසගත කිරීම අතිශයින් නිර්දේශ කෙරේ. එය ඔබගේ ස්ථානීය ජාලයෙන් පමණක් ප්රවේශ විය හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්රවේශයකින් ආරක්ෂා කර ගැනීම වැදගත් ය.",
|
||||
"install_auth_desc": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වියමන අතුරු මුහුණතට මුරපද සත්යාපනය වින්යාසගත කිරීම අතිශයින් නිර්දේශ කෙරේ. එය ඔබගේ ස්ථානීය ජාලයෙන් පමණක් ප්රවේශ විය හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්රවේශයකින් ආරක්ෂා කර ගැනීම වැදගත් ය.",
|
||||
"install_auth_username": "පරිශීලක නාමය",
|
||||
"install_auth_password": "මුරපදය",
|
||||
"install_auth_confirm": "මුරපදය තහවුරු කරන්න",
|
||||
@@ -284,7 +283,7 @@
|
||||
"open_dashboard": "උපකරණ පුවරුව විවෘත කරන්න",
|
||||
"install_saved": "සාර්ථකව සුරකින ලදි",
|
||||
"encryption_title": "සංකේතාංකනය",
|
||||
"encryption_desc": "ගුප්තකේතනය (HTTPS/TLS) සඳහා ව.නා.ප. සහ පරිපාලක වෙබ් අතුරු මුහුණත සහය දක්වයි",
|
||||
"encryption_desc": "ගුප්තකේතනය (HTTPS/TLS) සඳහා ව.නා.ප. සහ පරිපාලක වියමන අතුරු මුහුණත සහය දක්වයි",
|
||||
"encryption_config_saved": "සංකේතාංකන වින්යාසය සුරකින ලදි",
|
||||
"encryption_server": "සේවාදායකයේ නම",
|
||||
"encryption_server_enter": "ඔබගේ වසම් නාමය ඇතුළත් කරන්න",
|
||||
@@ -340,7 +339,6 @@
|
||||
"client_edit": "අනුග්රාහකය සංස්කරණය කරන්න",
|
||||
"client_identifier": "හඳුන්වනය",
|
||||
"ip_address": "අ.ජා. කෙ. (IP) ලිපිනය",
|
||||
"client_identifier_desc": "අනුග්රාහකයන් අ.ජා. කෙ. (IP) ලිපිනයක් හෝ මා.ප්ර.පා. (MAC) ලිපිනයක් මගින් හඳුනාගත හැකිය. මා.ප්ර.පා. හඳුන්වනයක් ලෙස භාවිතා කළ හැක්කේ ඇඩ්ගාර්ඩ් හෝම් ද <0>DHCP සේවාදායකයක්</0> නම් පමණක් බව කරුණාවෙන් සලකන්න. ",
|
||||
"form_enter_ip": "අ.ජා. කෙ. (IP) ඇතුළත් කරන්න",
|
||||
"form_enter_mac": "මා.ප්ර.පා. (MAC) ඇතුළත් කරන්න",
|
||||
"form_enter_id": "හඳුන්වනය ඇතුළත් කරන්න",
|
||||
@@ -462,7 +460,6 @@
|
||||
"check_ip": "අ.ජා. කෙ. (IP) ලිපින: {{ip}}",
|
||||
"check_cname": "අන්. නාමය (CNAME): {{cname}}",
|
||||
"check_reason": "හේතුව: {{reason}}",
|
||||
"check_rule": "නීතිය: {{rule}}",
|
||||
"check_service": "සේවාවෙහි නම: {{service}}",
|
||||
"service_name": "සේවාවේ නම",
|
||||
"check_not_found": "ඔබගේ පෙරහන් ලැයිස්තු තුළ සොයා ගත නොහැක",
|
||||
@@ -483,7 +480,7 @@
|
||||
"show_blocked_responses": "අවහිර කර ඇත",
|
||||
"show_whitelisted_responses": "සුදු ලැයිස්තුගත කර ඇත",
|
||||
"blocked_safebrowsing": "ආරක්ෂිත සෙවීම මගින් අවහිර කරන ලද",
|
||||
"blocked_adult_websites": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි",
|
||||
"blocked_adult_websites": "අවහිර කළ වැඩිහිටි වියමන අඩවි",
|
||||
"blocked_threats": "අවහිර කළ තර්ජන",
|
||||
"allowed": "අවසර ලත්",
|
||||
"filtered": "පෙරහන් කරන ලද",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Nesprávny formát IPv4",
|
||||
"form_error_mac_format": "Nesprávny MAC formát",
|
||||
"form_error_client_id_format": "Neplatný formát client ID",
|
||||
"form_error_server_name": "Neplatné meno servera",
|
||||
"form_error_positive": "Musí byť väčšie ako 0",
|
||||
"form_error_negative": "Musí byť číslo 0 alebo viac",
|
||||
"range_end_error": "Musí byť väčšie ako začiatok rozsahu",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Vlastná IP adresa",
|
||||
"blocking_ipv4": "Blokovanie IPv4",
|
||||
"blocking_ipv6": "Blokovanie IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "ID klienta",
|
||||
"client_id_placeholder": "Zadať ID klienta",
|
||||
"client_id_desc": "Rôznych klientov možno identifikovať podľa špeciálneho ID klienta. <a>Tu</a> sa dozviete viac o tom, ako identifikovať klientov.",
|
||||
"download_mobileconfig_doh": "Prevziať .mobileconfig pre DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Prevziať .mobileconfig pre DNS-over-TLS",
|
||||
"download_mobileconfig": "Stiahnuť konfiguračný súbor",
|
||||
"plain_dns": "Obyčajné DNS",
|
||||
"form_enter_rate_limit": "Zadajte rýchlostný limit",
|
||||
"rate_limit": "Rýchlostný limit",
|
||||
@@ -269,7 +276,6 @@
|
||||
"source_label": "Zdroj",
|
||||
"found_in_known_domain_db": "Nájdené v databáze známych domén.",
|
||||
"category_label": "Kategória",
|
||||
"rule_label": "Pravidlo",
|
||||
"list_label": "Zoznam",
|
||||
"unknown_filter": "Neznámy filter {{filterId}}",
|
||||
"known_tracker": "Známy sledovač",
|
||||
@@ -330,7 +336,6 @@
|
||||
"encryption_config_saved": "Konfigurácia šifrovania uložená",
|
||||
"encryption_server": "Meno servera",
|
||||
"encryption_server_enter": "Zadajte meno Vašej domény",
|
||||
"encryption_server_desc": "Ak chcete používať HTTPS, musíte zadať meno servera, ktoré zodpovedá Vášmu SSL certifikátu.",
|
||||
"encryption_redirect": "Automaticky presmerovať na HTTPS",
|
||||
"encryption_redirect_desc": "Ak je táto možnosť začiarknutá, služba AdGuard Home Vás automaticky presmeruje z adresy HTTP na adresy HTTPS.",
|
||||
"encryption_https": "HTTPS port",
|
||||
@@ -386,7 +391,6 @@
|
||||
"client_edit": "Upraviť klienta",
|
||||
"client_identifier": "Identifikátor",
|
||||
"ip_address": "IP adresa",
|
||||
"client_identifier_desc": "Klienti môžu byť identifikovaní podľa IP adresy, CIDR alebo MAC adresy. Upozorňujeme, že používanie MAC ako identifikátora je možné len vtedy, ak je AdGuard Home tiež <0>DHCP server</0>",
|
||||
"form_enter_ip": "Zadajte IP adresu",
|
||||
"form_enter_mac": "Zadajte MAC adresu",
|
||||
"form_enter_id": "Zadajte identifikátor",
|
||||
@@ -430,6 +434,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podporuje <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podporuje <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Viac implementácií nájdete <0>tu</0> a <1>tu</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Konfigurácia iOS a macOS",
|
||||
"setup_dns_notice": "Pre použitie <1>DNS-over-HTTPS</1> alebo <1>DNS-over-TLS</1>, potrebujete v nastaveniach AdGuard Home <0>nakonfigurovať šifrovanie</0>.",
|
||||
"rewrite_added": "DNS prepísanie pre \"{{key}}\" bolo úspešne pridané",
|
||||
"rewrite_deleted": "DNS prepísanie pre \"{{key}}\" bolo úspešne vymazané",
|
||||
@@ -529,7 +534,6 @@
|
||||
"check_ip": "IP adresy: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Dôvod: {{reason}}",
|
||||
"check_rule": "Pravidlo: {{rule}}",
|
||||
"check_service": "Meno služby: {{service}}",
|
||||
"service_name": "Názov služby",
|
||||
"check_not_found": "Nenašlo sa vo Vašom zozname filtrov",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Neveljaven format IP",
|
||||
"form_error_mac_format": "Neveljaven MAC format",
|
||||
"form_error_client_id_format": "Neveljaven format ID odjemalca",
|
||||
"form_error_server_name": "Neveljavno ime strežnika",
|
||||
"form_error_positive": "Mora biti večja od 0",
|
||||
"form_error_negative": "Mora biti enako ali več kot 0",
|
||||
"range_end_error": "Mora biti večji od začtka razpona",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "IP po meri",
|
||||
"blocking_ipv4": "Onemogočanje IPv4",
|
||||
"blocking_ipv6": "Onemogočanje IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-prek-HTTPS",
|
||||
"dns_over_tls": "DNS-prek-TLS",
|
||||
"dns_over_quic": "DNS-prek-QIUC",
|
||||
"client_id": "ID odjemalca",
|
||||
"client_id_placeholder": "Vnesite ID odjemalca",
|
||||
"client_id_desc": "Različne odjemalce je mogoče prepoznati s posebnim ID-jem odjemalca. <a>Tukaj</a> lahko izveste več o prepoznavanju odjemalcev.",
|
||||
"download_mobileconfig_doh": "Prenos .mobileconfig za DNS-preko-HTTPS",
|
||||
"download_mobileconfig_dot": "Prenos .mobileconfig za DNS-preko-TLS",
|
||||
"download_mobileconfig": "Prenesi nastavitveno datoteko",
|
||||
"plain_dns": "Navadni DNS",
|
||||
"form_enter_rate_limit": "Vnesite omejitev hitrosti",
|
||||
"rate_limit": "Omejitev hitrosti",
|
||||
@@ -269,7 +276,7 @@
|
||||
"source_label": "Vir",
|
||||
"found_in_known_domain_db": "Najdeno v zbirki podatkov znanih domen.",
|
||||
"category_label": "Kategorija",
|
||||
"rule_label": "Pravilo",
|
||||
"rule_label": "Pravila",
|
||||
"list_label": "Seznam",
|
||||
"unknown_filter": "Neznan filter {{filterId}}",
|
||||
"known_tracker": "Znan sledilec",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "Uredi odjemalca",
|
||||
"client_identifier": "Identifikator",
|
||||
"ip_address": "IP naslov",
|
||||
"client_identifier_desc": "Odjemalce je mogoče identificirati po naslovu IP, CIDR, MAC naslovu. Upoštevajte, da je uporaba MAC kot identifikatorja mogoča le, če je AdGuard Home tudi <0>strežnik DHCP</0>",
|
||||
"client_identifier_desc": "Odjemalce je mogoče prepoznati po naslovu IP, CIDR, naslovu MAC ali posebnem ID-ju odjemalca (lahko se uporablja za DoT/DoH/DoQ). <0>Tukaj</0> lahko izveste več o prepoznavanju odjemalcev.",
|
||||
"form_enter_ip": "Vnesite IP",
|
||||
"form_enter_mac": "Vnesite MAC",
|
||||
"form_enter_id": "Vnesi identifikatorja",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podpira <1>DNS-prek-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podpira <1>DNS-prek-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Našli boste več izvedb <0>tukaj</0> in <1>tukaj</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Nastavitve iOS in macOS",
|
||||
"setup_dns_notice": "Za uporabo <1>DNS-prek-HTTPS</1> ali <1>DNS-prek-TLS</1>, morate <0>konfigurirati šifriranje</0> v nastavitvah AdGuard Home.",
|
||||
"rewrite_added": "Uspešno je dodano DNS prepisovanje za \"{{key}}\"",
|
||||
"rewrite_deleted": "Uspešno je izbrisano DNS prepisovanje za \"{{key}}\"",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP naslovi: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Razlog: {{reason}}",
|
||||
"check_rule": "Pravilo: {{rule}}",
|
||||
"check_service": "Ime storitve: {{service}}",
|
||||
"service_name": "Ime storitve",
|
||||
"check_not_found": "Ni najdeno na vašem seznamu filtrov",
|
||||
|
||||
@@ -269,7 +269,6 @@
|
||||
"source_label": "Izvor",
|
||||
"found_in_known_domain_db": "Pronađeno u poznatim bazama podataka domena.",
|
||||
"category_label": "Kategorija",
|
||||
"rule_label": "Pravilo",
|
||||
"list_label": "Lista",
|
||||
"unknown_filter": "Nepoznat filter {{filterId}}",
|
||||
"known_tracker": "Poznato praćenje",
|
||||
@@ -330,7 +329,6 @@
|
||||
"encryption_config_saved": "Konfiguracija šifrovanja je sačuvana",
|
||||
"encryption_server": "Ime servera",
|
||||
"encryption_server_enter": "Unesite vaše ime domena",
|
||||
"encryption_server_desc": "Kako biste koristili HTTPS, potrebno je da unesete ime servera koje se podudara sa SSL sertifikatom.",
|
||||
"encryption_redirect": "Automatski preusmeri na HTTPS",
|
||||
"encryption_redirect_desc": "Ako je označeno, AdGuard Home će vas automatski preusmeravati sa HTTP na HTTPS adrese.",
|
||||
"encryption_https": "HTTPS port",
|
||||
@@ -386,7 +384,6 @@
|
||||
"client_edit": "Izmeni klijent",
|
||||
"client_identifier": "Identifikator",
|
||||
"ip_address": "IP adresa",
|
||||
"client_identifier_desc": "Klijenti mogu da budu prepoznati po IP adresi ili MAC adresi. Imajte na umu da je korišćenje MAC adrese kao identifikatora moguće samo ako je AdGuard Home takođe a <0>DHCP server</0>",
|
||||
"form_enter_ip": "Unesite IP",
|
||||
"form_enter_mac": "Unesite MAC",
|
||||
"form_enter_id": "Unesite identifikator",
|
||||
@@ -529,7 +526,6 @@
|
||||
"check_ip": "IP adrese: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Razlog: {{reason}}",
|
||||
"check_rule": "Pravilo: {{rule}}",
|
||||
"check_service": "Ime usluge: {{service}}",
|
||||
"service_name": "Ime usluge",
|
||||
"check_not_found": "Nije pronađeno na vašoj listi filtera",
|
||||
|
||||
@@ -177,7 +177,6 @@
|
||||
"source_label": "Källa",
|
||||
"found_in_known_domain_db": "Hittad i domändatabas.",
|
||||
"category_label": "Kategori",
|
||||
"rule_label": "Regel",
|
||||
"unknown_filter": "Okänt filter {{filterId}}",
|
||||
"install_welcome_title": "Välkommen till AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home är en DNS-server för nätverkstäckande annons- och spårningsblockering. Dess syfte är att de dig kontroll över hela nätverket och alla dina enheter, utan behov av att använda klientbaserade program.",
|
||||
@@ -235,7 +234,6 @@
|
||||
"encryption_config_saved": "Krypteringsinställningar sparade",
|
||||
"encryption_server": "Servernamn",
|
||||
"encryption_server_enter": "Skriv in ditt domännamn",
|
||||
"encryption_server_desc": "För att använda HTTPS behöver du skriva in servernamnet som stämmer överens med ditt SSL-certifikat.",
|
||||
"encryption_redirect": "Omdirigera till HTTPS automatiskt",
|
||||
"encryption_redirect_desc": "Om bockad kommer AdGuard Home automatiskt att omdirigera dig från HTTP till HTTPS-adresser.",
|
||||
"encryption_https": "HTTPS-port",
|
||||
@@ -287,7 +285,6 @@
|
||||
"client_edit": "Redigera klient",
|
||||
"client_identifier": "Identifikator",
|
||||
"ip_address": "IP-adress",
|
||||
"client_identifier_desc": "Klienter kan identifieras genom IP-adresser eller MAC-adresser. Notera att användning av MAC som identifierare bara är möjligt om AdGuard Home också är en <0>DHCP-server</0>",
|
||||
"form_enter_ip": "Skriv in IP",
|
||||
"form_enter_mac": "Skriv in MAC",
|
||||
"form_client_name": "Skriv in klientnamn",
|
||||
|
||||
@@ -200,7 +200,6 @@
|
||||
"source_label": "ที่มา",
|
||||
"found_in_known_domain_db": "พบในฐานข้อมูลโดเมนที่รู้จัก",
|
||||
"category_label": "ประเภท",
|
||||
"rule_label": "กฎ",
|
||||
"unknown_filter": "ตัวกรองที่ไม่รู้จัก {{filterId}}",
|
||||
"install_welcome_title": "ยินดีต้อนรับสู่ AdGuard Home",
|
||||
"install_welcome_desc": "AdGuard Home เป็นเซิร์ฟเวอร์ DNS ปิดกั้นโฆษณาและติดตามทั่วทั้งเครือข่าย วัตถุประสงค์คือเพื่อให้คุณควบคุมเครือข่ายทั้งหมดและอุปกรณ์ทั้งหมดของคุณและไม่จำเป็นต้องใช้โปรแกรมฝั่งไคลเอ็นต์",
|
||||
@@ -258,7 +257,6 @@
|
||||
"encryption_config_saved": "บันทึกการตั้งค่าเข้ารหัสเรียบร้อยแล้ว",
|
||||
"encryption_server": "ชื่อเซิร์ฟเวอร์",
|
||||
"encryption_server_enter": "ป้อนชื่อโดเมน",
|
||||
"encryption_server_desc": "ในการใช้ HTTPS คุณต้องป้อนชื่อเซิร์ฟเวอร์ที่ตรงกับใบรับรอง SSL ของคุณ",
|
||||
"encryption_redirect": "ไปเส้นทาง HTTPS อัตโนมัติ",
|
||||
"encryption_redirect_desc": "หากเลือกตัวเลือกนี้ AdGuard Home จะเปลี่ยนเส้นทางคุณจากที่อยู่ HTTP ไปยัง HTTPS โดยอัตโนมัติ",
|
||||
"encryption_https": "พอร์ท HTTPS",
|
||||
@@ -312,7 +310,6 @@
|
||||
"client_edit": "แก้ไขเครื่องลูกข่าย",
|
||||
"client_identifier": "ตรวจสอบโดย",
|
||||
"ip_address": "IP addresses",
|
||||
"client_identifier_desc": "ลูกค้าสามารถระบุได้โดยที่อยู่ IP, CIDR, ที่อยู่ MAC โปรดทราบว่าการใช้ MAC เป็นตัวระบุเป็นไปได้ก็ต่อเมื่อ AdGuard Home เป็น <0>เซิร์ฟเวอร์ DHCP</0> ด้วย",
|
||||
"form_enter_ip": "กรอก IP",
|
||||
"form_enter_mac": "กรอก MAC",
|
||||
"form_enter_id": "ป้อนตัวระบุ",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Geçersiz IP biçimi",
|
||||
"form_error_mac_format": "Geçersiz MAC biçimi",
|
||||
"form_error_client_id_format": "Geçersiz istemci kimliği biçimi",
|
||||
"form_error_server_name": "Geçersiz sunucu adı",
|
||||
"form_error_positive": "0'dan büyük olmalı",
|
||||
"form_error_negative": "0 veya daha büyük olmalıdır",
|
||||
"range_end_error": "Başlangıç aralığından daha büyük olmalı",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "Özel IP",
|
||||
"blocking_ipv4": "IPv4 engelleme",
|
||||
"blocking_ipv6": "IPv6 engelleme",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "İstemci Kimliği",
|
||||
"client_id_placeholder": "İstemci Kimliği girin",
|
||||
"client_id_desc": "Farklı istemciler, özel bir istemci kimliği ile tanımlanabilir. <a>Burada</a> istemcileri nasıl belirleyeceğiniz hakkında daha fazla bilgi edinebilirsiniz.",
|
||||
"download_mobileconfig_doh": "DNS-over-HTTPS için .mobileconfig dosyasını indir",
|
||||
"download_mobileconfig_dot": "DNS-over-TLS için .mobileconfig dosyasını indir",
|
||||
"download_mobileconfig": "Yapılandırma dosyasını indir",
|
||||
"plain_dns": "Sade DNS",
|
||||
"form_enter_rate_limit": "Sıklık limitini girin",
|
||||
"rate_limit": "Sıklık limiti",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "Şifreleme ayarı kaydedildi",
|
||||
"encryption_server": "Sunucu adı",
|
||||
"encryption_server_enter": "Alan adınızı girin",
|
||||
"encryption_server_desc": "HTTPS kullanmak için SSL sertifikanızla eşleşen sunucu adını girmeniz gerekir",
|
||||
"encryption_server_desc": "HTTPS kullanmak için, SSL sertifikanız veya joker karakter sertifikanızla eşleşen sunucu adını girmeniz gerekir. Alan ayarlanmazsa, herhangi bir alan adı için TKG bağlantılarını kabul eder.",
|
||||
"encryption_redirect": "Otomatik olarak HTTPS'e yönlendir",
|
||||
"encryption_redirect_desc": "Etkinleştirirseniz AdGuard Home sizi HTTP yerine HTTPS adreslerine yönlendirir.",
|
||||
"encryption_https": "HTTPS bağlantı noktası",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "İstemciyi düzenle",
|
||||
"client_identifier": "Tanımlayıcı",
|
||||
"ip_address": "IP adresi",
|
||||
"client_identifier_desc": "İstemciler IP adresleri veya MAC adresleri ile tanımlanabilir. Lütfen not edin, MAC adresi ile tanımlamayı kullanmak için AdGuard Home'un <0>DHCP Sunucusu</0> olması gerekir.",
|
||||
"client_identifier_desc": "İstemciler IP adresi, CIDR, MAC adresi veya özel bir istemci kimliği ile tanımlanabilir (DoT/DoH/DoQ için kullanılabilir). <0>Burada</0> istemcileri nasıl belirleyeceğiniz hakkında daha fazla bilgi edinebilirsiniz.",
|
||||
"form_enter_ip": "IP Girin",
|
||||
"form_enter_mac": "MAC Girin",
|
||||
"form_enter_id": "Tanımlayıcı girin",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0>, <1>DNS-over-HTTPS</1> destekler.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0>, <1>DNS-over-HTTPS</1> desteklemektedir.",
|
||||
"setup_dns_privacy_other_5": "<0>Burada</0> ve <1>burada</1> daha fazla uygulama bulacaksınız.",
|
||||
"setup_dns_privacy_ioc_mac": "iOS ve macOS yapılandırması",
|
||||
"setup_dns_notice": "<1>DNS-over-HTTPS</1> veya <1>DNS-over-TLS</1> kullanmak için, AdGuard Home ayarlarında <0>Şifreleme yapılandırmasını</0> yapmanız gerekir.",
|
||||
"rewrite_added": "\"{{key}}\" için DNS yeniden yazımı başarıyla eklendi",
|
||||
"rewrite_deleted": "\"{{key}}\" için DNS yeniden yazımı başarıyla silindi",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP adresleri: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Sebep: {{reason}}",
|
||||
"check_rule": "Kural: {{rule}}",
|
||||
"check_service": "Hizmet adı: {{service}}",
|
||||
"service_name": "Servis adı",
|
||||
"check_not_found": "Filtre listelerinizde bulunamadı",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Định dạng IPv4 không hợp lệ",
|
||||
"form_error_mac_format": "Định dạng MAC không hợp lệ",
|
||||
"form_error_client_id_format": "Định dạng client ID không hợp lệ",
|
||||
"form_error_server_name": "Tên máy chủ không hợp lệ",
|
||||
"form_error_positive": "Phải lớn hơn 0",
|
||||
"form_error_negative": "Phải lớn hơn hoặc bằng 0",
|
||||
"range_end_error": "Phải lớn hơn khoảng bắt đầu",
|
||||
@@ -133,6 +134,7 @@
|
||||
"encryption_settings": "Cài đặt mã hóa",
|
||||
"dhcp_settings": "Cài đặt DHCP",
|
||||
"upstream_dns": "Máy chủ DNS tìm kiếm",
|
||||
"upstream_dns_help": "Nhập địa chỉ máy chủ một trên mỗi dòng. <a>Tìm hiểu thêm</a> về cách định cấu hình máy chủ DNS ngược dòng.",
|
||||
"upstream_dns_configured_in_file": "Cấu hình tại {{path}}",
|
||||
"test_upstream_btn": "Kiểm tra",
|
||||
"upstreams": "Nguồn",
|
||||
@@ -197,6 +199,9 @@
|
||||
"unblock": "Bỏ chặn",
|
||||
"block": "Chặn",
|
||||
"disallow_this_client": "Không cho phép client này",
|
||||
"allow_this_client": "Cho phép ứng dụng khách này",
|
||||
"block_for_this_client_only": "Chỉ chặn ứng dụng khách này",
|
||||
"unblock_for_this_client_only": "Chỉ hủy chặn ứng dụng khách này",
|
||||
"time_table_header": "Thời gian",
|
||||
"date": "Ngày",
|
||||
"domain_name_table_header": "Tên miền",
|
||||
@@ -243,8 +248,16 @@
|
||||
"custom_ip": "IP tuỳ chỉnh",
|
||||
"blocking_ipv4": "Chặn IPv4",
|
||||
"blocking_ipv6": "Chặn IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "ID khách hàng",
|
||||
"client_id_placeholder": "Nhập ID khách hàng",
|
||||
"client_id_desc": "Các khách hàng khác nhau có thể được xác định bằng một ID khách hàng đặc biệt. <a>Tại đây</a> bạn có thể tìm hiểu thêm về cách xác định khách hàng.",
|
||||
"download_mobileconfig_doh": "Tải xuống .mobileconfig cho DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Tải xuống .mobileconfig cho DNS-over-TLS",
|
||||
"download_mobileconfig": "Tải xuống tệp cấu hình",
|
||||
"plain_dns": "DNS thuần",
|
||||
"form_enter_rate_limit": "Nhập giới hạn yêu cầu",
|
||||
"rate_limit": "Giới hạn yêu cầu",
|
||||
@@ -254,6 +267,7 @@
|
||||
"blocking_ipv4_desc": "Địa chỉ IP được trả lại cho một yêu cầu A bị chặn",
|
||||
"blocking_ipv6_desc": "Địa chỉ IP được trả lại cho một yêu cầu AAA bị chặn",
|
||||
"blocking_mode_default": "Mặc định: Trả lời với NXDOMAIN khi bị chặn bởi quy tắc kiểu Adblock; phản hồi với địa chỉ IP được chỉ định trong quy tắc khi bị chặn bởi quy tắc / etc / hosts-style",
|
||||
"blocking_mode_refused": "REFUSED: Trả lời bằng mã REFUSED",
|
||||
"blocking_mode_nxdomain": "NXDOMAIN: Phản hổi với mã NXDOMAIN",
|
||||
"blocking_mode_null_ip": "Null IP: Trả lời bằng không địa chỉ IP (0.0.0.0 cho A; :: cho AAAA)",
|
||||
"blocking_mode_custom_ip": "IP tùy chỉnh: Phản hồi với địa chỉ IP đã được tiết lập",
|
||||
@@ -262,7 +276,6 @@
|
||||
"source_label": "Nguồn",
|
||||
"found_in_known_domain_db": "Tìm thấy trong cơ sở dữ liệu tên miền",
|
||||
"category_label": "Thể loại",
|
||||
"rule_label": "Quy tắc",
|
||||
"list_label": "Danh sách",
|
||||
"unknown_filter": "Bộ lọc không rõ {{filterId}}",
|
||||
"known_tracker": "Theo dõi đã biết",
|
||||
@@ -323,13 +336,14 @@
|
||||
"encryption_config_saved": "Đã lưu cấu hình mã hóa",
|
||||
"encryption_server": "Tên máy chủ",
|
||||
"encryption_server_enter": "Nhập tên miền của bạn",
|
||||
"encryption_server_desc": "Để sử dụng HTTPS, bạn cần nhập tên máy chủ phù hợp với chứng chỉ SSL của bạn.",
|
||||
"encryption_redirect": "Tự động chuyển hướng đến HTTPS",
|
||||
"encryption_redirect_desc": "Nếu được chọn, AdGuard Home sẽ tự động chuyển hướng bạn từ địa chỉ HTTP sang địa chỉ HTTPS.",
|
||||
"encryption_https": "Cổng HTTPS",
|
||||
"encryption_https_desc": "Nếu cổng HTTPS được định cấu hình, giao diện quản trị viên AdGuard Home sẽ có thể truy cập thông qua HTTPS và nó cũng sẽ cung cấp DNS-over-HTTPS trên vị trí '/dns-query'.",
|
||||
"encryption_dot": "Cổng DNS-over-TLS",
|
||||
"encryption_dot_desc": "Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS-over-TLS trên cổng này.",
|
||||
"encryption_doq": "Cổng DNS-over-QUIC",
|
||||
"encryption_doq_desc": "Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS qua QUIC trên cổng này. Đó là thử nghiệm và có thể không đáng tin cậy. Ngoài ra, không có quá nhiều khách hàng hỗ trợ nó vào lúc này.",
|
||||
"encryption_certificates": "Chứng chỉ",
|
||||
"encryption_certificates_desc": "Để sử dụng mã hóa, bạn cần cung cấp chuỗi chứng chỉ SSL hợp lệ cho miền của mình. Bạn có thể nhận chứng chỉ miễn phí trên <0>{{link}}</0> hoặc bạn có thể mua chứng chỉ từ một trong các Cơ Quan Chứng Nhận tin cậy.",
|
||||
"encryption_certificates_input": "Sao chép/dán chứng chỉ được mã hóa PEM của bạn tại đây.",
|
||||
@@ -377,7 +391,6 @@
|
||||
"client_edit": "Chỉnh Sửa Máy Khách",
|
||||
"client_identifier": "Định danh",
|
||||
"ip_address": "Địa chỉ IP",
|
||||
"client_identifier_desc": "Các máy khách có thể được xác định bằng địa chỉ IP hoặc địa chỉ MAC. Xin lưu ý rằng chỉ có thể sử dụng MAC làm định danh nếu AdGuard Home cũng là <0>máy chủ DHCP</0>",
|
||||
"form_enter_ip": "Nhập IP",
|
||||
"form_enter_mac": "Nhập MAC",
|
||||
"form_enter_id": "Nhập định danh",
|
||||
@@ -408,6 +421,7 @@
|
||||
"dns_privacy": "DNS Riêng Tư",
|
||||
"setup_dns_privacy_1": "<0>DNS-over-TLS:</0> Sử dụng chuỗi <1>{{address}}</1>.",
|
||||
"setup_dns_privacy_2": "<0>DNS-over-HTTPS:</0> Sử dụng chuỗi <1>{{address}}</1>.",
|
||||
"setup_dns_privacy_3": "<0>Đây là danh sách phần mềm bạn có thể sử dụng.</0>",
|
||||
"setup_dns_privacy_4": "Trên thiết bị chạy iOS 14 hoặc macOS Big Sur bạn có thể tải tệp '.mobileconfig' đặc biệt có chứa máy chủ <highlight>DNS-over-HTTPS</highlight> hoặc <highlight>DNS-over-TLS</highlight> trong thiết lập DNS.",
|
||||
"setup_dns_privacy_android_1": "Android 9 hỗ trợ DNS trên TLS nguyên bản. Để định cấu hình, hãy đi tới Cài đặt → Mạng & internet → Nâng cao → DNS Riêng Tư và nhập tên miền của bạn vào đó.",
|
||||
"setup_dns_privacy_android_2": "<0>AdGuard for Android</0> hỗ trợ <1>DNS-over-HTTPS</1> và <1>DNS-over-TLS</1>.",
|
||||
@@ -420,6 +434,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> hỗ trợ <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> hỗ trợ <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Bạn sẽ tìm thấy nhiều triển khai hơn <0>tại đây</0> và <1>tại đây</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "Cấu hình iOS và macOS",
|
||||
"setup_dns_notice": "Để sử dụng <1>DNS-over-HTTPS</1> hoặc <1>DNS-over-TLS</1>, bạn cần <0>định cấu hình Mã hóa</0> trong cài đặt AdGuard Home.",
|
||||
"rewrite_added": "DNS viết lại cho \"{{key}}\" đã thêm thành công",
|
||||
"rewrite_deleted": "DNS viết lại cho \"{{key}}\" đã xóa thành công",
|
||||
@@ -519,8 +534,8 @@
|
||||
"check_ip": "Địa chỉ IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Lý do: {{reason}}",
|
||||
"check_rule": "Quy tắc: {{rule}}",
|
||||
"check_service": "Tên dịch vụ: {{service}}",
|
||||
"service_name": "Tên dịch vụ",
|
||||
"check_not_found": "Không tìm thấy trong danh sách bộ lọc của bạn",
|
||||
"client_confirm_block": "Bạn có muốn chặn người dùng {{ip}}?",
|
||||
"client_confirm_unblock": "Bạn có muốn bỏ chặn người dùng {{ip}}?",
|
||||
@@ -555,6 +570,12 @@
|
||||
"cache_size_desc": "Kích thước cache DNS (bytes)",
|
||||
"cache_ttl_min_override": "Ghi đè TTL tối thiểu",
|
||||
"cache_ttl_max_override": "Ghi đè TTL tối đa",
|
||||
"enter_cache_size": "Nhập kích thước bộ nhớ cache (byte)",
|
||||
"enter_cache_ttl_min_override": "Nhập TTL tối thiểu (giây)",
|
||||
"enter_cache_ttl_max_override": "Nhập TTL tối đa (giây)",
|
||||
"cache_ttl_min_override_desc": "Mở rộng giá trị thời gian tồn tại ngắn (giây) nhận được từ máy chủ ngược dòng khi phản hồi DNS vào bộ nhớ đệm",
|
||||
"cache_ttl_max_override_desc": "Đặt giá trị thời gian tồn tại tối đa (giây) cho các mục nhập trong bộ nhớ cache DNS",
|
||||
"ttl_cache_validation": "Giá trị TTL trong bộ nhớ cache tối thiểu phải nhỏ hơn hoặc bằng giá trị lớn nhất",
|
||||
"filter_category_general": "Chung",
|
||||
"filter_category_security": "Bảo mật",
|
||||
"filter_category_regional": "Khu vực",
|
||||
@@ -566,5 +587,8 @@
|
||||
"setup_config_to_enable_dhcp_server": "Thiết lập cấu hình để bật máy chủ DHCP",
|
||||
"original_response": "Phản hồi gốc",
|
||||
"click_to_view_queries": "Nhấp để xem truy xuất",
|
||||
"port_53_faq_link": "Cổng 53 thường được sử dụng \"DNSStubListener\" hoặc \"systemd-resolved\". Vui lòng đọc <0>hướng dẫn</0> để giải quyết vấn đề này."
|
||||
"port_53_faq_link": "Cổng 53 thường được sử dụng \"DNSStubListener\" hoặc \"systemd-resolved\". Vui lòng đọc <0>hướng dẫn</0> để giải quyết vấn đề này.",
|
||||
"adg_will_drop_dns_queries": "AdGuard Home sẽ loại bỏ tất cả các truy vấn DNS từ ứng dụng khách này.",
|
||||
"client_not_in_allowed_clients": "Ứng dụng khách không được phép vì nó không có trong danh sách \"Ứng dụng khách được phép\".",
|
||||
"experimental": "Thử nghiệm"
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "无效的 IPv4 格式",
|
||||
"form_error_mac_format": "无效的 MAC 格式",
|
||||
"form_error_client_id_format": "无效的客户端 ID 格式",
|
||||
"form_error_server_name": "无效的服务器名",
|
||||
"form_error_positive": "必须大于 0",
|
||||
"form_error_negative": "必须大于等于 0",
|
||||
"range_end_error": "必须大于范围起始值",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "自定义 IP",
|
||||
"blocking_ipv4": "拦截 IPv4",
|
||||
"blocking_ipv6": "拦截 IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "客户端 ID",
|
||||
"client_id_placeholder": "输入客户端 ID",
|
||||
"client_id_desc": "可根据一个特殊的客户端 ID 识别不同客户端。在 <a>这里</a>你可以了解到更多关于如何识别客户端的信息。",
|
||||
"download_mobileconfig_doh": "下载适用于 DNS-over-HTTPS 的 .mobileconfig",
|
||||
"download_mobileconfig_dot": "下载适用于 DNS-over-TLS 的 .mobileconfig",
|
||||
"download_mobileconfig": "下载配置文件",
|
||||
"plain_dns": "无加密DNS",
|
||||
"form_enter_rate_limit": "输入限制速率",
|
||||
"rate_limit": "速度限制",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "加密配置已保存",
|
||||
"encryption_server": "服务器名称",
|
||||
"encryption_server_enter": "输入您的域名",
|
||||
"encryption_server_desc": "若要使用 HTTPS ,您需要输入与 SSL 证书相匹配的服务器名称。",
|
||||
"encryption_server_desc": "为了使用 HTTPS,请您输入与 SSL 证书或通配证书相匹配的服务器名称。如此字段未设置,服务器将要为所有域名接受 TLS 连接。",
|
||||
"encryption_redirect": "HTTPS 自动重定向",
|
||||
"encryption_redirect_desc": "如果勾选此选项,AdGuard Home 将自动将您从 HTTP 重定向到 HTTPS 地址。",
|
||||
"encryption_https": "HTTPS 端口",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "编辑客户端",
|
||||
"client_identifier": "标识符",
|
||||
"ip_address": "IP 地址",
|
||||
"client_identifier_desc": "客户端可通过 IP 地址或 MAC 地址识别。请注意,如 AdGuard Home 也是 <0>DHCP 服务器</0>,则仅能将 MAC 用作标识符",
|
||||
"client_identifier_desc": "客户端可通过 IP 、MAC 地址、CIDR 或特殊 ID(可用于 DoT/DoH/DoQ)被识别。<0>这里</0>您可多了解如何识别客户端。",
|
||||
"form_enter_ip": "输入 IP",
|
||||
"form_enter_mac": "输入 MAC",
|
||||
"form_enter_id": "输入标识符",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> 支持 <1>DNS-over-HTTPS</1>。",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> 支持 <1>DNS-over-HTTPS</1>。",
|
||||
"setup_dns_privacy_other_5": "您可以从 <0>这里</0> 和 <1>这里</1> 找到更多的实施方案。",
|
||||
"setup_dns_privacy_ioc_mac": "iOS 和 macOS 配置",
|
||||
"setup_dns_notice": "为了使用 <1>DNS-over-HTTPS</1> 或者 <1>DNS-over-TLS</1> ,您需要在 AdGuard Home 设置中 <0>配置加密</0> 。",
|
||||
"rewrite_added": "已成功添加 \"{{key}}\" 的 DNS 重写",
|
||||
"rewrite_deleted": "已成功删除 \"{{key}}\" 的 DNS 重写",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP地址:{{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "原因:{{reason}}",
|
||||
"check_rule": "规则:{{rule}}",
|
||||
"check_service": "服务名称:{{service}}",
|
||||
"service_name": "服务名称",
|
||||
"check_not_found": "未在您的筛选列表中找到",
|
||||
|
||||
@@ -269,7 +269,6 @@
|
||||
"source_label": "來源",
|
||||
"found_in_known_domain_db": "在已知網域資料庫中找到。",
|
||||
"category_label": "類別",
|
||||
"rule_label": "規則",
|
||||
"list_label": "清單",
|
||||
"unknown_filter": "未知過濾器 {{filterId}}",
|
||||
"known_tracker": "已知追蹤器",
|
||||
@@ -330,7 +329,6 @@
|
||||
"encryption_config_saved": "加密設定已儲存",
|
||||
"encryption_server": "伺服器名稱",
|
||||
"encryption_server_enter": "輸入您的網域名稱",
|
||||
"encryption_server_desc": "要使用 HTTPS,您必須輸入與您 SSL 憑證相符的伺服器名稱。",
|
||||
"encryption_redirect": "自動重新導向到 HTTPS",
|
||||
"encryption_redirect_desc": "如果啟用,AdGuard Home 將會自動導向 HTTP 到 HTTPS。",
|
||||
"encryption_https": "HTTPS 連接埠",
|
||||
@@ -386,7 +384,6 @@
|
||||
"client_edit": "編輯用戶端",
|
||||
"client_identifier": "識別碼",
|
||||
"ip_address": "IP 位址",
|
||||
"client_identifier_desc": "可通過 IP 地址、CIDR、MAC 地址來辨識使用者裝置。注意:必須使用 AdGuard Home 內建 <0>DHCP 伺服器</0> 才能偵測 MAC 地址。",
|
||||
"form_enter_ip": "輸入 IP",
|
||||
"form_enter_mac": "輸入 MAC 地址",
|
||||
"form_enter_id": "輸入識別碼",
|
||||
@@ -529,7 +526,6 @@
|
||||
"check_ip": "IP 位址:{{ip}}",
|
||||
"check_cname": "CNAME:{{cname}}",
|
||||
"check_reason": "原因:{{reason}}",
|
||||
"check_rule": "規則:{{rule}}",
|
||||
"check_service": "服務名稱:{{service}}",
|
||||
"service_name": "服務名稱",
|
||||
"check_not_found": "未在您的過濾清單中找到",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"client_settings": "用戶端設定",
|
||||
"example_upstream_reserved": "您可以<0>為特定網域</0>指定上游 DNS",
|
||||
"example_upstream_reserved": "您可<0>對於特定的網域</0>明確指定 DNS 上游",
|
||||
"example_upstream_comment": "您可明確指定註解",
|
||||
"upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析網域",
|
||||
"parallel_requests": "並行的請求",
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "無效的 IP 格式",
|
||||
"form_error_mac_format": "無效的媒體存取控制(MAC)格式",
|
||||
"form_error_client_id_format": "無效的用戶端 ID 格式",
|
||||
"form_error_server_name": "無效的伺服器名稱",
|
||||
"form_error_positive": "必須大於 0",
|
||||
"form_error_negative": "必須等於或大於 0",
|
||||
"range_end_error": "必須大於起始範圍",
|
||||
@@ -247,10 +248,16 @@
|
||||
"custom_ip": "自訂的 IP",
|
||||
"blocking_ipv4": "封鎖 IPv4",
|
||||
"blocking_ipv6": "封鎖 IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "用戶端 ID",
|
||||
"client_id_placeholder": "輸入用戶端 ID",
|
||||
"client_id_desc": "不同的用戶端可根據特殊的用戶端 ID 被識別。<0>於此</0>,您可了解更多關於如何識別用戶端。",
|
||||
"download_mobileconfig_doh": "下載用於 DNS-over-HTTPS 的 .mobileconfig",
|
||||
"download_mobileconfig_dot": "下載用於 DNS-over-TLS 的 .mobileconfig",
|
||||
"download_mobileconfig": "下載配置檔案",
|
||||
"plain_dns": "一般的 DNS",
|
||||
"form_enter_rate_limit": "輸入速率限制",
|
||||
"rate_limit": "速率限制",
|
||||
@@ -330,7 +337,7 @@
|
||||
"encryption_config_saved": "加密配置被儲存",
|
||||
"encryption_server": "伺服器名稱",
|
||||
"encryption_server_enter": "輸入您的域名",
|
||||
"encryption_server_desc": "為了使用 HTTPS,您需要輸入與您的安全通訊端層(SSL)憑證相符的伺服器名稱。",
|
||||
"encryption_server_desc": "為了使用 HTTPS,您需要輸入與您的安全通訊端層(SSL)憑證或萬用字元憑證相符的伺服器名稱。如果此欄位未被設定,它將接受向任何網域的傳輸層安全性協定(TLS)連線。",
|
||||
"encryption_redirect": "自動地重新導向到 HTTPS",
|
||||
"encryption_redirect_desc": "如果被勾選,AdGuard Home 將自動地重新導向您從 HTTP 到 HTTPS 位址。",
|
||||
"encryption_https": "HTTPS 連接埠",
|
||||
@@ -386,7 +393,7 @@
|
||||
"client_edit": "編輯用戶端",
|
||||
"client_identifier": "識別碼",
|
||||
"ip_address": "IP 位址",
|
||||
"client_identifier_desc": "用戶端可被 IP 位址、無類別網域間路由(CIDR)或媒體存取控制(MAC)位址識別。請注意,只要 AdGuard Home 也是<0>動態主機設定協定(DHCP)伺服器</0>,使用 MAC 作為識別碼是可能的",
|
||||
"client_identifier_desc": "用戶端可根據 IP 位址、無類別網域間路由(CIDR)、媒體存取控制(MAC)位址或特殊的用戶端 ID(可被用於 DoT/DoH/DoQ)被識別。<0>於此</0>,您可了解更多關於如何識別用戶端。",
|
||||
"form_enter_ip": "輸入 IP",
|
||||
"form_enter_mac": "輸入媒體存取控制(MAC)",
|
||||
"form_enter_id": "輸入識別碼",
|
||||
@@ -430,6 +437,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> 支援 <1>DNS-over-HTTPS</1>。",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> 支援 <1>DNS-over-HTTPS</1>。",
|
||||
"setup_dns_privacy_other_5": "在<0>這裡</0>和<1>這裡</1>,您將發現更多的執行。",
|
||||
"setup_dns_privacy_ioc_mac": "iOS 和 macOS 配置",
|
||||
"setup_dns_notice": "為了使用 <1>DNS-over-HTTPS</1> 或 <1>DNS-over-TLS</1>,您需要在 AdGuard Home 設定裡<0>配置加密</0>。",
|
||||
"rewrite_added": "對於 \"{{key}}\" 之 DNS 改寫被成功地加入",
|
||||
"rewrite_deleted": "對於 \"{{key}}\" 之 DNS 改寫被成功地刪除",
|
||||
@@ -529,7 +537,6 @@
|
||||
"check_ip": "IP 位址:{{ip}}",
|
||||
"check_cname": "正規名稱(CNAME):{{cname}}",
|
||||
"check_reason": "原因:{{reason}}",
|
||||
"check_rule": "規則:{{rule}}",
|
||||
"check_service": "服務名稱:{{service}}",
|
||||
"service_name": "服務名稱",
|
||||
"check_not_found": "未在您的過濾器中被找到",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
--gray-d8: #d8d8d8;
|
||||
--gray-f3: #f3f3f3;
|
||||
--font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
|
||||
--font-size-disable-autozoom: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -13,9 +14,10 @@ body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */
|
||||
@media screen and (max-width: 767px) {
|
||||
input, select, textarea {
|
||||
font-size: 16px !important;
|
||||
font-size: var(--font-size-disable-autozoom);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5rem;
|
||||
word-wrap: break-word;
|
||||
font-size: 0.9375rem;
|
||||
font-size: var(--font-size-disable-autozoom);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -534,6 +534,7 @@ export const BLOCK_ACTIONS = {
|
||||
};
|
||||
|
||||
export const SCHEME_TO_PROTOCOL_MAP = {
|
||||
dnscrypt: 'dnscrypt',
|
||||
doh: 'dns_over_https',
|
||||
dot: 'dns_over_tls',
|
||||
doq: 'dns_over_quic',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */
|
||||
@media screen and (max-width: 767px) {
|
||||
input, select, textarea {
|
||||
font-size: 16px !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */
|
||||
@media screen and (max-width: 767px) {
|
||||
input, select, textarea {
|
||||
font-size: 16px !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"stylelint-webpack-plugin": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.0.0",
|
||||
"ts-loader": "^8.0.6",
|
||||
"ts-morph": "^8.1.2",
|
||||
"ts-morph": "^10.0.1",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"url-loader": "^4.1.1",
|
||||
|
||||
@@ -9,4 +9,5 @@ export const trimQuotes = (str: string) => {
|
||||
return str.replace(/\'|\"/g, '');
|
||||
};
|
||||
|
||||
export const GENERATOR_ENTITY_ALLIAS = 'Entities/';
|
||||
export const GENERATOR_ENTITY_ALLIAS = 'Entities/';
|
||||
export const BAD_REQUES_HELPER = 'BadRequesHelper';
|
||||
@@ -4,9 +4,10 @@ import { OPEN_API_PATH } from '../consts';
|
||||
|
||||
import EntitiesGenerator from './src/generateEntities';
|
||||
import ApisGenerator from './src/generateApis';
|
||||
import { OpenApi } from './src/utils';
|
||||
|
||||
|
||||
const generateApi = (openApi: Record<string, any>) => {
|
||||
const generateApi = (openApi: OpenApi) => {
|
||||
const ent = new EntitiesGenerator(openApi);
|
||||
ent.save();
|
||||
|
||||
@@ -14,5 +15,5 @@ const generateApi = (openApi: Record<string, any>) => {
|
||||
api.save();
|
||||
}
|
||||
|
||||
const openApiFile = fs.readFileSync(OPEN_API_PATH, 'utf8');
|
||||
const openApiFile = fs.readFileSync('./scripts/generator/v1.yaml', 'utf8');
|
||||
generateApi(YAML.parse(openApiFile));
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { stringify } from 'qs';
|
||||
import { number } from 'prop-types';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as morph from 'ts-morph';
|
||||
|
||||
import {
|
||||
API_DIR as API_DIR_CONST,
|
||||
BAD_REQUES_HELPER,
|
||||
GENERATOR_ENTITY_ALLIAS,
|
||||
} from '../../consts';
|
||||
import { toCamel, capitalize, schemaParamParser } from './utils';
|
||||
import { toCamel, capitalize, schemaParamParser, OpenApi, uncapitalize, RequestBody } from './utils';
|
||||
|
||||
|
||||
const API_DIR = path.resolve(API_DIR_CONST);
|
||||
@@ -20,11 +21,15 @@ if (!fs.existsSync(API_DIR)) {
|
||||
|
||||
const { Project, QuoteKind } = morph;
|
||||
|
||||
enum PROCESS_AS {
|
||||
JSON = 'JSON',
|
||||
TEXT = 'TEXT',
|
||||
EMPTY = 'EMPTY',
|
||||
}
|
||||
|
||||
class ApiGenerator {
|
||||
project = new Project({
|
||||
tsConfigFilePath: './tsconfig.json',
|
||||
addFilesFromTsConfig: false,
|
||||
manipulationSettings: {
|
||||
quoteKind: QuoteKind.Single,
|
||||
usePrefixAndSuffixTextForRename: false,
|
||||
@@ -32,7 +37,7 @@ class ApiGenerator {
|
||||
},
|
||||
});
|
||||
|
||||
openapi: Record<string, any>;
|
||||
openapi: OpenApi;
|
||||
|
||||
serverUrl: string;
|
||||
|
||||
@@ -47,20 +52,28 @@ class ApiGenerator {
|
||||
|
||||
apis: morph.SourceFile[] = [];
|
||||
|
||||
constructor(openapi: Record<string, any>) {
|
||||
methods = ['patch', 'delete', 'post', 'get', 'put', 'head', 'options', 'trace'];
|
||||
|
||||
constructor(openapi: OpenApi) {
|
||||
this.openapi = openapi;
|
||||
this.paths = openapi.paths;
|
||||
this.serverUrl = openapi.servers[0].url;
|
||||
|
||||
Object.keys(this.paths).forEach((pathKey) => {
|
||||
Object.keys(this.paths[pathKey]).forEach((method) => {
|
||||
Object.keys(this.paths[pathKey]).filter((method) => this.methods.includes(method)).forEach((method) => {
|
||||
const {
|
||||
tags, operationId, parameters, responses, requestBody, security,
|
||||
tags, operationId, responses, requestBody, security, "x-skip-web-api": skip
|
||||
} = this.paths[pathKey][method];
|
||||
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));
|
||||
|
||||
const parameters = this.paths[pathKey][method].parameters || this.paths[pathKey].parameters;
|
||||
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]));
|
||||
if (skip) {
|
||||
return;
|
||||
}
|
||||
if (!operationId) {
|
||||
console.log(pathKey);
|
||||
}
|
||||
if (this.controllers[controller]) {
|
||||
this.controllers[controller][operationId] = {
|
||||
this.controllers[controller][uncapitalize(operationId)] = {
|
||||
parameters,
|
||||
responses,
|
||||
method,
|
||||
@@ -69,7 +82,7 @@ class ApiGenerator {
|
||||
pathKey: pathKey.replace(/{/g, '${'),
|
||||
};
|
||||
} else {
|
||||
this.controllers[controller] = { [operationId]: {
|
||||
this.controllers[controller] = { [uncapitalize(operationId)]: {
|
||||
parameters,
|
||||
responses,
|
||||
method,
|
||||
@@ -97,7 +110,7 @@ class ApiGenerator {
|
||||
]);
|
||||
|
||||
// const schemaProperties = schemas[schemaName].properties;
|
||||
const importEntities: any[] = [];
|
||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||
|
||||
// add api class to file
|
||||
const apiClass = apiFile.addClass({
|
||||
@@ -111,29 +124,34 @@ class ApiGenerator {
|
||||
// for each operation add fetcher
|
||||
operationList.forEach((operation) => {
|
||||
const {
|
||||
requestBody, responses, parameters, method, pathKey, security,
|
||||
requestBody, responses, parameters, method, pathKey,
|
||||
} = controllerOperations[operation];
|
||||
|
||||
const queryParams: any[] = []; // { name, type }
|
||||
const bodyParam: any[] = []; // { name, type }
|
||||
const queryParams: { name: string, type: string, hasQuestionToken: boolean }[] = [];
|
||||
const bodyParam: { name: string, countedType: string, type?: string, isClass?: boolean, hasQuestionToken: boolean }[] = [];
|
||||
|
||||
|
||||
let contentType: string = '';
|
||||
|
||||
let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
|
||||
let contentType = '';
|
||||
if (parameters) {
|
||||
parameters.forEach((p: any) => {
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(p.schema, this.openapi);
|
||||
parameters.forEach((link: {$ref: string}) => {
|
||||
const temp = link.$ref.split('/').pop()
|
||||
const parameter = this.openapi.components.parameters[temp!];
|
||||
|
||||
const {
|
||||
type, isArray, isClass, isImport,
|
||||
} = schemaParamParser(parameter.schema, this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
importEntities.push({ type, isClass });
|
||||
}
|
||||
if (p.in === 'query') {
|
||||
if (parameter.in === 'query') {
|
||||
queryParams.push({
|
||||
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
|
||||
name: parameter.name, type: `${type}${isArray ? '[]' : ''}`, hasQuestionToken: !parameter.required });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (queryParams.length > 0) {
|
||||
const imp = apiFile.getImportDeclaration((i) => {
|
||||
return i.getModuleSpecifierValue() === 'qs';
|
||||
@@ -144,62 +162,120 @@ class ApiGenerator {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (requestBody) {
|
||||
let content = requestBody.content;
|
||||
const { $ref }: { $ref: string } = requestBody;
|
||||
|
||||
if (!content && $ref) {
|
||||
const name = $ref.split('/').pop() as string;
|
||||
content = this.openapi.components.requestBodies[name].content;
|
||||
}
|
||||
const name = $ref.split('/').pop();
|
||||
const { content, required } = this.openapi.components.requestBodies[name!];
|
||||
|
||||
|
||||
[contentType] = Object.keys(content);
|
||||
const data = content[contentType];
|
||||
const data = content[contentType as keyof RequestBody['content']]!;
|
||||
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(data.schema, this.openapi);
|
||||
const {
|
||||
type, isArray, isClass, isImport,
|
||||
} = schemaParamParser(data.schema, this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
|
||||
importEntities.push({ type: type, isClass });
|
||||
bodyParam.push({
|
||||
name: type.toLowerCase(),
|
||||
countedType: `${isClass ? 'I' : ''}${type}${isArray ? '[]' : ''}`,
|
||||
isClass,
|
||||
type,
|
||||
hasQuestionToken: !required
|
||||
});
|
||||
} else {
|
||||
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
|
||||
bodyParam.push({
|
||||
name: 'data',
|
||||
countedType: `${type}${isArray ? '[]' : ''}`,
|
||||
hasQuestionToken: !required });
|
||||
|
||||
}
|
||||
}
|
||||
if (responses['200']) {
|
||||
const { content, headers } = responses['200'];
|
||||
if (content && (content['*/*'] || content['application/json'])) {
|
||||
const { schema, examples } = content['*/*'] || content['application/json'];
|
||||
|
||||
if (!schema) {
|
||||
process.exit(0);
|
||||
const responsesCodes = Object.keys(responses);
|
||||
const responsesSchema = responsesCodes.map((code) => {
|
||||
const refLink = responses[code].$ref.split('/').pop();
|
||||
const ref = this.openapi.components.responses[refLink];
|
||||
|
||||
interface ResponseSchema {
|
||||
code: number,
|
||||
[PROCESS_AS.JSON]?: ReturnType<typeof schemaParamParser>;
|
||||
[PROCESS_AS.TEXT]?: {
|
||||
schema?: ReturnType<typeof schemaParamParser>;
|
||||
xErrorCode?: string;
|
||||
onlyText: boolean;
|
||||
}
|
||||
|
||||
const propType = schemaParamParser(schema, this.openapi);
|
||||
const [pType, , isClass, isImport] = propType;
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
}
|
||||
hasResponseBodyType = propType;
|
||||
[PROCESS_AS.EMPTY]?: boolean;
|
||||
}
|
||||
}
|
||||
let returnType = '';
|
||||
if (hasResponseBodyType) {
|
||||
const [pType, isArray, isClass] = hasResponseBodyType as any;
|
||||
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
||||
returnType = data;
|
||||
} else {
|
||||
returnType = 'Promise<number';
|
||||
}
|
||||
const shouldValidate = bodyParam.filter(b => b.isClass);
|
||||
if (shouldValidate.length > 0) {
|
||||
returnType += ' | string[]';
|
||||
}
|
||||
// append Error to default type return;
|
||||
returnType += ' | Error>';
|
||||
const responseSchema: ResponseSchema = { code: Number(code) };
|
||||
|
||||
if (!ref.content) {
|
||||
responseSchema[PROCESS_AS.EMPTY] = true;
|
||||
return responseSchema;
|
||||
}
|
||||
if (ref.content?.['application/json']) {
|
||||
const { schema } = ref.content['application/json'];
|
||||
responseSchema[PROCESS_AS.JSON] = schemaParamParser(schema, this.openapi);
|
||||
}
|
||||
if (ref.content?.['text/palin']) {
|
||||
const {
|
||||
"x-error-class": xErrorClass,
|
||||
"x-error-code": xErrorCode,
|
||||
} = ref.content['text/palin'];
|
||||
if (xErrorClass) {
|
||||
const schemaLink = xErrorClass.split('/').pop();
|
||||
const schema = this.openapi.components.schemas[schemaLink!];
|
||||
responseSchema[PROCESS_AS.TEXT] = {
|
||||
schema: schemaParamParser(schema, this.openapi),
|
||||
xErrorCode,
|
||||
onlyText: false,
|
||||
}
|
||||
} else {
|
||||
responseSchema[PROCESS_AS.TEXT] = { onlyText: true };
|
||||
}
|
||||
}
|
||||
return responseSchema;
|
||||
});
|
||||
|
||||
|
||||
let returnTypes = new Set();
|
||||
|
||||
bodyParam.forEach((param) => {
|
||||
if (param.isClass) {
|
||||
returnTypes.add(BAD_REQUES_HELPER);
|
||||
importEntities.push({ type: BAD_REQUES_HELPER, isClass: true });
|
||||
}
|
||||
})
|
||||
|
||||
responsesSchema.forEach((responseSchema) => {
|
||||
if (responseSchema[PROCESS_AS.JSON]) {
|
||||
const { type, isClass, isImport } = responseSchema[PROCESS_AS.JSON]!;
|
||||
returnTypes.add(type);
|
||||
if (isImport) {
|
||||
importEntities.push({ type: type, isClass });
|
||||
}
|
||||
}
|
||||
if (responseSchema[PROCESS_AS.TEXT]) {
|
||||
const { onlyText, schema } = responseSchema[PROCESS_AS.TEXT]!;
|
||||
if (onlyText) {
|
||||
returnTypes.add('string');
|
||||
} else {
|
||||
const { type, isClass, isImport } = schema!;
|
||||
returnTypes.add(type);
|
||||
if (isImport) {
|
||||
importEntities.push({ type, isClass });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (responseSchema[PROCESS_AS.EMPTY]) {
|
||||
returnTypes.add('number');
|
||||
}
|
||||
});
|
||||
returnTypes.add('undefined');
|
||||
const returnType = `Promise<${Array.from(returnTypes).join(' | ')}>`;
|
||||
|
||||
const fetcher = apiClass.addMethod({
|
||||
isAsync: true,
|
||||
@@ -211,23 +287,19 @@ class ApiGenerator {
|
||||
fetcher.addParameters(params);
|
||||
|
||||
fetcher.setBodyText((w) => {
|
||||
// Add data to URLSearchParams
|
||||
if (contentType === 'text/plain') {
|
||||
bodyParam.forEach((b) => {
|
||||
w.writeLine(`const params = String(${b.name});`);
|
||||
});
|
||||
} else {
|
||||
if (contentType === 'application/json') {
|
||||
const shouldValidate = bodyParam.filter(b => b.isClass);
|
||||
if (shouldValidate.length > 0) {
|
||||
w.writeLine(`const haveError: string[] = [];`);
|
||||
shouldValidate.forEach((b) => {
|
||||
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
|
||||
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
|
||||
w.writeLine(`haveError.push(...${b.name}.validate());`);
|
||||
});
|
||||
w.writeLine(`if (haveError.length > 0) {`);
|
||||
w.writeLine(` return Promise.resolve(haveError);`)
|
||||
w.writeLine(` return Promise.resolve(new ${BAD_REQUES_HELPER}(haveError));`)
|
||||
w.writeLine(`}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch return of fetch in case on queryParams
|
||||
if (queryParams.length > 0) {
|
||||
w.writeLine('const queryParams = {');
|
||||
@@ -243,37 +315,36 @@ class ApiGenerator {
|
||||
w.writeLine(` method: '${method.toUpperCase()}',`);
|
||||
|
||||
// add Fetch options
|
||||
if (contentType && contentType !== 'multipart/form-data') {
|
||||
w.writeLine(' headers: {');
|
||||
w.writeLine(` 'Content-Type': '${contentType}',`);
|
||||
w.writeLine(' },');
|
||||
}
|
||||
if (contentType) {
|
||||
switch (contentType) {
|
||||
case 'text/plain':
|
||||
w.writeLine(' body: params,');
|
||||
break;
|
||||
default:
|
||||
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
|
||||
break;
|
||||
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}.serialize()` : b.name).join(', ')}),`);
|
||||
}
|
||||
|
||||
w.writeLine('}).then(async (res) => {');
|
||||
responsesSchema.forEach((responseSchema) => {
|
||||
const { code } = responseSchema;
|
||||
w.writeLine(` if (res.status === ${code}) {`);
|
||||
if (responseSchema[PROCESS_AS.EMPTY]) {
|
||||
w.writeLine(' return res.status;');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle response
|
||||
if (hasResponseBodyType) {
|
||||
w.writeLine('}).then(async (res) => {');
|
||||
w.writeLine(' if (res.status === 200) {');
|
||||
w.writeLine(' return res.json();');
|
||||
} else {
|
||||
w.writeLine('}).then(async (res) => {');
|
||||
w.writeLine(' if (res.status === 200) {');
|
||||
w.writeLine(' return res.status;');
|
||||
}
|
||||
|
||||
// Handle Error
|
||||
w.writeLine(' } else {');
|
||||
w.writeLine(' return new Error(String(res.status));');
|
||||
w.writeLine(' }');
|
||||
if (responseSchema[PROCESS_AS.TEXT]?.onlyText) {
|
||||
w.writeLine(' return res.text();')
|
||||
}
|
||||
if (responseSchema[PROCESS_AS.JSON] && responseSchema[PROCESS_AS.TEXT]) {
|
||||
const { type } = responseSchema[PROCESS_AS.JSON]!;
|
||||
const { schema, xErrorCode } = responseSchema[PROCESS_AS.TEXT]!;
|
||||
const { type: errType } = schema!;
|
||||
w.writeLine(' try {');
|
||||
w.writeLine(` return new ${type}(await res.json());`);
|
||||
w.writeLine(' } catch {');
|
||||
w.writeLine(` return new ${errType}({ msg: await res.text() code: ${xErrorCode}} as any);`);
|
||||
w.writeLine(' }');
|
||||
}
|
||||
if (responseSchema[PROCESS_AS.JSON]) {
|
||||
const { type } = responseSchema[PROCESS_AS.JSON]!;
|
||||
w.writeLine(` return new ${type}(await res.json());`);
|
||||
}
|
||||
w.writeLine(` }`);
|
||||
})
|
||||
w.writeLine('})');
|
||||
});
|
||||
});
|
||||
@@ -288,17 +359,16 @@ class ApiGenerator {
|
||||
}
|
||||
});
|
||||
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||
const { type: pType, isClass } = ie;
|
||||
const { type: type, isClass } = ie;
|
||||
if (isClass) {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
||||
defaultImport: pType,
|
||||
namedImports: [`I${pType}`],
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
|
||||
defaultImport: type,
|
||||
});
|
||||
} else {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
||||
namedImports: [pType],
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
|
||||
namedImports: [type],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import * as path from 'path';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as morph from 'ts-morph';
|
||||
|
||||
import { ENT_DIR } from '../../consts';
|
||||
import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils';
|
||||
import { ENT_DIR, BAD_REQUES_HELPER } from '../../consts';
|
||||
import { TYPES, toCamel, schemaParamParser, capitalize, OpenApi, Schema } from './utils';
|
||||
|
||||
const { Project, QuoteKind } = morph;
|
||||
|
||||
@@ -17,7 +17,6 @@ if (!fs.existsSync(EntDir)) {
|
||||
class EntitiesGenerator {
|
||||
project = new Project({
|
||||
tsConfigFilePath: './tsconfig.json',
|
||||
addFilesFromTsConfig: false,
|
||||
manipulationSettings: {
|
||||
quoteKind: QuoteKind.Single,
|
||||
usePrefixAndSuffixTextForRename: false,
|
||||
@@ -25,491 +24,480 @@ class EntitiesGenerator {
|
||||
},
|
||||
});
|
||||
|
||||
openapi: Record<string, any>;
|
||||
openapi: OpenApi;
|
||||
|
||||
schemas: Record<string, any>;
|
||||
schemas: Record<string, Schema>;
|
||||
|
||||
schemaNames: string[];
|
||||
|
||||
entities: morph.SourceFile[] = [];
|
||||
|
||||
constructor(openapi: Record<string, any>) {
|
||||
constructor(openapi: OpenApi) {
|
||||
this.openapi = openapi;
|
||||
this.schemas = openapi.components.schemas;
|
||||
this.schemaNames = Object.keys(this.schemas);
|
||||
this.generateEntities();
|
||||
this.generateUtils();
|
||||
}
|
||||
|
||||
|
||||
generateUtils = () => {
|
||||
const helperFile = this.project.createSourceFile(`${EntDir}/${BAD_REQUES_HELPER}.ts`);
|
||||
helperFile.addImportDeclaration({
|
||||
moduleSpecifier: `./BadRequestResp`,
|
||||
defaultImport: 'BadRequestResp',
|
||||
});
|
||||
helperFile.addImportDeclaration({
|
||||
moduleSpecifier: `./ErrorCode`,
|
||||
namedImports: ['ErrorCode'],
|
||||
});
|
||||
const helperClass = helperFile.addClass({
|
||||
name: 'BadRequestHelper',
|
||||
isDefaultExport: true,
|
||||
extends: 'BadRequestResp',
|
||||
properties: [{
|
||||
type: 'string[]',
|
||||
name: 'fields'
|
||||
}]
|
||||
});
|
||||
const helperConstructor = helperClass.addConstructor({
|
||||
parameters: [{
|
||||
type: 'string[]',
|
||||
name: 'fields'
|
||||
}],
|
||||
});
|
||||
helperConstructor.setBodyText((w) => {
|
||||
w.writeLine('super({ code: ErrorCode.JSN001, msg: \'Wrong fields value\' });');
|
||||
w.writeLine('this.fields = fields;')
|
||||
});
|
||||
this.entities.push(helperFile);
|
||||
}
|
||||
|
||||
generateEntities = () => {
|
||||
this.schemaNames.forEach(this.generateEntity);
|
||||
};
|
||||
|
||||
generateEntity = (sName: string) => {
|
||||
const { properties, type, oneOf } = this.schemas[sName];
|
||||
generateEntity = (schemaName: string) => {
|
||||
const { properties, type, oneOf, enum: en } = this.schemas[schemaName];
|
||||
const notAClass = !properties && TYPES[type as keyof typeof TYPES];
|
||||
|
||||
if (oneOf) {
|
||||
this.generateOneOf(sName);
|
||||
this.generateOneOf(schemaName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (en) {
|
||||
this.generateEnum(schemaName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (notAClass) {
|
||||
this.generateEnum(sName);
|
||||
this.generatePrimitive(schemaName)
|
||||
} else {
|
||||
this.generateClass(sName);
|
||||
this.generateClass(schemaName);
|
||||
}
|
||||
};
|
||||
|
||||
generateEnum = (sName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
||||
generatePrimitive = (schemaName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
|
||||
entityFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
const { type: schemaType, description, pattern } = this.schemas[schemaName];
|
||||
if (description) {
|
||||
entityFile.addStatements(['\n/*', `Description: ${description}`, '*/\n']);
|
||||
}
|
||||
|
||||
const { enum: enumMembers } = this.schemas[sName];
|
||||
if (pattern) {
|
||||
entityFile.addStatements(`const pattern = new RegExp('${pattern}')`);
|
||||
}
|
||||
|
||||
const type: string = TYPES[schemaType as keyof typeof TYPES];
|
||||
|
||||
const entityClass = entityFile.addClass({
|
||||
name: schemaName,
|
||||
isDefaultExport: true,
|
||||
extends: capitalize(type),
|
||||
});
|
||||
|
||||
|
||||
|
||||
const ctor = entityClass.addConstructor({
|
||||
parameters: [{
|
||||
name: 'v',
|
||||
type,
|
||||
|
||||
}],
|
||||
});
|
||||
ctor.setBodyText((w) => {
|
||||
const { minLength, minimum, maxLength, maximum } = this.schemas[schemaName];
|
||||
|
||||
if (type === 'string') {
|
||||
if (pattern) {
|
||||
w.writeLine('if (!v.match(pattern)) {');
|
||||
w.writeLine(' throw new Error();');
|
||||
w.writeLine('}');
|
||||
}
|
||||
if (typeof minLength === 'number') {
|
||||
w.writeLine(`if (v.length < ${minLength}) {`);
|
||||
w.writeLine(' throw new Error();');
|
||||
w.writeLine('}');
|
||||
}
|
||||
if (typeof maxLength === 'number') {
|
||||
w.writeLine(`if (v.length > ${maxLength}) {`);
|
||||
w.writeLine(' throw new Error();');
|
||||
w.writeLine('}');
|
||||
}
|
||||
}
|
||||
if (type === 'number') {
|
||||
if (typeof minimum === 'number') {
|
||||
w.writeLine(`if (v.length < ${minimum}) {`);
|
||||
w.writeLine(' throw new Error();');
|
||||
w.writeLine('}');
|
||||
}
|
||||
if (typeof maximum === 'number') {
|
||||
w.writeLine(`if (v.length > ${maximum}) {`);
|
||||
w.writeLine(' throw new Error();');
|
||||
w.writeLine('}');
|
||||
}
|
||||
}
|
||||
w.writeLine('super(v);');
|
||||
});
|
||||
|
||||
this.entities.push(entityFile);
|
||||
};
|
||||
|
||||
|
||||
generateEnum = (schemaName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
|
||||
entityFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'',
|
||||
]);
|
||||
const { enum: enumMembers, description, example } = this.schemas[schemaName];
|
||||
if (description) {
|
||||
entityFile.addStatements(['\n/*', `Description: ${description}`, '*/\n']);
|
||||
}
|
||||
entityFile.addEnum({
|
||||
name: sName,
|
||||
members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })),
|
||||
name: schemaName,
|
||||
members: enumMembers!.map((e: string) => ({ name: e.toUpperCase(), value: e })),
|
||||
isExported: true,
|
||||
});
|
||||
|
||||
this.entities.push(entityFile);
|
||||
};
|
||||
|
||||
generateOneOf = (sName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
||||
generateOneOf = (schemaName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
|
||||
entityFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||
const entities = this.schemas[sName].oneOf.map((elem: any) => {
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(elem, this.openapi);
|
||||
importEntities.push({ type: pType, isClass });
|
||||
return { type: pType, isArray };
|
||||
const entities = this.schemas[schemaName].oneOf.map((elem: any) => {
|
||||
const {
|
||||
type: type, isArray, isClass, isImport,
|
||||
} = schemaParamParser(elem, this.openapi);
|
||||
importEntities.push({ type: type, isClass });
|
||||
return { type: type, isArray };
|
||||
});
|
||||
entityFile.addTypeAlias({
|
||||
name: sName,
|
||||
name: schemaName,
|
||||
isExported: true,
|
||||
type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
|
||||
})
|
||||
|
||||
// add import
|
||||
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||
const { type: pType, isClass } = ie;
|
||||
const { type: type, isClass } = ie;
|
||||
if (isClass) {
|
||||
entityFile.addImportDeclaration({
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [`I${pType}`],
|
||||
moduleSpecifier: `./${type}`,
|
||||
namedImports: [`I${type}`],
|
||||
});
|
||||
} else {
|
||||
entityFile.addImportDeclaration({
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [pType],
|
||||
moduleSpecifier: `./${type}`,
|
||||
namedImports: [type],
|
||||
});
|
||||
}
|
||||
});
|
||||
this.entities.push(entityFile);
|
||||
}
|
||||
|
||||
generateClass = (sName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
||||
generateClass = (schemaName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
|
||||
entityFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
|
||||
let { properties, required, allOf, $ref } = this.schemas[schemaName];
|
||||
|
||||
if (allOf) {
|
||||
const refLink: string = allOf.find((obj: Record<string, any>) => obj.$ref).$ref;
|
||||
let ref: any = refLink.split('/')
|
||||
ref = ref.pop();
|
||||
|
||||
const reasign = allOf.find((obj: Record<string, any>) => !obj.$ref);
|
||||
const newSchema: Schema = { ...this.schemas[ref], ...reasign };
|
||||
|
||||
properties = newSchema.properties;
|
||||
required = newSchema.required;
|
||||
}
|
||||
|
||||
const { properties: sProps, required, $ref, additionalProperties } = this.schemas[sName];
|
||||
if ($ref) {
|
||||
const temp = $ref.split('/');
|
||||
const importSchemaName = `${temp[temp.length - 1]}`;
|
||||
const refLink = $ref.split('/').pop()!;
|
||||
entityFile.addImportDeclaration({
|
||||
defaultImport: importSchemaName,
|
||||
moduleSpecifier: `./${importSchemaName}`,
|
||||
namedImports: [`I${importSchemaName}`],
|
||||
defaultImport: refLink,
|
||||
moduleSpecifier: `./${refLink}`,
|
||||
namedImports: [`I${refLink}`],
|
||||
});
|
||||
|
||||
entityFile.addTypeAlias({
|
||||
name: `I${sName}`,
|
||||
type: `I${importSchemaName}`,
|
||||
name: `I${schemaName}`,
|
||||
type: `I${refLink}`,
|
||||
isExported: true,
|
||||
})
|
||||
|
||||
entityFile.addStatements(`export default ${importSchemaName};`);
|
||||
const entityClass = entityFile.addClass({
|
||||
name: schemaName,
|
||||
isDefaultExport: true,
|
||||
extends: refLink,
|
||||
})
|
||||
const ctor = entityClass.addConstructor({
|
||||
parameters: [{
|
||||
name: 'props',
|
||||
type: `I${schemaName}`,
|
||||
}],
|
||||
})
|
||||
ctor.setBodyText((w) => {
|
||||
w.writeLine('super(props);')
|
||||
});
|
||||
this.entities.push(entityFile);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||
const entityInterface = entityFile.addInterface({
|
||||
name: `I${sName}`,
|
||||
name: `I${schemaName}`,
|
||||
isExported: true,
|
||||
});
|
||||
|
||||
const sortedSProps = Object.keys(sProps || {}).sort();
|
||||
const additionalPropsOnly = additionalProperties && sortedSProps.length === 0;
|
||||
|
||||
const sortedProperties = Object.keys(properties || {}).sort();
|
||||
let importEntities: { type: string, isClass: boolean }[] = [];
|
||||
|
||||
type SortedPropertiesTypesValues = ReturnType<typeof schemaParamParser> & {
|
||||
computedType: string;
|
||||
isRequired: boolean;
|
||||
}
|
||||
const sortedPropertiesTypes = sortedProperties.reduce((data, propertyName) => {
|
||||
const isRequired = !!(required && required.includes(propertyName));
|
||||
const parsed = schemaParamParser(properties![propertyName], this.openapi);
|
||||
data[propertyName] = {
|
||||
...parsed,
|
||||
isRequired,
|
||||
computedType: `${parsed.type}${parsed.isArray ? '[]' : ''}${isRequired ? '' : ' | undefined'}`
|
||||
};
|
||||
return data;
|
||||
}, {} as Record<string, SortedPropertiesTypesValues>);
|
||||
|
||||
// add server response interface to entityFile
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const [
|
||||
pType, isArray, isClass, isImport, isAdditional
|
||||
] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
sortedProperties.forEach((propertyName) => {
|
||||
const {
|
||||
type, isArray, isClass, isImport
|
||||
} = sortedPropertiesTypes[propertyName];
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
importEntities.push({ type: type, isClass });
|
||||
}
|
||||
const propertyType = isAdditional
|
||||
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
|
||||
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
||||
entityInterface.addProperty({
|
||||
name: sPropName,
|
||||
type: propertyType,
|
||||
name: propertyName,
|
||||
type: `${isClass ? 'I' : ''}${type}${isArray ? '[]' : ''}`,
|
||||
hasQuestionToken: !(
|
||||
(required && required.includes(sPropName)) || sProps[sPropName].required
|
||||
(required && required.includes(propertyName)) || properties![propertyName].required
|
||||
),
|
||||
});
|
||||
});
|
||||
if (additionalProperties) {
|
||||
const [
|
||||
pType, isArray, isClass, isImport, isAdditional
|
||||
] = schemaParamParser(additionalProperties, this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
}
|
||||
const type = isAdditional
|
||||
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
|
||||
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
||||
entityInterface.addIndexSignature({
|
||||
keyName: 'key',
|
||||
keyType: 'string',
|
||||
returnType: additionalPropsOnly ? type : `${type} | undefined`,
|
||||
});
|
||||
}
|
||||
|
||||
// add import
|
||||
const imports: { type: string, isClass: boolean }[] = [];
|
||||
const types: string[] = [];
|
||||
importEntities.forEach((i) => {
|
||||
importEntities = importEntities.filter((i) => {
|
||||
const { type } = i;
|
||||
if (!types.includes(type)) {
|
||||
imports.push(i);
|
||||
types.push(type);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||
const { type: pType, isClass } = ie;
|
||||
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||
const { type: type, isClass } = ie;
|
||||
if (isClass) {
|
||||
entityFile.addImportDeclaration({
|
||||
defaultImport: pType,
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [`I${pType}`],
|
||||
defaultImport: type,
|
||||
moduleSpecifier: `./${type}`,
|
||||
namedImports: [`I${type}`],
|
||||
});
|
||||
} else {
|
||||
entityFile.addImportDeclaration({
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [pType],
|
||||
moduleSpecifier: `./${type}`,
|
||||
namedImports: [type],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const entityClass = entityFile.addClass({
|
||||
name: sName,
|
||||
name: schemaName,
|
||||
isDefaultExport: true,
|
||||
});
|
||||
|
||||
// addProperties to class;
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
|
||||
const isRequred = (required && required.includes(sPropName))
|
||||
|| sProps[sPropName].required;
|
||||
|
||||
const propertyType = isAdditional
|
||||
? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }`
|
||||
: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`;
|
||||
sortedProperties.forEach((propertyName) => {
|
||||
const { type, isArray, isClass, isEnum, isRequired, computedType } = sortedPropertiesTypes[propertyName];
|
||||
|
||||
entityClass.addProperty({
|
||||
name: `_${sPropName}`,
|
||||
name: `_${propertyName}`,
|
||||
isReadonly: true,
|
||||
type: propertyType,
|
||||
type: computedType,
|
||||
});
|
||||
|
||||
const getter = entityClass.addGetAccessor({
|
||||
name: toCamel(sPropName),
|
||||
returnType: propertyType,
|
||||
statements: [`return this._${sPropName};`],
|
||||
name: toCamel(propertyName),
|
||||
returnType: computedType,
|
||||
statements: [`return this._${propertyName};`],
|
||||
});
|
||||
const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
||||
const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = properties![propertyName];
|
||||
|
||||
if (description || example) {
|
||||
getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
|
||||
}
|
||||
|
||||
if (minItems) {
|
||||
entityClass.addGetAccessor({
|
||||
entityClass.addProperty({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MinItems`,
|
||||
statements: [`return ${minItems};`],
|
||||
isReadonly: true,
|
||||
name: `${capitalize(toCamel(propertyName))}MinItems`,
|
||||
initializer: `${minItems}`,
|
||||
});
|
||||
}
|
||||
if (maxItems) {
|
||||
entityClass.addGetAccessor({
|
||||
entityClass.addProperty({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MaxItems`,
|
||||
statements: [`return ${maxItems};`],
|
||||
isReadonly: true,
|
||||
name: `${capitalize(toCamel(propertyName))}MaxItems`,
|
||||
initializer: `${maxItems}`,
|
||||
});
|
||||
}
|
||||
if (typeof minLength === 'number') {
|
||||
entityClass.addGetAccessor({
|
||||
entityClass.addProperty({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MinLength`,
|
||||
statements: [`return ${minLength};`],
|
||||
isReadonly: true,
|
||||
name: `${capitalize(toCamel(propertyName))}MinLength`,
|
||||
initializer: `${minLength}`,
|
||||
});
|
||||
}
|
||||
if (maxLength) {
|
||||
entityClass.addGetAccessor({
|
||||
entityClass.addProperty({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MaxLength`,
|
||||
statements: [`return ${maxLength};`],
|
||||
isReadonly: true,
|
||||
name: `${capitalize(toCamel(propertyName))}MaxLength`,
|
||||
initializer: `${maxLength}`,
|
||||
});
|
||||
}
|
||||
if (typeof minimum === 'number') {
|
||||
entityClass.addGetAccessor({
|
||||
entityClass.addProperty({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MinValue`,
|
||||
statements: [`return ${minimum};`],
|
||||
isReadonly: true,
|
||||
name: `${capitalize(toCamel(propertyName))}MinValue`,
|
||||
initializer: `${minimum}`,
|
||||
});
|
||||
}
|
||||
if (maximum) {
|
||||
entityClass.addGetAccessor({
|
||||
entityClass.addProperty({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MaxValue`,
|
||||
statements: [`return ${maximum};`],
|
||||
isReadonly: true,
|
||||
name: `${capitalize(toCamel(propertyName))}MaxValue`,
|
||||
initializer: `${maximum}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!(isArray && isClass) && !isClass) {
|
||||
const isEnum = !isClass && isImport;
|
||||
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
|
||||
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
||||
const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number';
|
||||
if (isRequired || haveValidationFields) {
|
||||
const prop = toCamel(sPropName);
|
||||
const validateField = entityClass.addMethod({
|
||||
isStatic: true,
|
||||
name: `${prop}Validate`,
|
||||
returnType: `boolean`,
|
||||
parameters: [{
|
||||
name: prop,
|
||||
type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`,
|
||||
}],
|
||||
})
|
||||
|
||||
validateField.setBodyText((w) => {
|
||||
w.write('return ');
|
||||
const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`;
|
||||
if (pType === 'string') {
|
||||
if (isArray) {
|
||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`);
|
||||
} else {
|
||||
if (typeof minLength === 'number' && maxLength) {
|
||||
w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`);
|
||||
}
|
||||
if (typeof minLength !== 'number' || !maxLength) {
|
||||
w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'number') {
|
||||
if (isArray) {
|
||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true)`);
|
||||
} else {
|
||||
if (typeof minimum === 'number' && maximum) {
|
||||
w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`);
|
||||
}
|
||||
if (typeof minimum !== 'number' || !maximum) {
|
||||
w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'boolean') {
|
||||
w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
|
||||
} else if (isEnum) {
|
||||
if (isArray){
|
||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`);
|
||||
} else {
|
||||
w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
|
||||
}
|
||||
}
|
||||
|
||||
w.write(';');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (additionalProperties) {
|
||||
const [
|
||||
pType, isArray, isClass, isImport, isAdditional
|
||||
] = schemaParamParser(additionalProperties, this.openapi);
|
||||
const type = `Record<string, ${pType}${isArray ? '[]' : ''}>`;
|
||||
|
||||
entityClass.addProperty({
|
||||
name: additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`,
|
||||
isReadonly: true,
|
||||
type: type,
|
||||
});
|
||||
}
|
||||
|
||||
// add constructor;
|
||||
const ctor = entityClass.addConstructor({
|
||||
parameters: [{
|
||||
name: 'props',
|
||||
type: `I${sName}`,
|
||||
type: `I${schemaName}`,
|
||||
}],
|
||||
});
|
||||
ctor.setBodyText((w) => {
|
||||
if (additionalProperties) {
|
||||
const [
|
||||
pType, isArray, isClass, isImport, isAdditional
|
||||
] = schemaParamParser(additionalProperties, this.openapi);
|
||||
w.writeLine(`this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`} = Object.entries(props).reduce<Record<string, ${pType}>>((prev, [key, value]) => {`);
|
||||
if (isClass) {
|
||||
w.writeLine(` prev[key] = new ${pType}(value!);`);
|
||||
sortedProperties.forEach((propertyName) => {
|
||||
const { type, isArray, isClass, isRequired } = sortedPropertiesTypes[propertyName];
|
||||
|
||||
const indent = !isRequired ? ' ' : '';
|
||||
if (!isRequired) {
|
||||
if ((type === 'boolean' || type === 'number' || type ==='string') && !isClass && !isArray) {
|
||||
w.writeLine(`if (typeof props.${propertyName} === '${type}') {`);
|
||||
} else {
|
||||
w.writeLine(`if (props.${propertyName}) {`);
|
||||
}
|
||||
}
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(`${indent}this._${propertyName} = props.${propertyName}.map((p) => new ${type}(p));`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(`${indent}this._${propertyName} = new ${type}(props.${propertyName});`);
|
||||
} else {
|
||||
w.writeLine(' prev[key] = value!;')
|
||||
}
|
||||
w.writeLine(' return prev;');
|
||||
w.writeLine('}, {})');
|
||||
return;
|
||||
}
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const [
|
||||
pType, isArray, isClass, , isAdditional
|
||||
] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
const req = (required && required.includes(sPropName))
|
||||
|| sProps[sPropName].required;
|
||||
if (!req) {
|
||||
if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) {
|
||||
w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`);
|
||||
if (type === 'string' && !isArray) {
|
||||
w.writeLine(`${indent}this._${propertyName} = props.${propertyName}.trim();`);
|
||||
} else {
|
||||
w.writeLine(`if (props.${sPropName}) {`);
|
||||
w.writeLine(`${indent}this._${propertyName} = props.${propertyName};`);
|
||||
}
|
||||
}
|
||||
if (isAdditional) {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => {
|
||||
return { ...prev, [key]: new ${pType}(p[key])};
|
||||
},{}))`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
||||
return { ...prev, [key]: new ${pType}(props.${sPropName}[key])};
|
||||
},{})`);
|
||||
} else {
|
||||
if (pType === 'string' && !isArray) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
||||
return { ...prev, [key]: props.${sPropName}[key].trim()};
|
||||
},{})`);
|
||||
} else {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
||||
return { ...prev, [key]: props.${sPropName}[key]};
|
||||
},{})`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`);
|
||||
} else {
|
||||
if (pType === 'string' && !isArray) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.trim();`);
|
||||
} else {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!req) {
|
||||
if (!isRequired) {
|
||||
w.writeLine('}');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// add serialize method;
|
||||
const serialize = entityClass.addMethod({
|
||||
isStatic: false,
|
||||
name: 'serialize',
|
||||
returnType: `I${sName}`,
|
||||
returnType: `I${schemaName}`,
|
||||
});
|
||||
serialize.setBodyText((w) => {
|
||||
if (additionalProperties) {
|
||||
const [
|
||||
pType, isArray, isClass, isImport, isAdditional
|
||||
] = schemaParamParser(additionalProperties, this.openapi);
|
||||
w.writeLine(`return Object.entries(this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`}).reduce<Record<string, ${isClass ? 'I' : ''}${pType}>>((prev, [key, value]) => {`);
|
||||
if (isClass) {
|
||||
w.writeLine(` prev[key] = value.serialize();`);
|
||||
} else {
|
||||
w.writeLine(' prev[key] = value;')
|
||||
}
|
||||
w.writeLine(' return prev;');
|
||||
w.writeLine('}, {})');
|
||||
return;
|
||||
}
|
||||
w.writeLine(`const data: I${sName} = {`);
|
||||
w.writeLine(`const data: I${schemaName} = {`);
|
||||
|
||||
const unReqFields: string[] = [];
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const req = (required && required.includes(sPropName))
|
||||
|| sProps[sPropName].required;
|
||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
if (!req) {
|
||||
unReqFields.push(sPropName);
|
||||
|
||||
sortedProperties.forEach((propertyName) => {
|
||||
const {isArray, isClass, isRequired } = sortedPropertiesTypes[propertyName];
|
||||
if (!isRequired) {
|
||||
unReqFields.push(propertyName);
|
||||
return;
|
||||
}
|
||||
if (isAdditional) {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce<Record<string, any>>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`);
|
||||
} else {
|
||||
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`);
|
||||
}
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` ${propertyName}: this._${propertyName}.map((p) => p.serialize()),`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` ${propertyName}: this._${propertyName}.serialize(),`);
|
||||
} else {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName}.serialize(),`);
|
||||
} else {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName},`);
|
||||
}
|
||||
w.writeLine(` ${propertyName}: this._${propertyName},`);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
w.writeLine('};');
|
||||
unReqFields.forEach((sPropName) => {
|
||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`);
|
||||
if (isAdditional) {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`);
|
||||
} else {
|
||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`);
|
||||
}
|
||||
|
||||
unReqFields.forEach((propertyName) => {
|
||||
const { isArray, isClass } = sortedPropertiesTypes[propertyName];
|
||||
w.writeLine(`if (typeof this._${propertyName} !== 'undefined') {`);
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` data.${propertyName} = this._${propertyName}.map((p) => p.serialize());`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` data.${propertyName} = this._${propertyName}.serialize();`);
|
||||
} else {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName}.serialize();`);
|
||||
} else {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName};`);
|
||||
|
||||
}
|
||||
w.writeLine(` data.${propertyName} = this._${propertyName};`);
|
||||
|
||||
}
|
||||
|
||||
w.writeLine(`}`);
|
||||
});
|
||||
w.writeLine('return data;');
|
||||
@@ -522,74 +510,55 @@ class EntitiesGenerator {
|
||||
returnType: `string[]`,
|
||||
})
|
||||
validate.setBodyText((w) => {
|
||||
if (additionalPropsOnly) {
|
||||
w.writeLine('return []')
|
||||
return;
|
||||
}
|
||||
w.writeLine('const validate = {');
|
||||
Object.keys(sProps || {}).forEach((sPropName) => {
|
||||
const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
w.writeLine('const validateRequired = {');
|
||||
Object.keys(properties || {}).forEach((propertyName) => {
|
||||
const { isArray, isClass, type, isRequired } = sortedPropertiesTypes[propertyName];
|
||||
const { maxLength, minLength, maximum, minimum } = properties![propertyName];
|
||||
|
||||
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
||||
|
||||
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
|
||||
const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`;
|
||||
const nonRequiredCall = isRequired ? `this._${propertyName}` : `!this._${propertyName} ? true : this._${propertyName}`;
|
||||
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`);
|
||||
} else if (isClass && !isAdditional) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`);
|
||||
w.writeLine(` ${propertyName}: ${nonRequiredCall}.reduce<boolean>((result, p) => result && p.validate().length === 0, true),`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` ${propertyName}: ${nonRequiredCall}.validate().length === 0,`);
|
||||
} else {
|
||||
if (pType === 'string') {
|
||||
if (type === 'string') {
|
||||
if (isArray) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`);
|
||||
w.writeLine(` ${propertyName}: ${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'string', true),`);
|
||||
} else {
|
||||
if (typeof minLength === 'number' && maxLength) {
|
||||
w.writeLine(` ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`);
|
||||
w.writeLine(` ${propertyName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`);
|
||||
}
|
||||
if (typeof minLength !== 'number' || !maxLength) {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`);
|
||||
w.writeLine(` ${propertyName}: ${isRequired ? `typeof this._${propertyName} === 'string' && !!this._${propertyName}.trim()` : `!this._${propertyName} ? true : typeof this._${propertyName} === 'string'`},`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'number') {
|
||||
} else if (type === 'number') {
|
||||
if (isArray) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`);
|
||||
w.writeLine(` ${propertyName}: ${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true),`);
|
||||
} else {
|
||||
if (typeof minimum === 'number' && maximum) {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`);
|
||||
w.writeLine(` ${propertyName}: ${isRequired ? `this._${propertyName} >= ${minimum} && this._${propertyName} <= ${maximum}` : `!this._${propertyName} ? true : ((this._${propertyName} >= ${minimum}) && (this._${propertyName} <= ${maximum}))`},`);
|
||||
}
|
||||
if (typeof minimum !== 'number' || !maximum) {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`);
|
||||
w.writeLine(` ${propertyName}: ${isRequired ? `typeof this._${propertyName} === 'number'` : `!this._${propertyName} ? true : typeof this._${propertyName} === 'number'`},`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'boolean') {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`);
|
||||
} else if (type === 'boolean') {
|
||||
w.writeLine(` ${propertyName}: ${isRequired ? `typeof this._${propertyName} === 'boolean'` : `!this._${propertyName} ? true : typeof this._${propertyName} === 'boolean'`},`);
|
||||
}
|
||||
}
|
||||
});
|
||||
w.writeLine('};');
|
||||
w.writeLine('const isError: string[] = [];')
|
||||
w.writeLine('Object.keys(validate).forEach((key) => {');
|
||||
w.writeLine(' if (!(validate as any)[key]) {');
|
||||
w.writeLine(' isError.push(key);');
|
||||
w.writeLine('const errorInFields: string[] = [];')
|
||||
w.writeLine('Object.keys(validateRequired).forEach((key) => {');
|
||||
w.writeLine(' if (!(validateRequired as any)[key]) {');
|
||||
w.writeLine(' errorInFields.push(key);');
|
||||
w.writeLine(' }');
|
||||
w.writeLine('});');
|
||||
w.writeLine('return isError;');
|
||||
|
||||
w.writeLine('return errorInFields;');
|
||||
});
|
||||
|
||||
// add update method;
|
||||
const update = entityClass.addMethod({
|
||||
isStatic: false,
|
||||
name: 'update',
|
||||
returnType: `${sName}`,
|
||||
});
|
||||
update.addParameter({
|
||||
name: 'props',
|
||||
type: additionalPropsOnly ? `I${sName}` : `Partial<I${sName}>`,
|
||||
});
|
||||
update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); });
|
||||
|
||||
this.entities.push(entityFile);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,65 +19,154 @@ const TYPES = {
|
||||
boolean: 'boolean',
|
||||
};
|
||||
|
||||
export enum SchemaType {
|
||||
STRING = 'string',
|
||||
OBJECT = 'object',
|
||||
ARRAY = 'array',
|
||||
BOOLEAN = 'boolean',
|
||||
NUMBER = 'number',
|
||||
INTEGER = 'integer',
|
||||
}
|
||||
|
||||
export interface Schema {
|
||||
allOf?: any[];
|
||||
example?: string;
|
||||
properties?: Record<string, Schema>;
|
||||
required?: string[];
|
||||
description?: string;
|
||||
enum?: string[];
|
||||
type: SchemaType;
|
||||
pattern?: string;
|
||||
oneOf?: any
|
||||
items?: Schema;
|
||||
additionalProperties?: Schema;
|
||||
$ref?: string;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
maximum?: number;
|
||||
minimum?: number;
|
||||
}
|
||||
|
||||
export interface Parameter {
|
||||
description?: string;
|
||||
example?: string;
|
||||
in?: 'query' | 'body' | 'headers';
|
||||
name: string;
|
||||
schema: Schema;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface RequestBody {
|
||||
content: {
|
||||
'application/json'?: {
|
||||
schema: Schema;
|
||||
example?: string;
|
||||
};
|
||||
}
|
||||
required?: boolean;
|
||||
}
|
||||
export interface Response {
|
||||
content: {
|
||||
'application/json'?: {
|
||||
schema: Schema;
|
||||
example?: string;
|
||||
};
|
||||
'text/palin'?: {
|
||||
example?: string;
|
||||
'x-error-class'?: string;
|
||||
'x-error-code'?: string;
|
||||
}
|
||||
}
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Schemas {
|
||||
parameters: Record<string, Parameter>;
|
||||
requestBodies: Record<string, RequestBody>;
|
||||
responses: Record<string, Response>;
|
||||
schemas: Record<string, Schema>;
|
||||
}
|
||||
|
||||
export interface OpenApi {
|
||||
components: Schemas;
|
||||
paths: any;
|
||||
servers: {
|
||||
description: string;
|
||||
url: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param schemaProp: valueof shema.properties[key]
|
||||
* @param openApi: openapi object
|
||||
* @returns [propType - basicType or import one, isArray, isClass, isImport]
|
||||
*/
|
||||
const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boolean, boolean, boolean] => {
|
||||
interface SchemaParamParserReturn {
|
||||
type: string;
|
||||
isArray: boolean;
|
||||
isClass: boolean;
|
||||
isImport: boolean;
|
||||
isAdditional: boolean;
|
||||
isEnum: boolean;
|
||||
}
|
||||
|
||||
const schemaParamParser = (schemaProp: Schema, openApi: OpenApi): SchemaParamParserReturn => {
|
||||
let type = '';
|
||||
let isImport = false;
|
||||
let isClass = false;
|
||||
let isArray = false;
|
||||
let isAdditional = false;
|
||||
let isEnum = false;
|
||||
|
||||
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
|
||||
const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref).split('/');
|
||||
type = (schemaProp.$ref || schemaProp.additionalProperties?.$ref)!.split('/').pop()!;
|
||||
|
||||
if (schemaProp.additionalProperties) {
|
||||
isAdditional = true;
|
||||
}
|
||||
|
||||
type = `${temp[temp.length - 1]}`;
|
||||
|
||||
const cl = openApi ? openApi.components.schemas[type] : {};
|
||||
const cl = openApi.components.schemas[type];
|
||||
|
||||
if (cl.allOf) {
|
||||
const ref = cl.allOf.find((e) => !!e.$ref);
|
||||
const link = schemaParamParser(ref, openApi);
|
||||
return {...link, type};
|
||||
}
|
||||
|
||||
if (cl.$ref) {
|
||||
const link = schemaParamParser(cl, openApi);
|
||||
link.shift();
|
||||
return [type, ...link] as any;
|
||||
return {...link, type};
|
||||
}
|
||||
|
||||
if (cl.type === 'string' && cl.enum) {
|
||||
isImport = true;
|
||||
isEnum = true;
|
||||
}
|
||||
|
||||
if (cl.type === 'object' && !cl.oneOf) {
|
||||
isClass = true;
|
||||
isImport = true;
|
||||
} else if (cl.type === 'array') {
|
||||
const temp: any = schemaParamParser(cl.items, openApi);
|
||||
type = `${temp[0]}`;
|
||||
const temp = schemaParamParser(cl.items!, openApi);
|
||||
type = temp.type;
|
||||
isArray = true;
|
||||
isClass = isClass || temp[2];
|
||||
isImport = isImport || temp[3];
|
||||
isClass = isClass || temp.isClass;
|
||||
isImport = isImport || temp.isImport;
|
||||
isEnum = isEnum || temp.isEnum;
|
||||
}
|
||||
} else if (schemaProp.type === 'array') {
|
||||
const temp: any = schemaParamParser(schemaProp.items, openApi);
|
||||
type = `${temp[0]}`;
|
||||
const temp = schemaParamParser(schemaProp.items!, openApi);
|
||||
type = temp.type
|
||||
isArray = true;
|
||||
isClass = isClass || temp[2];
|
||||
isImport = isImport || temp[3];
|
||||
isClass = isClass || temp.isClass;
|
||||
isImport = isImport || temp.isImport;
|
||||
isEnum = isEnum || temp.isEnum;
|
||||
} else {
|
||||
type = (TYPES as Record<any, string>)[schemaProp.type];
|
||||
}
|
||||
if (!type) {
|
||||
// TODO: Fix bug with Error fields.
|
||||
type = 'any';
|
||||
// throw new Error('Failed to find entity type');
|
||||
}
|
||||
|
||||
return [type, isArray, isClass, isImport, isAdditional];
|
||||
return { type, isArray, isClass, isImport, isAdditional, isEnum };
|
||||
};
|
||||
|
||||
export { TYPES, toCamel, capitalize, uncapitalize, schemaParamParser };
|
||||
|
||||
@@ -370,17 +370,16 @@
|
||||
remark "^13.0.0"
|
||||
unist-util-find-all-after "^3.0.2"
|
||||
|
||||
"@ts-morph/common@~0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.6.0.tgz#cbd4ee57c5ef971511b9c5778e0bb8eb27de4783"
|
||||
integrity sha512-pI35nZz5bs3tL3btSVX2cWkAE8rc80F+Fn4TwSC6bQvn7fgn9IyLXVcAfpG6X6NBY5wN9TkSWXn/QYUkBvR/Fw==
|
||||
"@ts-morph/common@~0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.8.0.tgz#ae7b292df8258040465c50b378108ec8f09a9516"
|
||||
integrity sha512-YbjWiMXLMKxWxcMqP47nwZVWVBwoF5B65dtRz0lya2LetjldAPxTxRbRo1n4Iszr2tSvzXeaa+f1AbULmfc5uA==
|
||||
dependencies:
|
||||
"@dsherret/to-absolute-glob" "^2.0.2"
|
||||
fast-glob "^3.2.4"
|
||||
fs-extra "^9.0.1"
|
||||
fast-glob "^3.2.5"
|
||||
is-negated-glob "^1.0.0"
|
||||
multimatch "^4.0.0"
|
||||
typescript "~4.0.2"
|
||||
mkdirp "^1.0.4"
|
||||
multimatch "^5.0.0"
|
||||
|
||||
"@types/anymatch@*":
|
||||
version "1.3.1"
|
||||
@@ -1172,11 +1171,6 @@ async@^2.6.2:
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
at-least-node@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
atob@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
@@ -1593,7 +1587,7 @@ coa@^2.0.2:
|
||||
chalk "^2.4.1"
|
||||
q "^1.1.2"
|
||||
|
||||
code-block-writer@^10.1.0:
|
||||
code-block-writer@^10.1.1:
|
||||
version "10.1.1"
|
||||
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f"
|
||||
integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==
|
||||
@@ -2910,9 +2904,9 @@ fastest-levenshtein@^1.0.12:
|
||||
integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb"
|
||||
integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
|
||||
integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
@@ -3084,16 +3078,6 @@ fs-extra@^8.1.0:
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^9.0.1:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
||||
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
|
||||
dependencies:
|
||||
at-least-node "^1.0.0"
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-minipass@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||
@@ -3267,11 +3251,16 @@ gonzales-pe@^4.3.0:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.4:
|
||||
version "4.2.4"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
version "4.2.6"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
|
||||
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
|
||||
|
||||
handle-thing@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||
@@ -4145,15 +4134,6 @@ jsonfile@^4.0.0:
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
"jsx-ast-utils@^2.4.1 || ^3.0.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82"
|
||||
@@ -4784,10 +4764,10 @@ multicast-dns@^6.0.1:
|
||||
dns-packet "^1.3.1"
|
||||
thunky "^1.0.2"
|
||||
|
||||
multimatch@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3"
|
||||
integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==
|
||||
multimatch@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6"
|
||||
integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==
|
||||
dependencies:
|
||||
"@types/minimatch" "^3.0.3"
|
||||
array-differ "^3.0.0"
|
||||
@@ -6292,6 +6272,11 @@ querystringify@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
|
||||
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3"
|
||||
integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==
|
||||
|
||||
quick-lru@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
||||
@@ -7086,9 +7071,11 @@ rimraf@^3.0.2:
|
||||
glob "^7.1.3"
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.1.10"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef"
|
||||
integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
@@ -7942,14 +7929,14 @@ ts-loader@^8.0.6:
|
||||
micromatch "^4.0.0"
|
||||
semver "^7.3.4"
|
||||
|
||||
ts-morph@^8.1.2:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-8.2.0.tgz#41d83cd501cbd897eb029ac489d6d5b927555c57"
|
||||
integrity sha512-NHHWu+7I2/AOZiTni5w3f+xCfIxrkzPCcQbTGa81Yk3pr23a4h9xLLEE6tIGuYIubWjkjr9QVC3ITqgmA5touQ==
|
||||
ts-morph@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-10.0.1.tgz#5a620cc4ef85e3e6d161989e690f44d0a0f723b0"
|
||||
integrity sha512-T1zufImtp5goTLTFhzi7XuKR1y/f+Jwz1gSULzB045LFjXuoqVlR87sfkpyWow8u2JwgusCJrhOnwmHCFNutTQ==
|
||||
dependencies:
|
||||
"@dsherret/to-absolute-glob" "^2.0.2"
|
||||
"@ts-morph/common" "~0.6.0"
|
||||
code-block-writer "^10.1.0"
|
||||
"@ts-morph/common" "~0.8.0"
|
||||
code-block-writer "^10.1.1"
|
||||
|
||||
ts-node@^9.0.0:
|
||||
version "9.1.1"
|
||||
@@ -8032,11 +8019,6 @@ typescript@^4.0.3:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
|
||||
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
||||
|
||||
typescript@~4.0.2:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
|
||||
integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
|
||||
|
||||
unc-path-regex@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
|
||||
@@ -8112,11 +8094,6 @@ universalify@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.14
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.33.9
|
||||
github.com/AdguardTeam/golibs v0.4.4
|
||||
github.com/AdguardTeam/urlfilter v0.14.2
|
||||
github.com/AdguardTeam/urlfilter v0.14.3
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/ameshkov/dnscrypt/v2 v2.0.1
|
||||
github.com/digineo/go-ipset/v2 v2.2.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -26,8 +26,8 @@ github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
|
||||
github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw=
|
||||
github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||
github.com/AdguardTeam/urlfilter v0.14.2 h1:k26vEYz0mT/liDGZ0JGIBLYLMHaisIGX1UR0qaVnO4k=
|
||||
github.com/AdguardTeam/urlfilter v0.14.2/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U=
|
||||
github.com/AdguardTeam/urlfilter v0.14.3 h1:MBaLEXS0LRQNbHtLkDCYhHINDPtkevPrYWGiOUuLJU4=
|
||||
github.com/AdguardTeam/urlfilter v0.14.3/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLimitReadCloser(t *testing.T) {
|
||||
@@ -78,11 +79,11 @@ func TestLimitedReadCloser_Read(t *testing.T) {
|
||||
buf := make([]byte, tc.limit+1)
|
||||
|
||||
lreader, err := LimitReadCloser(readCloser, tc.limit)
|
||||
assert.Nil(t, err)
|
||||
require.Nil(t, err)
|
||||
|
||||
n, err := lreader.Read(buf)
|
||||
assert.Equal(t, n, tc.want)
|
||||
assert.Equal(t, tc.err, err)
|
||||
require.Equal(t, tc.err, err)
|
||||
assert.Equal(t, tc.want, n)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
45
internal/aghtest/os.go
Normal file
45
internal/aghtest/os.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package aghtest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// PrepareTestDir returns the full path to temporary created directory and
|
||||
// registers the appropriate cleanup for *t.
|
||||
func PrepareTestDir(t *testing.T) (dir string) {
|
||||
t.Helper()
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.Nil(t, err)
|
||||
|
||||
dir, err = ioutil.TempDir(wd, "agh-test")
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, dir)
|
||||
|
||||
t.Cleanup(func() {
|
||||
// TODO(e.burkov): Replace with t.TempDir methods after updating
|
||||
// go version to 1.15.
|
||||
start := time.Now()
|
||||
for {
|
||||
err := os.RemoveAll(dir)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" || time.Since(start) >= 500*time.Millisecond {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
return dir
|
||||
}
|
||||
@@ -19,7 +19,12 @@ import (
|
||||
|
||||
const (
|
||||
defaultDiscoverTime = time.Second * 3
|
||||
leaseExpireStatic = 1
|
||||
// leaseExpireStatic is used to define the Expiry field for static
|
||||
// leases.
|
||||
//
|
||||
// TODO(e.burkov): Remove it when static leases determining mechanism
|
||||
// will be improved.
|
||||
leaseExpireStatic = 1
|
||||
)
|
||||
|
||||
var webHandlersRegistered = false
|
||||
@@ -37,12 +42,24 @@ type Lease struct {
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for *Lease.
|
||||
func (l *Lease) MarshalJSON() ([]byte, error) {
|
||||
var expiryStr string
|
||||
if expiry := l.Expiry; expiry.Unix() != leaseExpireStatic {
|
||||
// The front-end is waiting for RFC 3999 format of the time
|
||||
// value. It also shouldn't got an Expiry field for static
|
||||
// leases.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.
|
||||
expiryStr = expiry.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
type lease Lease
|
||||
return json.Marshal(&struct {
|
||||
HWAddr string `json:"mac"`
|
||||
Expiry string `json:"expires,omitempty"`
|
||||
*lease
|
||||
}{
|
||||
HWAddr: l.HWAddr.String(),
|
||||
Expiry: expiryStr,
|
||||
lease: (*lease)(l),
|
||||
})
|
||||
}
|
||||
@@ -117,14 +134,14 @@ type ServerInterface interface {
|
||||
}
|
||||
|
||||
// Create - create object
|
||||
func Create(config ServerConfig) *Server {
|
||||
func Create(conf ServerConfig) *Server {
|
||||
s := &Server{}
|
||||
|
||||
s.conf.Enabled = config.Enabled
|
||||
s.conf.InterfaceName = config.InterfaceName
|
||||
s.conf.HTTPRegister = config.HTTPRegister
|
||||
s.conf.ConfigModified = config.ConfigModified
|
||||
s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename)
|
||||
s.conf.Enabled = conf.Enabled
|
||||
s.conf.InterfaceName = conf.InterfaceName
|
||||
s.conf.HTTPRegister = conf.HTTPRegister
|
||||
s.conf.ConfigModified = conf.ConfigModified
|
||||
s.conf.DBFilePath = filepath.Join(conf.WorkDir, dbFilename)
|
||||
|
||||
if !webHandlersRegistered && s.conf.HTTPRegister != nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -145,7 +162,7 @@ func Create(config ServerConfig) *Server {
|
||||
}
|
||||
|
||||
var err4, err6 error
|
||||
v4conf := config.Conf4
|
||||
v4conf := conf.Conf4
|
||||
v4conf.Enabled = s.conf.Enabled
|
||||
if len(v4conf.RangeStart) == 0 {
|
||||
v4conf.Enabled = false
|
||||
@@ -154,7 +171,7 @@ func Create(config ServerConfig) *Server {
|
||||
v4conf.notify = s.onNotify
|
||||
s.srv4, err4 = v4Create(v4conf)
|
||||
|
||||
v6conf := config.Conf6
|
||||
v6conf := conf.Conf6
|
||||
v6conf.Enabled = s.conf.Enabled
|
||||
if len(v6conf.RangeStart) == 0 {
|
||||
v6conf.Enabled = false
|
||||
@@ -172,6 +189,9 @@ func Create(config ServerConfig) *Server {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.conf.Conf4 = conf.Conf4
|
||||
s.conf.Conf6 = conf.Conf6
|
||||
|
||||
if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled {
|
||||
log.Error("Can't enable DHCP server because neither DHCPv4 nor DHCPv6 servers are configured")
|
||||
return nil
|
||||
@@ -245,14 +265,10 @@ const (
|
||||
LeasesAll = LeasesDynamic | LeasesStatic
|
||||
)
|
||||
|
||||
// Leases returns the list of current DHCP leases (thread-safe)
|
||||
func (s *Server) Leases(flags int) []Lease {
|
||||
result := s.srv4.GetLeases(flags)
|
||||
|
||||
v6leases := s.srv6.GetLeases(flags)
|
||||
result = append(result, v6leases...)
|
||||
|
||||
return result
|
||||
// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for
|
||||
// concurrent use.
|
||||
func (s *Server) Leases(flags int) (leases []Lease) {
|
||||
return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...)
|
||||
}
|
||||
|
||||
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
|
||||
@@ -290,14 +306,22 @@ func parseOptionString(s string) (uint8, []byte) {
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
case "ip":
|
||||
ip := net.ParseIP(sval)
|
||||
if ip == nil {
|
||||
return 0, nil
|
||||
}
|
||||
val = ip
|
||||
|
||||
// Most DHCP options require IPv4, so do not put the 16-byte
|
||||
// version if we can. Otherwise, the clients will receive weird
|
||||
// data that looks like four IPv4 addresses.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2688.
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
val = ip4
|
||||
} else {
|
||||
val = ip
|
||||
}
|
||||
default:
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -20,116 +20,171 @@ func TestMain(m *testing.M) {
|
||||
func testNotify(flags uint32) {
|
||||
}
|
||||
|
||||
// Leases database store/load
|
||||
// Leases database store/load.
|
||||
func TestDB(t *testing.T) {
|
||||
var err error
|
||||
s := Server{}
|
||||
s.conf.DBFilePath = dbFilename
|
||||
s := Server{
|
||||
conf: ServerConfig{
|
||||
DBFilePath: dbFilename,
|
||||
},
|
||||
}
|
||||
|
||||
conf := V4ServerConf{
|
||||
s.srv4, err = v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: testNotify,
|
||||
}
|
||||
s.srv4, err = v4Create(conf)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
s.srv6, err = v6Create(V6ServerConf{})
|
||||
assert.Nil(t, err)
|
||||
require.Nil(t, err)
|
||||
|
||||
l := Lease{}
|
||||
l.IP = net.IP{192, 168, 10, 100}
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
exp1 := time.Now().Add(time.Hour)
|
||||
l.Expiry = exp1
|
||||
leases := []Lease{{
|
||||
IP: net.IP{192, 168, 10, 100},
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
Expiry: time.Now().Add(time.Hour),
|
||||
}, {
|
||||
IP: net.IP{192, 168, 10, 101},
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB},
|
||||
}}
|
||||
|
||||
srv4, ok := s.srv4.(*v4Server)
|
||||
assert.True(t, ok)
|
||||
require.True(t, ok)
|
||||
|
||||
srv4.addLease(&l)
|
||||
srv4.addLease(&leases[0])
|
||||
require.Nil(t, s.srv4.AddStaticLease(leases[1]))
|
||||
|
||||
l2 := Lease{}
|
||||
l2.IP = net.IP{192, 168, 10, 101}
|
||||
l2.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:bb")
|
||||
err = s.srv4.AddStaticLease(l2)
|
||||
assert.Nil(t, err)
|
||||
|
||||
_ = os.Remove("leases.db")
|
||||
s.dbStore()
|
||||
t.Cleanup(func() {
|
||||
assert.Nil(t, os.Remove(dbFilename))
|
||||
})
|
||||
s.srv4.ResetLeases(nil)
|
||||
|
||||
s.dbLoad()
|
||||
|
||||
ll := s.srv4.GetLeases(LeasesAll)
|
||||
require.Len(t, ll, len(leases))
|
||||
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:bb", ll[0].HWAddr.String())
|
||||
assert.True(t, net.IP{192, 168, 10, 101}.Equal(ll[0].IP))
|
||||
assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr)
|
||||
assert.Equal(t, leases[1].IP, ll[0].IP)
|
||||
assert.EqualValues(t, leaseExpireStatic, ll[0].Expiry.Unix())
|
||||
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ll[1].HWAddr.String())
|
||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(ll[1].IP))
|
||||
assert.Equal(t, exp1.Unix(), ll[1].Expiry.Unix())
|
||||
|
||||
_ = os.Remove("leases.db")
|
||||
assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr)
|
||||
assert.Equal(t, leases[0].IP, ll[1].IP)
|
||||
assert.Equal(t, leases[0].Expiry.Unix(), ll[1].Expiry.Unix())
|
||||
}
|
||||
|
||||
func TestIsValidSubnetMask(t *testing.T) {
|
||||
assert.True(t, isValidSubnetMask([]byte{255, 255, 255, 0}))
|
||||
assert.True(t, isValidSubnetMask([]byte{255, 255, 254, 0}))
|
||||
assert.True(t, isValidSubnetMask([]byte{255, 255, 252, 0}))
|
||||
assert.False(t, isValidSubnetMask([]byte{255, 255, 253, 0}))
|
||||
assert.False(t, isValidSubnetMask([]byte{255, 255, 255, 1}))
|
||||
testCases := []struct {
|
||||
mask net.IP
|
||||
want bool
|
||||
}{{
|
||||
mask: net.IP{255, 255, 255, 0},
|
||||
want: true,
|
||||
}, {
|
||||
mask: net.IP{255, 255, 254, 0},
|
||||
want: true,
|
||||
}, {
|
||||
mask: net.IP{255, 255, 252, 0},
|
||||
want: true,
|
||||
}, {
|
||||
mask: net.IP{255, 255, 253, 0},
|
||||
}, {
|
||||
mask: net.IP{255, 255, 255, 1},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.mask.String(), func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, isValidSubnetMask(tc.mask))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLeases(t *testing.T) {
|
||||
dynLeases := []*Lease{}
|
||||
staticLeases := []*Lease{}
|
||||
dynLeases := []*Lease{{
|
||||
HWAddr: net.HardwareAddr{1, 2, 3, 4},
|
||||
}, {
|
||||
HWAddr: net.HardwareAddr{1, 2, 3, 5},
|
||||
}}
|
||||
|
||||
lease := &Lease{}
|
||||
lease.HWAddr = []byte{1, 2, 3, 4}
|
||||
dynLeases = append(dynLeases, lease)
|
||||
lease = new(Lease)
|
||||
lease.HWAddr = []byte{1, 2, 3, 5}
|
||||
dynLeases = append(dynLeases, lease)
|
||||
|
||||
lease = new(Lease)
|
||||
lease.HWAddr = []byte{1, 2, 3, 4}
|
||||
lease.IP = []byte{0, 2, 3, 4}
|
||||
staticLeases = append(staticLeases, lease)
|
||||
lease = new(Lease)
|
||||
lease.HWAddr = []byte{2, 2, 3, 4}
|
||||
staticLeases = append(staticLeases, lease)
|
||||
staticLeases := []*Lease{{
|
||||
HWAddr: net.HardwareAddr{1, 2, 3, 4},
|
||||
IP: net.IP{0, 2, 3, 4},
|
||||
}, {
|
||||
HWAddr: net.HardwareAddr{2, 2, 3, 4},
|
||||
}}
|
||||
|
||||
leases := normalizeLeases(staticLeases, dynLeases)
|
||||
require.Len(t, leases, 3)
|
||||
|
||||
assert.Len(t, leases, 3)
|
||||
assert.True(t, bytes.Equal(leases[0].HWAddr, []byte{1, 2, 3, 4}))
|
||||
assert.True(t, bytes.Equal(leases[0].IP, []byte{0, 2, 3, 4}))
|
||||
assert.True(t, bytes.Equal(leases[1].HWAddr, []byte{2, 2, 3, 4}))
|
||||
assert.True(t, bytes.Equal(leases[2].HWAddr, []byte{1, 2, 3, 5}))
|
||||
assert.Equal(t, leases[0].HWAddr, dynLeases[0].HWAddr)
|
||||
assert.Equal(t, leases[0].IP, staticLeases[0].IP)
|
||||
assert.Equal(t, leases[1].HWAddr, staticLeases[1].HWAddr)
|
||||
assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr)
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
code, val := parseOptionString(" 12 hex abcdef ")
|
||||
assert.EqualValues(t, 12, code)
|
||||
assert.True(t, bytes.Equal([]byte{0xab, 0xcd, 0xef}, val))
|
||||
testCases := []struct {
|
||||
name string
|
||||
optStr string
|
||||
wantVal []byte
|
||||
wantCode uint8
|
||||
}{{
|
||||
name: "success_hex",
|
||||
optStr: "12 hex abcdef",
|
||||
wantVal: []byte{0xab, 0xcd, 0xef},
|
||||
wantCode: 12,
|
||||
}, {
|
||||
name: "bad_hex",
|
||||
optStr: "12 hex abcdefx",
|
||||
wantVal: nil,
|
||||
wantCode: 0,
|
||||
}, {
|
||||
name: "success_ip",
|
||||
optStr: "123 ip 1.2.3.4",
|
||||
wantVal: net.IP{1, 2, 3, 4},
|
||||
wantCode: 123,
|
||||
}, {
|
||||
name: "success_ipv6",
|
||||
optStr: "123 ip ::1234",
|
||||
wantVal: net.IP{
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0x12, 0x34,
|
||||
},
|
||||
wantCode: 123,
|
||||
}, {
|
||||
name: "bad_code",
|
||||
optStr: "256 ip 1.1.1.1",
|
||||
wantVal: nil,
|
||||
wantCode: 0,
|
||||
}, {
|
||||
name: "negative_code",
|
||||
optStr: "-1 ip 1.1.1.1",
|
||||
wantVal: nil,
|
||||
wantCode: 0,
|
||||
}, {
|
||||
name: "bad_ip",
|
||||
optStr: "12 ip 1.1.1.1x",
|
||||
wantVal: nil,
|
||||
wantCode: 0,
|
||||
}, {
|
||||
name: "bad_mode",
|
||||
wantVal: nil,
|
||||
optStr: "12 x 1.1.1.1",
|
||||
wantCode: 0,
|
||||
}}
|
||||
|
||||
code, _ = parseOptionString(" 12 hex abcdef1 ")
|
||||
assert.EqualValues(t, 0, code)
|
||||
|
||||
code, val = parseOptionString("123 ip 1.2.3.4")
|
||||
assert.EqualValues(t, 123, code)
|
||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(net.IP(val)))
|
||||
|
||||
code, _ = parseOptionString("256 ip 1.1.1.1")
|
||||
assert.EqualValues(t, 0, code)
|
||||
code, _ = parseOptionString("-1 ip 1.1.1.1")
|
||||
assert.EqualValues(t, 0, code)
|
||||
code, _ = parseOptionString("12 ip 1.1.1.1x")
|
||||
assert.EqualValues(t, 0, code)
|
||||
code, _ = parseOptionString("12 x 1.1.1.1")
|
||||
assert.EqualValues(t, 0, code)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
code, val := parseOptionString(tc.optStr)
|
||||
require.Equal(t, tc.wantCode, code)
|
||||
if tc.wantVal != nil {
|
||||
assert.Equal(t, tc.wantVal, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package dhcpd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/sysutil"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/util"
|
||||
"github.com/AdguardTeam/golibs/jsonutil"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
@@ -29,7 +29,11 @@ type v4ServerConfJSON struct {
|
||||
LeaseDuration uint32 `json:"lease_duration"`
|
||||
}
|
||||
|
||||
func v4JSONToServerConf(j v4ServerConfJSON) V4ServerConf {
|
||||
func v4JSONToServerConf(j *v4ServerConfJSON) V4ServerConf {
|
||||
if j == nil {
|
||||
return V4ServerConf{}
|
||||
}
|
||||
|
||||
return V4ServerConf{
|
||||
GatewayIP: j.GatewayIP,
|
||||
SubnetMask: j.SubnetMask,
|
||||
@@ -44,7 +48,11 @@ type v6ServerConfJSON struct {
|
||||
LeaseDuration uint32 `json:"lease_duration"`
|
||||
}
|
||||
|
||||
func v6JSONToServerConf(j v6ServerConfJSON) V6ServerConf {
|
||||
func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
|
||||
if j == nil {
|
||||
return V6ServerConf{}
|
||||
}
|
||||
|
||||
return V6ServerConf{
|
||||
RangeStart: j.RangeStart,
|
||||
LeaseDuration: j.LeaseDuration,
|
||||
@@ -83,24 +91,44 @@ func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
type dhcpServerConfigJSON struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
InterfaceName string `json:"interface_name"`
|
||||
V4 v4ServerConfJSON `json:"v4"`
|
||||
V6 v6ServerConfJSON `json:"v6"`
|
||||
}
|
||||
|
||||
func (s *Server) enableDHCP(ifaceName string) (code int, err error) {
|
||||
var hasStaticIP bool
|
||||
hasStaticIP, err = sysutil.IfaceHasStaticIP(ifaceName)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("checking static ip: %w", err)
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
// ErrPermission may happen here on Linux systems where
|
||||
// AdGuard Home is installed using Snap. That doesn't
|
||||
// necessarily mean that the machine doesn't have
|
||||
// a static IP, so we can assume that it has and go on.
|
||||
// If the machine doesn't, we'll get an error later.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2667.
|
||||
//
|
||||
// TODO(a.garipov): I was thinking about moving this
|
||||
// into IfaceHasStaticIP, but then we wouldn't be able
|
||||
// to log it. Think about it more.
|
||||
log.Info("error while checking static ip: %s; "+
|
||||
"assuming machine has static ip and going on", err)
|
||||
hasStaticIP = true
|
||||
} else if errors.Is(err, sysutil.ErrNoStaticIPInfo) {
|
||||
// Couldn't obtain a definitive answer. Assume static
|
||||
// IP an go on.
|
||||
log.Info("can't check for static ip; " +
|
||||
"assuming machine has static ip and going on")
|
||||
hasStaticIP = true
|
||||
} else {
|
||||
err = fmt.Errorf("checking static ip: %w", err)
|
||||
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
if !hasStaticIP {
|
||||
err = sysutil.IfaceSetStaticIP(ifaceName)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("setting static ip: %w", err)
|
||||
err = fmt.Errorf("setting static ip: %w", err)
|
||||
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,14 +140,22 @@ func (s *Server) enableDHCP(ifaceName string) (code int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
newconfig := dhcpServerConfigJSON{}
|
||||
newconfig.Enabled = s.conf.Enabled
|
||||
newconfig.InterfaceName = s.conf.InterfaceName
|
||||
type dhcpServerConfigJSON struct {
|
||||
V4 *v4ServerConfJSON `json:"v4"`
|
||||
V6 *v6ServerConfJSON `json:"v6"`
|
||||
InterfaceName string `json:"interface_name"`
|
||||
Enabled nullBool `json:"enabled"`
|
||||
}
|
||||
|
||||
js, err := jsonutil.DecodeObject(&newconfig, r.Body)
|
||||
func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf := dhcpServerConfigJSON{}
|
||||
conf.Enabled = boolToNullBool(s.conf.Enabled)
|
||||
conf.InterfaceName = s.conf.InterfaceName
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&conf)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
|
||||
httpError(r, w, http.StatusBadRequest,
|
||||
"failed to parse new dhcp config json: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -129,62 +165,72 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
v4Enabled := false
|
||||
v6Enabled := false
|
||||
|
||||
if js.Exists("v4") {
|
||||
v4conf := v4JSONToServerConf(newconfig.V4)
|
||||
v4conf.Enabled = newconfig.Enabled
|
||||
if len(v4conf.RangeStart) == 0 {
|
||||
v4conf.Enabled = false
|
||||
if conf.V4 != nil {
|
||||
v4Conf := v4JSONToServerConf(conf.V4)
|
||||
v4Conf.Enabled = conf.Enabled == nbTrue
|
||||
if len(v4Conf.RangeStart) == 0 {
|
||||
v4Conf.Enabled = false
|
||||
}
|
||||
|
||||
v4Enabled = v4conf.Enabled
|
||||
v4conf.InterfaceName = newconfig.InterfaceName
|
||||
v4Enabled = v4Conf.Enabled
|
||||
v4Conf.InterfaceName = conf.InterfaceName
|
||||
|
||||
c4 := V4ServerConf{}
|
||||
s.srv4.WriteDiskConfig4(&c4)
|
||||
v4conf.notify = c4.notify
|
||||
v4conf.ICMPTimeout = c4.ICMPTimeout
|
||||
v4Conf.notify = c4.notify
|
||||
v4Conf.ICMPTimeout = c4.ICMPTimeout
|
||||
|
||||
s4, err = v4Create(v4conf)
|
||||
s4, err = v4Create(v4Conf)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "invalid dhcpv4 configuration: %s", err)
|
||||
httpError(r, w, http.StatusBadRequest,
|
||||
"invalid dhcpv4 configuration: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if js.Exists("v6") {
|
||||
v6conf := v6JSONToServerConf(newconfig.V6)
|
||||
v6conf.Enabled = newconfig.Enabled
|
||||
if len(v6conf.RangeStart) == 0 {
|
||||
v6conf.Enabled = false
|
||||
if conf.V6 != nil {
|
||||
v6Conf := v6JSONToServerConf(conf.V6)
|
||||
v6Conf.Enabled = conf.Enabled == nbTrue
|
||||
if len(v6Conf.RangeStart) == 0 {
|
||||
v6Conf.Enabled = false
|
||||
}
|
||||
|
||||
v6Enabled = v6conf.Enabled
|
||||
v6conf.InterfaceName = newconfig.InterfaceName
|
||||
v6conf.notify = s.onNotify
|
||||
// Don't overwrite the RA/SLAAC settings from the config file.
|
||||
//
|
||||
// TODO(a.garipov): Perhaps include them into the request to
|
||||
// allow changing them from the HTTP API?
|
||||
v6Conf.RASLAACOnly = s.conf.Conf6.RASLAACOnly
|
||||
v6Conf.RAAllowSLAAC = s.conf.Conf6.RAAllowSLAAC
|
||||
|
||||
s6, err = v6Create(v6conf)
|
||||
v6Enabled = v6Conf.Enabled
|
||||
v6Conf.InterfaceName = conf.InterfaceName
|
||||
v6Conf.notify = s.onNotify
|
||||
|
||||
s6, err = v6Create(v6Conf)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "invalid dhcpv6 configuration: %s", err)
|
||||
httpError(r, w, http.StatusBadRequest,
|
||||
"invalid dhcpv6 configuration: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if newconfig.Enabled && !v4Enabled && !v6Enabled {
|
||||
httpError(r, w, http.StatusBadRequest, "dhcpv4 or dhcpv6 configuration must be complete")
|
||||
if conf.Enabled == nbTrue && !v4Enabled && !v6Enabled {
|
||||
httpError(r, w, http.StatusBadRequest,
|
||||
"dhcpv4 or dhcpv6 configuration must be complete")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s.Stop()
|
||||
|
||||
if js.Exists("enabled") {
|
||||
s.conf.Enabled = newconfig.Enabled
|
||||
if conf.Enabled != nbNull {
|
||||
s.conf.Enabled = conf.Enabled == nbTrue
|
||||
}
|
||||
|
||||
if js.Exists("interface_name") {
|
||||
s.conf.InterfaceName = newconfig.InterfaceName
|
||||
if conf.InterfaceName != "" {
|
||||
s.conf.InterfaceName = conf.InterfaceName
|
||||
}
|
||||
|
||||
if s4 != nil {
|
||||
@@ -200,7 +246,7 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if s.conf.Enabled {
|
||||
var code int
|
||||
code, err = s.enableDHCP(newconfig.InterfaceName)
|
||||
code, err = s.enableDHCP(conf.InterfaceName)
|
||||
if err != nil {
|
||||
httpError(r, w, code, "enabling dhcp: %s", err)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServer_notImplemented(t *testing.T) {
|
||||
@@ -14,7 +15,7 @@ func TestServer_notImplemented(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodGet, "/unsupported", nil)
|
||||
assert.Nil(t, err)
|
||||
require.Nil(t, err)
|
||||
|
||||
h(w, r)
|
||||
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
||||
58
internal/dhcpd/nullbool.go
Normal file
58
internal/dhcpd/nullbool.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// nullBool is a nullable boolean. Use these in JSON requests and responses
|
||||
// instead of pointers to bool.
|
||||
//
|
||||
// TODO(a.garipov): Inspect uses of *bool, move this type into some new package
|
||||
// if we need it somewhere else.
|
||||
type nullBool uint8
|
||||
|
||||
// nullBool values
|
||||
const (
|
||||
nbNull nullBool = iota
|
||||
nbTrue
|
||||
nbFalse
|
||||
)
|
||||
|
||||
// String implements the fmt.Stringer interface for nullBool.
|
||||
func (nb nullBool) String() (s string) {
|
||||
switch nb {
|
||||
case nbNull:
|
||||
return "null"
|
||||
case nbTrue:
|
||||
return "true"
|
||||
case nbFalse:
|
||||
return "false"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("!invalid nullBool %d", uint8(nb))
|
||||
}
|
||||
|
||||
// boolToNullBool converts a bool into a nullBool.
|
||||
func boolToNullBool(cond bool) (nb nullBool) {
|
||||
if cond {
|
||||
return nbTrue
|
||||
}
|
||||
|
||||
return nbFalse
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for *nullBool.
|
||||
func (nb *nullBool) UnmarshalJSON(b []byte) (err error) {
|
||||
if len(b) == 0 || bytes.Equal(b, []byte("null")) {
|
||||
*nb = nbNull
|
||||
} else if bytes.Equal(b, []byte("true")) {
|
||||
*nb = nbTrue
|
||||
} else if bytes.Equal(b, []byte("false")) {
|
||||
*nb = nbFalse
|
||||
} else {
|
||||
return fmt.Errorf("invalid nullBool value %q", b)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
69
internal/dhcpd/nullbool_test.go
Normal file
69
internal/dhcpd/nullbool_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNullBool_UnmarshalText(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantErrMsg string
|
||||
want nullBool
|
||||
}{{
|
||||
name: "empty",
|
||||
data: []byte{},
|
||||
wantErrMsg: "",
|
||||
want: nbNull,
|
||||
}, {
|
||||
name: "null",
|
||||
data: []byte("null"),
|
||||
wantErrMsg: "",
|
||||
want: nbNull,
|
||||
}, {
|
||||
name: "true",
|
||||
data: []byte("true"),
|
||||
wantErrMsg: "",
|
||||
want: nbTrue,
|
||||
}, {
|
||||
name: "false",
|
||||
data: []byte("false"),
|
||||
wantErrMsg: "",
|
||||
want: nbFalse,
|
||||
}, {
|
||||
name: "invalid",
|
||||
data: []byte("flase"),
|
||||
wantErrMsg: `invalid nullBool value "flase"`,
|
||||
want: nbNull,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got nullBool
|
||||
err := got.UnmarshalJSON(tc.data)
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
want := nbTrue
|
||||
var got struct {
|
||||
A nullBool
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(`{"A":true}`), &got)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, want, got.A)
|
||||
})
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
)
|
||||
|
||||
type raCtx struct {
|
||||
raAllowSlaac bool // send RA packets without MO flags
|
||||
raSlaacOnly bool // send RA packets with MO flags
|
||||
raAllowSLAAC bool // send RA packets without MO flags
|
||||
raSLAACOnly bool // send RA packets with MO flags
|
||||
ipAddr net.IP // source IP address (link-local-unicast)
|
||||
dnsIPAddr net.IP // IP address for DNS Server option
|
||||
prefixIPAddr net.IP // IP address for Prefix option
|
||||
@@ -159,7 +159,7 @@ func createICMPv6RAPacket(params icmpv6RA) []byte {
|
||||
func (ra *raCtx) Init() error {
|
||||
ra.stop.Store(0)
|
||||
ra.conn = nil
|
||||
if !(ra.raAllowSlaac || ra.raSlaacOnly) {
|
||||
if !(ra.raAllowSLAAC || ra.raSLAACOnly) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -167,8 +167,8 @@ func (ra *raCtx) Init() error {
|
||||
ra.ipAddr, ra.dnsIPAddr)
|
||||
|
||||
params := icmpv6RA{
|
||||
managedAddressConfiguration: !ra.raSlaacOnly,
|
||||
otherConfiguration: !ra.raSlaacOnly,
|
||||
managedAddressConfiguration: !ra.raSLAACOnly,
|
||||
otherConfiguration: !ra.raSLAACOnly,
|
||||
mtu: uint32(ra.iface.MTU),
|
||||
prefixLen: 64,
|
||||
recursiveDNSServer: ra.dnsIPAddr,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
@@ -9,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestRA(t *testing.T) {
|
||||
ra := icmpv6RA{
|
||||
data := createICMPv6RAPacket(icmpv6RA{
|
||||
managedAddressConfiguration: false,
|
||||
otherConfiguration: true,
|
||||
mtu: 1500,
|
||||
@@ -17,8 +16,7 @@ func TestRA(t *testing.T) {
|
||||
prefixLen: 64,
|
||||
recursiveDNSServer: net.ParseIP("fe80::800:27ff:fe00:0"),
|
||||
sourceLinkLayerAddress: []byte{0x0a, 0x00, 0x27, 0x00, 0x00, 0x00},
|
||||
}
|
||||
data := createICMPv6RAPacket(ra)
|
||||
})
|
||||
dataCorrect := []byte{
|
||||
0x86, 0x00, 0x00, 0x00, 0x40, 0x40, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||
@@ -27,5 +25,5 @@ func TestRA(t *testing.T) {
|
||||
0x19, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x10, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x08, 0x00, 0x27, 0xff, 0xfe, 0x00, 0x00, 0x00,
|
||||
}
|
||||
assert.True(t, bytes.Equal(data, dataCorrect))
|
||||
assert.Equal(t, dataCorrect, data)
|
||||
}
|
||||
|
||||
@@ -79,12 +79,12 @@ type V6ServerConf struct {
|
||||
|
||||
// The first IP address for dynamic leases
|
||||
// The last allowed IP address ends with 0xff byte
|
||||
RangeStart net.IP `yaml:"range_start"`
|
||||
RangeStart net.IP `yaml:"range_start" json:"range_start"`
|
||||
|
||||
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds
|
||||
|
||||
RaSlaacOnly bool `yaml:"ra_slaac_only" json:"-"` // send ICMPv6.RA packets without MO flags
|
||||
RaAllowSlaac bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags
|
||||
RASLAACOnly bool `yaml:"ra_slaac_only" json:"-"` // send ICMPv6.RA packets without MO flags
|
||||
RAAllowSLAAC bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags
|
||||
|
||||
ipStart net.IP // starting IP address for dynamic leases
|
||||
leaseTime time.Duration // the time during which a dynamic lease is considered valid
|
||||
|
||||
@@ -23,7 +23,8 @@ type v4Server struct {
|
||||
srv *server4.Server
|
||||
leasesLock sync.Mutex
|
||||
leases []*Lease
|
||||
ipAddrs [256]byte
|
||||
// TODO(e.burkov): This field type should be a normal bitmap.
|
||||
ipAddrs [256]byte
|
||||
|
||||
conf V4ServerConf
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeIface struct {
|
||||
@@ -79,8 +80,8 @@ func TestIfaceIPAddrs(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, gotErr := ifaceIPAddrs(tc.iface, tc.ipv)
|
||||
require.True(t, errors.Is(gotErr, tc.wantErr))
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.True(t, errors.Is(gotErr, tc.wantErr))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,12 +141,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) {
|
||||
want: nil,
|
||||
wantErr: errTest,
|
||||
}, {
|
||||
name: "ipv4_wait",
|
||||
iface: &waitingFakeIface{
|
||||
addrs: []net.Addr{addr4},
|
||||
err: nil,
|
||||
n: 1,
|
||||
},
|
||||
name: "ipv4_wait",
|
||||
iface: &waitingFakeIface{addrs: []net.Addr{addr4}, err: nil, n: 1},
|
||||
ipv: ipVersion4,
|
||||
want: []net.IP{ip4, ip4},
|
||||
wantErr: nil,
|
||||
@@ -168,12 +165,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) {
|
||||
want: nil,
|
||||
wantErr: errTest,
|
||||
}, {
|
||||
name: "ipv6_wait",
|
||||
iface: &waitingFakeIface{
|
||||
addrs: []net.Addr{addr6},
|
||||
err: nil,
|
||||
n: 1,
|
||||
},
|
||||
name: "ipv6_wait",
|
||||
iface: &waitingFakeIface{addrs: []net.Addr{addr6}, err: nil, n: 1},
|
||||
ipv: ipVersion6,
|
||||
want: []net.IP{ip6, ip6},
|
||||
wantErr: nil,
|
||||
@@ -182,8 +175,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, gotErr := ifaceDNSIPAddrs(tc.iface, tc.ipv, 2, 0)
|
||||
require.True(t, errors.Is(gotErr, tc.wantErr))
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.True(t, errors.Is(gotErr, tc.wantErr))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,172 +8,182 @@ import (
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func notify4(flags uint32) {
|
||||
}
|
||||
|
||||
func TestV4StaticLeaseAddRemove(t *testing.T) {
|
||||
conf := V4ServerConf{
|
||||
func TestV4_AddRemove_static(t *testing.T) {
|
||||
s, err := v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: notify4,
|
||||
}
|
||||
s, err := v4Create(conf)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
assert.Empty(t, ls)
|
||||
|
||||
// add static lease
|
||||
l := Lease{}
|
||||
l.IP = net.IP{192, 168, 10, 150}
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
|
||||
// try to add the same static lease - fail
|
||||
// Add static lease.
|
||||
l := Lease{
|
||||
IP: net.IP{192, 168, 10, 150},
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
require.Nil(t, s.AddStaticLease(l))
|
||||
assert.NotNil(t, s.AddStaticLease(l))
|
||||
|
||||
// check
|
||||
ls = s.GetLeases(LeasesStatic)
|
||||
assert.Len(t, ls, 1)
|
||||
assert.True(t, net.IP{192, 168, 10, 150}.Equal(ls[0].IP))
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
require.Len(t, ls, 1)
|
||||
assert.True(t, l.IP.Equal(ls[0].IP))
|
||||
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
|
||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
||||
|
||||
// try to remove static lease - fail
|
||||
l.IP = net.IP{192, 168, 10, 110}
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.NotNil(t, s.RemoveStaticLease(l))
|
||||
// Try to remove static lease.
|
||||
assert.NotNil(t, s.RemoveStaticLease(Lease{
|
||||
IP: net.IP{192, 168, 10, 110},
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}))
|
||||
|
||||
// remove static lease
|
||||
l.IP = net.IP{192, 168, 10, 150}
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.RemoveStaticLease(l))
|
||||
|
||||
// check
|
||||
// Remove static lease.
|
||||
require.Nil(t, s.RemoveStaticLease(l))
|
||||
ls = s.GetLeases(LeasesStatic)
|
||||
assert.Empty(t, ls)
|
||||
}
|
||||
|
||||
func TestV4StaticLeaseAddReplaceDynamic(t *testing.T) {
|
||||
conf := V4ServerConf{
|
||||
func TestV4_AddReplace(t *testing.T) {
|
||||
sIface, err := v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: notify4,
|
||||
}
|
||||
sIface, err := v4Create(conf)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
s, ok := sIface.(*v4Server)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
// add dynamic lease
|
||||
ld := Lease{}
|
||||
ld.IP = net.IP{192, 168, 10, 150}
|
||||
ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa")
|
||||
s.addLease(&ld)
|
||||
dynLeases := []Lease{{
|
||||
IP: net.IP{192, 168, 10, 150},
|
||||
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}, {
|
||||
IP: net.IP{192, 168, 10, 151},
|
||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}}
|
||||
|
||||
// add dynamic lease
|
||||
{
|
||||
ld := Lease{}
|
||||
ld.IP = net.IP{192, 168, 10, 151}
|
||||
ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
||||
s.addLease(&ld)
|
||||
for i := range dynLeases {
|
||||
s.addLease(&dynLeases[i])
|
||||
}
|
||||
|
||||
// add static lease with the same IP
|
||||
l := Lease{}
|
||||
l.IP = net.IP{192, 168, 10, 150}
|
||||
l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
stLeases := []Lease{{
|
||||
IP: net.IP{192, 168, 10, 150},
|
||||
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}, {
|
||||
IP: net.IP{192, 168, 10, 152},
|
||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}}
|
||||
|
||||
// add static lease with the same MAC
|
||||
l = Lease{}
|
||||
l.IP = net.IP{192, 168, 10, 152}
|
||||
l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
for _, l := range stLeases {
|
||||
require.Nil(t, s.AddStaticLease(l))
|
||||
}
|
||||
|
||||
// check
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
assert.Len(t, ls, 2)
|
||||
require.Len(t, ls, 2)
|
||||
|
||||
assert.True(t, net.IP{192, 168, 10, 150}.Equal(ls[0].IP))
|
||||
assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
||||
|
||||
assert.True(t, net.IP{192, 168, 10, 152}.Equal(ls[1].IP))
|
||||
assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String())
|
||||
assert.EqualValues(t, leaseExpireStatic, ls[1].Expiry.Unix())
|
||||
for i, l := range ls {
|
||||
assert.True(t, stLeases[i].IP.Equal(l.IP))
|
||||
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
||||
assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
func TestV4StaticLeaseGet(t *testing.T) {
|
||||
conf := V4ServerConf{
|
||||
func TestV4StaticLease_Get(t *testing.T) {
|
||||
var err error
|
||||
sIface, err := v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: notify4,
|
||||
}
|
||||
sIface, err := v4Create(conf)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
s, ok := sIface.(*v4Server)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, err)
|
||||
require.True(t, ok)
|
||||
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
||||
|
||||
l := Lease{}
|
||||
l.IP = net.IP{192, 168, 10, 150}
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
l := Lease{
|
||||
IP: net.IP{192, 168, 10, 150},
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
require.Nil(t, s.AddStaticLease(l))
|
||||
|
||||
// "Discover"
|
||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
req, _ := dhcpv4.NewDiscovery(mac)
|
||||
resp, _ := dhcpv4.NewReplyFromRequest(req)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
var req, resp *dhcpv4.DHCPv4
|
||||
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||
|
||||
// check "Offer"
|
||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
|
||||
assert.True(t, net.IP{192, 168, 10, 150}.Equal(resp.YourIPAddr))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.Router()[0]))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.ServerIdentifier()))
|
||||
assert.True(t, net.IP{255, 255, 255, 0}.Equal(net.IP(resp.SubnetMask())))
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
t.Run("discover", func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
// "Request"
|
||||
req, _ = dhcpv4.NewRequestFromOffer(resp)
|
||||
resp, _ = dhcpv4.NewReplyFromRequest(req)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
req, err = dhcpv4.NewDiscovery(mac)
|
||||
require.Nil(t, err)
|
||||
|
||||
// check "Ack"
|
||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
|
||||
assert.True(t, net.IP{192, 168, 10, 150}.Equal(resp.YourIPAddr))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.Router()[0]))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.ServerIdentifier()))
|
||||
assert.True(t, net.IP{255, 255, 255, 0}.Equal(net.IP(resp.SubnetMask())))
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
resp, err = dhcpv4.NewReplyFromRequest(req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("offer", func(t *testing.T) {
|
||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
||||
assert.Equal(t, s.conf.subnetMask, resp.SubnetMask())
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
})
|
||||
|
||||
t.Run("request", func(t *testing.T) {
|
||||
req, err = dhcpv4.NewRequestFromOffer(resp)
|
||||
require.Nil(t, err)
|
||||
|
||||
resp, err = dhcpv4.NewReplyFromRequest(req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("ack", func(t *testing.T) {
|
||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
||||
assert.Equal(t, s.conf.subnetMask, resp.SubnetMask())
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
})
|
||||
|
||||
dnsAddrs := resp.DNS()
|
||||
assert.Len(t, dnsAddrs, 1)
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(dnsAddrs[0]))
|
||||
require.Len(t, dnsAddrs, 1)
|
||||
assert.True(t, s.conf.GatewayIP.Equal(dnsAddrs[0]))
|
||||
|
||||
// check lease
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
assert.Len(t, ls, 1)
|
||||
assert.True(t, net.IP{192, 168, 10, 150}.Equal(ls[0].IP))
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
t.Run("check_lease", func(t *testing.T) {
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
require.Len(t, ls, 1)
|
||||
assert.True(t, l.IP.Equal(ls[0].IP))
|
||||
assert.Equal(t, mac, ls[0].HWAddr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestV4DynamicLeaseGet(t *testing.T) {
|
||||
conf := V4ServerConf{
|
||||
func TestV4DynamicLease_Get(t *testing.T) {
|
||||
var err error
|
||||
sIface, err := v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
@@ -184,58 +194,97 @@ func TestV4DynamicLeaseGet(t *testing.T) {
|
||||
"81 hex 303132",
|
||||
"82 ip 1.2.3.4",
|
||||
},
|
||||
}
|
||||
sIface, err := v4Create(conf)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
s, ok := sIface.(*v4Server)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, err)
|
||||
require.True(t, ok)
|
||||
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
||||
|
||||
// "Discover"
|
||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
req, _ := dhcpv4.NewDiscovery(mac)
|
||||
resp, _ := dhcpv4.NewReplyFromRequest(req)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
var req, resp *dhcpv4.DHCPv4
|
||||
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||
|
||||
// check "Offer"
|
||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
|
||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(resp.YourIPAddr))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.Router()[0]))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.ServerIdentifier()))
|
||||
assert.True(t, net.IP{255, 255, 255, 0}.Equal(net.IP(resp.SubnetMask())))
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
assert.Equal(t, []byte("012"), resp.Options[uint8(dhcpv4.OptionFQDN)])
|
||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(net.IP(resp.Options[uint8(dhcpv4.OptionRelayAgentInformation)])))
|
||||
t.Run("discover", func(t *testing.T) {
|
||||
req, err = dhcpv4.NewDiscovery(mac)
|
||||
require.Nil(t, err)
|
||||
|
||||
// "Request"
|
||||
req, _ = dhcpv4.NewRequestFromOffer(resp)
|
||||
resp, _ = dhcpv4.NewReplyFromRequest(req)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
resp, err = dhcpv4.NewReplyFromRequest(req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
// check "Ack"
|
||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
|
||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(resp.YourIPAddr))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.Router()[0]))
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.ServerIdentifier()))
|
||||
assert.True(t, net.IP{255, 255, 255, 0}.Equal(net.IP(resp.SubnetMask())))
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
t.Run("offer", func(t *testing.T) {
|
||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||
assert.True(t, s.conf.RangeStart.Equal(resp.YourIPAddr))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
||||
assert.Equal(t, s.conf.subnetMask, resp.SubnetMask())
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
assert.Equal(t, []byte("012"), resp.Options[uint8(dhcpv4.OptionFQDN)])
|
||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(net.IP(resp.Options[uint8(dhcpv4.OptionRelayAgentInformation)])))
|
||||
})
|
||||
|
||||
t.Run("request", func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
req, err = dhcpv4.NewRequestFromOffer(resp)
|
||||
require.Nil(t, err)
|
||||
|
||||
resp, err = dhcpv4.NewReplyFromRequest(req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 1, s.process(req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("ack", func(t *testing.T) {
|
||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||
assert.True(t, s.conf.RangeStart.Equal(resp.YourIPAddr))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
|
||||
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
||||
assert.Equal(t, s.conf.subnetMask, resp.SubnetMask())
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||
})
|
||||
|
||||
dnsAddrs := resp.DNS()
|
||||
assert.Len(t, dnsAddrs, 1)
|
||||
require.Len(t, dnsAddrs, 1)
|
||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(dnsAddrs[0]))
|
||||
|
||||
// check lease
|
||||
ls := s.GetLeases(LeasesDynamic)
|
||||
assert.Len(t, ls, 1)
|
||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(ls[0].IP))
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
t.Run("check_lease", func(t *testing.T) {
|
||||
ls := s.GetLeases(LeasesDynamic)
|
||||
assert.Len(t, ls, 1)
|
||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(ls[0].IP))
|
||||
assert.Equal(t, mac, ls[0].HWAddr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIP4InRange(t *testing.T) {
|
||||
start := net.IP{192, 168, 10, 100}
|
||||
stop := net.IP{192, 168, 10, 200}
|
||||
assert.False(t, ip4InRange(start, stop, net.IP{192, 168, 10, 99}))
|
||||
assert.False(t, ip4InRange(start, stop, net.IP{192, 168, 11, 100}))
|
||||
assert.False(t, ip4InRange(start, stop, net.IP{192, 168, 11, 201}))
|
||||
assert.True(t, ip4InRange(start, stop, net.IP{192, 168, 10, 100}))
|
||||
|
||||
testCases := []struct {
|
||||
ip net.IP
|
||||
want bool
|
||||
}{{
|
||||
ip: net.IP{192, 168, 10, 99},
|
||||
want: false,
|
||||
}, {
|
||||
ip: net.IP{192, 168, 11, 100},
|
||||
want: false,
|
||||
}, {
|
||||
ip: net.IP{192, 168, 11, 201},
|
||||
want: false,
|
||||
}, {
|
||||
ip: start,
|
||||
want: true,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.ip.String(), func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, ip4InRange(start, stop, tc.ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,8 +552,8 @@ func (s *v6Server) initRA(iface *net.Interface) error {
|
||||
}
|
||||
}
|
||||
|
||||
s.ra.raAllowSlaac = s.conf.RaAllowSlaac
|
||||
s.ra.raSlaacOnly = s.conf.RaSlaacOnly
|
||||
s.ra.raAllowSLAAC = s.conf.RAAllowSLAAC
|
||||
s.ra.raSLAACOnly = s.conf.RASLAACOnly
|
||||
s.ra.dnsIPAddr = s.ra.ipAddr
|
||||
s.ra.prefixIPAddr = s.conf.ipStart
|
||||
s.ra.ifaceName = s.conf.InterfaceName
|
||||
@@ -594,7 +594,7 @@ func (s *v6Server) Start() error {
|
||||
}
|
||||
|
||||
// don't initialize DHCPv6 server if we must force the clients to use SLAAC
|
||||
if s.conf.RaSlaacOnly {
|
||||
if s.conf.RASLAACOnly {
|
||||
log.Debug("DHCPv6: not starting DHCPv6 server due to ra_slaac_only=true")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,220 +9,283 @@ import (
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/insomniacslk/dhcp/iana"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func notify6(flags uint32) {
|
||||
}
|
||||
|
||||
func TestV6StaticLeaseAddRemove(t *testing.T) {
|
||||
conf := V6ServerConf{
|
||||
func TestV6_AddRemove_static(t *testing.T) {
|
||||
s, err := v6Create(V6ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.ParseIP("2001::1"),
|
||||
notify: notify6,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Empty(t, s.GetLeases(LeasesStatic))
|
||||
|
||||
// Add static lease.
|
||||
l := Lease{
|
||||
IP: net.ParseIP("2001::1"),
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
s, err := v6Create(conf)
|
||||
assert.Nil(t, err)
|
||||
require.Nil(t, s.AddStaticLease(l))
|
||||
|
||||
// Try to add the same static lease.
|
||||
require.NotNil(t, s.AddStaticLease(l))
|
||||
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
assert.Empty(t, ls)
|
||||
|
||||
// add static lease
|
||||
l := Lease{}
|
||||
l.IP = net.ParseIP("2001::1")
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
|
||||
// try to add static lease - fail
|
||||
assert.NotNil(t, s.AddStaticLease(l))
|
||||
|
||||
// check
|
||||
ls = s.GetLeases(LeasesStatic)
|
||||
assert.Len(t, ls, 1)
|
||||
assert.Equal(t, "2001::1", ls[0].IP.String())
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
require.Len(t, ls, 1)
|
||||
assert.Equal(t, l.IP, ls[0].IP)
|
||||
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
|
||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
||||
|
||||
// try to remove static lease - fail
|
||||
l.IP = net.ParseIP("2001::2")
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.NotNil(t, s.RemoveStaticLease(l))
|
||||
// Try to remove non-existent static lease.
|
||||
require.NotNil(t, s.RemoveStaticLease(Lease{
|
||||
IP: net.ParseIP("2001::2"),
|
||||
HWAddr: l.HWAddr,
|
||||
}))
|
||||
|
||||
// remove static lease
|
||||
l.IP = net.ParseIP("2001::1")
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.RemoveStaticLease(l))
|
||||
// Remove static lease.
|
||||
require.Nil(t, s.RemoveStaticLease(l))
|
||||
|
||||
// check
|
||||
ls = s.GetLeases(LeasesStatic)
|
||||
assert.Empty(t, ls)
|
||||
assert.Empty(t, s.GetLeases(LeasesStatic))
|
||||
}
|
||||
|
||||
func TestV6StaticLeaseAddReplaceDynamic(t *testing.T) {
|
||||
conf := V6ServerConf{
|
||||
func TestV6_AddReplace(t *testing.T) {
|
||||
sIface, err := v6Create(V6ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.ParseIP("2001::1"),
|
||||
notify: notify6,
|
||||
}
|
||||
sIface, err := v6Create(conf)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
s, ok := sIface.(*v6Server)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
// add dynamic lease
|
||||
ld := Lease{}
|
||||
ld.IP = net.ParseIP("2001::1")
|
||||
ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa")
|
||||
s.addLease(&ld)
|
||||
// Add dynamic leases.
|
||||
dynLeases := []*Lease{{
|
||||
IP: net.ParseIP("2001::1"),
|
||||
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}, {
|
||||
IP: net.ParseIP("2001::2"),
|
||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}}
|
||||
|
||||
// add dynamic lease
|
||||
{
|
||||
ld := Lease{}
|
||||
ld.IP = net.ParseIP("2001::2")
|
||||
ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
||||
s.addLease(&ld)
|
||||
for _, l := range dynLeases {
|
||||
s.addLease(l)
|
||||
}
|
||||
|
||||
// add static lease with the same IP
|
||||
l := Lease{}
|
||||
l.IP = net.ParseIP("2001::1")
|
||||
l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
stLeases := []Lease{{
|
||||
IP: net.ParseIP("2001::1"),
|
||||
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}, {
|
||||
IP: net.ParseIP("2001::3"),
|
||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}}
|
||||
|
||||
// add static lease with the same MAC
|
||||
l = Lease{}
|
||||
l.IP = net.ParseIP("2001::3")
|
||||
l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
for _, l := range stLeases {
|
||||
require.Nil(t, s.AddStaticLease(l))
|
||||
}
|
||||
|
||||
// check
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
assert.Len(t, ls, 2)
|
||||
require.Len(t, ls, 2)
|
||||
|
||||
assert.Equal(t, "2001::1", ls[0].IP.String())
|
||||
assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
||||
|
||||
assert.Equal(t, "2001::3", ls[1].IP.String())
|
||||
assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String())
|
||||
assert.EqualValues(t, leaseExpireStatic, ls[1].Expiry.Unix())
|
||||
for i, l := range ls {
|
||||
assert.True(t, stLeases[i].IP.Equal(l.IP))
|
||||
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
||||
assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
func TestV6GetLease(t *testing.T) {
|
||||
conf := V6ServerConf{
|
||||
var err error
|
||||
sIface, err := v6Create(V6ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.ParseIP("2001::1"),
|
||||
notify: notify6,
|
||||
}
|
||||
sIface, err := v6Create(conf)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
s, ok := sIface.(*v6Server)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, err)
|
||||
s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")}
|
||||
require.True(t, ok)
|
||||
|
||||
dnsAddr := net.ParseIP("2000::1")
|
||||
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
|
||||
s.sid = dhcpv6.Duid{
|
||||
Type: dhcpv6.DUID_LLT,
|
||||
HwType: iana.HWTypeEthernet,
|
||||
Type: dhcpv6.DUID_LLT,
|
||||
HwType: iana.HWTypeEthernet,
|
||||
LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
|
||||
l := Lease{}
|
||||
l.IP = net.ParseIP("2001::1")
|
||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
assert.Nil(t, s.AddStaticLease(l))
|
||||
l := Lease{
|
||||
IP: net.ParseIP("2001::1"),
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
require.Nil(t, s.AddStaticLease(l))
|
||||
|
||||
// "Solicit"
|
||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
req, _ := dhcpv6.NewSolicit(mac)
|
||||
msg, _ := req.GetInnerMessage()
|
||||
resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg)
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
var req, resp, msg *dhcpv6.Message
|
||||
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||
t.Run("solicit", func(t *testing.T) {
|
||||
req, err = dhcpv6.NewSolicit(mac)
|
||||
require.Nil(t, err)
|
||||
|
||||
msg, err = req.GetInnerMessage()
|
||||
require.Nil(t, err)
|
||||
|
||||
resp, err = dhcpv6.NewAdvertiseFromSolicit(msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
resp.AddOption(dhcpv6.OptServerID(s.sid))
|
||||
|
||||
// check "Advertise"
|
||||
assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
||||
oia := resp.Options.OneIANA()
|
||||
oiaAddr := oia.Options.OneAddress()
|
||||
assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String())
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
||||
var oia *dhcpv6.OptIANA
|
||||
var oiaAddr *dhcpv6.OptIAAddress
|
||||
t.Run("advertise", func(t *testing.T) {
|
||||
require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
||||
oia = resp.Options.OneIANA()
|
||||
oiaAddr = oia.Options.OneAddress()
|
||||
|
||||
// "Request"
|
||||
req, _ = dhcpv6.NewRequestFromAdvertise(resp)
|
||||
msg, _ = req.GetInnerMessage()
|
||||
resp, _ = dhcpv6.NewReplyFromMessage(msg)
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
assert.Equal(t, l.IP, oiaAddr.IPv6Addr)
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
||||
})
|
||||
|
||||
// check "Reply"
|
||||
assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
|
||||
oia = resp.Options.OneIANA()
|
||||
oiaAddr = oia.Options.OneAddress()
|
||||
assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String())
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
||||
t.Run("request", func(t *testing.T) {
|
||||
req, err = dhcpv6.NewRequestFromAdvertise(resp)
|
||||
require.Nil(t, err)
|
||||
|
||||
msg, err = req.GetInnerMessage()
|
||||
require.Nil(t, err)
|
||||
|
||||
resp, err = dhcpv6.NewReplyFromMessage(msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("reply", func(t *testing.T) {
|
||||
require.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
|
||||
oia = resp.Options.OneIANA()
|
||||
oiaAddr = oia.Options.OneAddress()
|
||||
|
||||
assert.Equal(t, l.IP, oiaAddr.IPv6Addr)
|
||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
||||
})
|
||||
|
||||
dnsAddrs := resp.Options.DNS()
|
||||
assert.Len(t, dnsAddrs, 1)
|
||||
assert.Equal(t, "2000::1", dnsAddrs[0].String())
|
||||
require.Len(t, dnsAddrs, 1)
|
||||
assert.Equal(t, dnsAddr, dnsAddrs[0])
|
||||
|
||||
// check lease
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
assert.Len(t, ls, 1)
|
||||
assert.Equal(t, "2001::1", ls[0].IP.String())
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
t.Run("lease", func(t *testing.T) {
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
require.Len(t, ls, 1)
|
||||
assert.Equal(t, l.IP, ls[0].IP)
|
||||
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestV6GetDynamicLease(t *testing.T) {
|
||||
conf := V6ServerConf{
|
||||
sIface, err := v6Create(V6ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.ParseIP("2001::2"),
|
||||
notify: notify6,
|
||||
}
|
||||
sIface, err := v6Create(conf)
|
||||
})
|
||||
require.Nil(t, err)
|
||||
s, ok := sIface.(*v6Server)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, err)
|
||||
s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")}
|
||||
s.sid = dhcpv6.Duid{
|
||||
Type: dhcpv6.DUID_LLT,
|
||||
HwType: iana.HWTypeEthernet,
|
||||
}
|
||||
s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
require.True(t, ok)
|
||||
|
||||
// "Solicit"
|
||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
req, _ := dhcpv6.NewSolicit(mac)
|
||||
msg, _ := req.GetInnerMessage()
|
||||
resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg)
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
dnsAddr := net.ParseIP("2000::1")
|
||||
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
|
||||
s.sid = dhcpv6.Duid{
|
||||
Type: dhcpv6.DUID_LLT,
|
||||
HwType: iana.HWTypeEthernet,
|
||||
LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
|
||||
var req, resp, msg *dhcpv6.Message
|
||||
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||
t.Run("solicit", func(t *testing.T) {
|
||||
req, err = dhcpv6.NewSolicit(mac)
|
||||
require.Nil(t, err)
|
||||
|
||||
msg, err = req.GetInnerMessage()
|
||||
require.Nil(t, err)
|
||||
|
||||
resp, err = dhcpv6.NewAdvertiseFromSolicit(msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
resp.AddOption(dhcpv6.OptServerID(s.sid))
|
||||
|
||||
// check "Advertise"
|
||||
assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
||||
oia := resp.Options.OneIANA()
|
||||
oiaAddr := oia.Options.OneAddress()
|
||||
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
|
||||
var oia *dhcpv6.OptIANA
|
||||
var oiaAddr *dhcpv6.OptIAAddress
|
||||
t.Run("advertise", func(t *testing.T) {
|
||||
require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
||||
oia = resp.Options.OneIANA()
|
||||
oiaAddr = oia.Options.OneAddress()
|
||||
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
|
||||
})
|
||||
|
||||
// "Request"
|
||||
req, _ = dhcpv6.NewRequestFromAdvertise(resp)
|
||||
msg, _ = req.GetInnerMessage()
|
||||
resp, _ = dhcpv6.NewReplyFromMessage(msg)
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
t.Run("request", func(t *testing.T) {
|
||||
req, err = dhcpv6.NewRequestFromAdvertise(resp)
|
||||
require.Nil(t, err)
|
||||
|
||||
// check "Reply"
|
||||
assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
|
||||
oia = resp.Options.OneIANA()
|
||||
oiaAddr = oia.Options.OneAddress()
|
||||
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
|
||||
msg, err = req.GetInnerMessage()
|
||||
require.Nil(t, err)
|
||||
|
||||
resp, err = dhcpv6.NewReplyFromMessage(msg)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.True(t, s.process(msg, req, resp))
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("reply", func(t *testing.T) {
|
||||
require.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
|
||||
oia = resp.Options.OneIANA()
|
||||
oiaAddr = oia.Options.OneAddress()
|
||||
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
|
||||
})
|
||||
|
||||
dnsAddrs := resp.Options.DNS()
|
||||
assert.Len(t, dnsAddrs, 1)
|
||||
assert.Equal(t, "2000::1", dnsAddrs[0].String())
|
||||
require.Len(t, dnsAddrs, 1)
|
||||
assert.Equal(t, dnsAddr, dnsAddrs[0])
|
||||
|
||||
// check lease
|
||||
ls := s.GetLeases(LeasesDynamic)
|
||||
assert.Len(t, ls, 1)
|
||||
assert.Equal(t, "2001::2", ls[0].IP.String())
|
||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
||||
|
||||
assert.False(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::1")))
|
||||
assert.False(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2002::2")))
|
||||
assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::2")))
|
||||
assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::3")))
|
||||
t.Run("lease", func(t *testing.T) {
|
||||
ls := s.GetLeases(LeasesDynamic)
|
||||
require.Len(t, ls, 1)
|
||||
assert.Equal(t, "2001::2", ls[0].IP.String())
|
||||
assert.Equal(t, mac, ls[0].HWAddr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIP6InRange(t *testing.T) {
|
||||
start := net.ParseIP("2001::2")
|
||||
|
||||
testCases := []struct {
|
||||
ip net.IP
|
||||
want bool
|
||||
}{{
|
||||
ip: net.ParseIP("2001::1"),
|
||||
want: false,
|
||||
}, {
|
||||
ip: net.ParseIP("2002::2"),
|
||||
want: false,
|
||||
}, {
|
||||
ip: start,
|
||||
want: true,
|
||||
}, {
|
||||
ip: net.ParseIP("2001::3"),
|
||||
want: true,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.ip.String(), func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, ip6InRange(start, tc.ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
165
internal/dnsforward/clientid.go
Normal file
165
internal/dnsforward/clientid.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
)
|
||||
|
||||
const maxDomainPartLen = 64
|
||||
|
||||
// ValidateClientID returns an error if clientID is not a valid client ID.
|
||||
func ValidateClientID(clientID string) (err error) {
|
||||
if len(clientID) > maxDomainPartLen {
|
||||
return fmt.Errorf("client id %q is too long, max: %d", clientID, maxDomainPartLen)
|
||||
}
|
||||
|
||||
for i, r := range clientID {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid char %q at index %d in client id %q", r, i, clientID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clientIDFromClientServerName extracts and validates a client ID. hostSrvName
|
||||
// is the server name of the host. cliSrvName is the server name as sent by the
|
||||
// client. When strict is true, and client and host server name don't match,
|
||||
// clientIDFromClientServerName will return an error.
|
||||
func clientIDFromClientServerName(hostSrvName, cliSrvName string, strict bool) (clientID string, err error) {
|
||||
if hostSrvName == cliSrvName {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(cliSrvName, hostSrvName) {
|
||||
if !strict {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("client server name %q doesn't match host server name %q", cliSrvName, hostSrvName)
|
||||
}
|
||||
|
||||
clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1]
|
||||
err = ValidateClientID(clientID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid client id: %w", err)
|
||||
}
|
||||
|
||||
return clientID, nil
|
||||
}
|
||||
|
||||
// processClientIDHTTPS extracts the client's ID from the path of the
|
||||
// client's DNS-over-HTTPS request.
|
||||
func processClientIDHTTPS(ctx *dnsContext) (rc resultCode) {
|
||||
pctx := ctx.proxyCtx
|
||||
r := pctx.HTTPRequest
|
||||
if r == nil {
|
||||
ctx.err = fmt.Errorf("proxy ctx http request of proto %s is nil", pctx.Proto)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
origPath := r.URL.Path
|
||||
parts := strings.Split(path.Clean(origPath), "/")
|
||||
if parts[0] == "" {
|
||||
parts = parts[1:]
|
||||
}
|
||||
|
||||
if len(parts) == 0 || parts[0] != "dns-query" {
|
||||
ctx.err = fmt.Errorf("client id check: invalid path %q", origPath)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
clientID := ""
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// Just /dns-query, no client ID.
|
||||
return resultCodeSuccess
|
||||
case 2:
|
||||
clientID = parts[1]
|
||||
default:
|
||||
ctx.err = fmt.Errorf("client id check: invalid path %q: extra parts", origPath)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
err := ValidateClientID(clientID)
|
||||
if err != nil {
|
||||
ctx.err = fmt.Errorf("client id check: invalid client id: %w", err)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
ctx.clientID = clientID
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// tlsConn is a narrow interface for *tls.Conn to simplify testing.
|
||||
type tlsConn interface {
|
||||
ConnectionState() (cs tls.ConnectionState)
|
||||
}
|
||||
|
||||
// quicSession is a narrow interface for quic.Session to simplify testing.
|
||||
type quicSession interface {
|
||||
ConnectionState() (cs quic.ConnectionState)
|
||||
}
|
||||
|
||||
// processClientID extracts the client's ID from the server name of the client's
|
||||
// DOT or DOQ request or the path of the client's DOH.
|
||||
func processClientID(dctx *dnsContext) (rc resultCode) {
|
||||
pctx := dctx.proxyCtx
|
||||
proto := pctx.Proto
|
||||
if proto == proxy.ProtoHTTPS {
|
||||
return processClientIDHTTPS(dctx)
|
||||
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
srvConf := dctx.srv.conf
|
||||
hostSrvName := srvConf.TLSConfig.ServerName
|
||||
if hostSrvName == "" {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
cliSrvName := ""
|
||||
if proto == proxy.ProtoTLS {
|
||||
conn := pctx.Conn
|
||||
tc, ok := conn.(tlsConn)
|
||||
if !ok {
|
||||
dctx.err = fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
cliSrvName = tc.ConnectionState().ServerName
|
||||
} else if proto == proxy.ProtoQUIC {
|
||||
qs, ok := pctx.QUICSession.(quicSession)
|
||||
if !ok {
|
||||
dctx.err = fmt.Errorf("proxy ctx quic session of proto %s is %T, want quic.Session", proto, pctx.QUICSession)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
cliSrvName = qs.ConnectionState().ServerName
|
||||
}
|
||||
|
||||
clientID, err := clientIDFromClientServerName(hostSrvName, cliSrvName, srvConf.StrictSNICheck)
|
||||
if err != nil {
|
||||
dctx.err = fmt.Errorf("client id check: %w", err)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
dctx.clientID = clientID
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testTLSConn is a tlsConn for tests.
|
||||
@@ -53,6 +54,7 @@ func TestProcessClientID(t *testing.T) {
|
||||
wantClientID string
|
||||
wantErrMsg string
|
||||
wantRes resultCode
|
||||
strictSNI bool
|
||||
}{{
|
||||
name: "udp",
|
||||
proto: proxy.ProtoUDP,
|
||||
@@ -61,6 +63,7 @@ func TestProcessClientID(t *testing.T) {
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
wantRes: resultCodeSuccess,
|
||||
strictSNI: false,
|
||||
}, {
|
||||
name: "tls_no_client_id",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -69,6 +72,26 @@ func TestProcessClientID(t *testing.T) {
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
wantRes: resultCodeSuccess,
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "tls_no_client_server_name",
|
||||
proto: proxy.ProtoTLS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "",
|
||||
wantClientID: "",
|
||||
wantErrMsg: `client id check: client server name "" ` +
|
||||
`doesn't match host server name "example.com"`,
|
||||
wantRes: resultCodeError,
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "tls_no_client_server_name_no_strict",
|
||||
proto: proxy.ProtoTLS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "",
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
wantRes: resultCodeSuccess,
|
||||
strictSNI: false,
|
||||
}, {
|
||||
name: "tls_client_id",
|
||||
proto: proxy.ProtoTLS,
|
||||
@@ -77,30 +100,39 @@ func TestProcessClientID(t *testing.T) {
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
wantRes: resultCodeSuccess,
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "tls_client_id_hostname_error",
|
||||
proto: proxy.ProtoTLS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "cli.example.net",
|
||||
wantClientID: "",
|
||||
wantErrMsg: `client id check: client server name "cli.example.net" doesn't match host server name "example.com"`,
|
||||
wantRes: resultCodeError,
|
||||
wantErrMsg: `client id check: client server name "cli.example.net" ` +
|
||||
`doesn't match host server name "example.com"`,
|
||||
wantRes: resultCodeError,
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "tls_invalid_client_id",
|
||||
proto: proxy.ProtoTLS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "!!!.example.com",
|
||||
wantClientID: "",
|
||||
wantErrMsg: `client id check: invalid client id: invalid char '!' at index 0 in client id "!!!"`,
|
||||
wantRes: resultCodeError,
|
||||
wantErrMsg: `client id check: invalid client id: invalid char '!' ` +
|
||||
`at index 0 in client id "!!!"`,
|
||||
wantRes: resultCodeError,
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "tls_client_id_too_long",
|
||||
proto: proxy.ProtoTLS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789.example.com",
|
||||
name: "tls_client_id_too_long",
|
||||
proto: proxy.ProtoTLS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: `abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmno` +
|
||||
`pqrstuvwxyz0123456789.example.com`,
|
||||
wantClientID: "",
|
||||
wantErrMsg: `client id check: invalid client id: client id "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" is too long, max: 64`,
|
||||
wantRes: resultCodeError,
|
||||
wantErrMsg: `client id check: invalid client id: client id "abcdefghijklmno` +
|
||||
`pqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" ` +
|
||||
`is too long, max: 64`,
|
||||
wantRes: resultCodeError,
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "quic_client_id",
|
||||
proto: proxy.ProtoQUIC,
|
||||
@@ -109,14 +141,17 @@ func TestProcessClientID(t *testing.T) {
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
wantRes: resultCodeSuccess,
|
||||
strictSNI: true,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tlsConf := TLSConfig{
|
||||
ServerName: tc.hostSrvName,
|
||||
StrictSNICheck: tc.strictSNI,
|
||||
}
|
||||
srv := &Server{
|
||||
conf: ServerConfig{
|
||||
TLSConfig: TLSConfig{ServerName: tc.hostSrvName},
|
||||
},
|
||||
conf: ServerConfig{TLSConfig: tlsConf},
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
@@ -146,10 +181,11 @@ func TestProcessClientID(t *testing.T) {
|
||||
assert.Equal(t, tc.wantRes, res)
|
||||
assert.Equal(t, tc.wantClientID, dctx.clientID)
|
||||
|
||||
if tc.wantErrMsg != "" && assert.NotNil(t, dctx.err) {
|
||||
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
|
||||
} else {
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.Nil(t, dctx.err)
|
||||
} else {
|
||||
require.NotNil(t, dctx.err)
|
||||
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -202,8 +238,9 @@ func TestProcessClientID_https(t *testing.T) {
|
||||
name: "invalid_client_id",
|
||||
path: "/dns-query/!!!",
|
||||
wantClientID: "",
|
||||
wantErrMsg: `client id check: invalid client id: invalid char '!' at index 0 in client id "!!!"`,
|
||||
wantRes: resultCodeError,
|
||||
wantErrMsg: `client id check: invalid client id: invalid char '!'` +
|
||||
` at index 0 in client id "!!!"`,
|
||||
wantRes: resultCodeError,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -225,10 +262,11 @@ func TestProcessClientID_https(t *testing.T) {
|
||||
assert.Equal(t, tc.wantRes, res)
|
||||
assert.Equal(t, tc.wantClientID, dctx.clientID)
|
||||
|
||||
if tc.wantErrMsg != "" && assert.NotNil(t, dctx.err) {
|
||||
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
|
||||
} else {
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.Nil(t, dctx.err)
|
||||
} else {
|
||||
require.NotNil(t, dctx.err)
|
||||
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -282,7 +282,7 @@ func (s *Server) prepareUpstreamSettings() error {
|
||||
}
|
||||
|
||||
if len(upstreamConfig.Upstreams) == 0 {
|
||||
log.Info("Warning: no default upstream servers specified, using %v", defaultDNS)
|
||||
log.Info("warning: no default upstream servers specified, using %v", defaultDNS)
|
||||
uc, err := proxy.ParseUpstreamsConfig(defaultDNS, s.conf.BootstrapDNS, DefaultTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dns: failed to parse default upstreams: %v", err)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,7 +10,6 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/util"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
@@ -234,154 +230,6 @@ func processInternalHosts(ctx *dnsContext) (rc resultCode) {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
const maxDomainPartLen = 64
|
||||
|
||||
// ValidateClientID returns an error if clientID is not a valid client ID.
|
||||
func ValidateClientID(clientID string) (err error) {
|
||||
if len(clientID) > maxDomainPartLen {
|
||||
return fmt.Errorf("client id %q is too long, max: %d", clientID, maxDomainPartLen)
|
||||
}
|
||||
|
||||
for i, r := range clientID {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid char %q at index %d in client id %q", r, i, clientID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clientIDFromClientServerName extracts and validates a client ID. hostSrvName
|
||||
// is the server name of the host. cliSrvName is the server name as sent by the
|
||||
// client.
|
||||
func clientIDFromClientServerName(hostSrvName, cliSrvName string) (clientID string, err error) {
|
||||
if hostSrvName == cliSrvName {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(cliSrvName, hostSrvName) {
|
||||
return "", fmt.Errorf("client server name %q doesn't match host server name %q", cliSrvName, hostSrvName)
|
||||
}
|
||||
|
||||
clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1]
|
||||
err = ValidateClientID(clientID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid client id: %w", err)
|
||||
}
|
||||
|
||||
return clientID, nil
|
||||
}
|
||||
|
||||
// processClientIDHTTPS extracts the client's ID from the path of the
|
||||
// client's DNS-over-HTTPS request.
|
||||
func processClientIDHTTPS(ctx *dnsContext) (rc resultCode) {
|
||||
pctx := ctx.proxyCtx
|
||||
r := pctx.HTTPRequest
|
||||
if r == nil {
|
||||
ctx.err = fmt.Errorf("proxy ctx http request of proto %s is nil", pctx.Proto)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
origPath := r.URL.Path
|
||||
parts := strings.Split(path.Clean(origPath), "/")
|
||||
if parts[0] == "" {
|
||||
parts = parts[1:]
|
||||
}
|
||||
|
||||
if len(parts) == 0 || parts[0] != "dns-query" {
|
||||
ctx.err = fmt.Errorf("client id check: invalid path %q", origPath)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
clientID := ""
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// Just /dns-query, no client ID.
|
||||
return resultCodeSuccess
|
||||
case 2:
|
||||
clientID = parts[1]
|
||||
default:
|
||||
ctx.err = fmt.Errorf("client id check: invalid path %q: extra parts", origPath)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
err := ValidateClientID(clientID)
|
||||
if err != nil {
|
||||
ctx.err = fmt.Errorf("client id check: invalid client id: %w", err)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
ctx.clientID = clientID
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// tlsConn is a narrow interface for *tls.Conn to simplify testing.
|
||||
type tlsConn interface {
|
||||
ConnectionState() (cs tls.ConnectionState)
|
||||
}
|
||||
|
||||
// quicSession is a narrow interface for quic.Session to simplify testing.
|
||||
type quicSession interface {
|
||||
ConnectionState() (cs quic.ConnectionState)
|
||||
}
|
||||
|
||||
// processClientID extracts the client's ID from the server name of the client's
|
||||
// DOT or DOQ request or the path of the client's DOH.
|
||||
func processClientID(ctx *dnsContext) (rc resultCode) {
|
||||
pctx := ctx.proxyCtx
|
||||
proto := pctx.Proto
|
||||
if proto == proxy.ProtoHTTPS {
|
||||
return processClientIDHTTPS(ctx)
|
||||
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
hostSrvName := ctx.srv.conf.TLSConfig.ServerName
|
||||
if hostSrvName == "" {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
cliSrvName := ""
|
||||
if proto == proxy.ProtoTLS {
|
||||
conn := pctx.Conn
|
||||
tc, ok := conn.(tlsConn)
|
||||
if !ok {
|
||||
ctx.err = fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
cliSrvName = tc.ConnectionState().ServerName
|
||||
} else if proto == proxy.ProtoQUIC {
|
||||
qs, ok := pctx.QUICSession.(quicSession)
|
||||
if !ok {
|
||||
ctx.err = fmt.Errorf("proxy ctx quic session of proto %s is %T, want quic.Session", proto, pctx.QUICSession)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
cliSrvName = qs.ConnectionState().ServerName
|
||||
}
|
||||
|
||||
clientID, err := clientIDFromClientServerName(hostSrvName, cliSrvName)
|
||||
if err != nil {
|
||||
ctx.err = fmt.Errorf("client id check: %w", err)
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
ctx.clientID = clientID
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// Respond to PTR requests if the target IP address is leased by our DHCP server
|
||||
func processInternalIPAddrs(ctx *dnsContext) (rc resultCode) {
|
||||
s := ctx.srv
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/utils"
|
||||
@@ -314,6 +315,11 @@ func ValidateUpstreams(upstreams []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := proxy.ParseUpstreamsConfig(upstreams, []string{}, DefaultTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var defaultUpstreamFound bool
|
||||
for _, u := range upstreams {
|
||||
d, err := validateUpstream(u)
|
||||
|
||||
@@ -251,12 +251,15 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||
|
||||
// Allow the frontend from the HTTP origin to send requests to the HTTPS
|
||||
// server. This can happen when the user has just set up HTTPS with
|
||||
// redirects.
|
||||
// redirects. Prevent cache-related errors by setting the Vary header.
|
||||
//
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin.
|
||||
originURL := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: r.Host,
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", originURL.String())
|
||||
w.Header().Set("Vary", "Origin")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -295,15 +295,20 @@ func run(args options) {
|
||||
BindPort: config.BindPort,
|
||||
BetaBindPort: config.BetaBindPort,
|
||||
|
||||
ReadTimeout: ReadTimeout,
|
||||
ReadHeaderTimeout: ReadHeaderTimeout,
|
||||
WriteTimeout: WriteTimeout,
|
||||
ReadTimeout: readTimeout,
|
||||
ReadHeaderTimeout: readHdrTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
}
|
||||
Context.web = CreateWeb(&webConf)
|
||||
if Context.web == nil {
|
||||
log.Fatalf("Can't initialize Web module")
|
||||
}
|
||||
|
||||
Context.ipDetector, err = newIPDetector()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !Context.firstRun {
|
||||
err := initDNSServer()
|
||||
if err != nil {
|
||||
@@ -315,6 +320,7 @@ func run(args options) {
|
||||
go func() {
|
||||
err := startDNSServer()
|
||||
if err != nil {
|
||||
closeDNSServer()
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
@@ -324,11 +330,6 @@ func run(args options) {
|
||||
}
|
||||
}
|
||||
|
||||
Context.ipDetector, err = newIPDetector()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
Context.web.Start()
|
||||
|
||||
// wait indefinitely for other go-routines to complete their job
|
||||
|
||||
@@ -5,16 +5,15 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIPDetector_detectSpecialNetwork(t *testing.T) {
|
||||
var ipd *ipDetector
|
||||
var err error
|
||||
|
||||
t.Run("newIPDetector", func(t *testing.T) {
|
||||
var err error
|
||||
ipd, err = newIPDetector()
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
ipd, err = newIPDetector()
|
||||
require.Nil(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -22,15 +22,43 @@ func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Ha
|
||||
return wrapped
|
||||
}
|
||||
|
||||
// RequestBodySizeLimit is maximum request body length in bytes.
|
||||
const RequestBodySizeLimit = 64 * 1024
|
||||
// defaultReqBodySzLim is the default maximum request body size.
|
||||
const defaultReqBodySzLim = 64 * 1024
|
||||
|
||||
// largerReqBodySzLim is the maximum request body size for APIs expecting larger
|
||||
// requests.
|
||||
const largerReqBodySzLim = 4 * 1024 * 1024
|
||||
|
||||
// expectsLargerRequests shows if this request should use a larger body size
|
||||
// limit. These are exceptions for poorly designed current APIs as well as APIs
|
||||
// that are designed to expect large files and requests. Remove once the new,
|
||||
// better APIs are up.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2666 and
|
||||
// https://github.com/AdguardTeam/AdGuardHome/issues/2675.
|
||||
func expectsLargerRequests(r *http.Request) (ok bool) {
|
||||
m := r.Method
|
||||
if m != http.MethodPost {
|
||||
return false
|
||||
}
|
||||
|
||||
p := r.URL.Path
|
||||
return p == "/control/access/set" ||
|
||||
p == "/control/filtering/set_rules"
|
||||
}
|
||||
|
||||
// limitRequestBody wraps underlying handler h, making it's request's body Read
|
||||
// method limited.
|
||||
func limitRequestBody(h http.Handler) (limited http.Handler) {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
r.Body, err = aghio.LimitReadCloser(r.Body, RequestBodySizeLimit)
|
||||
|
||||
var szLim int64 = defaultReqBodySzLim
|
||||
if expectsLargerRequests(r) {
|
||||
szLim = largerReqBodySzLim
|
||||
}
|
||||
|
||||
r.Body, err = aghio.LimitReadCloser(r.Body, szLim)
|
||||
if err != nil {
|
||||
log.Error("limitRequestBody: %s", err)
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLimitRequestBody(t *testing.T) {
|
||||
errReqLimitReached := &aghio.LimitReachedError{
|
||||
Limit: RequestBodySizeLimit,
|
||||
Limit: defaultReqBodySzLim,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -28,8 +29,8 @@ func TestLimitRequestBody(t *testing.T) {
|
||||
wantErr: nil,
|
||||
}, {
|
||||
name: "so_big",
|
||||
body: string(make([]byte, RequestBodySizeLimit+1)),
|
||||
want: make([]byte, RequestBodySizeLimit),
|
||||
body: string(make([]byte, defaultReqBodySzLim+1)),
|
||||
want: make([]byte, defaultReqBodySzLim),
|
||||
wantErr: errReqLimitReached,
|
||||
}, {
|
||||
name: "empty",
|
||||
@@ -60,8 +61,8 @@ func TestLimitRequestBody(t *testing.T) {
|
||||
|
||||
lim.ServeHTTP(res, req)
|
||||
|
||||
require.Equal(t, tc.wantErr, err)
|
||||
assert.Equal(t, tc.want, res.Body.Bytes())
|
||||
assert.Equal(t, tc.wantErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,183 +3,108 @@ package home
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpgrade1to2(t *testing.T) {
|
||||
// let's create test config for 1 schema version
|
||||
diskConfig := createTestDiskConfig(1)
|
||||
// any is a convenient alias for interface{}.
|
||||
type any = interface{}
|
||||
|
||||
// update config
|
||||
err := upgradeSchema1to2(&diskConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Can't upgrade schema version from 1 to 2")
|
||||
}
|
||||
// object is a convenient alias for map[string]interface{}.
|
||||
type object = map[string]any
|
||||
|
||||
// ensure that schema version was bumped
|
||||
compareSchemaVersion(t, diskConfig["schema_version"], 2)
|
||||
func TestUpgradeSchema1to2(t *testing.T) {
|
||||
diskConf := testDiskConf(1)
|
||||
|
||||
// old coredns entry should be removed
|
||||
_, ok := diskConfig["coredns"]
|
||||
if ok {
|
||||
t.Fatalf("Core DNS config was not removed after upgrade schema version from 1 to 2")
|
||||
}
|
||||
err := upgradeSchema1to2(&diskConf)
|
||||
require.Nil(t, err)
|
||||
|
||||
// pull out new dns config
|
||||
dnsMap, ok := diskConfig["dns"]
|
||||
if !ok {
|
||||
t.Fatalf("No DNS config after upgrade schema version from 1 to 2")
|
||||
}
|
||||
require.Equal(t, diskConf["schema_version"], 2)
|
||||
|
||||
// cast dns configurations to maps and compare them
|
||||
oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(1))
|
||||
newDNSConfig := castInterfaceToMap(t, dnsMap)
|
||||
compareConfigs(t, &oldDNSConfig, &newDNSConfig)
|
||||
_, ok := diskConf["coredns"]
|
||||
require.False(t, ok)
|
||||
|
||||
dnsMap, ok := diskConf["dns"]
|
||||
require.True(t, ok)
|
||||
|
||||
oldDNSConf := convertToObject(t, testDNSConf(1))
|
||||
newDNSConf := convertToObject(t, dnsMap)
|
||||
assert.Equal(t, oldDNSConf, newDNSConf)
|
||||
|
||||
// exclude dns config and schema version from disk config comparison
|
||||
oldExcludedEntries := []string{"coredns", "schema_version"}
|
||||
newExcludedEntries := []string{"dns", "schema_version"}
|
||||
oldDiskConfig := createTestDiskConfig(1)
|
||||
compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, oldExcludedEntries, newExcludedEntries)
|
||||
oldDiskConf := testDiskConf(1)
|
||||
assertEqualExcept(t, oldDiskConf, diskConf, oldExcludedEntries, newExcludedEntries)
|
||||
}
|
||||
|
||||
func TestUpgrade2to3(t *testing.T) {
|
||||
// let's create test config
|
||||
diskConfig := createTestDiskConfig(2)
|
||||
func TestUpgradeSchema2to3(t *testing.T) {
|
||||
diskConf := testDiskConf(2)
|
||||
|
||||
// upgrade schema from 2 to 3
|
||||
err := upgradeSchema2to3(&diskConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Can't update schema version from 2 to 3: %s", err)
|
||||
}
|
||||
err := upgradeSchema2to3(&diskConf)
|
||||
require.Nil(t, err)
|
||||
|
||||
// check new schema version
|
||||
compareSchemaVersion(t, diskConfig["schema_version"], 3)
|
||||
require.Equal(t, diskConf["schema_version"], 3)
|
||||
|
||||
// pull out new dns configuration
|
||||
dnsMap, ok := diskConfig["dns"]
|
||||
if !ok {
|
||||
t.Fatalf("No dns config in new configuration")
|
||||
}
|
||||
dnsMap, ok := diskConf["dns"]
|
||||
require.True(t, ok)
|
||||
|
||||
// cast dns configuration to map
|
||||
newDNSConfig := castInterfaceToMap(t, dnsMap)
|
||||
|
||||
// check if bootstrap DNS becomes an array
|
||||
bootstrapDNS := newDNSConfig["bootstrap_dns"]
|
||||
newDNSConf := convertToObject(t, dnsMap)
|
||||
bootstrapDNS := newDNSConf["bootstrap_dns"]
|
||||
switch v := bootstrapDNS.(type) {
|
||||
case []string:
|
||||
if len(v) != 1 {
|
||||
t.Fatalf("Wrong count of bootsrap DNS servers: %d", len(v))
|
||||
}
|
||||
|
||||
if v[0] != "8.8.8.8:53" {
|
||||
t.Fatalf("Bootsrap DNS server is not 8.8.8.8:53 : %s", v[0])
|
||||
}
|
||||
require.Len(t, v, 1)
|
||||
require.Equal(t, "8.8.8.8:53", v[0])
|
||||
default:
|
||||
t.Fatalf("Wrong type for bootsrap DNS: %T", v)
|
||||
t.Fatalf("wrong type for bootsrap dns: %T", v)
|
||||
}
|
||||
|
||||
// exclude bootstrap DNS from DNS configs comparison
|
||||
excludedEntries := []string{"bootstrap_dns"}
|
||||
oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(2))
|
||||
compareConfigsWithoutEntries(t, &oldDNSConfig, &newDNSConfig, excludedEntries, excludedEntries)
|
||||
oldDNSConf := convertToObject(t, testDNSConf(2))
|
||||
assertEqualExcept(t, oldDNSConf, newDNSConf, excludedEntries, excludedEntries)
|
||||
|
||||
// excluded dns config and schema version from disk config comparison
|
||||
excludedEntries = []string{"dns", "schema_version"}
|
||||
oldDiskConfig := createTestDiskConfig(2)
|
||||
compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, excludedEntries, excludedEntries)
|
||||
oldDiskConf := testDiskConf(2)
|
||||
assertEqualExcept(t, oldDiskConf, diskConf, excludedEntries, excludedEntries)
|
||||
}
|
||||
|
||||
func castInterfaceToMap(t *testing.T, oldConfig interface{}) (newConfig map[string]interface{}) {
|
||||
newConfig = make(map[string]interface{})
|
||||
switch v := oldConfig.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
func convertToObject(t *testing.T, oldConf any) (newConf object) {
|
||||
t.Helper()
|
||||
|
||||
switch v := oldConf.(type) {
|
||||
case map[any]any:
|
||||
newConf = make(object, len(v))
|
||||
for key, value := range v {
|
||||
newConfig[fmt.Sprint(key)] = value
|
||||
newConf[fmt.Sprint(key)] = value
|
||||
}
|
||||
case map[string]interface{}:
|
||||
case object:
|
||||
newConf = make(object, len(v))
|
||||
for key, value := range v {
|
||||
newConfig[key] = value
|
||||
newConf[key] = value
|
||||
}
|
||||
default:
|
||||
t.Fatalf("DNS configuration is not a map")
|
||||
t.Fatalf("dns configuration is not a map, got %T", oldConf)
|
||||
}
|
||||
return
|
||||
|
||||
return newConf
|
||||
}
|
||||
|
||||
// compareConfigsWithoutEntry removes entries from configs and returns result of compareConfigs
|
||||
func compareConfigsWithoutEntries(t *testing.T, oldConfig, newConfig *map[string]interface{}, oldKey, newKey []string) {
|
||||
for _, k := range oldKey {
|
||||
delete(*oldConfig, k)
|
||||
// assertEqualExcept removes entries from configs and compares them.
|
||||
func assertEqualExcept(t *testing.T, oldConf, newConf object, oldKeys, newKeys []string) {
|
||||
t.Helper()
|
||||
|
||||
for _, k := range oldKeys {
|
||||
delete(oldConf, k)
|
||||
}
|
||||
for _, k := range newKey {
|
||||
delete(*newConfig, k)
|
||||
for _, k := range newKeys {
|
||||
delete(newConf, k)
|
||||
}
|
||||
compareConfigs(t, oldConfig, newConfig)
|
||||
|
||||
assert.Equal(t, oldConf, newConf)
|
||||
}
|
||||
|
||||
// compares configs before and after schema upgrade
|
||||
func compareConfigs(t *testing.T, oldConfig, newConfig *map[string]interface{}) {
|
||||
if len(*oldConfig) != len(*newConfig) {
|
||||
t.Fatalf("wrong config entries count! Before upgrade: %d; After upgrade: %d", len(*oldConfig), len(*oldConfig))
|
||||
}
|
||||
|
||||
// Check old and new entries
|
||||
for k, v := range *newConfig {
|
||||
switch value := v.(type) {
|
||||
case string:
|
||||
if value != (*oldConfig)[k] {
|
||||
t.Fatalf("wrong value for string %s. Before update: %s; After update: %s", k, (*oldConfig)[k], value)
|
||||
}
|
||||
case int:
|
||||
if value != (*oldConfig)[k] {
|
||||
t.Fatalf("wrong value for int %s. Before update: %d; After update: %d", k, (*oldConfig)[k], value)
|
||||
}
|
||||
case []string:
|
||||
for i, line := range value {
|
||||
if len((*oldConfig)[k].([]string)) != len(value) {
|
||||
t.Fatalf("wrong array length for %s. Before update: %d; After update: %d", k, len((*oldConfig)[k].([]string)), len(value))
|
||||
}
|
||||
if (*oldConfig)[k].([]string)[i] != line {
|
||||
t.Fatalf("wrong data for string array %s. Before update: %s; After update: %s", k, (*oldConfig)[k].([]string)[i], line)
|
||||
}
|
||||
}
|
||||
case bool:
|
||||
if v != (*oldConfig)[k].(bool) {
|
||||
t.Fatalf("wrong boolean value for %s", k)
|
||||
}
|
||||
case []filter:
|
||||
if len((*oldConfig)[k].([]filter)) != len(value) {
|
||||
t.Fatalf("wrong filters count. Before update: %d; After update: %d", len((*oldConfig)[k].([]filter)), len(value))
|
||||
}
|
||||
for i, newFilter := range value {
|
||||
oldFilter := (*oldConfig)[k].([]filter)[i]
|
||||
if oldFilter.Enabled != newFilter.Enabled || oldFilter.Name != newFilter.Name || oldFilter.RulesCount != newFilter.RulesCount {
|
||||
t.Fatalf("old filter %s not equals new filter %s", oldFilter.Name, newFilter.Name)
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Fatalf("uknown data type for %s: %T", k, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compareSchemaVersion check if newSchemaVersion equals schemaVersion
|
||||
func compareSchemaVersion(t *testing.T, newSchemaVersion interface{}, schemaVersion int) {
|
||||
switch v := newSchemaVersion.(type) {
|
||||
case int:
|
||||
if v != schemaVersion {
|
||||
t.Fatalf("Wrong schema version in new config file")
|
||||
}
|
||||
default:
|
||||
t.Fatalf("Schema version is not an integer after update")
|
||||
}
|
||||
}
|
||||
|
||||
func createTestDiskConfig(schemaVersion int) (diskConfig map[string]interface{}) {
|
||||
diskConfig = make(map[string]interface{})
|
||||
diskConfig["language"] = "en"
|
||||
diskConfig["filters"] = []filter{
|
||||
func testDiskConf(schemaVersion int) (diskConf object) {
|
||||
filters := []filter{
|
||||
{
|
||||
URL: "https://filters.adtidy.org/android/filters/111_optimized.txt",
|
||||
Name: "Latvian filter",
|
||||
@@ -191,40 +116,51 @@ func createTestDiskConfig(schemaVersion int) (diskConfig map[string]interface{})
|
||||
RulesCount: 200,
|
||||
},
|
||||
}
|
||||
diskConfig["user_rules"] = []string{}
|
||||
diskConfig["schema_version"] = schemaVersion
|
||||
diskConfig["bind_host"] = "0.0.0.0"
|
||||
diskConfig["bind_port"] = 80
|
||||
diskConfig["auth_name"] = "name"
|
||||
diskConfig["auth_pass"] = "pass"
|
||||
dnsConfig := createTestDNSConfig(schemaVersion)
|
||||
if schemaVersion > 1 {
|
||||
diskConfig["dns"] = dnsConfig
|
||||
} else {
|
||||
diskConfig["coredns"] = dnsConfig
|
||||
diskConf = object{
|
||||
"language": "en",
|
||||
"filters": filters,
|
||||
"user_rules": []string{},
|
||||
"schema_version": schemaVersion,
|
||||
"bind_host": "0.0.0.0",
|
||||
"bind_port": 80,
|
||||
"auth_name": "name",
|
||||
"auth_pass": "pass",
|
||||
}
|
||||
return diskConfig
|
||||
|
||||
dnsConf := testDNSConf(schemaVersion)
|
||||
if schemaVersion > 1 {
|
||||
diskConf["dns"] = dnsConf
|
||||
} else {
|
||||
diskConf["coredns"] = dnsConf
|
||||
}
|
||||
|
||||
return diskConf
|
||||
}
|
||||
|
||||
func createTestDNSConfig(schemaVersion int) map[interface{}]interface{} {
|
||||
dnsConfig := make(map[interface{}]interface{})
|
||||
dnsConfig["port"] = 53
|
||||
dnsConfig["blocked_response_ttl"] = 10
|
||||
dnsConfig["querylog_enabled"] = true
|
||||
dnsConfig["ratelimit"] = 20
|
||||
dnsConfig["bootstrap_dns"] = "8.8.8.8:53"
|
||||
if schemaVersion > 2 {
|
||||
dnsConfig["bootstrap_dns"] = []string{"8.8.8.8:53"}
|
||||
// testDNSConf creates a DNS config for test the way gopkg.in/yaml.v2 would
|
||||
// unmarshal it. In YAML, keys aren't guaranteed to always only be strings.
|
||||
func testDNSConf(schemaVersion int) (dnsConf map[any]any) {
|
||||
dnsConf = map[any]any{
|
||||
"port": 53,
|
||||
"blocked_response_ttl": 10,
|
||||
"querylog_enabled": true,
|
||||
"ratelimit": 20,
|
||||
"bootstrap_dns": "8.8.8.8:53",
|
||||
"parental_sensitivity": 13,
|
||||
"ratelimit_whitelist": []string{},
|
||||
"upstream_dns": []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"},
|
||||
"filtering_enabled": true,
|
||||
"refuse_any": true,
|
||||
"parental_enabled": true,
|
||||
"bind_host": "0.0.0.0",
|
||||
"protection_enabled": true,
|
||||
"safesearch_enabled": true,
|
||||
"safebrowsing_enabled": true,
|
||||
}
|
||||
dnsConfig["parental_sensitivity"] = 13
|
||||
dnsConfig["ratelimit_whitelist"] = []string{}
|
||||
dnsConfig["upstream_dns"] = []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"}
|
||||
dnsConfig["filtering_enabled"] = true
|
||||
dnsConfig["refuse_any"] = true
|
||||
dnsConfig["parental_enabled"] = true
|
||||
dnsConfig["bind_host"] = "0.0.0.0"
|
||||
dnsConfig["protection_enabled"] = true
|
||||
dnsConfig["safesearch_enabled"] = true
|
||||
dnsConfig["safebrowsing_enabled"] = true
|
||||
return dnsConfig
|
||||
|
||||
if schemaVersion > 2 {
|
||||
dnsConf["bootstrap_dns"] = []string{"8.8.8.8:53"}
|
||||
}
|
||||
|
||||
return dnsConf
|
||||
}
|
||||
|
||||
@@ -16,17 +16,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// ReadTimeout is the maximum duration for reading the entire request,
|
||||
// readTimeout is the maximum duration for reading the entire request,
|
||||
// including the body.
|
||||
ReadTimeout = 10 * time.Second
|
||||
|
||||
// ReadHeaderTimeout is the amount of time allowed to read request
|
||||
// headers.
|
||||
ReadHeaderTimeout = 10 * time.Second
|
||||
|
||||
// WriteTimeout is the maximum duration before timing out writes of the
|
||||
readTimeout = 60 * time.Second
|
||||
// readHdrTimeout is the amount of time allowed to read request headers.
|
||||
readHdrTimeout = 60 * time.Second
|
||||
// writeTimeout is the maximum duration before timing out writes of the
|
||||
// response.
|
||||
WriteTimeout = 10 * time.Second
|
||||
writeTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
type webConfig struct {
|
||||
@@ -191,7 +188,10 @@ func (web *Web) Start() {
|
||||
WriteTimeout: web.conf.WriteTimeout,
|
||||
}
|
||||
go func() {
|
||||
errs <- web.httpServerBeta.ListenAndServe()
|
||||
betaErr := web.httpServerBeta.ListenAndServe()
|
||||
if betaErr != nil {
|
||||
log.Error("starting beta http server: %s", betaErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ func (web *Web) tlsServerLoop() {
|
||||
RootCAs: Context.tlsRoots,
|
||||
CipherSuites: Context.tlsCiphers,
|
||||
},
|
||||
Handler: Context.mux,
|
||||
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
||||
ReadTimeout: web.conf.ReadTimeout,
|
||||
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
|
||||
WriteTimeout: web.conf.WriteTimeout,
|
||||
|
||||
@@ -53,6 +53,7 @@ func NewClientProto(s string) (cp ClientProto, err error) {
|
||||
ClientProtoDOH,
|
||||
ClientProtoDOQ,
|
||||
ClientProtoDOT,
|
||||
ClientProtoDNSCrypt,
|
||||
ClientProtoPlain:
|
||||
|
||||
return cp, nil
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package querylog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -14,226 +14,260 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
func prepareTestDir() string {
|
||||
const dir = "./agh-test"
|
||||
_ = os.RemoveAll(dir)
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
return dir
|
||||
}
|
||||
|
||||
// Check adding and loading (with filtering) entries from disk and memory
|
||||
// TestQueryLog tests adding and loading (with filtering) entries from disk and
|
||||
// memory.
|
||||
func TestQueryLog(t *testing.T) {
|
||||
conf := Config{
|
||||
l := newQueryLog(Config{
|
||||
Enabled: true,
|
||||
FileEnabled: true,
|
||||
Interval: 1,
|
||||
MemSize: 100,
|
||||
}
|
||||
conf.BaseDir = prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
|
||||
l := newQueryLog(conf)
|
||||
BaseDir: aghtest.PrepareTestDir(t),
|
||||
})
|
||||
|
||||
// add disk entries
|
||||
// Add disk entries.
|
||||
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
// write to disk (first file)
|
||||
_ = l.flushLogBuffer(true)
|
||||
// start writing to the second file
|
||||
_ = l.rotate()
|
||||
// add disk entries
|
||||
// Write to disk (first file).
|
||||
require.Nil(t, l.flushLogBuffer(true))
|
||||
// Start writing to the second file.
|
||||
require.Nil(t, l.rotate())
|
||||
// Add disk entries.
|
||||
addEntry(l, "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
|
||||
// write to disk
|
||||
_ = l.flushLogBuffer(true)
|
||||
// add memory entries
|
||||
// Write to disk.
|
||||
require.Nil(t, l.flushLogBuffer(true))
|
||||
// Add memory entries.
|
||||
addEntry(l, "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
|
||||
addEntry(l, "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
|
||||
|
||||
// get all entries
|
||||
params := newSearchParams()
|
||||
entries, _ := l.search(params)
|
||||
assert.Len(t, entries, 4)
|
||||
assertLogEntry(t, entries[0], "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
|
||||
assertLogEntry(t, entries[1], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
|
||||
assertLogEntry(t, entries[2], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
|
||||
assertLogEntry(t, entries[3], "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
type tcAssertion struct {
|
||||
num int
|
||||
host string
|
||||
answer, client net.IP
|
||||
}
|
||||
|
||||
// search by domain (strict)
|
||||
params = newSearchParams()
|
||||
params.searchCriteria = append(params.searchCriteria, searchCriteria{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: true,
|
||||
value: "TEST.example.org",
|
||||
})
|
||||
entries, _ = l.search(params)
|
||||
assert.Len(t, entries, 1)
|
||||
assertLogEntry(t, entries[0], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
|
||||
testCases := []struct {
|
||||
name string
|
||||
sCr []searchCriteria
|
||||
want []tcAssertion
|
||||
}{{
|
||||
name: "all",
|
||||
sCr: []searchCriteria{},
|
||||
want: []tcAssertion{
|
||||
{num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
|
||||
{num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
|
||||
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
|
||||
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
|
||||
},
|
||||
}, {
|
||||
name: "by_domain_strict",
|
||||
sCr: []searchCriteria{{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: true,
|
||||
value: "TEST.example.org",
|
||||
}},
|
||||
want: []tcAssertion{{
|
||||
num: 0, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3),
|
||||
}},
|
||||
}, {
|
||||
name: "by_domain_non-strict",
|
||||
sCr: []searchCriteria{{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: false,
|
||||
value: "example.ORG",
|
||||
}},
|
||||
want: []tcAssertion{
|
||||
{num: 0, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
|
||||
{num: 1, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
|
||||
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
|
||||
},
|
||||
}, {
|
||||
name: "by_client_ip_strict",
|
||||
sCr: []searchCriteria{{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: true,
|
||||
value: "2.2.2.2",
|
||||
}},
|
||||
want: []tcAssertion{{
|
||||
num: 0, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2),
|
||||
}},
|
||||
}, {
|
||||
name: "by_client_ip_non-strict",
|
||||
sCr: []searchCriteria{{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: false,
|
||||
value: "2.2.2",
|
||||
}},
|
||||
want: []tcAssertion{
|
||||
{num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
|
||||
{num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
|
||||
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
|
||||
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
|
||||
},
|
||||
}}
|
||||
|
||||
// search by domain (not strict)
|
||||
params = newSearchParams()
|
||||
params.searchCriteria = append(params.searchCriteria, searchCriteria{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: false,
|
||||
value: "example.ORG",
|
||||
})
|
||||
entries, _ = l.search(params)
|
||||
assert.Len(t, entries, 3)
|
||||
assertLogEntry(t, entries[0], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
|
||||
assertLogEntry(t, entries[1], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
|
||||
assertLogEntry(t, entries[2], "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
params := newSearchParams()
|
||||
params.searchCriteria = tc.sCr
|
||||
|
||||
// search by client IP (strict)
|
||||
params = newSearchParams()
|
||||
params.searchCriteria = append(params.searchCriteria, searchCriteria{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: true,
|
||||
value: "2.2.2.2",
|
||||
})
|
||||
entries, _ = l.search(params)
|
||||
assert.Len(t, entries, 1)
|
||||
assertLogEntry(t, entries[0], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
|
||||
|
||||
// search by client IP (part of)
|
||||
params = newSearchParams()
|
||||
params.searchCriteria = append(params.searchCriteria, searchCriteria{
|
||||
criteriaType: ctDomainOrClient,
|
||||
strict: false,
|
||||
value: "2.2.2",
|
||||
})
|
||||
entries, _ = l.search(params)
|
||||
assert.Len(t, entries, 4)
|
||||
assertLogEntry(t, entries[0], "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
|
||||
assertLogEntry(t, entries[1], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
|
||||
assertLogEntry(t, entries[2], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
|
||||
assertLogEntry(t, entries[3], "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
entries, _ := l.search(params)
|
||||
require.Len(t, entries, len(tc.want))
|
||||
for _, want := range tc.want {
|
||||
assertLogEntry(t, entries[want.num], want.host, want.answer, want.client)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryLogOffsetLimit(t *testing.T) {
|
||||
conf := Config{
|
||||
l := newQueryLog(Config{
|
||||
Enabled: true,
|
||||
Interval: 1,
|
||||
MemSize: 100,
|
||||
}
|
||||
conf.BaseDir = prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
|
||||
l := newQueryLog(conf)
|
||||
BaseDir: aghtest.PrepareTestDir(t),
|
||||
})
|
||||
|
||||
// add 10 entries to the log
|
||||
for i := 0; i < 10; i++ {
|
||||
addEntry(l, "second.example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
const (
|
||||
entNum = 10
|
||||
firstPageDomain = "first.example.org"
|
||||
secondPageDomain = "second.example.org"
|
||||
)
|
||||
// Add entries to the log.
|
||||
for i := 0; i < entNum; i++ {
|
||||
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
}
|
||||
// write them to disk (first file)
|
||||
_ = l.flushLogBuffer(true)
|
||||
// add 10 more entries to the log (memory)
|
||||
for i := 0; i < 10; i++ {
|
||||
addEntry(l, "first.example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
// Write them to the first file.
|
||||
require.Nil(t, l.flushLogBuffer(true))
|
||||
// Add more to the in-memory part of log.
|
||||
for i := 0; i < entNum; i++ {
|
||||
addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
}
|
||||
|
||||
// First page
|
||||
params := newSearchParams()
|
||||
params.offset = 0
|
||||
params.limit = 10
|
||||
entries, _ := l.search(params)
|
||||
assert.Len(t, entries, 10)
|
||||
assert.Equal(t, entries[0].QHost, "first.example.org")
|
||||
assert.Equal(t, entries[9].QHost, "first.example.org")
|
||||
|
||||
// Second page
|
||||
params.offset = 10
|
||||
params.limit = 10
|
||||
entries, _ = l.search(params)
|
||||
assert.Len(t, entries, 10)
|
||||
assert.Equal(t, entries[0].QHost, "second.example.org")
|
||||
assert.Equal(t, entries[9].QHost, "second.example.org")
|
||||
testCases := []struct {
|
||||
name string
|
||||
offset int
|
||||
limit int
|
||||
wantLen int
|
||||
want string
|
||||
}{{
|
||||
name: "page_1",
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
wantLen: 10,
|
||||
want: firstPageDomain,
|
||||
}, {
|
||||
name: "page_2",
|
||||
offset: 10,
|
||||
limit: 10,
|
||||
wantLen: 10,
|
||||
want: secondPageDomain,
|
||||
}, {
|
||||
name: "page_2.5",
|
||||
offset: 15,
|
||||
limit: 10,
|
||||
wantLen: 5,
|
||||
want: secondPageDomain,
|
||||
}, {
|
||||
name: "page_3",
|
||||
offset: 20,
|
||||
limit: 10,
|
||||
wantLen: 0,
|
||||
}}
|
||||
|
||||
// Second and a half page
|
||||
params.offset = 15
|
||||
params.limit = 10
|
||||
entries, _ = l.search(params)
|
||||
assert.Len(t, entries, 5)
|
||||
assert.Equal(t, entries[0].QHost, "second.example.org")
|
||||
assert.Equal(t, entries[4].QHost, "second.example.org")
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
params.offset = tc.offset
|
||||
params.limit = tc.limit
|
||||
entries, _ := l.search(params)
|
||||
|
||||
// Third page
|
||||
params.offset = 20
|
||||
params.limit = 10
|
||||
entries, _ = l.search(params)
|
||||
assert.Empty(t, entries)
|
||||
require.Len(t, entries, tc.wantLen)
|
||||
|
||||
if tc.wantLen > 0 {
|
||||
assert.Equal(t, entries[0].QHost, tc.want)
|
||||
assert.Equal(t, entries[tc.wantLen-1].QHost, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryLogMaxFileScanEntries(t *testing.T) {
|
||||
conf := Config{
|
||||
l := newQueryLog(Config{
|
||||
Enabled: true,
|
||||
FileEnabled: true,
|
||||
Interval: 1,
|
||||
MemSize: 100,
|
||||
}
|
||||
conf.BaseDir = prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
|
||||
l := newQueryLog(conf)
|
||||
BaseDir: aghtest.PrepareTestDir(t),
|
||||
})
|
||||
|
||||
// add 10 entries to the log
|
||||
for i := 0; i < 10; i++ {
|
||||
const entNum = 10
|
||||
// Add entries to the log.
|
||||
for i := 0; i < entNum; i++ {
|
||||
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
}
|
||||
// write them to disk (first file)
|
||||
_ = l.flushLogBuffer(true)
|
||||
// Write them to disk.
|
||||
require.Nil(t, l.flushLogBuffer(true))
|
||||
|
||||
params := newSearchParams()
|
||||
params.maxFileScanEntries = 5 // do not scan more than 5 records
|
||||
entries, _ := l.search(params)
|
||||
assert.Len(t, entries, 5)
|
||||
|
||||
params.maxFileScanEntries = 0 // disable the limit
|
||||
entries, _ = l.search(params)
|
||||
assert.Len(t, entries, 10)
|
||||
for _, maxFileScanEntries := range []int{5, 0} {
|
||||
t.Run(fmt.Sprintf("limit_%d", maxFileScanEntries), func(t *testing.T) {
|
||||
params.maxFileScanEntries = maxFileScanEntries
|
||||
entries, _ := l.search(params)
|
||||
assert.Len(t, entries, entNum-maxFileScanEntries)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryLogFileDisabled(t *testing.T) {
|
||||
conf := Config{
|
||||
l := newQueryLog(Config{
|
||||
Enabled: true,
|
||||
FileEnabled: false,
|
||||
Interval: 1,
|
||||
MemSize: 2,
|
||||
}
|
||||
conf.BaseDir = prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
|
||||
l := newQueryLog(conf)
|
||||
BaseDir: aghtest.PrepareTestDir(t),
|
||||
})
|
||||
|
||||
addEntry(l, "example1.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
addEntry(l, "example2.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
// The oldest entry is going to be removed from memory buffer.
|
||||
addEntry(l, "example3.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||
// the oldest entry is now removed from mem buffer
|
||||
|
||||
params := newSearchParams()
|
||||
ll, _ := l.search(params)
|
||||
assert.Len(t, ll, 2)
|
||||
require.Len(t, ll, 2)
|
||||
assert.Equal(t, "example3.org", ll[0].QHost)
|
||||
assert.Equal(t, "example2.org", ll[1].QHost)
|
||||
}
|
||||
|
||||
func addEntry(l *queryLog, host string, answerStr, client net.IP) {
|
||||
q := dns.Msg{}
|
||||
q.Question = append(q.Question, dns.Question{
|
||||
Name: host + ".",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
})
|
||||
|
||||
a := dns.Msg{}
|
||||
a.Question = append(a.Question, q.Question[0])
|
||||
answer := new(dns.A)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: q.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
q := dns.Msg{
|
||||
Question: []dns.Question{{
|
||||
Name: host + ".",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
}},
|
||||
}
|
||||
|
||||
a := dns.Msg{
|
||||
Question: q.Question,
|
||||
Answer: []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: answerStr,
|
||||
}},
|
||||
}
|
||||
answer.A = answerStr
|
||||
a.Answer = append(a.Answer, answer)
|
||||
res := dnsfilter.Result{
|
||||
IsFiltered: true,
|
||||
Reason: dnsfilter.Rewritten,
|
||||
@@ -254,19 +288,22 @@ func addEntry(l *queryLog, host string, answerStr, client net.IP) {
|
||||
l.Add(params)
|
||||
}
|
||||
|
||||
func assertLogEntry(t *testing.T, entry *logEntry, host string, answer, client net.IP) bool {
|
||||
func assertLogEntry(t *testing.T, entry *logEntry, host string, answer, client net.IP) {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, entry)
|
||||
|
||||
assert.Equal(t, host, entry.QHost)
|
||||
assert.Equal(t, client, entry.IP)
|
||||
assert.Equal(t, "A", entry.QType)
|
||||
assert.Equal(t, "IN", entry.QClass)
|
||||
|
||||
msg := new(dns.Msg)
|
||||
assert.Nil(t, msg.Unpack(entry.Answer))
|
||||
assert.Len(t, msg.Answer, 1)
|
||||
msg := &dns.Msg{}
|
||||
require.Nil(t, msg.Unpack(entry.Answer))
|
||||
require.Len(t, msg.Answer, 1)
|
||||
|
||||
ip := proxyutil.GetIPFromDNSRecord(msg.Answer[0]).To16()
|
||||
assert.NotNil(t, ip)
|
||||
assert.Equal(t, answer, ip)
|
||||
return true
|
||||
}
|
||||
|
||||
func testEntries() (entries []*logEntry) {
|
||||
@@ -332,8 +369,8 @@ func TestLogEntriesByTime_sort(t *testing.T) {
|
||||
entries := testEntries()
|
||||
sort.Sort(logEntriesByTimeDesc(entries))
|
||||
|
||||
for i := 1; i < len(entries); i++ {
|
||||
assert.False(t, entries[i].Time.After(entries[i-1].Time),
|
||||
"%s %s", entries[i].Time, entries[i-1].Time)
|
||||
for i := range entries[1:] {
|
||||
assert.False(t, entries[i+1].Time.After(entries[i].Time),
|
||||
"%s %s", entries[i+1].Time, entries[i].Time)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,347 +2,347 @@ package querylog
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQLogFileEmpty(t *testing.T) {
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFile := prepareTestFile(testDir, 0)
|
||||
// prepareTestFiles prepares several test query log files, each with the
|
||||
// specified lines count.
|
||||
func prepareTestFiles(t *testing.T, filesNum, linesNum int) []string {
|
||||
t.Helper()
|
||||
|
||||
// create the new QLogFile instance
|
||||
q, err := NewQLogFile(testFile)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, q)
|
||||
defer q.Close()
|
||||
|
||||
// seek to the start
|
||||
pos, err := q.SeekStart()
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, 0, pos)
|
||||
|
||||
// try reading anyway
|
||||
line, err := q.ReadNext()
|
||||
assert.Equal(t, io.EOF, err)
|
||||
assert.Empty(t, line)
|
||||
}
|
||||
|
||||
func TestQLogFileLarge(t *testing.T) {
|
||||
// should be large enough
|
||||
count := 50000
|
||||
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFile := prepareTestFile(testDir, count)
|
||||
|
||||
// create the new QLogFile instance
|
||||
q, err := NewQLogFile(testFile)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, q)
|
||||
defer q.Close()
|
||||
|
||||
// seek to the start
|
||||
pos, err := q.SeekStart()
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqualValues(t, 0, pos)
|
||||
|
||||
read := 0
|
||||
var line string
|
||||
for err == nil {
|
||||
line, err = q.ReadNext()
|
||||
if err == nil {
|
||||
assert.NotZero(t, len(line))
|
||||
read++
|
||||
}
|
||||
if filesNum == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
assert.Equal(t, count, read)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
func TestQLogFileSeekLargeFile(t *testing.T) {
|
||||
// more or less big file
|
||||
count := 10000
|
||||
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFile := prepareTestFile(testDir, count)
|
||||
|
||||
// create the new QLogFile instance
|
||||
q, err := NewQLogFile(testFile)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, q)
|
||||
defer q.Close()
|
||||
|
||||
// CASE 1: NOT TOO OLD LINE
|
||||
testSeekLineQLogFile(t, q, 300)
|
||||
|
||||
// CASE 2: OLD LINE
|
||||
testSeekLineQLogFile(t, q, count-300)
|
||||
|
||||
// CASE 3: FIRST LINE
|
||||
testSeekLineQLogFile(t, q, 0)
|
||||
|
||||
// CASE 4: LAST LINE
|
||||
testSeekLineQLogFile(t, q, count)
|
||||
|
||||
// CASE 5: Seek non-existent (too low)
|
||||
_, _, err = q.SeekTS(123)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// CASE 6: Seek non-existent (too high)
|
||||
ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
|
||||
_, _, err = q.SeekTS(ts.UnixNano())
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// CASE 7: "Almost" found
|
||||
line, err := getQLogFileLine(q, count/2)
|
||||
assert.Nil(t, err)
|
||||
// ALMOST the record we need
|
||||
timestamp := readQLogTimestamp(line) - 1
|
||||
assert.NotEqualValues(t, 0, timestamp)
|
||||
_, depth, err := q.SeekTS(timestamp)
|
||||
assert.NotNil(t, err)
|
||||
assert.LessOrEqual(t, depth, int(math.Log2(float64(count))+3))
|
||||
}
|
||||
|
||||
func TestQLogFileSeekSmallFile(t *testing.T) {
|
||||
// more or less big file
|
||||
count := 10
|
||||
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFile := prepareTestFile(testDir, count)
|
||||
|
||||
// create the new QLogFile instance
|
||||
q, err := NewQLogFile(testFile)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, q)
|
||||
defer q.Close()
|
||||
|
||||
// CASE 1: NOT TOO OLD LINE
|
||||
testSeekLineQLogFile(t, q, 2)
|
||||
|
||||
// CASE 2: OLD LINE
|
||||
testSeekLineQLogFile(t, q, count-2)
|
||||
|
||||
// CASE 3: FIRST LINE
|
||||
testSeekLineQLogFile(t, q, 0)
|
||||
|
||||
// CASE 4: LAST LINE
|
||||
testSeekLineQLogFile(t, q, count)
|
||||
|
||||
// CASE 5: Seek non-existent (too low)
|
||||
_, _, err = q.SeekTS(123)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// CASE 6: Seek non-existent (too high)
|
||||
ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
|
||||
_, _, err = q.SeekTS(ts.UnixNano())
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// CASE 7: "Almost" found
|
||||
line, err := getQLogFileLine(q, count/2)
|
||||
assert.Nil(t, err)
|
||||
// ALMOST the record we need
|
||||
timestamp := readQLogTimestamp(line) - 1
|
||||
assert.NotEqualValues(t, 0, timestamp)
|
||||
_, depth, err := q.SeekTS(timestamp)
|
||||
assert.NotNil(t, err)
|
||||
assert.LessOrEqual(t, depth, int(math.Log2(float64(count))+3))
|
||||
}
|
||||
|
||||
func testSeekLineQLogFile(t *testing.T, q *QLogFile, lineNumber int) {
|
||||
line, err := getQLogFileLine(q, lineNumber)
|
||||
assert.Nil(t, err)
|
||||
ts := readQLogTimestamp(line)
|
||||
assert.NotEqualValues(t, 0, ts)
|
||||
|
||||
// try seeking to that line now
|
||||
pos, _, err := q.SeekTS(ts)
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqualValues(t, 0, pos)
|
||||
|
||||
testLine, err := q.ReadNext()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, line, testLine)
|
||||
}
|
||||
|
||||
func getQLogFileLine(q *QLogFile, lineNumber int) (string, error) {
|
||||
_, err := q.SeekStart()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := 1; i < lineNumber; i++ {
|
||||
_, err := q.ReadNext()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return q.ReadNext()
|
||||
}
|
||||
|
||||
// Check adding and loading (with filtering) entries from disk and memory
|
||||
func TestQLogFile(t *testing.T) {
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFile := prepareTestFile(testDir, 2)
|
||||
|
||||
// create the new QLogFile instance
|
||||
q, err := NewQLogFile(testFile)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, q)
|
||||
defer q.Close()
|
||||
|
||||
// seek to the start
|
||||
pos, err := q.SeekStart()
|
||||
assert.Nil(t, err)
|
||||
assert.Greater(t, pos, int64(0))
|
||||
|
||||
// read first line
|
||||
line, err := q.ReadNext()
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, line, "0.0.0.2")
|
||||
assert.True(t, strings.HasPrefix(line, "{"), line)
|
||||
assert.True(t, strings.HasSuffix(line, "}"), line)
|
||||
|
||||
// read second line
|
||||
line, err = q.ReadNext()
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, 0, q.position)
|
||||
assert.Contains(t, line, "0.0.0.1")
|
||||
assert.True(t, strings.HasPrefix(line, "{"), line)
|
||||
assert.True(t, strings.HasSuffix(line, "}"), line)
|
||||
|
||||
// try reading again (there's nothing to read anymore)
|
||||
line, err = q.ReadNext()
|
||||
assert.Equal(t, io.EOF, err)
|
||||
assert.Empty(t, line)
|
||||
}
|
||||
|
||||
// prepareTestFile - prepares a test query log file with the specified number of lines
|
||||
func prepareTestFile(dir string, linesCount int) string {
|
||||
return prepareTestFiles(dir, 1, linesCount)[0]
|
||||
}
|
||||
|
||||
// prepareTestFiles - prepares several test query log files
|
||||
// each of them -- with the specified linesCount
|
||||
func prepareTestFiles(dir string, filesCount, linesCount int) []string {
|
||||
format := `{"IP":"${IP}","T":"${TIMESTAMP}","QH":"example.org","QT":"A","QC":"IN","Answer":"AAAAAAABAAEAAAAAB2V4YW1wbGUDb3JnAAABAAEHZXhhbXBsZQNvcmcAAAEAAQAAAAAABAECAwQ=","Result":{},"Elapsed":0,"Upstream":"upstream"}`
|
||||
const strV = "\"%s\""
|
||||
const nl = "\n"
|
||||
const format = `{"IP":` + strV + `,"T":` + strV + `,` +
|
||||
`"QH":"example.org","QT":"A","QC":"IN",` +
|
||||
`"Answer":"AAAAAAABAAEAAAAAB2V4YW1wbGUDb3JnAAABAAEHZXhhbXBsZQNvcmcAAAEAAQAAAAAABAECAwQ=",` +
|
||||
`"Result":{},"Elapsed":0,"Upstream":"upstream"}` + nl
|
||||
|
||||
lineTime, _ := time.Parse(time.RFC3339Nano, "2020-02-18T22:36:35.920973+03:00")
|
||||
lineIP := uint32(0)
|
||||
|
||||
files := make([]string, filesCount)
|
||||
for j := 0; j < filesCount; j++ {
|
||||
f, _ := ioutil.TempFile(dir, "*.txt")
|
||||
files[filesCount-j-1] = f.Name()
|
||||
dir := aghtest.PrepareTestDir(t)
|
||||
|
||||
for i := 0; i < linesCount; i++ {
|
||||
files := make([]string, filesNum)
|
||||
for j := range files {
|
||||
f, err := ioutil.TempFile(dir, "*.txt")
|
||||
require.Nil(t, err)
|
||||
files[filesNum-j-1] = f.Name()
|
||||
|
||||
for i := 0; i < linesNum; i++ {
|
||||
lineIP++
|
||||
lineTime = lineTime.Add(time.Second)
|
||||
|
||||
ip := make(net.IP, 4)
|
||||
binary.BigEndian.PutUint32(ip, lineIP)
|
||||
|
||||
line := format
|
||||
line = strings.ReplaceAll(line, "${IP}", ip.String())
|
||||
line = strings.ReplaceAll(line, "${TIMESTAMP}", lineTime.Format(time.RFC3339Nano))
|
||||
line := fmt.Sprintf(format, ip, lineTime.Format(time.RFC3339Nano))
|
||||
|
||||
_, _ = f.WriteString(line)
|
||||
_, _ = f.WriteString("\n")
|
||||
_, err = f.WriteString(line)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func TestQLogSeek(t *testing.T) {
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
// prepareTestFile prepares a test query log file with the specified number of
|
||||
// lines.
|
||||
func prepareTestFile(t *testing.T, linesCount int) string {
|
||||
t.Helper()
|
||||
|
||||
d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"}
|
||||
{"T":"2020-08-31T18:44:25.376690873+03:00"}
|
||||
{"T":"2020-08-31T18:44:25.382540454+03:00"}`
|
||||
f, _ := ioutil.TempFile(testDir, "*.txt")
|
||||
_, _ = f.WriteString(d)
|
||||
defer f.Close()
|
||||
|
||||
q, err := NewQLogFile(f.Name())
|
||||
assert.Nil(t, err)
|
||||
defer q.Close()
|
||||
|
||||
target, _ := time.Parse(time.RFC3339, "2020-08-31T18:44:25.376690873+03:00")
|
||||
|
||||
_, depth, err := q.SeekTS(target.UnixNano())
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, depth)
|
||||
return prepareTestFiles(t, 1, linesCount)[0]
|
||||
}
|
||||
|
||||
func TestQLogSeek_ErrTSTooLate(t *testing.T) {
|
||||
testDir := prepareTestDir()
|
||||
// newTestQLogFile creates new *QLogFile for tests and registers the required
|
||||
// cleanup functions.
|
||||
func newTestQLogFile(t *testing.T, linesNum int) (file *QLogFile) {
|
||||
t.Helper()
|
||||
|
||||
testFile := prepareTestFile(t, linesNum)
|
||||
|
||||
// Create the new QLogFile instance.
|
||||
file, err := NewQLogFile(testFile)
|
||||
require.Nil(t, err)
|
||||
assert.NotNil(t, file)
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll(testDir)
|
||||
assert.Nil(t, file.Close())
|
||||
})
|
||||
|
||||
d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"}
|
||||
{"T":"2020-08-31T18:44:25.376690873+03:00"}
|
||||
{"T":"2020-08-31T18:44:25.382540454+03:00"}
|
||||
`
|
||||
f, err := ioutil.TempFile(testDir, "*.txt")
|
||||
assert.Nil(t, err)
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(d)
|
||||
assert.Nil(t, err)
|
||||
|
||||
q, err := NewQLogFile(f.Name())
|
||||
assert.Nil(t, err)
|
||||
defer q.Close()
|
||||
|
||||
target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:25.382540454+03:00")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, depth, err := q.SeekTS(target.UnixNano() + int64(time.Second))
|
||||
assert.Equal(t, ErrTSTooLate, err)
|
||||
assert.Equal(t, 2, depth)
|
||||
return file
|
||||
}
|
||||
|
||||
func TestQLogSeek_ErrTSTooEarly(t *testing.T) {
|
||||
testDir := prepareTestDir()
|
||||
func TestQLogFile_ReadNext(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
linesNum int
|
||||
}{{
|
||||
name: "empty",
|
||||
linesNum: 0,
|
||||
}, {
|
||||
name: "large",
|
||||
linesNum: 50000,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
q := newTestQLogFile(t, tc.linesNum)
|
||||
|
||||
// Calculate the expected position.
|
||||
fileInfo, err := q.file.Stat()
|
||||
require.Nil(t, err)
|
||||
var expPos int64
|
||||
if expPos = fileInfo.Size(); expPos > 0 {
|
||||
expPos--
|
||||
}
|
||||
|
||||
// Seek to the start.
|
||||
pos, err := q.SeekStart()
|
||||
require.Nil(t, err)
|
||||
require.EqualValues(t, expPos, pos)
|
||||
|
||||
var read int
|
||||
var line string
|
||||
for err == nil {
|
||||
line, err = q.ReadNext()
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, line)
|
||||
read++
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, io.EOF, err)
|
||||
assert.Equal(t, tc.linesNum, read)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQLogFile_SeekTS_good(t *testing.T) {
|
||||
linesCases := []struct {
|
||||
name string
|
||||
num int
|
||||
}{{
|
||||
name: "large",
|
||||
num: 10000,
|
||||
}, {
|
||||
name: "small",
|
||||
num: 10,
|
||||
}}
|
||||
|
||||
for _, l := range linesCases {
|
||||
testCases := []struct {
|
||||
name string
|
||||
linesNum int
|
||||
line int
|
||||
}{{
|
||||
name: "not_too_old",
|
||||
line: 2,
|
||||
}, {
|
||||
name: "old",
|
||||
line: l.num - 2,
|
||||
}, {
|
||||
name: "first",
|
||||
line: 0,
|
||||
}, {
|
||||
name: "last",
|
||||
line: l.num,
|
||||
}}
|
||||
|
||||
q := newTestQLogFile(t, l.num)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(l.name+"_"+tc.name, func(t *testing.T) {
|
||||
line, err := getQLogFileLine(q, tc.line)
|
||||
require.Nil(t, err)
|
||||
ts := readQLogTimestamp(line)
|
||||
assert.NotEqualValues(t, 0, ts)
|
||||
|
||||
// Try seeking to that line now.
|
||||
pos, _, err := q.SeekTS(ts)
|
||||
require.Nil(t, err)
|
||||
assert.NotEqualValues(t, 0, pos)
|
||||
|
||||
testLine, err := q.ReadNext()
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, line, testLine)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQLogFile_SeekTS_bad(t *testing.T) {
|
||||
linesCases := []struct {
|
||||
name string
|
||||
num int
|
||||
}{{
|
||||
name: "large",
|
||||
num: 10000,
|
||||
}, {
|
||||
name: "small",
|
||||
num: 10,
|
||||
}}
|
||||
|
||||
for _, l := range linesCases {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ts int64
|
||||
leq bool
|
||||
}{{
|
||||
name: "non-existent_long_ago",
|
||||
}, {
|
||||
name: "non-existent_far_ahead",
|
||||
}, {
|
||||
name: "almost",
|
||||
leq: true,
|
||||
}}
|
||||
|
||||
q := newTestQLogFile(t, l.num)
|
||||
testCases[0].ts = 123
|
||||
|
||||
lateTS, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
|
||||
testCases[1].ts = lateTS.UnixNano()
|
||||
|
||||
line, err := getQLogFileLine(q, l.num/2)
|
||||
require.Nil(t, err)
|
||||
testCases[2].ts = readQLogTimestamp(line) - 1
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.NotEqualValues(t, 0, tc.ts)
|
||||
|
||||
_, depth, err := q.SeekTS(tc.ts)
|
||||
assert.NotEmpty(t, l.num)
|
||||
require.NotNil(t, err)
|
||||
if tc.leq {
|
||||
assert.LessOrEqual(t, depth, int(math.Log2(float64(l.num))+3))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getQLogFileLine(q *QLogFile, lineNumber int) (line string, err error) {
|
||||
if _, err = q.SeekStart(); err != nil {
|
||||
return line, err
|
||||
}
|
||||
|
||||
for i := 1; i < lineNumber; i++ {
|
||||
if _, err = q.ReadNext(); err != nil {
|
||||
return line, err
|
||||
}
|
||||
}
|
||||
|
||||
return q.ReadNext()
|
||||
}
|
||||
|
||||
// Check adding and loading (with filtering) entries from disk and memory.
|
||||
func TestQLogFile(t *testing.T) {
|
||||
// Create the new QLogFile instance.
|
||||
q := newTestQLogFile(t, 2)
|
||||
|
||||
// Seek to the start.
|
||||
pos, err := q.SeekStart()
|
||||
require.Nil(t, err)
|
||||
assert.Greater(t, pos, int64(0))
|
||||
|
||||
// Read first line.
|
||||
line, err := q.ReadNext()
|
||||
require.Nil(t, err)
|
||||
assert.Contains(t, line, "0.0.0.2")
|
||||
assert.True(t, strings.HasPrefix(line, "{"), line)
|
||||
assert.True(t, strings.HasSuffix(line, "}"), line)
|
||||
|
||||
// Read second line.
|
||||
line, err = q.ReadNext()
|
||||
require.Nil(t, err)
|
||||
assert.EqualValues(t, 0, q.position)
|
||||
assert.Contains(t, line, "0.0.0.1")
|
||||
assert.True(t, strings.HasPrefix(line, "{"), line)
|
||||
assert.True(t, strings.HasSuffix(line, "}"), line)
|
||||
|
||||
// Try reading again (there's nothing to read anymore).
|
||||
line, err = q.ReadNext()
|
||||
require.Equal(t, io.EOF, err)
|
||||
assert.Empty(t, line)
|
||||
}
|
||||
|
||||
func NewTestQLogFileData(t *testing.T, data string) (file *QLogFile) {
|
||||
f, err := ioutil.TempFile(aghtest.PrepareTestDir(t), "*.txt")
|
||||
require.Nil(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll(testDir)
|
||||
assert.Nil(t, f.Close())
|
||||
})
|
||||
|
||||
d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"}
|
||||
{"T":"2020-08-31T18:44:25.376690873+03:00"}
|
||||
{"T":"2020-08-31T18:44:25.382540454+03:00"}
|
||||
`
|
||||
f, err := ioutil.TempFile(testDir, "*.txt")
|
||||
assert.Nil(t, err)
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(data)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, err = f.WriteString(d)
|
||||
assert.Nil(t, err)
|
||||
file, err = NewQLogFile(f.Name())
|
||||
require.Nil(t, err)
|
||||
t.Cleanup(func() {
|
||||
assert.Nil(t, file.Close())
|
||||
})
|
||||
|
||||
q, err := NewQLogFile(f.Name())
|
||||
assert.Nil(t, err)
|
||||
defer q.Close()
|
||||
|
||||
target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:23.911246629+03:00")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, depth, err := q.SeekTS(target.UnixNano() - int64(time.Second))
|
||||
assert.Equal(t, ErrTSTooEarly, err)
|
||||
assert.Equal(t, 1, depth)
|
||||
return file
|
||||
}
|
||||
|
||||
func TestQLog_Seek(t *testing.T) {
|
||||
const nl = "\n"
|
||||
const strV = "%s"
|
||||
const recs = `{"T":"` + strV + `","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"}` + nl +
|
||||
`{"T":"` + strV + `"}` + nl +
|
||||
`{"T":"` + strV + `"}` + nl
|
||||
timestamp, _ := time.Parse(time.RFC3339Nano, "2020-08-31T18:44:25.376690873+03:00")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
delta int
|
||||
wantErr error
|
||||
wantDepth int
|
||||
}{{
|
||||
name: "ok",
|
||||
delta: 0,
|
||||
wantErr: nil,
|
||||
wantDepth: 2,
|
||||
}, {
|
||||
name: "too_late",
|
||||
delta: 2,
|
||||
wantErr: ErrTSTooLate,
|
||||
wantDepth: 2,
|
||||
}, {
|
||||
name: "too_early",
|
||||
delta: -2,
|
||||
wantErr: ErrTSTooEarly,
|
||||
wantDepth: 1,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := fmt.Sprintf(recs,
|
||||
timestamp.Add(-time.Second).Format(time.RFC3339Nano),
|
||||
timestamp.Format(time.RFC3339Nano),
|
||||
timestamp.Add(time.Second).Format(time.RFC3339Nano),
|
||||
)
|
||||
|
||||
q := NewTestQLogFileData(t, data)
|
||||
|
||||
_, depth, err := q.SeekTS(timestamp.Add(time.Second * time.Duration(tc.delta)).UnixNano())
|
||||
require.Truef(t, errors.Is(err, tc.wantErr), "%v", err)
|
||||
assert.Equal(t, tc.wantDepth, depth)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,110 +3,77 @@ package querylog
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQLogReaderEmpty(t *testing.T) {
|
||||
r, err := NewQLogReader([]string{})
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, r)
|
||||
defer r.Close()
|
||||
// newTestQLogReader creates new *QLogReader for tests and registers the
|
||||
// required cleanup functions.
|
||||
func newTestQLogReader(t *testing.T, filesNum, linesNum int) (reader *QLogReader) {
|
||||
t.Helper()
|
||||
|
||||
// seek to the start
|
||||
err = r.SeekStart()
|
||||
assert.Nil(t, err)
|
||||
testFiles := prepareTestFiles(t, filesNum, linesNum)
|
||||
|
||||
line, err := r.ReadNext()
|
||||
assert.Empty(t, line)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
// Create the new QLogReader instance.
|
||||
reader, err := NewQLogReader(testFiles)
|
||||
require.Nil(t, err)
|
||||
assert.NotNil(t, reader)
|
||||
t.Cleanup(func() {
|
||||
assert.Nil(t, reader.Close())
|
||||
})
|
||||
|
||||
return reader
|
||||
}
|
||||
|
||||
func TestQLogReaderOneFile(t *testing.T) {
|
||||
// let's do one small file
|
||||
count := 10
|
||||
filesCount := 1
|
||||
func TestQLogReader(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
filesNum int
|
||||
linesNum int
|
||||
}{{
|
||||
name: "empty",
|
||||
filesNum: 0,
|
||||
linesNum: 0,
|
||||
}, {
|
||||
name: "one_file",
|
||||
filesNum: 1,
|
||||
linesNum: 10,
|
||||
}, {
|
||||
name: "multiple_files",
|
||||
filesNum: 5,
|
||||
linesNum: 10000,
|
||||
}}
|
||||
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFiles := prepareTestFiles(testDir, filesCount, count)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := newTestQLogReader(t, tc.filesNum, tc.linesNum)
|
||||
|
||||
r, err := NewQLogReader(testFiles)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, r)
|
||||
defer r.Close()
|
||||
// Seek to the start.
|
||||
err := r.SeekStart()
|
||||
require.Nil(t, err)
|
||||
|
||||
// seek to the start
|
||||
err = r.SeekStart()
|
||||
assert.Nil(t, err)
|
||||
// Read everything.
|
||||
var read int
|
||||
var line string
|
||||
for err == nil {
|
||||
line, err = r.ReadNext()
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, line)
|
||||
read++
|
||||
}
|
||||
}
|
||||
|
||||
// read everything
|
||||
read := 0
|
||||
var line string
|
||||
for err == nil {
|
||||
line, err = r.ReadNext()
|
||||
if err == nil {
|
||||
assert.True(t, len(line) > 0)
|
||||
read++
|
||||
}
|
||||
require.Equal(t, io.EOF, err)
|
||||
assert.Equal(t, tc.filesNum*tc.linesNum, read)
|
||||
})
|
||||
}
|
||||
|
||||
assert.Equal(t, count*filesCount, read)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
func TestQLogReaderMultipleFiles(t *testing.T) {
|
||||
// should be large enough
|
||||
count := 10000
|
||||
filesCount := 5
|
||||
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFiles := prepareTestFiles(testDir, filesCount, count)
|
||||
|
||||
r, err := NewQLogReader(testFiles)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, r)
|
||||
defer r.Close()
|
||||
|
||||
// seek to the start
|
||||
err = r.SeekStart()
|
||||
assert.Nil(t, err)
|
||||
|
||||
// read everything
|
||||
read := 0
|
||||
var line string
|
||||
for err == nil {
|
||||
line, err = r.ReadNext()
|
||||
if err == nil {
|
||||
assert.True(t, len(line) > 0)
|
||||
read++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, count*filesCount, read)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
func TestQLogReader_Seek(t *testing.T) {
|
||||
count := 10000
|
||||
filesCount := 2
|
||||
|
||||
testDir := prepareTestDir()
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll(testDir)
|
||||
})
|
||||
testFiles := prepareTestFiles(testDir, filesCount, count)
|
||||
|
||||
r, err := NewQLogReader(testFiles)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, r)
|
||||
t.Cleanup(func() {
|
||||
_ = r.Close()
|
||||
})
|
||||
r := newTestQLogReader(t, 2, 10000)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -114,7 +81,7 @@ func TestQLogReader_Seek(t *testing.T) {
|
||||
want error
|
||||
}{{
|
||||
name: "not_too_old",
|
||||
time: "2020-02-19T04:04:56.920973+03:00",
|
||||
time: "2020-02-18T22:39:35.920973+03:00",
|
||||
want: nil,
|
||||
}, {
|
||||
name: "old",
|
||||
@@ -122,7 +89,7 @@ func TestQLogReader_Seek(t *testing.T) {
|
||||
want: nil,
|
||||
}, {
|
||||
name: "first",
|
||||
time: "2020-02-19T04:09:55.920973+03:00",
|
||||
time: "2020-02-18T22:36:36.920973+03:00",
|
||||
want: nil,
|
||||
}, {
|
||||
name: "last",
|
||||
@@ -147,28 +114,20 @@ func TestQLogReader_Seek(t *testing.T) {
|
||||
timestamp, err := time.Parse(time.RFC3339Nano, tc.time)
|
||||
assert.Nil(t, err)
|
||||
|
||||
if tc.name == "first" {
|
||||
assert.True(t, true)
|
||||
}
|
||||
|
||||
err = r.SeekTS(timestamp.UnixNano())
|
||||
assert.True(t, errors.Is(err, tc.want), err)
|
||||
assert.True(t, errors.Is(err, tc.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQLogReader_ReadNext(t *testing.T) {
|
||||
count := 10
|
||||
filesCount := 1
|
||||
|
||||
testDir := prepareTestDir()
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll(testDir)
|
||||
})
|
||||
testFiles := prepareTestFiles(testDir, filesCount, count)
|
||||
|
||||
r, err := NewQLogReader(testFiles)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, r)
|
||||
t.Cleanup(func() {
|
||||
_ = r.Close()
|
||||
})
|
||||
const linesNum = 10
|
||||
const filesNum = 1
|
||||
r := newTestQLogReader(t, filesNum, linesNum)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -180,7 +139,7 @@ func TestQLogReader_ReadNext(t *testing.T) {
|
||||
want: nil,
|
||||
}, {
|
||||
name: "too_big",
|
||||
start: count + 1,
|
||||
start: linesNum + 1,
|
||||
want: io.EOF,
|
||||
}}
|
||||
|
||||
@@ -199,70 +158,3 @@ func TestQLogReader_ReadNext(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Remove the tests below. Make tests above more compelling.
|
||||
func TestQLogReaderSeek(t *testing.T) {
|
||||
// more or less big file
|
||||
count := 10000
|
||||
filesCount := 2
|
||||
|
||||
testDir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(testDir) }()
|
||||
testFiles := prepareTestFiles(testDir, filesCount, count)
|
||||
|
||||
r, err := NewQLogReader(testFiles)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, r)
|
||||
defer r.Close()
|
||||
|
||||
// CASE 1: NOT TOO OLD LINE
|
||||
testSeekLineQLogReader(t, r, 300)
|
||||
|
||||
// CASE 2: OLD LINE
|
||||
testSeekLineQLogReader(t, r, count-300)
|
||||
|
||||
// CASE 3: FIRST LINE
|
||||
testSeekLineQLogReader(t, r, 0)
|
||||
|
||||
// CASE 4: LAST LINE
|
||||
testSeekLineQLogReader(t, r, count)
|
||||
|
||||
// CASE 5: Seek non-existent (too low)
|
||||
err = r.SeekTS(123)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// CASE 6: Seek non-existent (too high)
|
||||
ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
|
||||
err = r.SeekTS(ts.UnixNano())
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func testSeekLineQLogReader(t *testing.T, r *QLogReader, lineNumber int) {
|
||||
line, err := getQLogReaderLine(r, lineNumber)
|
||||
assert.Nil(t, err)
|
||||
ts := readQLogTimestamp(line)
|
||||
assert.NotEqualValues(t, 0, ts)
|
||||
|
||||
// try seeking to that line now
|
||||
err = r.SeekTS(ts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
testLine, err := r.ReadNext()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, line, testLine)
|
||||
}
|
||||
|
||||
func getQLogReaderLine(r *QLogReader, lineNumber int) (string, error) {
|
||||
err := r.SeekStart()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := 1; i < lineNumber; i++ {
|
||||
_, err := r.ReadNext()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return r.ReadNext()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -34,28 +35,30 @@ func TestStats(t *testing.T) {
|
||||
Filename: "./stats.db",
|
||||
LimitDays: 1,
|
||||
}
|
||||
|
||||
s, err := createObject(conf)
|
||||
require.Nil(t, err)
|
||||
t.Cleanup(func() {
|
||||
s.clear()
|
||||
s.Close()
|
||||
assert.Nil(t, os.Remove(conf.Filename))
|
||||
})
|
||||
|
||||
s, _ := createObject(conf)
|
||||
|
||||
e := Entry{}
|
||||
|
||||
e.Domain = "domain"
|
||||
e.Client = "127.0.0.1"
|
||||
e.Result = RFiltered
|
||||
e.Time = 123456
|
||||
s.Update(e)
|
||||
|
||||
e.Domain = "domain"
|
||||
e.Client = "127.0.0.1"
|
||||
e.Result = RNotFiltered
|
||||
e.Time = 123456
|
||||
s.Update(e)
|
||||
s.Update(Entry{
|
||||
Domain: "domain",
|
||||
Client: "127.0.0.1",
|
||||
Result: RFiltered,
|
||||
Time: 123456,
|
||||
})
|
||||
s.Update(Entry{
|
||||
Domain: "domain",
|
||||
Client: "127.0.0.1",
|
||||
Result: RNotFiltered,
|
||||
Time: 123456,
|
||||
})
|
||||
|
||||
d, ok := s.getData()
|
||||
assert.True(t, ok)
|
||||
require.True(t, ok)
|
||||
|
||||
a := []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
||||
assert.True(t, UIntArrayEquals(d.DNSQueries, a))
|
||||
@@ -70,12 +73,15 @@ func TestStats(t *testing.T) {
|
||||
assert.True(t, UIntArrayEquals(d.ReplacedParental, a))
|
||||
|
||||
m := d.TopQueried
|
||||
require.NotEmpty(t, m)
|
||||
assert.EqualValues(t, 1, m[0]["domain"])
|
||||
|
||||
m = d.TopBlocked
|
||||
require.NotEmpty(t, m)
|
||||
assert.EqualValues(t, 1, m[0]["domain"])
|
||||
|
||||
m = d.TopClients
|
||||
require.NotEmpty(t, m)
|
||||
assert.EqualValues(t, 2, m[0]["127.0.0.1"])
|
||||
|
||||
assert.EqualValues(t, 2, d.NumDNSQueries)
|
||||
@@ -86,81 +92,69 @@ func TestStats(t *testing.T) {
|
||||
assert.EqualValues(t, 0.123456, d.AvgProcessingTime)
|
||||
|
||||
topClients := s.GetTopClientsIP(2)
|
||||
require.NotEmpty(t, topClients)
|
||||
assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0]))
|
||||
|
||||
s.clear()
|
||||
s.Close()
|
||||
}
|
||||
|
||||
func TestLargeNumbers(t *testing.T) {
|
||||
var hour int32 = 1
|
||||
var hour int32 = 0
|
||||
newID := func() uint32 {
|
||||
// use "atomic" to make Go race detector happy
|
||||
// Use "atomic" to make go race detector happy.
|
||||
return uint32(atomic.LoadInt32(&hour))
|
||||
}
|
||||
|
||||
// log.SetLevel(log.DEBUG)
|
||||
conf := Config{
|
||||
Filename: "./stats.db",
|
||||
LimitDays: 1,
|
||||
UnitID: newID,
|
||||
}
|
||||
s, err := createObject(conf)
|
||||
require.Nil(t, err)
|
||||
t.Cleanup(func() {
|
||||
s.Close()
|
||||
assert.Nil(t, os.Remove(conf.Filename))
|
||||
})
|
||||
|
||||
s, _ := createObject(conf)
|
||||
e := Entry{}
|
||||
// Number of distinct clients and domains every hour.
|
||||
const n = 1000
|
||||
|
||||
n := 1000 // number of distinct clients and domains every hour
|
||||
for h := 0; h != 12; h++ {
|
||||
if h != 0 {
|
||||
atomic.AddInt32(&hour, 1)
|
||||
}
|
||||
for i := 0; i != n; i++ {
|
||||
e.Domain = fmt.Sprintf("domain%d", i)
|
||||
ip := net.IP{127, 0, 0, 1}
|
||||
ip[2] = byte((i & 0xff00) >> 8)
|
||||
ip[3] = byte(i & 0xff)
|
||||
e.Client = ip.String()
|
||||
e.Result = RNotFiltered
|
||||
e.Time = 123456
|
||||
s.Update(e)
|
||||
for h := 0; h < 12; h++ {
|
||||
atomic.AddInt32(&hour, 1)
|
||||
for i := 0; i < n; i++ {
|
||||
s.Update(Entry{
|
||||
Domain: fmt.Sprintf("domain%d", i),
|
||||
Client: net.IP{
|
||||
127,
|
||||
0,
|
||||
byte((i & 0xff00) >> 8),
|
||||
byte(i & 0xff),
|
||||
}.String(),
|
||||
Result: RNotFiltered,
|
||||
Time: 123456,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
d, ok := s.getData()
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, int(hour)*n, d.NumDNSQueries)
|
||||
|
||||
s.Close()
|
||||
require.True(t, ok)
|
||||
assert.EqualValues(t, hour*n, d.NumDNSQueries)
|
||||
}
|
||||
|
||||
// this code is a chunk copied from getData() that generates aggregate data per day
|
||||
func aggregateDataPerDay(firstID uint32) int {
|
||||
firstDayID := (firstID + 24 - 1) / 24 * 24 // align_ceil(24)
|
||||
a := []uint64{}
|
||||
var sum uint64
|
||||
id := firstDayID
|
||||
nextDayID := firstDayID + 24
|
||||
for i := firstDayID - firstID; int(i) != 720; i++ {
|
||||
sum++
|
||||
if id == nextDayID {
|
||||
a = append(a, sum)
|
||||
sum = 0
|
||||
nextDayID += 24
|
||||
func TestStatsCollector(t *testing.T) {
|
||||
ng := func(_ *unitDB) uint64 {
|
||||
return 0
|
||||
}
|
||||
units := make([]*unitDB, 720)
|
||||
|
||||
t.Run("hours", func(t *testing.T) {
|
||||
statsData := statsCollector(units, 0, Hours, ng)
|
||||
assert.Len(t, statsData, 720)
|
||||
})
|
||||
|
||||
t.Run("days", func(t *testing.T) {
|
||||
for i := 0; i != 25; i++ {
|
||||
statsData := statsCollector(units, uint32(i), Days, ng)
|
||||
require.Lenf(t, statsData, 30, "i=%d", i)
|
||||
}
|
||||
id++
|
||||
}
|
||||
if id <= nextDayID {
|
||||
a = append(a, sum)
|
||||
}
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func TestAggregateDataPerTimeUnit(t *testing.T) {
|
||||
for i := 0; i != 25; i++ {
|
||||
alen := aggregateDataPerDay(uint32(i))
|
||||
assert.Equalf(t, 30, alen, "i=%d", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@@ -11,10 +12,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// TODO(a.garipov): Rewrite all of this. Add proper error handling and
|
||||
// inspection. Improve logging. Decrease complexity.
|
||||
|
||||
const (
|
||||
maxDomains = 100 // max number of top domains to store in file or return via Get()
|
||||
maxClients = 100 // max number of top clients to store in file or return via Get()
|
||||
@@ -61,11 +66,12 @@ type unitDB struct {
|
||||
TimeAvg uint32 // usec
|
||||
}
|
||||
|
||||
func createObject(conf Config) (*statsCtx, error) {
|
||||
s := statsCtx{}
|
||||
func createObject(conf Config) (s *statsCtx, err error) {
|
||||
s = &statsCtx{}
|
||||
if !checkInterval(conf.LimitDays) {
|
||||
conf.LimitDays = 1
|
||||
}
|
||||
|
||||
s.conf = &Config{}
|
||||
*s.conf = conf
|
||||
s.conf.limit = conf.LimitDays * 24
|
||||
@@ -84,27 +90,43 @@ func createObject(conf Config) (*statsCtx, error) {
|
||||
log.Tracef("Deleting old units...")
|
||||
firstID := id - s.conf.limit - 1
|
||||
unitDel := 0
|
||||
forEachBkt := func(name []byte, b *bolt.Bucket) error {
|
||||
id := uint32(btoi(name))
|
||||
if id < firstID {
|
||||
err := tx.DeleteBucket(name)
|
||||
if err != nil {
|
||||
log.Debug("tx.DeleteBucket: %s", err)
|
||||
|
||||
// TODO(a.garipov): See if this is actually necessary. Looks
|
||||
// like a rather bizarre solution.
|
||||
errStop := agherr.Error("stop iteration")
|
||||
forEachBkt := func(name []byte, _ *bolt.Bucket) (cberr error) {
|
||||
nameID := uint32(btoi(name))
|
||||
if nameID < firstID {
|
||||
cberr = tx.DeleteBucket(name)
|
||||
if cberr != nil {
|
||||
log.Debug("stats: tx.DeleteBucket: %s", cberr)
|
||||
|
||||
return nil
|
||||
}
|
||||
log.Debug("Stats: deleted unit %d", id)
|
||||
|
||||
log.Debug("stats: deleted unit %d", nameID)
|
||||
unitDel++
|
||||
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("")
|
||||
|
||||
return errStop
|
||||
}
|
||||
|
||||
err = tx.ForEach(forEachBkt)
|
||||
if err != nil && !errors.Is(err, errStop) {
|
||||
log.Debug("stats: deleting units: %s", err)
|
||||
}
|
||||
_ = tx.ForEach(forEachBkt)
|
||||
|
||||
udb = s.loadUnitFromDB(tx, id)
|
||||
|
||||
if unitDel != 0 {
|
||||
s.commitTxn(tx)
|
||||
} else {
|
||||
_ = tx.Rollback()
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
log.Debug("rolling back: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,8 +137,9 @@ func createObject(conf Config) (*statsCtx, error) {
|
||||
}
|
||||
s.unit = &u
|
||||
|
||||
log.Debug("Stats: initialized")
|
||||
return &s, nil
|
||||
log.Debug("stats: initialized")
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *statsCtx) Start() {
|
||||
@@ -133,7 +156,7 @@ func (s *statsCtx) dbOpen() bool {
|
||||
log.Tracef("db.Open...")
|
||||
s.db, err = bolt.Open(s.conf.Filename, 0o644, nil)
|
||||
if err != nil {
|
||||
log.Error("Stats: open DB: %s: %s", s.conf.Filename, err)
|
||||
log.Error("stats: open DB: %s: %s", s.conf.Filename, err)
|
||||
if err.Error() == "invalid argument" {
|
||||
log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/internal/wiki/Getting-Started#limitations")
|
||||
}
|
||||
@@ -262,10 +285,13 @@ func (s *statsCtx) periodicFlush() {
|
||||
func (s *statsCtx) deleteUnit(tx *bolt.Tx, id uint32) bool {
|
||||
err := tx.DeleteBucket(unitName(id))
|
||||
if err != nil {
|
||||
log.Tracef("bolt DeleteBucket: %s", err)
|
||||
log.Tracef("stats: bolt DeleteBucket: %s", err)
|
||||
|
||||
return false
|
||||
}
|
||||
log.Debug("Stats: deleted unit %d", id)
|
||||
|
||||
log.Debug("stats: deleted unit %d", id)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -390,7 +416,7 @@ func (s *statsCtx) setLimit(limitDays int) {
|
||||
conf := *s.conf
|
||||
conf.limit = uint32(limitDays) * 24
|
||||
s.conf = &conf
|
||||
log.Debug("Stats: set limit: %d", limitDays)
|
||||
log.Debug("stats: set limit: %d", limitDays)
|
||||
}
|
||||
|
||||
func (s *statsCtx) WriteDiskConfig(dc *DiskConfig) {
|
||||
@@ -415,7 +441,7 @@ func (s *statsCtx) Close() {
|
||||
log.Tracef("db.Close")
|
||||
}
|
||||
|
||||
log.Debug("Stats: closed")
|
||||
log.Debug("stats: closed")
|
||||
}
|
||||
|
||||
// Reset counters and clear database
|
||||
@@ -443,7 +469,7 @@ func (s *statsCtx) clear() {
|
||||
|
||||
_ = s.dbOpen()
|
||||
|
||||
log.Debug("Stats: cleared")
|
||||
log.Debug("stats: cleared")
|
||||
}
|
||||
|
||||
// Get Client IP address
|
||||
@@ -528,6 +554,57 @@ func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) {
|
||||
return units, firstID
|
||||
}
|
||||
|
||||
// numsGetter is a signature for statsCollector argument.
|
||||
type numsGetter func(u *unitDB) (num uint64)
|
||||
|
||||
// statsCollector collects statisctics for the given *unitDB slice by specified
|
||||
// timeUnit using ng to retrieve data.
|
||||
func statsCollector(units []*unitDB, firstID uint32, timeUnit TimeUnit, ng numsGetter) (nums []uint64) {
|
||||
if timeUnit == Hours {
|
||||
for _, u := range units {
|
||||
nums = append(nums, ng(u))
|
||||
}
|
||||
} else {
|
||||
// Per time unit counters: 720 hours may span 31 days, so we
|
||||
// skip data for the first day in this case.
|
||||
// align_ceil(24)
|
||||
firstDayID := (firstID + 24 - 1) / 24 * 24
|
||||
|
||||
var sum uint64
|
||||
id := firstDayID
|
||||
nextDayID := firstDayID + 24
|
||||
for i := int(firstDayID - firstID); i != len(units); i++ {
|
||||
sum += ng(units[i])
|
||||
if id == nextDayID {
|
||||
nums = append(nums, sum)
|
||||
sum = 0
|
||||
nextDayID += 24
|
||||
}
|
||||
id++
|
||||
}
|
||||
if id <= nextDayID {
|
||||
nums = append(nums, sum)
|
||||
}
|
||||
}
|
||||
return nums
|
||||
}
|
||||
|
||||
// pairsGetter is a signature for topsCollector argument.
|
||||
type pairsGetter func(u *unitDB) (pairs []countPair)
|
||||
|
||||
// topsCollector collects statistics about highest values fro the given *unitDB
|
||||
// slice using pg to retrieve data.
|
||||
func topsCollector(units []*unitDB, max int, pg pairsGetter) []map[string]uint64 {
|
||||
m := map[string]uint64{}
|
||||
for _, u := range units {
|
||||
for _, it := range pg(u) {
|
||||
m[it.Name] += it.Count
|
||||
}
|
||||
}
|
||||
a2 := convertMapToSlice(m, max)
|
||||
return convertTopSlice(a2)
|
||||
}
|
||||
|
||||
/* Algorithm:
|
||||
. Prepare array of N units, where N is the value of "limit" configuration setting
|
||||
. Load data for the most recent units from file
|
||||
@@ -568,65 +645,25 @@ func (s *statsCtx) getData() (statsResponse, bool) {
|
||||
return statsResponse{}, false
|
||||
}
|
||||
|
||||
// per time unit counters:
|
||||
// 720 hours may span 31 days, so we skip data for the first day in this case
|
||||
firstDayID := (firstID + 24 - 1) / 24 * 24 // align_ceil(24)
|
||||
|
||||
statsCollector := func(numsGetter func(u *unitDB) (num uint64)) (nums []uint64) {
|
||||
if timeUnit == Hours {
|
||||
for _, u := range units {
|
||||
nums = append(nums, numsGetter(u))
|
||||
}
|
||||
} else {
|
||||
var sum uint64
|
||||
id := firstDayID
|
||||
nextDayID := firstDayID + 24
|
||||
for i := int(firstDayID - firstID); i != len(units); i++ {
|
||||
sum += numsGetter(units[i])
|
||||
if id == nextDayID {
|
||||
nums = append(nums, sum)
|
||||
sum = 0
|
||||
nextDayID += 24
|
||||
}
|
||||
id++
|
||||
}
|
||||
if id <= nextDayID {
|
||||
nums = append(nums, sum)
|
||||
}
|
||||
}
|
||||
return nums
|
||||
}
|
||||
|
||||
topsCollector := func(max int, pairsGetter func(u *unitDB) (pairs []countPair)) []map[string]uint64 {
|
||||
m := map[string]uint64{}
|
||||
for _, u := range units {
|
||||
for _, it := range pairsGetter(u) {
|
||||
m[it.Name] += it.Count
|
||||
}
|
||||
}
|
||||
a2 := convertMapToSlice(m, max)
|
||||
return convertTopSlice(a2)
|
||||
}
|
||||
|
||||
dnsQueries := statsCollector(func(u *unitDB) (num uint64) { return u.NTotal })
|
||||
dnsQueries := statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NTotal })
|
||||
if timeUnit != Hours && len(dnsQueries) != int(limit/24) {
|
||||
log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit)
|
||||
}
|
||||
|
||||
data := statsResponse{
|
||||
DNSQueries: dnsQueries,
|
||||
BlockedFiltering: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }),
|
||||
ReplacedSafebrowsing: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }),
|
||||
ReplacedParental: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }),
|
||||
TopQueried: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }),
|
||||
TopBlocked: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }),
|
||||
TopClients: topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }),
|
||||
BlockedFiltering: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }),
|
||||
ReplacedSafebrowsing: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }),
|
||||
ReplacedParental: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RParental] }),
|
||||
TopQueried: topsCollector(units, maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }),
|
||||
TopBlocked: topsCollector(units, maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }),
|
||||
TopClients: topsCollector(units, maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }),
|
||||
}
|
||||
|
||||
// total counters:
|
||||
|
||||
sum := unitDB{}
|
||||
sum.NResult = make([]uint64, rLast)
|
||||
// Total counters:
|
||||
sum := unitDB{
|
||||
NResult: make([]uint64, rLast),
|
||||
}
|
||||
timeN := 0
|
||||
for _, u := range units {
|
||||
sum.NTotal += u.NTotal
|
||||
|
||||
@@ -5,10 +5,17 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// ErrNoStaticIPInfo is returned by IfaceHasStaticIP when no information about
|
||||
// the IP being static is available.
|
||||
const ErrNoStaticIPInfo agherr.Error = "no information about static ip"
|
||||
|
||||
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
||||
// If it can't give a definitive answer, it returns false and an error for which
|
||||
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
||||
func IfaceHasStaticIP(ifaceName string) (has bool, err error) {
|
||||
return ifaceHasStaticIP(ifaceName)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ import (
|
||||
const maxConfigFileSize = 1024 * 1024
|
||||
|
||||
func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
|
||||
var f *os.File
|
||||
// TODO(a.garipov): Currently, this function returns the first
|
||||
// definitive result. So if /etc/dhcpcd.conf has a static IP while
|
||||
// /etc/network/interfaces doesn't, it will return true. Perhaps this
|
||||
// is not the most desirable behavior.
|
||||
|
||||
for _, check := range []struct {
|
||||
checker func(io.Reader, string) (bool, error)
|
||||
filePath string
|
||||
@@ -32,28 +36,37 @@ func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
|
||||
checker: ifacesStaticConfig,
|
||||
filePath: "/etc/network/interfaces",
|
||||
}} {
|
||||
var f *os.File
|
||||
f, err = os.Open(check.filePath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
// ErrNotExist can happen here if there is no such file.
|
||||
// This is normal, as not every system uses those files.
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
err = nil
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fileReadCloser, err := aghio.LimitReadCloser(f, maxConfigFileSize)
|
||||
var fileReadCloser io.ReadCloser
|
||||
fileReadCloser, err = aghio.LimitReadCloser(f, maxConfigFileSize)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer fileReadCloser.Close()
|
||||
|
||||
has, err = check.checker(fileReadCloser, ifaceName)
|
||||
if has || err != nil {
|
||||
break
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return has, nil
|
||||
}
|
||||
|
||||
return has, err
|
||||
return false, ErrNoStaticIPInfo
|
||||
}
|
||||
|
||||
// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const nl = "\n"
|
||||
@@ -48,7 +49,7 @@ func TestDHCPCDStaticConfig(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := bytes.NewReader(tc.data)
|
||||
has, err := dhcpcdStaticConfig(r, "wlan0")
|
||||
assert.Nil(t, err)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tc.want, has)
|
||||
})
|
||||
}
|
||||
@@ -85,26 +86,36 @@ func TestIfacesStaticConfig(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := bytes.NewReader(tc.data)
|
||||
has, err := ifacesStaticConfig(r, "enp0s3")
|
||||
assert.Nil(t, err)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tc.want, has)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetStaticIPdhcpcdConf(t *testing.T) {
|
||||
dhcpcdConf := nl + `interface wlan0` + nl +
|
||||
`static ip_address=192.168.0.2/24` + nl +
|
||||
`static routers=192.168.0.1` + nl +
|
||||
`static domain_name_servers=192.168.0.2` + nl + nl
|
||||
testCases := []struct {
|
||||
name string
|
||||
dhcpcdConf string
|
||||
routers net.IP
|
||||
}{{
|
||||
name: "with_gateway",
|
||||
dhcpcdConf: nl + `interface wlan0` + nl +
|
||||
`static ip_address=192.168.0.2/24` + nl +
|
||||
`static routers=192.168.0.1` + nl +
|
||||
`static domain_name_servers=192.168.0.2` + nl + nl,
|
||||
routers: net.IP{192, 168, 0, 1},
|
||||
}, {
|
||||
name: "without_gateway",
|
||||
dhcpcdConf: nl + `interface wlan0` + nl +
|
||||
`static ip_address=192.168.0.2/24` + nl +
|
||||
`static domain_name_servers=192.168.0.2` + nl + nl,
|
||||
routers: nil,
|
||||
}}
|
||||
|
||||
s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 2})
|
||||
assert.Equal(t, dhcpcdConf, s)
|
||||
|
||||
// without gateway
|
||||
dhcpcdConf = nl + `interface wlan0` + nl +
|
||||
`static ip_address=192.168.0.2/24` + nl +
|
||||
`static domain_name_servers=192.168.0.2` + nl + nl
|
||||
|
||||
s = updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", nil, net.IP{192, 168, 0, 2})
|
||||
assert.Equal(t, dhcpcdConf, s)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", tc.routers, net.IP{192, 168, 0, 2})
|
||||
assert.Equal(t, tc.dhcpcdConf, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,114 +11,162 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
func prepareTestDir() string {
|
||||
const dir = "./agh-test"
|
||||
_ = os.RemoveAll(dir)
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
return dir
|
||||
func prepareTestFile(t *testing.T) (f *os.File) {
|
||||
t.Helper()
|
||||
|
||||
dir := aghtest.PrepareTestDir(t)
|
||||
|
||||
f, err := ioutil.TempFile(dir, "")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, f)
|
||||
t.Cleanup(func() {
|
||||
assert.Nil(t, f.Close())
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func assertWriting(t *testing.T, f *os.File, strs ...string) {
|
||||
t.Helper()
|
||||
|
||||
for _, str := range strs {
|
||||
n, err := f.WriteString(str)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, n, len(str))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoHostsResolution(t *testing.T) {
|
||||
ah := AutoHosts{}
|
||||
ah := &AutoHosts{}
|
||||
|
||||
dir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(dir) }()
|
||||
|
||||
f, _ := ioutil.TempFile(dir, "")
|
||||
defer func() { _ = os.Remove(f.Name()) }()
|
||||
defer f.Close()
|
||||
|
||||
_, _ = f.WriteString(" 127.0.0.1 host localhost # comment \n")
|
||||
_, _ = f.WriteString(" ::1 localhost#comment \n")
|
||||
f := prepareTestFile(t)
|
||||
|
||||
assertWriting(t, f,
|
||||
" 127.0.0.1 host localhost # comment \n",
|
||||
" ::1 localhost#comment \n",
|
||||
)
|
||||
ah.Init(f.Name())
|
||||
|
||||
// Existing host
|
||||
ips := ah.Process("localhost", dns.TypeA)
|
||||
assert.NotNil(t, ips)
|
||||
assert.Len(t, ips, 1)
|
||||
assert.Equal(t, net.ParseIP("127.0.0.1"), ips[0])
|
||||
t.Run("existing_host", func(t *testing.T) {
|
||||
ips := ah.Process("localhost", dns.TypeA)
|
||||
require.Len(t, ips, 1)
|
||||
assert.Equal(t, net.IPv4(127, 0, 0, 1), ips[0])
|
||||
})
|
||||
|
||||
// Unknown host
|
||||
ips = ah.Process("newhost", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
t.Run("unknown_host", func(t *testing.T) {
|
||||
ips := ah.Process("newhost", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
|
||||
// Unknown host (comment)
|
||||
ips = ah.Process("comment", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
// Comment.
|
||||
ips = ah.Process("comment", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
})
|
||||
|
||||
// Test hosts file
|
||||
table := ah.List()
|
||||
names, ok := table["127.0.0.1"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, []string{"host", "localhost"}, names)
|
||||
t.Run("hosts_file", func(t *testing.T) {
|
||||
names, ok := ah.List()["127.0.0.1"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []string{"host", "localhost"}, names)
|
||||
})
|
||||
|
||||
// Test PTR
|
||||
a, _ := dns.ReverseAddr("127.0.0.1")
|
||||
a = strings.TrimSuffix(a, ".")
|
||||
hosts := ah.ProcessReverse(a, dns.TypePTR)
|
||||
if assert.Len(t, hosts, 2) {
|
||||
assert.Equal(t, hosts[0], "host")
|
||||
}
|
||||
t.Run("ptr", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
wantIP string
|
||||
wantLen int
|
||||
wantHost string
|
||||
}{
|
||||
{wantIP: "127.0.0.1", wantLen: 2, wantHost: "host"},
|
||||
{wantIP: "::1", wantLen: 1, wantHost: "localhost"},
|
||||
}
|
||||
|
||||
a, _ = dns.ReverseAddr("::1")
|
||||
a = strings.TrimSuffix(a, ".")
|
||||
hosts = ah.ProcessReverse(a, dns.TypePTR)
|
||||
if assert.Len(t, hosts, 1) {
|
||||
assert.Equal(t, hosts[0], "localhost")
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
a, err := dns.ReverseAddr(tc.wantIP)
|
||||
require.Nil(t, err)
|
||||
|
||||
a = strings.TrimSuffix(a, ".")
|
||||
hosts := ah.ProcessReverse(a, dns.TypePTR)
|
||||
require.Len(t, hosts, tc.wantLen)
|
||||
assert.Equal(t, tc.wantHost, hosts[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAutoHostsFSNotify(t *testing.T) {
|
||||
ah := AutoHosts{}
|
||||
ah := &AutoHosts{}
|
||||
|
||||
dir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(dir) }()
|
||||
f := prepareTestFile(t)
|
||||
|
||||
f, _ := ioutil.TempFile(dir, "")
|
||||
defer func() { _ = os.Remove(f.Name()) }()
|
||||
defer f.Close()
|
||||
|
||||
// Init
|
||||
_, _ = f.WriteString(" 127.0.0.1 host localhost \n")
|
||||
assertWriting(t, f, " 127.0.0.1 host localhost \n")
|
||||
ah.Init(f.Name())
|
||||
|
||||
// Unknown host
|
||||
ips := ah.Process("newhost", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
t.Run("unknown_host", func(t *testing.T) {
|
||||
ips := ah.Process("newhost", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
})
|
||||
|
||||
// Stat monitoring for changes
|
||||
// Start monitoring for changes.
|
||||
ah.Start()
|
||||
defer ah.Close()
|
||||
t.Cleanup(ah.Close)
|
||||
|
||||
// Update file
|
||||
_, _ = f.WriteString("127.0.0.2 newhost\n")
|
||||
_ = f.Sync()
|
||||
assertWriting(t, f, "127.0.0.2 newhost\n")
|
||||
require.Nil(t, f.Sync())
|
||||
|
||||
// wait until fsnotify has triggerred and processed the file-modification event
|
||||
// Wait until fsnotify has triggerred and processed the
|
||||
// file-modification event.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Check if we are notified about changes
|
||||
ips = ah.Process("newhost", dns.TypeA)
|
||||
assert.NotNil(t, ips)
|
||||
assert.Len(t, ips, 1)
|
||||
assert.True(t, net.IP{127, 0, 0, 2}.Equal(ips[0]))
|
||||
t.Run("notified", func(t *testing.T) {
|
||||
ips := ah.Process("newhost", dns.TypeA)
|
||||
assert.NotNil(t, ips)
|
||||
require.Len(t, ips, 1)
|
||||
assert.True(t, net.IP{127, 0, 0, 2}.Equal(ips[0]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIP(t *testing.T) {
|
||||
assert.True(t, net.IP{127, 0, 0, 1}.Equal(DNSUnreverseAddr("1.0.0.127.in-addr.arpa")))
|
||||
assert.Equal(t, "::abcd:1234", DNSUnreverseAddr("4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa").String())
|
||||
assert.Equal(t, "::abcd:1234", DNSUnreverseAddr("4.3.2.1.d.c.B.A.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa").String())
|
||||
func TestDNSReverseAddr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
have string
|
||||
want net.IP
|
||||
}{{
|
||||
name: "good_ipv4",
|
||||
have: "1.0.0.127.in-addr.arpa",
|
||||
want: net.IP{127, 0, 0, 1},
|
||||
}, {
|
||||
name: "good_ipv6",
|
||||
have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
|
||||
want: net.ParseIP("::abcd:1234"),
|
||||
}, {
|
||||
name: "good_ipv6_case",
|
||||
have: "4.3.2.1.d.c.B.A.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
|
||||
want: net.ParseIP("::abcd:1234"),
|
||||
}, {
|
||||
name: "bad_ipv4_dot",
|
||||
have: "1.0.0.127.in-addr.arpa.",
|
||||
}, {
|
||||
name: "wrong_ipv4",
|
||||
have: ".0.0.127.in-addr.arpa",
|
||||
}, {
|
||||
name: "wrong_ipv6",
|
||||
have: ".3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
|
||||
}, {
|
||||
name: "bad_ipv6_dot",
|
||||
have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0..ip6.arpa",
|
||||
}, {
|
||||
name: "bad_ipv6_space",
|
||||
have: "4.3.2.1.d.c.b. .0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
|
||||
}}
|
||||
|
||||
assert.Nil(t, DNSUnreverseAddr("1.0.0.127.in-addr.arpa."))
|
||||
assert.Nil(t, DNSUnreverseAddr(".0.0.127.in-addr.arpa"))
|
||||
assert.Nil(t, DNSUnreverseAddr(".3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa"))
|
||||
assert.Nil(t, DNSUnreverseAddr("4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0..ip6.arpa"))
|
||||
assert.Nil(t, DNSUnreverseAddr("4.3.2.1.d.c.b. .0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa"))
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ip := DNSUnreverseAddr(tc.have)
|
||||
assert.True(t, tc.want.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSplitNext(t *testing.T) {
|
||||
s := " a,b , c "
|
||||
|
||||
assert.Equal(t, "a", SplitNext(&s, ','))
|
||||
assert.Equal(t, "b", SplitNext(&s, ','))
|
||||
assert.Equal(t, "c", SplitNext(&s, ','))
|
||||
assert.Empty(t, s)
|
||||
require.Empty(t, s)
|
||||
}
|
||||
|
||||
@@ -2,22 +2,15 @@ package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetValidNetInterfacesForWeb(t *testing.T) {
|
||||
ifaces, err := GetValidNetInterfacesForWeb()
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot get net interfaces: %s", err)
|
||||
}
|
||||
if len(ifaces) == 0 {
|
||||
t.Fatalf("No net interfaces found")
|
||||
}
|
||||
|
||||
require.Nilf(t, err, "Cannot get net interfaces: %s", err)
|
||||
require.NotEmpty(t, ifaces, "No net interfaces found")
|
||||
for _, iface := range ifaces {
|
||||
if len(iface.Addresses) == 0 {
|
||||
t.Fatalf("No addresses found for %s", iface.Name)
|
||||
}
|
||||
|
||||
t.Logf("%v", iface)
|
||||
require.NotEmptyf(t, iface.Addresses, "No addresses found for %s", iface.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
## v0.105: API changes
|
||||
|
||||
### New `"client_id"` field in `GET /querylog` response
|
||||
|
||||
* The new field `"client_id"` of `QueryLogItem` objects is the ID sent by the
|
||||
client for encrypted requests, if there was any. See the
|
||||
"[Identifying clients]" section of our wiki.
|
||||
|
||||
### New `"dnscrypt"` `"client_proto"` value in `GET /querylog` response
|
||||
|
||||
* The field `"client_proto"` can now have the value `"dnscrypt"` when the
|
||||
@@ -69,6 +75,8 @@
|
||||
|
||||
As well as other documentation fixes.
|
||||
|
||||
[Identifying clients]: https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#idclient
|
||||
|
||||
## v0.103: API changes
|
||||
|
||||
### API: replace settings in GET /control/dns_info & POST /control/dns_config
|
||||
|
||||
@@ -228,7 +228,13 @@ main() {
|
||||
|
||||
download "${URL}" "${PKG_NAME}" || error_exit "Cannot download the package"
|
||||
|
||||
unpack "${PKG_NAME}" "${OUT_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package"
|
||||
if [ "${OS}" = "darwin" ]; then
|
||||
# TODO: remove this after v0.106.0 release
|
||||
mkdir "${AGH_DIR}"
|
||||
unpack "${PKG_NAME}" "${AGH_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package"
|
||||
else
|
||||
unpack "${PKG_NAME}" "${OUT_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package"
|
||||
fi
|
||||
|
||||
# Install AdGuard Home service and run it.
|
||||
( cd "${AGH_DIR}" && ./AdGuardHome -s install || error_exit "Cannot install AdGuardHome as a service" )
|
||||
|
||||
@@ -17,7 +17,15 @@ set -e -f -u
|
||||
readonly channel="$CHANNEL"
|
||||
readonly commit="$COMMIT"
|
||||
readonly dist_dir="$DIST_DIR"
|
||||
readonly version="$VERSION"
|
||||
|
||||
if [ "${VERSION:-}" = 'v0.0.0' -o "${VERSION:-}" = '' ]
|
||||
then
|
||||
readonly version="$(sh ./scripts/make/version.sh)"
|
||||
else
|
||||
readonly version="$VERSION"
|
||||
fi
|
||||
|
||||
echo $version
|
||||
|
||||
# Allow users to use sudo.
|
||||
readonly sudo_cmd="${SUDO:-}"
|
||||
|
||||
@@ -80,10 +80,18 @@ const request = (url, locale) => (
|
||||
return `${locale} - Not OK`;
|
||||
}));
|
||||
|
||||
/**
|
||||
* Sleep.
|
||||
* @param {number} ms
|
||||
*/
|
||||
const sleep = (ms) => new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
/**
|
||||
* Download locales
|
||||
*/
|
||||
const download = () => {
|
||||
const download = async () => {
|
||||
const locales = LOCALES_LIST;
|
||||
|
||||
if (!TWOSKY_URI) {
|
||||
@@ -91,10 +99,16 @@ const download = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const requests = locales.map((locale) => {
|
||||
const requests = [];
|
||||
for (let i = 0; i < locales.length; i++) {
|
||||
const locale = locales[i];
|
||||
const url = getRequestUrl(locale, TWOSKY_URI, TWOSKY_PROJECT_ID);
|
||||
return request(url, locale);
|
||||
});
|
||||
requests.push(request(url, locale));
|
||||
|
||||
// Don't request the Crowdin API too aggressively to prevent spurious
|
||||
// 400 errors.
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
Promise
|
||||
.all(requests)
|
||||
|
||||
466
scripts/translations/package-lock.json
generated
466
scripts/translations/package-lock.json
generated
@@ -1,8 +1,466 @@
|
||||
{
|
||||
"name": "translations",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.5.5",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
|
||||
"integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"node_modules/aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
|
||||
"integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw=="
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
|
||||
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
|
||||
"dependencies": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"node_modules/extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
]
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
},
|
||||
"node_modules/forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"dependencies": {
|
||||
"ajv": "^6.5.5",
|
||||
"har-schema": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8",
|
||||
"npm": ">=1.3.7"
|
||||
}
|
||||
},
|
||||
"node_modules/is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
},
|
||||
"node_modules/isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"node_modules/jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.2.3",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.37.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
|
||||
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.21",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
|
||||
"integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
|
||||
"dependencies": {
|
||||
"mime-db": "~1.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.1.29",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
|
||||
"integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/request": {
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"dependencies": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.0",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/request-promise": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz",
|
||||
"integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=",
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.0",
|
||||
"request-promise-core": "1.1.1",
|
||||
"stealthy-require": "^1.1.0",
|
||||
"tough-cookie": ">=2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/request-promise-core": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
|
||||
"integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
|
||||
"dependencies": {
|
||||
"lodash": "^4.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz",
|
||||
"integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==",
|
||||
"dependencies": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stealthy-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.24",
|
||||
"punycode": "^1.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js/node_modules/punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.5.5",
|
||||
@@ -205,9 +663,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.37.0",
|
||||
|
||||
@@ -10,7 +10,9 @@ initialisms = [
|
||||
, "MX"
|
||||
, "PTR"
|
||||
, "QUIC"
|
||||
, "RA"
|
||||
, "SDNS"
|
||||
, "SLAAC"
|
||||
, "SVCB"
|
||||
]
|
||||
dot_import_whitelist = []
|
||||
|
||||
Reference in New Issue
Block a user