Compare commits
32 Commits
v0.105.0
...
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 |
12
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,11 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a bug report to help us improve AdGuard Home
|
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
|
### Prerequisites
|
||||||
|
|
||||||
@@ -17,16 +15,18 @@ Please answer the following questions for yourself before submitting an issue. *
|
|||||||
|
|
||||||
### Issue Details
|
### 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:**
|
* **Version of AdGuard Home server:**
|
||||||
* <!-- (e.g. v1.0) -->
|
* <!-- (e.g. v0.123.4) -->
|
||||||
* **How did you install AdGuard Home:**
|
* **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:**
|
* **How did you setup DNS configuration:**
|
||||||
* <!-- (System/Router/IoT) -->
|
* <!-- (System/Router/IoT) -->
|
||||||
* **If it's a router or IoT, please write device model:**
|
* **If it's a router or IoT, please write device model:**
|
||||||
* <!-- (e.g. Raspberry Pi 3 Model B) -->
|
* <!-- (e.g. Raspberry Pi 3 Model B) -->
|
||||||
|
* **CPU architecture:**
|
||||||
|
* <!-- (e.g. AMD64, MIPS, etc.) -->
|
||||||
* **Operating system and version:**
|
* **Operating system and version:**
|
||||||
* <!-- (e.g. Ubuntu 18.04.1) -->
|
* <!-- (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
|
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
|
### 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.
|
# 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.
|
# Number of days of inactivity before a stale issue is closed.
|
||||||
'daysUntilClose': 7
|
'daysUntilClose': 15
|
||||||
# Issues with these labels will never be considered stale.
|
# Issues with these labels will never be considered stale.
|
||||||
'exemptLabels':
|
'exemptLabels':
|
||||||
- 'bug'
|
- 'bug'
|
||||||
- 'enhancement'
|
- 'enhancement'
|
||||||
- 'feature request'
|
- 'feature request'
|
||||||
- 'localization'
|
- 'localization'
|
||||||
|
- 'needs investigation'
|
||||||
- 'recurrent'
|
- 'recurrent'
|
||||||
|
- 'research'
|
||||||
# Label to use when marking an issue as stale.
|
# Label to use when marking an issue as stale.
|
||||||
'staleLabel': 'wontfix'
|
'staleLabel': 'wontfix'
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable.
|
# Comment to post when marking an issue as stale. Set to `false` to disable.
|
||||||
'markComment': >
|
'markComment': >
|
||||||
This issue has been automatically marked as stale because it has not had
|
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.
|
for your contributions.
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable.
|
# Comment to post when closing a stale issue. Set to `false` to disable.
|
||||||
'closeComment': false
|
'closeComment': false
|
||||||
|
|||||||
68
CHANGELOG.md
68
CHANGELOG.md
@@ -13,6 +13,65 @@ and this project adheres to
|
|||||||
## [v0.106.0] - 2021-04-26
|
## [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
|
## [v0.105.0] - 2021-02-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -151,9 +210,12 @@ and this project adheres to
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.2...HEAD
|
||||||
[v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0
|
[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.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
|
[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).
|
"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**
|
**Known limitations**
|
||||||
|
|
||||||
@@ -192,6 +192,12 @@ cd AdGuardHome
|
|||||||
make
|
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.
|
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.
|
**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_ip_format": "Няслушны фармат IP-адраса",
|
||||||
"form_error_mac_format": "Некарэктны фармат MAC",
|
"form_error_mac_format": "Некарэктны фармат MAC",
|
||||||
"form_error_client_id_format": "Няслушны фармат ID кліента",
|
"form_error_client_id_format": "Няслушны фармат ID кліента",
|
||||||
|
"form_error_server_name": "Няслушнае імя сервера",
|
||||||
"form_error_positive": "Павінна быць больш 0",
|
"form_error_positive": "Павінна быць больш 0",
|
||||||
"form_error_negative": "Павінна быць не менш 0",
|
"form_error_negative": "Павінна быць не менш 0",
|
||||||
"range_end_error": "Павінен перавышаць пачатак дыяпазону",
|
"range_end_error": "Павінен перавышаць пачатак дыяпазону",
|
||||||
@@ -247,10 +248,16 @@
|
|||||||
"custom_ip": "Свой IP",
|
"custom_ip": "Свой IP",
|
||||||
"blocking_ipv4": "Блакаванне IPv4",
|
"blocking_ipv4": "Блакаванне IPv4",
|
||||||
"blocking_ipv6": "Блакаванне IPv6",
|
"blocking_ipv6": "Блакаванне IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"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_doh": "Спампаваць .mobileconfig для DNS-over-HTTPS",
|
||||||
"download_mobileconfig_dot": "Спампаваць .mobileconfig для DNS-over-TLS",
|
"download_mobileconfig_dot": "Спампаваць .mobileconfig для DNS-over-TLS",
|
||||||
|
"download_mobileconfig": "Загрузіць файл канфігурацыі",
|
||||||
"plain_dns": "Нешыфраваны DNS",
|
"plain_dns": "Нешыфраваны DNS",
|
||||||
"form_enter_rate_limit": "Увядзіце rate limit",
|
"form_enter_rate_limit": "Увядзіце rate limit",
|
||||||
"rate_limit": "Ограничение скорости",
|
"rate_limit": "Ограничение скорости",
|
||||||
@@ -269,6 +276,7 @@
|
|||||||
"source_label": "Крыніца",
|
"source_label": "Крыніца",
|
||||||
"found_in_known_domain_db": "Знойдзены ў базе вядомых даменаў.",
|
"found_in_known_domain_db": "Знойдзены ў базе вядомых даменаў.",
|
||||||
"category_label": "Катэгорыя",
|
"category_label": "Катэгорыя",
|
||||||
|
"rule_label": "Правіла(ы)",
|
||||||
"list_label": "Спіс",
|
"list_label": "Спіс",
|
||||||
"unknown_filter": "Невядомы фільтр {{filterId}}",
|
"unknown_filter": "Невядомы фільтр {{filterId}}",
|
||||||
"known_tracker": "Вядомы трэкер",
|
"known_tracker": "Вядомы трэкер",
|
||||||
@@ -329,6 +337,7 @@
|
|||||||
"encryption_config_saved": "Налады шыфравання захаваны",
|
"encryption_config_saved": "Налады шыфравання захаваны",
|
||||||
"encryption_server": "Імя сервера",
|
"encryption_server": "Імя сервера",
|
||||||
"encryption_server_enter": "Увядзіце ваша даменавае імя",
|
"encryption_server_enter": "Увядзіце ваша даменавае імя",
|
||||||
|
"encryption_server_desc": "Для выкарыстання HTTPS вам трэба ўвесці імя сервера, якое падыходзіць вашаму SSL-сертыфікату.",
|
||||||
"encryption_redirect": "Аўтаматычна перанакіроўваць на HTTPS",
|
"encryption_redirect": "Аўтаматычна перанакіроўваць на HTTPS",
|
||||||
"encryption_redirect_desc": "Калі ўлучана, AdGuard Home будзе аўтаматычна перанакіроўваць вас з HTTP на HTTPS адрас.",
|
"encryption_redirect_desc": "Калі ўлучана, AdGuard Home будзе аўтаматычна перанакіроўваць вас з HTTP на HTTPS адрас.",
|
||||||
"encryption_https": "Порт HTTPS",
|
"encryption_https": "Порт HTTPS",
|
||||||
@@ -384,6 +393,7 @@
|
|||||||
"client_edit": "Рэдагаваць кліента",
|
"client_edit": "Рэдагаваць кліента",
|
||||||
"client_identifier": "Ідэнтыфікатар",
|
"client_identifier": "Ідэнтыфікатар",
|
||||||
"ip_address": "IP-адрас",
|
"ip_address": "IP-адрас",
|
||||||
|
"client_identifier_desc": "Кліенты могуць быць ідэнтыфікаваны па IP-адрасе, CIDR ці MAC-адрасу. Звярніце ўвагу, што выкарыстанне MAC як ідэнтыфікатара магчыма, толькі калі AdGuard Home таксама з'яўляецца і <0>DHCP-серверам</0>",
|
||||||
"form_enter_ip": "Увядзіце IP",
|
"form_enter_ip": "Увядзіце IP",
|
||||||
"form_enter_mac": "Увядзіце MAC",
|
"form_enter_mac": "Увядзіце MAC",
|
||||||
"form_enter_id": "Увядзіце ідэнтыфікатар",
|
"form_enter_id": "Увядзіце ідэнтыфікатар",
|
||||||
@@ -427,6 +437,7 @@
|
|||||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> падтрымвае <1>DNS-over-HTTPS</1>.",
|
"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_4": "<0>Mozilla Firefox</0> падтрымвае <1>DNS-over-HTTPS</1>.",
|
||||||
"setup_dns_privacy_other_5": "Вы можаце знайсці яшчэ варыянты <0>тут</0> і <1>тут</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.",
|
"setup_dns_notice": "Каб выкарыстоўваць <1>DNS-over-HTTPS</1> ці <1>DNS-over-TLS</1>, вам патрэбна <0>наладзіць шыфраванне</0> у наладах AdGuard Home.",
|
||||||
"rewrite_added": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова дададзена",
|
"rewrite_added": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова дададзена",
|
||||||
"rewrite_deleted": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова выдалена",
|
"rewrite_deleted": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова выдалена",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Vlastní IP",
|
"custom_ip": "Vlastní IP",
|
||||||
"blocking_ipv4": "Blokování IPv4",
|
"blocking_ipv4": "Blokování IPv4",
|
||||||
"blocking_ipv6": "Blokování IPv6",
|
"blocking_ipv6": "Blokování IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS přes HTTPS",
|
"dns_over_https": "DNS přes HTTPS",
|
||||||
"dns_over_tls": "DNS přes TLS",
|
"dns_over_tls": "DNS přes TLS",
|
||||||
"dns_over_quic": "DNS skrze QUIC",
|
"dns_over_quic": "DNS skrze QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Tilpasset IP",
|
"custom_ip": "Tilpasset IP",
|
||||||
"blocking_ipv4": "IPv4-blokering",
|
"blocking_ipv4": "IPv4-blokering",
|
||||||
"blocking_ipv6": "IPv6-blokering",
|
"blocking_ipv6": "IPv6-blokering",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-Quic",
|
"dns_over_quic": "DNS-over-Quic",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Benutzerdefinierte IP",
|
"custom_ip": "Benutzerdefinierte IP",
|
||||||
"blocking_ipv4": "IPv4-Sperren",
|
"blocking_ipv4": "IPv4-Sperren",
|
||||||
"blocking_ipv6": "IPv6-Sperren",
|
"blocking_ipv6": "IPv6-Sperren",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS (DNS-Abrage über HTTPS)",
|
"dns_over_https": "DNS-over-HTTPS (DNS-Abrage über HTTPS)",
|
||||||
"dns_over_tls": "DNS-over-TLS (DNS-Abrage über TLS)",
|
"dns_over_tls": "DNS-over-TLS (DNS-Abrage über TLS)",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Custom IP",
|
"custom_ip": "Custom IP",
|
||||||
"blocking_ipv4": "Blocking IPv4",
|
"blocking_ipv4": "Blocking IPv4",
|
||||||
"blocking_ipv6": "Blocking IPv6",
|
"blocking_ipv6": "Blocking IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,15 +248,16 @@
|
|||||||
"custom_ip": "IP personalizada",
|
"custom_ip": "IP personalizada",
|
||||||
"blocking_ipv4": "Bloqueo de IPv4",
|
"blocking_ipv4": "Bloqueo de IPv4",
|
||||||
"blocking_ipv6": "Bloqueo de IPv6",
|
"blocking_ipv6": "Bloqueo de IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS mediante HTTPS",
|
"dns_over_https": "DNS mediante HTTPS",
|
||||||
"dns_over_tls": "DNS mediante TLS",
|
"dns_over_tls": "DNS mediante TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS mediante QUIC",
|
||||||
"client_id": "ID de cliente",
|
"client_id": "ID de cliente",
|
||||||
"client_id_placeholder": "Ingresa tu ID de cliente",
|
"client_id_placeholder": "Ingresa el ID del cliente",
|
||||||
"client_id_desc": "Varios clientes se pueden identificar mediante un ID de cliente especial. <a>Aquí</a> puede aprender más sobre cómo identificar clientes.",
|
"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_doh": "Descargar .mobileconfig para DNS mediante HTTPS",
|
||||||
"download_mobileconfig_dot": "Descargar .mobileconfig para DNS mediante TLS",
|
"download_mobileconfig_dot": "Descargar .mobileconfig para DNS mediante TLS",
|
||||||
"download_mobileconfig": "Descargar el archivo de configuración",
|
"download_mobileconfig": "Descargar archivo de configuración",
|
||||||
"plain_dns": "DNS simple",
|
"plain_dns": "DNS simple",
|
||||||
"form_enter_rate_limit": "Ingresa el límite de cantidad",
|
"form_enter_rate_limit": "Ingresa el límite de cantidad",
|
||||||
"rate_limit": "Límite de cantidad",
|
"rate_limit": "Límite de cantidad",
|
||||||
@@ -275,7 +276,7 @@
|
|||||||
"source_label": "Fuente",
|
"source_label": "Fuente",
|
||||||
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
|
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
|
||||||
"category_label": "Categoría",
|
"category_label": "Categoría",
|
||||||
"rule_label": "Regla(s)",
|
"rule_label": "Regla",
|
||||||
"list_label": "Lista",
|
"list_label": "Lista",
|
||||||
"unknown_filter": "Filtro desconocido {{filterId}}",
|
"unknown_filter": "Filtro desconocido {{filterId}}",
|
||||||
"known_tracker": "Rastreador conocido",
|
"known_tracker": "Rastreador conocido",
|
||||||
@@ -336,7 +337,7 @@
|
|||||||
"encryption_config_saved": "Configuración de cifrado guardado",
|
"encryption_config_saved": "Configuración de cifrado guardado",
|
||||||
"encryption_server": "Nombre del servidor",
|
"encryption_server": "Nombre del servidor",
|
||||||
"encryption_server_enter": "Ingresa el nombre del dominio",
|
"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 o certificado Wildcard. Si el campo no está establecido, el servidor aceptará conexiones TLS para cualquier dominio.",
|
"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": "Redireccionar a HTTPS automáticamente",
|
||||||
"encryption_redirect_desc": "Si está marcado, AdGuard Home redireccionará automáticamente de HTTP a las direcciones HTTPS.",
|
"encryption_redirect_desc": "Si está marcado, AdGuard Home redireccionará automáticamente de HTTP a las direcciones HTTPS.",
|
||||||
"encryption_https": "Puerto HTTPS",
|
"encryption_https": "Puerto HTTPS",
|
||||||
@@ -392,7 +393,7 @@
|
|||||||
"client_edit": "Editar cliente",
|
"client_edit": "Editar cliente",
|
||||||
"client_identifier": "Identificador",
|
"client_identifier": "Identificador",
|
||||||
"ip_address": "Dirección IP",
|
"ip_address": "Dirección IP",
|
||||||
"client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP, MAC, CIDR o un especial ID de cliente (puede ser utilizado para DoT/DoH/DoQ). <0>Aquí</0> puede obtener más información sobre cómo identificar clientes.",
|
"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_ip": "Ingresa la IP",
|
||||||
"form_enter_mac": "Ingresa la MAC",
|
"form_enter_mac": "Ingresa la MAC",
|
||||||
"form_enter_id": "Ingresa el identificador",
|
"form_enter_id": "Ingresa el identificador",
|
||||||
@@ -436,7 +437,7 @@
|
|||||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> soporta <1>DNS mediante HTTPS</1>.",
|
"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_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_other_5": "Encontrarás más implementaciones <0>aquí</0> y <1>aquí</1>.",
|
||||||
"setup_dns_privacy_ioc_mac": "La configuración de iOS y macOS ",
|
"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.",
|
"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_added": "Reescritura DNS para \"{{key}}\" añadido correctamente",
|
||||||
"rewrite_deleted": "Reescritura DNS para \"{{key}}\" eliminado correctamente",
|
"rewrite_deleted": "Reescritura DNS para \"{{key}}\" eliminado correctamente",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "IP personnalisée",
|
"custom_ip": "IP personnalisée",
|
||||||
"blocking_ipv4": "Blocage IPv4",
|
"blocking_ipv4": "Blocage IPv4",
|
||||||
"blocking_ipv6": "Blocage IPv6",
|
"blocking_ipv6": "Blocage IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"form_error_ip_format": "Nevažeći format IP adrese",
|
"form_error_ip_format": "Nevažeći format IP adrese",
|
||||||
"form_error_mac_format": "Nevažeći MAC format",
|
"form_error_mac_format": "Nevažeći MAC format",
|
||||||
"form_error_client_id_format": "Nevažeći format ID-a klijenta",
|
"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_positive": "Mora biti veće od 0",
|
||||||
"form_error_negative": "Mora biti jednako ili 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",
|
"range_end_error": "Mora biti veće od početne vrijednosti raspona",
|
||||||
@@ -247,10 +248,16 @@
|
|||||||
"custom_ip": "Prilagođen IP",
|
"custom_ip": "Prilagođen IP",
|
||||||
"blocking_ipv4": "Blokiranje IPv4",
|
"blocking_ipv4": "Blokiranje IPv4",
|
||||||
"blocking_ipv6": "Blokiranje IPv6",
|
"blocking_ipv6": "Blokiranje IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"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_doh": "Preuzmi .mobileconfig za DNS-over-HTTPS",
|
||||||
"download_mobileconfig_dot": "Preuzmi .mobileconfig za DNS-over-TLS",
|
"download_mobileconfig_dot": "Preuzmi .mobileconfig za DNS-over-TLS",
|
||||||
|
"download_mobileconfig": "Preuzmite konfiguracijsku datoteku",
|
||||||
"plain_dns": "Obični DNS",
|
"plain_dns": "Obični DNS",
|
||||||
"form_enter_rate_limit": "Unesite ograničenje",
|
"form_enter_rate_limit": "Unesite ograničenje",
|
||||||
"rate_limit": "Ograničenje",
|
"rate_limit": "Ograničenje",
|
||||||
@@ -269,6 +276,7 @@
|
|||||||
"source_label": "Izvor",
|
"source_label": "Izvor",
|
||||||
"found_in_known_domain_db": "Pronađeno u bazi poznatih domena.",
|
"found_in_known_domain_db": "Pronađeno u bazi poznatih domena.",
|
||||||
"category_label": "Kategorija",
|
"category_label": "Kategorija",
|
||||||
|
"rule_label": "Pravilo",
|
||||||
"list_label": "Popis",
|
"list_label": "Popis",
|
||||||
"unknown_filter": "Nepoznati filtar {{filterId}}",
|
"unknown_filter": "Nepoznati filtar {{filterId}}",
|
||||||
"known_tracker": "Poznati pratitelj",
|
"known_tracker": "Poznati pratitelj",
|
||||||
@@ -329,6 +337,7 @@
|
|||||||
"encryption_config_saved": "Spremljene postavke šifriranja",
|
"encryption_config_saved": "Spremljene postavke šifriranja",
|
||||||
"encryption_server": "Naziv poslužitelja",
|
"encryption_server": "Naziv poslužitelja",
|
||||||
"encryption_server_enter": "Unesite naziv domene",
|
"encryption_server_enter": "Unesite naziv domene",
|
||||||
|
"encryption_server_desc": "Kako biste koristili HTTPS, morate unijeti naziv poslužitelja koji odgovara vašem SSL certifikatu.",
|
||||||
"encryption_redirect": "Automatski preusmjeri na HTTPS",
|
"encryption_redirect": "Automatski preusmjeri na HTTPS",
|
||||||
"encryption_redirect_desc": "Ako je omogućeno, AdGuard Home će vas automatski preusmjeravati s HTTP na HTTPS adrese.",
|
"encryption_redirect_desc": "Ako je omogućeno, AdGuard Home će vas automatski preusmjeravati s HTTP na HTTPS adrese.",
|
||||||
"encryption_https": "HTTPS port",
|
"encryption_https": "HTTPS port",
|
||||||
@@ -384,6 +393,7 @@
|
|||||||
"client_edit": "Uredi klijenta",
|
"client_edit": "Uredi klijenta",
|
||||||
"client_identifier": "Identifikator",
|
"client_identifier": "Identifikator",
|
||||||
"ip_address": "IP adresa",
|
"ip_address": "IP adresa",
|
||||||
|
"client_identifier_desc": "Klijenti se mogu prepoznati po IP adresi, CIDR-u ili MAC adresi. Imajte na umu da je upotreba MAC-a kao identifikatora, moguća samo ako je AdGuard Home također i <0>DHCP poslužitelj</0>",
|
||||||
"form_enter_ip": "Unesite IP adresu",
|
"form_enter_ip": "Unesite IP adresu",
|
||||||
"form_enter_mac": "Unesite MAC adresu",
|
"form_enter_mac": "Unesite MAC adresu",
|
||||||
"form_enter_id": "Unesi identifikator",
|
"form_enter_id": "Unesi identifikator",
|
||||||
@@ -427,6 +437,7 @@
|
|||||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podržava <1>DNS-over-HTTPS</1>.",
|
"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_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_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.",
|
"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_added": "DNS prijepis za \"{{key}}\" je uspješno dodan",
|
||||||
"rewrite_deleted": "DNS prijepis za \"{{key}}\" je uspješno uklonjen",
|
"rewrite_deleted": "DNS prijepis za \"{{key}}\" je uspješno uklonjen",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "IP personalizzato",
|
"custom_ip": "IP personalizzato",
|
||||||
"blocking_ipv4": "Blocca IPv4",
|
"blocking_ipv4": "Blocca IPv4",
|
||||||
"blocking_ipv6": "Blocca IPv6",
|
"blocking_ipv6": "Blocca IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS su HTTPS",
|
"dns_over_https": "DNS su HTTPS",
|
||||||
"dns_over_tls": "DNS su TLS",
|
"dns_over_tls": "DNS su TLS",
|
||||||
"dns_over_quic": "DNS su Quic",
|
"dns_over_quic": "DNS su Quic",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "カスタムIP",
|
"custom_ip": "カスタムIP",
|
||||||
"blocking_ipv4": "ブロック中のIPv4",
|
"blocking_ipv4": "ブロック中のIPv4",
|
||||||
"blocking_ipv6": "ブロック中のIPv6",
|
"blocking_ipv6": "ブロック中のIPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "사용자 지정 IP",
|
"custom_ip": "사용자 지정 IP",
|
||||||
"blocking_ipv4": "IPv4 차단",
|
"blocking_ipv4": "IPv4 차단",
|
||||||
"blocking_ipv6": "IPv6 차단",
|
"blocking_ipv6": "IPv6 차단",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Aangepast IP",
|
"custom_ip": "Aangepast IP",
|
||||||
"blocking_ipv4": "Blokkeren IP4",
|
"blocking_ipv4": "Blokkeren IP4",
|
||||||
"blocking_ipv6": "Blokkeren IP6",
|
"blocking_ipv6": "Blokkeren IP6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-via-HTTPS",
|
"dns_over_https": "DNS-via-HTTPS",
|
||||||
"dns_over_tls": "DNS-via-TLS",
|
"dns_over_tls": "DNS-via-TLS",
|
||||||
"dns_over_quic": "DNS-via-QUIC",
|
"dns_over_quic": "DNS-via-QUIC",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"form_error_ip_format": "Ugyldig IPv4-format",
|
"form_error_ip_format": "Ugyldig IPv4-format",
|
||||||
"form_error_mac_format": "Ugyldig MAC-format",
|
"form_error_mac_format": "Ugyldig MAC-format",
|
||||||
"form_error_client_id_format": "Ugyldig ID-klientformat",
|
"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_positive": "Må være høyere enn 0",
|
||||||
"form_error_negative": "Må være ≥0",
|
"form_error_negative": "Må være ≥0",
|
||||||
"range_end_error": "Må være høyere enn rekkeviddens start",
|
"range_end_error": "Må være høyere enn rekkeviddens start",
|
||||||
@@ -247,8 +248,11 @@
|
|||||||
"custom_ip": "Tilpasset IP",
|
"custom_ip": "Tilpasset IP",
|
||||||
"blocking_ipv4": "IPv4-blokkering",
|
"blocking_ipv4": "IPv4-blokkering",
|
||||||
"blocking_ipv6": "IPv6-blokkering",
|
"blocking_ipv6": "IPv6-blokkering",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"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_doh": "Last ned .mobileconfig for DNS-over-HTTPS",
|
||||||
"download_mobileconfig_dot": "Last ned .mobileconfig for DNS-over-TLS",
|
"download_mobileconfig_dot": "Last ned .mobileconfig for DNS-over-TLS",
|
||||||
"plain_dns": "Ordinær DNS",
|
"plain_dns": "Ordinær DNS",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Niestandardowy adres IP",
|
"custom_ip": "Niestandardowy adres IP",
|
||||||
"blocking_ipv4": "Blokowanie IPv4",
|
"blocking_ipv4": "Blokowanie IPv4",
|
||||||
"blocking_ipv6": "Blokowanie IPv6",
|
"blocking_ipv6": "Blokowanie IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "IP personalizado",
|
"custom_ip": "IP personalizado",
|
||||||
"blocking_ipv4": "Bloqueando IPv4",
|
"blocking_ipv4": "Bloqueando IPv4",
|
||||||
"blocking_ipv6": "Bloqueando IPv6",
|
"blocking_ipv6": "Bloqueando IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-sobre-HTTPS",
|
"dns_over_https": "DNS-sobre-HTTPS",
|
||||||
"dns_over_tls": "DNS-sobre-TLS",
|
"dns_over_tls": "DNS-sobre-TLS",
|
||||||
"dns_over_quic": "DNS-sobre-QUIC",
|
"dns_over_quic": "DNS-sobre-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "IP Personalizado",
|
"custom_ip": "IP Personalizado",
|
||||||
"blocking_ipv4": "A bloquear IPv4",
|
"blocking_ipv4": "A bloquear IPv4",
|
||||||
"blocking_ipv6": "A bloquear IPv6",
|
"blocking_ipv6": "A bloquear IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-sobre-HTTPS",
|
"dns_over_https": "DNS-sobre-HTTPS",
|
||||||
"dns_over_tls": "DNS-sobre-TLS",
|
"dns_over_tls": "DNS-sobre-TLS",
|
||||||
"dns_over_quic": "DNS-sobre-QUIC",
|
"dns_over_quic": "DNS-sobre-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "IP personalizat",
|
"custom_ip": "IP personalizat",
|
||||||
"blocking_ipv4": "Blocarea IPv4",
|
"blocking_ipv4": "Blocarea IPv4",
|
||||||
"blocking_ipv6": "Blocarea IPv6",
|
"blocking_ipv6": "Blocarea IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Свой IP",
|
"custom_ip": "Свой IP",
|
||||||
"blocking_ipv4": "Блокировка IPv4",
|
"blocking_ipv4": "Блокировка IPv4",
|
||||||
"blocking_ipv6": "Блокировка IPv6",
|
"blocking_ipv6": "Блокировка IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
"enabled_save_search_toast": "ආරක්ෂිත සෙවීම සබල කර ඇත",
|
"enabled_save_search_toast": "ආරක්ෂිත සෙවීම සබල කර ඇත",
|
||||||
"enabled_table_header": "සබල කර ඇත",
|
"enabled_table_header": "සබල කර ඇත",
|
||||||
"name_table_header": "නම",
|
"name_table_header": "නම",
|
||||||
"list_url_table_header": "URL ලැයිස්තුව",
|
"list_url_table_header": "ඒ.ස.නි.(URL) ලැයිස්තුව",
|
||||||
"rules_count_table_header": "නීති ගණන",
|
"rules_count_table_header": "නීති ගණන",
|
||||||
"last_time_updated_table_header": "අවසන් වරට යාවත්කාලීන කරන ලද",
|
"last_time_updated_table_header": "අවසන් වරට යාවත්කාලීන කරන ලද",
|
||||||
"actions_table_header": "ක්රියාමාර්ග",
|
"actions_table_header": "ක්රියාමාර්ග",
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
"add_allowlist": "අවසර දීමේ ලැයිස්තුවක් එකතු කරන්න",
|
"add_allowlist": "අවසර දීමේ ලැයිස්තුවක් එකතු කරන්න",
|
||||||
"cancel_btn": "අහෝසි කරන්න",
|
"cancel_btn": "අහෝසි කරන්න",
|
||||||
"enter_name_hint": "නම ඇතුළත් කරන්න",
|
"enter_name_hint": "නම ඇතුළත් කරන්න",
|
||||||
"enter_url_or_path_hint": "ලැයිස්තුවක URL හෝ ස්ථීර මාර්ගයක් ඇතුළත් කරන්න",
|
"enter_url_or_path_hint": "ලැයිස්තුවක ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයක් ඇතුළත් කරන්න",
|
||||||
"check_updates_btn": "යාවත්කාල පරීක්ෂා කරන්න",
|
"check_updates_btn": "යාවත්කාල පරීක්ෂා කරන්න",
|
||||||
"new_blocklist": "නව අවහිර කිරීමේ ලැයිස්තුව",
|
"new_blocklist": "නව අවහිර කිරීමේ ලැයිස්තුව",
|
||||||
"new_allowlist": "නව අවසර දීමේ ලැයිස්තුව",
|
"new_allowlist": "නව අවසර දීමේ ලැයිස්තුව",
|
||||||
@@ -142,10 +142,10 @@
|
|||||||
"edit_allowlist": "අවසර දීමේ ලැයිස්තුව සංස්කරණය කරන්න",
|
"edit_allowlist": "අවසර දීමේ ලැයිස්තුව සංස්කරණය කරන්න",
|
||||||
"choose_blocklist": "අවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න",
|
"choose_blocklist": "අවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න",
|
||||||
"choose_allowlist": "අනවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න",
|
"choose_allowlist": "අනවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න",
|
||||||
"enter_valid_blocklist": "අවහිර කිරීමේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.",
|
"enter_valid_blocklist": "අවහිර කිරීමේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුළත් කරන්න.",
|
||||||
"enter_valid_allowlist": "අවසර දීමේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.",
|
"enter_valid_allowlist": "අවසර දීමේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුළත් කරන්න.",
|
||||||
"form_error_url_format": "වලංගු නොවන URL ආකෘතියකි",
|
"form_error_url_format": "වලංගු නොවන ඒ.ස.නි.(URL) ආකෘතියකි",
|
||||||
"form_error_url_or_path_format": "ලැයිස්තුවක වලංගු නොවන URL හෝ ස්ථීර මාර්ගයකි",
|
"form_error_url_or_path_format": "ලැයිස්තුවක වලංගු නොවන ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයකි",
|
||||||
"custom_filter_rules": "අභිරුචි පෙරීමේ නීති",
|
"custom_filter_rules": "අභිරුචි පෙරීමේ නීති",
|
||||||
"custom_filter_rules_hint": "පේළියකට එක් නීතියක් බැගින් ඇතුළත් කරන්න. ඔබට දැන්වීම් අවහිර කිරීමේ නීති හෝ ධාරක ගොනු පද ගැලපුම් භාවිතා කළ හැකිය.",
|
"custom_filter_rules_hint": "පේළියකට එක් නීතියක් බැගින් ඇතුළත් කරන්න. ඔබට දැන්වීම් අවහිර කිරීමේ නීති හෝ ධාරක ගොනු පද ගැලපුම් භාවිතා කළ හැකිය.",
|
||||||
"examples_title": "උදාහරණ",
|
"examples_title": "උදාහරණ",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Vlastná IP adresa",
|
"custom_ip": "Vlastná IP adresa",
|
||||||
"blocking_ipv4": "Blokovanie IPv4",
|
"blocking_ipv4": "Blokovanie IPv4",
|
||||||
"blocking_ipv6": "Blokovanie IPv6",
|
"blocking_ipv6": "Blokovanie IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "IP po meri",
|
"custom_ip": "IP po meri",
|
||||||
"blocking_ipv4": "Onemogočanje IPv4",
|
"blocking_ipv4": "Onemogočanje IPv4",
|
||||||
"blocking_ipv6": "Onemogočanje IPv6",
|
"blocking_ipv6": "Onemogočanje IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-prek-HTTPS",
|
"dns_over_https": "DNS-prek-HTTPS",
|
||||||
"dns_over_tls": "DNS-prek-TLS",
|
"dns_over_tls": "DNS-prek-TLS",
|
||||||
"dns_over_quic": "DNS-prek-QIUC",
|
"dns_over_quic": "DNS-prek-QIUC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "Özel IP",
|
"custom_ip": "Özel IP",
|
||||||
"blocking_ipv4": "IPv4 engelleme",
|
"blocking_ipv4": "IPv4 engelleme",
|
||||||
"blocking_ipv6": "IPv6 engelleme",
|
"blocking_ipv6": "IPv6 engelleme",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "IP tuỳ chỉnh",
|
"custom_ip": "IP tuỳ chỉnh",
|
||||||
"blocking_ipv4": "Chặn IPv4",
|
"blocking_ipv4": "Chặn IPv4",
|
||||||
"blocking_ipv6": "Chặn IPv6",
|
"blocking_ipv6": "Chặn IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
"custom_ip": "自定义 IP",
|
"custom_ip": "自定义 IP",
|
||||||
"blocking_ipv4": "拦截 IPv4",
|
"blocking_ipv4": "拦截 IPv4",
|
||||||
"blocking_ipv6": "拦截 IPv6",
|
"blocking_ipv6": "拦截 IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
"dns_over_quic": "DNS-over-QUIC",
|
"dns_over_quic": "DNS-over-QUIC",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"client_settings": "用戶端設定",
|
"client_settings": "用戶端設定",
|
||||||
"example_upstream_reserved": "您可以<0>為特定網域</0>指定上游 DNS",
|
"example_upstream_reserved": "您可<0>對於特定的網域</0>明確指定 DNS 上游",
|
||||||
"example_upstream_comment": "您可明確指定註解",
|
"example_upstream_comment": "您可明確指定註解",
|
||||||
"upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析網域",
|
"upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析網域",
|
||||||
"parallel_requests": "並行的請求",
|
"parallel_requests": "並行的請求",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"form_error_ip_format": "無效的 IP 格式",
|
"form_error_ip_format": "無效的 IP 格式",
|
||||||
"form_error_mac_format": "無效的媒體存取控制(MAC)格式",
|
"form_error_mac_format": "無效的媒體存取控制(MAC)格式",
|
||||||
"form_error_client_id_format": "無效的用戶端 ID 格式",
|
"form_error_client_id_format": "無效的用戶端 ID 格式",
|
||||||
|
"form_error_server_name": "無效的伺服器名稱",
|
||||||
"form_error_positive": "必須大於 0",
|
"form_error_positive": "必須大於 0",
|
||||||
"form_error_negative": "必須等於或大於 0",
|
"form_error_negative": "必須等於或大於 0",
|
||||||
"range_end_error": "必須大於起始範圍",
|
"range_end_error": "必須大於起始範圍",
|
||||||
@@ -247,10 +248,16 @@
|
|||||||
"custom_ip": "自訂的 IP",
|
"custom_ip": "自訂的 IP",
|
||||||
"blocking_ipv4": "封鎖 IPv4",
|
"blocking_ipv4": "封鎖 IPv4",
|
||||||
"blocking_ipv6": "封鎖 IPv6",
|
"blocking_ipv6": "封鎖 IPv6",
|
||||||
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"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_doh": "下載用於 DNS-over-HTTPS 的 .mobileconfig",
|
||||||
"download_mobileconfig_dot": "下載用於 DNS-over-TLS 的 .mobileconfig",
|
"download_mobileconfig_dot": "下載用於 DNS-over-TLS 的 .mobileconfig",
|
||||||
|
"download_mobileconfig": "下載配置檔案",
|
||||||
"plain_dns": "一般的 DNS",
|
"plain_dns": "一般的 DNS",
|
||||||
"form_enter_rate_limit": "輸入速率限制",
|
"form_enter_rate_limit": "輸入速率限制",
|
||||||
"rate_limit": "速率限制",
|
"rate_limit": "速率限制",
|
||||||
@@ -386,6 +393,7 @@
|
|||||||
"client_edit": "編輯用戶端",
|
"client_edit": "編輯用戶端",
|
||||||
"client_identifier": "識別碼",
|
"client_identifier": "識別碼",
|
||||||
"ip_address": "IP 位址",
|
"ip_address": "IP 位址",
|
||||||
|
"client_identifier_desc": "用戶端可根據 IP 位址、無類別網域間路由(CIDR)、媒體存取控制(MAC)位址或特殊的用戶端 ID(可被用於 DoT/DoH/DoQ)被識別。<0>於此</0>,您可了解更多關於如何識別用戶端。",
|
||||||
"form_enter_ip": "輸入 IP",
|
"form_enter_ip": "輸入 IP",
|
||||||
"form_enter_mac": "輸入媒體存取控制(MAC)",
|
"form_enter_mac": "輸入媒體存取控制(MAC)",
|
||||||
"form_enter_id": "輸入識別碼",
|
"form_enter_id": "輸入識別碼",
|
||||||
@@ -429,6 +437,7 @@
|
|||||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> 支援 <1>DNS-over-HTTPS</1>。",
|
"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_4": "<0>Mozilla Firefox</0> 支援 <1>DNS-over-HTTPS</1>。",
|
||||||
"setup_dns_privacy_other_5": "在<0>這裡</0>和<1>這裡</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>。",
|
"setup_dns_notice": "為了使用 <1>DNS-over-HTTPS</1> 或 <1>DNS-over-TLS</1>,您需要在 AdGuard Home 設定裡<0>配置加密</0>。",
|
||||||
"rewrite_added": "對於 \"{{key}}\" 之 DNS 改寫被成功地加入",
|
"rewrite_added": "對於 \"{{key}}\" 之 DNS 改寫被成功地加入",
|
||||||
"rewrite_deleted": "對於 \"{{key}}\" 之 DNS 改寫被成功地刪除",
|
"rewrite_deleted": "對於 \"{{key}}\" 之 DNS 改寫被成功地刪除",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
--gray-d8: #d8d8d8;
|
--gray-d8: #d8d8d8;
|
||||||
--gray-f3: #f3f3f3;
|
--gray-f3: #f3f3f3;
|
||||||
--font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
|
--font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
|
||||||
|
--font-size-disable-autozoom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -13,9 +14,10 @@ body {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
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) {
|
@media screen and (max-width: 767px) {
|
||||||
input, select, textarea {
|
input, select, textarea {
|
||||||
font-size: 16px !important;
|
font-size: var(--font-size-disable-autozoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
font-size: 0.9375rem;
|
font-size: var(--font-size-disable-autozoom);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -534,6 +534,7 @@ export const BLOCK_ACTIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SCHEME_TO_PROTOCOL_MAP = {
|
export const SCHEME_TO_PROTOCOL_MAP = {
|
||||||
|
dnscrypt: 'dnscrypt',
|
||||||
doh: 'dns_over_https',
|
doh: 'dns_over_https',
|
||||||
dot: 'dns_over_tls',
|
dot: 'dns_over_tls',
|
||||||
doq: 'dns_over_quic',
|
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) {
|
@media screen and (max-width: 767px) {
|
||||||
input, select, textarea {
|
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) {
|
@media screen and (max-width: 767px) {
|
||||||
input, select, textarea {
|
input, select, textarea {
|
||||||
font-size: 16px !important;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
"stylelint-webpack-plugin": "^2.1.1",
|
"stylelint-webpack-plugin": "^2.1.1",
|
||||||
"terser-webpack-plugin": "^5.0.0",
|
"terser-webpack-plugin": "^5.0.0",
|
||||||
"ts-loader": "^8.0.6",
|
"ts-loader": "^8.0.6",
|
||||||
"ts-morph": "^8.1.2",
|
"ts-morph": "^10.0.1",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.0.0",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export const trimQuotes = (str: string) => {
|
|||||||
return str.replace(/\'|\"/g, '');
|
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 EntitiesGenerator from './src/generateEntities';
|
||||||
import ApisGenerator from './src/generateApis';
|
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);
|
const ent = new EntitiesGenerator(openApi);
|
||||||
ent.save();
|
ent.save();
|
||||||
|
|
||||||
@@ -14,5 +15,5 @@ const generateApi = (openApi: Record<string, any>) => {
|
|||||||
api.save();
|
api.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
const openApiFile = fs.readFileSync(OPEN_API_PATH, 'utf8');
|
const openApiFile = fs.readFileSync('./scripts/generator/v1.yaml', 'utf8');
|
||||||
generateApi(YAML.parse(openApiFile));
|
generateApi(YAML.parse(openApiFile));
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { stringify } from 'qs';
|
import { number } from 'prop-types';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as morph from 'ts-morph';
|
import * as morph from 'ts-morph';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
API_DIR as API_DIR_CONST,
|
API_DIR as API_DIR_CONST,
|
||||||
|
BAD_REQUES_HELPER,
|
||||||
GENERATOR_ENTITY_ALLIAS,
|
GENERATOR_ENTITY_ALLIAS,
|
||||||
} from '../../consts';
|
} 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);
|
const API_DIR = path.resolve(API_DIR_CONST);
|
||||||
@@ -20,11 +21,15 @@ if (!fs.existsSync(API_DIR)) {
|
|||||||
|
|
||||||
const { Project, QuoteKind } = morph;
|
const { Project, QuoteKind } = morph;
|
||||||
|
|
||||||
|
enum PROCESS_AS {
|
||||||
|
JSON = 'JSON',
|
||||||
|
TEXT = 'TEXT',
|
||||||
|
EMPTY = 'EMPTY',
|
||||||
|
}
|
||||||
|
|
||||||
class ApiGenerator {
|
class ApiGenerator {
|
||||||
project = new Project({
|
project = new Project({
|
||||||
tsConfigFilePath: './tsconfig.json',
|
tsConfigFilePath: './tsconfig.json',
|
||||||
addFilesFromTsConfig: false,
|
|
||||||
manipulationSettings: {
|
manipulationSettings: {
|
||||||
quoteKind: QuoteKind.Single,
|
quoteKind: QuoteKind.Single,
|
||||||
usePrefixAndSuffixTextForRename: false,
|
usePrefixAndSuffixTextForRename: false,
|
||||||
@@ -32,7 +37,7 @@ class ApiGenerator {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
openapi: Record<string, any>;
|
openapi: OpenApi;
|
||||||
|
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
|
|
||||||
@@ -47,20 +52,28 @@ class ApiGenerator {
|
|||||||
|
|
||||||
apis: morph.SourceFile[] = [];
|
apis: morph.SourceFile[] = [];
|
||||||
|
|
||||||
constructor(openapi: Record<string, any>) {
|
methods = ['patch', 'delete', 'post', 'get', 'put', 'head', 'options', 'trace'];
|
||||||
|
|
||||||
|
constructor(openapi: OpenApi) {
|
||||||
this.openapi = openapi;
|
this.openapi = openapi;
|
||||||
this.paths = openapi.paths;
|
this.paths = openapi.paths;
|
||||||
this.serverUrl = openapi.servers[0].url;
|
this.serverUrl = openapi.servers[0].url;
|
||||||
|
|
||||||
Object.keys(this.paths).forEach((pathKey) => {
|
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 {
|
const {
|
||||||
tags, operationId, parameters, responses, requestBody, security,
|
tags, operationId, responses, requestBody, security, "x-skip-web-api": skip
|
||||||
} = this.paths[pathKey][method];
|
} = 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]) {
|
if (this.controllers[controller]) {
|
||||||
this.controllers[controller][operationId] = {
|
this.controllers[controller][uncapitalize(operationId)] = {
|
||||||
parameters,
|
parameters,
|
||||||
responses,
|
responses,
|
||||||
method,
|
method,
|
||||||
@@ -69,7 +82,7 @@ class ApiGenerator {
|
|||||||
pathKey: pathKey.replace(/{/g, '${'),
|
pathKey: pathKey.replace(/{/g, '${'),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.controllers[controller] = { [operationId]: {
|
this.controllers[controller] = { [uncapitalize(operationId)]: {
|
||||||
parameters,
|
parameters,
|
||||||
responses,
|
responses,
|
||||||
method,
|
method,
|
||||||
@@ -97,7 +110,7 @@ class ApiGenerator {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// const schemaProperties = schemas[schemaName].properties;
|
// const schemaProperties = schemas[schemaName].properties;
|
||||||
const importEntities: any[] = [];
|
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||||
|
|
||||||
// add api class to file
|
// add api class to file
|
||||||
const apiClass = apiFile.addClass({
|
const apiClass = apiFile.addClass({
|
||||||
@@ -111,29 +124,34 @@ class ApiGenerator {
|
|||||||
// for each operation add fetcher
|
// for each operation add fetcher
|
||||||
operationList.forEach((operation) => {
|
operationList.forEach((operation) => {
|
||||||
const {
|
const {
|
||||||
requestBody, responses, parameters, method, pathKey, security,
|
requestBody, responses, parameters, method, pathKey,
|
||||||
} = controllerOperations[operation];
|
} = controllerOperations[operation];
|
||||||
|
|
||||||
const queryParams: any[] = []; // { name, type }
|
const queryParams: { name: string, type: string, hasQuestionToken: boolean }[] = [];
|
||||||
const bodyParam: any[] = []; // { name, type }
|
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) {
|
if (parameters) {
|
||||||
parameters.forEach((p: any) => {
|
parameters.forEach((link: {$ref: string}) => {
|
||||||
const [
|
const temp = link.$ref.split('/').pop()
|
||||||
pType, isArray, isClass, isImport,
|
const parameter = this.openapi.components.parameters[temp!];
|
||||||
] = schemaParamParser(p.schema, this.openapi);
|
|
||||||
|
const {
|
||||||
|
type, isArray, isClass, isImport,
|
||||||
|
} = schemaParamParser(parameter.schema, this.openapi);
|
||||||
|
|
||||||
if (isImport) {
|
if (isImport) {
|
||||||
importEntities.push({ type: pType, isClass });
|
importEntities.push({ type, isClass });
|
||||||
}
|
}
|
||||||
if (p.in === 'query') {
|
if (parameter.in === 'query') {
|
||||||
queryParams.push({
|
queryParams.push({
|
||||||
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
|
name: parameter.name, type: `${type}${isArray ? '[]' : ''}`, hasQuestionToken: !parameter.required });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.length > 0) {
|
if (queryParams.length > 0) {
|
||||||
const imp = apiFile.getImportDeclaration((i) => {
|
const imp = apiFile.getImportDeclaration((i) => {
|
||||||
return i.getModuleSpecifierValue() === 'qs';
|
return i.getModuleSpecifierValue() === 'qs';
|
||||||
@@ -144,62 +162,120 @@ class ApiGenerator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestBody) {
|
if (requestBody) {
|
||||||
let content = requestBody.content;
|
|
||||||
const { $ref }: { $ref: string } = requestBody;
|
const { $ref }: { $ref: string } = requestBody;
|
||||||
|
|
||||||
if (!content && $ref) {
|
const name = $ref.split('/').pop();
|
||||||
const name = $ref.split('/').pop() as string;
|
const { content, required } = this.openapi.components.requestBodies[name!];
|
||||||
content = this.openapi.components.requestBodies[name].content;
|
|
||||||
}
|
|
||||||
|
|
||||||
[contentType] = Object.keys(content);
|
[contentType] = Object.keys(content);
|
||||||
const data = content[contentType];
|
const data = content[contentType as keyof RequestBody['content']]!;
|
||||||
|
|
||||||
const [
|
const {
|
||||||
pType, isArray, isClass, isImport,
|
type, isArray, isClass, isImport,
|
||||||
] = schemaParamParser(data.schema, this.openapi);
|
} = schemaParamParser(data.schema, this.openapi);
|
||||||
|
|
||||||
if (isImport) {
|
if (isImport) {
|
||||||
importEntities.push({ type: pType, isClass });
|
importEntities.push({ type: type, isClass });
|
||||||
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
|
bodyParam.push({
|
||||||
|
name: type.toLowerCase(),
|
||||||
|
countedType: `${isClass ? 'I' : ''}${type}${isArray ? '[]' : ''}`,
|
||||||
|
isClass,
|
||||||
|
type,
|
||||||
|
hasQuestionToken: !required
|
||||||
|
});
|
||||||
} else {
|
} 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) {
|
const responsesCodes = Object.keys(responses);
|
||||||
process.exit(0);
|
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;
|
||||||
}
|
}
|
||||||
|
[PROCESS_AS.EMPTY]?: boolean;
|
||||||
const propType = schemaParamParser(schema, this.openapi);
|
|
||||||
const [pType, , isClass, isImport] = propType;
|
|
||||||
|
|
||||||
if (isImport) {
|
|
||||||
importEntities.push({ type: pType, isClass });
|
|
||||||
}
|
|
||||||
hasResponseBodyType = propType;
|
|
||||||
}
|
}
|
||||||
}
|
const responseSchema: ResponseSchema = { code: Number(code) };
|
||||||
let returnType = '';
|
|
||||||
if (hasResponseBodyType) {
|
if (!ref.content) {
|
||||||
const [pType, isArray, isClass] = hasResponseBodyType as any;
|
responseSchema[PROCESS_AS.EMPTY] = true;
|
||||||
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
return responseSchema;
|
||||||
returnType = data;
|
}
|
||||||
} else {
|
if (ref.content?.['application/json']) {
|
||||||
returnType = 'Promise<number';
|
const { schema } = ref.content['application/json'];
|
||||||
}
|
responseSchema[PROCESS_AS.JSON] = schemaParamParser(schema, this.openapi);
|
||||||
const shouldValidate = bodyParam.filter(b => b.isClass);
|
}
|
||||||
if (shouldValidate.length > 0) {
|
if (ref.content?.['text/palin']) {
|
||||||
returnType += ' | string[]';
|
const {
|
||||||
}
|
"x-error-class": xErrorClass,
|
||||||
// append Error to default type return;
|
"x-error-code": xErrorCode,
|
||||||
returnType += ' | Error>';
|
} = 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({
|
const fetcher = apiClass.addMethod({
|
||||||
isAsync: true,
|
isAsync: true,
|
||||||
@@ -211,23 +287,19 @@ class ApiGenerator {
|
|||||||
fetcher.addParameters(params);
|
fetcher.addParameters(params);
|
||||||
|
|
||||||
fetcher.setBodyText((w) => {
|
fetcher.setBodyText((w) => {
|
||||||
// Add data to URLSearchParams
|
if (contentType === 'application/json') {
|
||||||
if (contentType === 'text/plain') {
|
const shouldValidate = bodyParam.filter(b => b.isClass);
|
||||||
bodyParam.forEach((b) => {
|
|
||||||
w.writeLine(`const params = String(${b.name});`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (shouldValidate.length > 0) {
|
if (shouldValidate.length > 0) {
|
||||||
w.writeLine(`const haveError: string[] = [];`);
|
w.writeLine(`const haveError: string[] = [];`);
|
||||||
shouldValidate.forEach((b) => {
|
shouldValidate.forEach((b) => {
|
||||||
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
|
w.writeLine(`haveError.push(...${b.name}.validate());`);
|
||||||
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
|
|
||||||
});
|
});
|
||||||
w.writeLine(`if (haveError.length > 0) {`);
|
w.writeLine(`if (haveError.length > 0) {`);
|
||||||
w.writeLine(` return Promise.resolve(haveError);`)
|
w.writeLine(` return Promise.resolve(new ${BAD_REQUES_HELPER}(haveError));`)
|
||||||
w.writeLine(`}`);
|
w.writeLine(`}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch return of fetch in case on queryParams
|
// Switch return of fetch in case on queryParams
|
||||||
if (queryParams.length > 0) {
|
if (queryParams.length > 0) {
|
||||||
w.writeLine('const queryParams = {');
|
w.writeLine('const queryParams = {');
|
||||||
@@ -243,37 +315,36 @@ class ApiGenerator {
|
|||||||
w.writeLine(` method: '${method.toUpperCase()}',`);
|
w.writeLine(` method: '${method.toUpperCase()}',`);
|
||||||
|
|
||||||
// add Fetch options
|
// add Fetch options
|
||||||
if (contentType && contentType !== 'multipart/form-data') {
|
|
||||||
w.writeLine(' headers: {');
|
|
||||||
w.writeLine(` 'Content-Type': '${contentType}',`);
|
|
||||||
w.writeLine(' },');
|
|
||||||
}
|
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
switch (contentType) {
|
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}.serialize()` : b.name).join(', ')}),`);
|
||||||
case 'text/plain':
|
}
|
||||||
w.writeLine(' body: params,');
|
|
||||||
break;
|
w.writeLine('}).then(async (res) => {');
|
||||||
default:
|
responsesSchema.forEach((responseSchema) => {
|
||||||
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
|
const { code } = responseSchema;
|
||||||
break;
|
w.writeLine(` if (res.status === ${code}) {`);
|
||||||
|
if (responseSchema[PROCESS_AS.EMPTY]) {
|
||||||
|
w.writeLine(' return res.status;');
|
||||||
}
|
}
|
||||||
}
|
if (responseSchema[PROCESS_AS.TEXT]?.onlyText) {
|
||||||
|
w.writeLine(' return res.text();')
|
||||||
// Handle response
|
}
|
||||||
if (hasResponseBodyType) {
|
if (responseSchema[PROCESS_AS.JSON] && responseSchema[PROCESS_AS.TEXT]) {
|
||||||
w.writeLine('}).then(async (res) => {');
|
const { type } = responseSchema[PROCESS_AS.JSON]!;
|
||||||
w.writeLine(' if (res.status === 200) {');
|
const { schema, xErrorCode } = responseSchema[PROCESS_AS.TEXT]!;
|
||||||
w.writeLine(' return res.json();');
|
const { type: errType } = schema!;
|
||||||
} else {
|
w.writeLine(' try {');
|
||||||
w.writeLine('}).then(async (res) => {');
|
w.writeLine(` return new ${type}(await res.json());`);
|
||||||
w.writeLine(' if (res.status === 200) {');
|
w.writeLine(' } catch {');
|
||||||
w.writeLine(' return res.status;');
|
w.writeLine(` return new ${errType}({ msg: await res.text() code: ${xErrorCode}} as any);`);
|
||||||
}
|
w.writeLine(' }');
|
||||||
|
}
|
||||||
// Handle Error
|
if (responseSchema[PROCESS_AS.JSON]) {
|
||||||
w.writeLine(' } else {');
|
const { type } = responseSchema[PROCESS_AS.JSON]!;
|
||||||
w.writeLine(' return new Error(String(res.status));');
|
w.writeLine(` return new ${type}(await res.json());`);
|
||||||
w.writeLine(' }');
|
}
|
||||||
|
w.writeLine(` }`);
|
||||||
|
})
|
||||||
w.writeLine('})');
|
w.writeLine('})');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -288,17 +359,16 @@ class ApiGenerator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||||
const { type: pType, isClass } = ie;
|
const { type: type, isClass } = ie;
|
||||||
if (isClass) {
|
if (isClass) {
|
||||||
apiFile.addImportDeclaration({
|
apiFile.addImportDeclaration({
|
||||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
|
||||||
defaultImport: pType,
|
defaultImport: type,
|
||||||
namedImports: [`I${pType}`],
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
apiFile.addImportDeclaration({
|
apiFile.addImportDeclaration({
|
||||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
|
||||||
namedImports: [pType],
|
namedImports: [type],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import * as path from 'path';
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as morph from 'ts-morph';
|
import * as morph from 'ts-morph';
|
||||||
|
|
||||||
import { ENT_DIR } from '../../consts';
|
import { ENT_DIR, BAD_REQUES_HELPER } from '../../consts';
|
||||||
import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils';
|
import { TYPES, toCamel, schemaParamParser, capitalize, OpenApi, Schema } from './utils';
|
||||||
|
|
||||||
const { Project, QuoteKind } = morph;
|
const { Project, QuoteKind } = morph;
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@ if (!fs.existsSync(EntDir)) {
|
|||||||
class EntitiesGenerator {
|
class EntitiesGenerator {
|
||||||
project = new Project({
|
project = new Project({
|
||||||
tsConfigFilePath: './tsconfig.json',
|
tsConfigFilePath: './tsconfig.json',
|
||||||
addFilesFromTsConfig: false,
|
|
||||||
manipulationSettings: {
|
manipulationSettings: {
|
||||||
quoteKind: QuoteKind.Single,
|
quoteKind: QuoteKind.Single,
|
||||||
usePrefixAndSuffixTextForRename: false,
|
usePrefixAndSuffixTextForRename: false,
|
||||||
@@ -25,491 +24,480 @@ class EntitiesGenerator {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
openapi: Record<string, any>;
|
openapi: OpenApi;
|
||||||
|
|
||||||
schemas: Record<string, any>;
|
schemas: Record<string, Schema>;
|
||||||
|
|
||||||
schemaNames: string[];
|
schemaNames: string[];
|
||||||
|
|
||||||
entities: morph.SourceFile[] = [];
|
entities: morph.SourceFile[] = [];
|
||||||
|
|
||||||
constructor(openapi: Record<string, any>) {
|
constructor(openapi: OpenApi) {
|
||||||
this.openapi = openapi;
|
this.openapi = openapi;
|
||||||
this.schemas = openapi.components.schemas;
|
this.schemas = openapi.components.schemas;
|
||||||
this.schemaNames = Object.keys(this.schemas);
|
this.schemaNames = Object.keys(this.schemas);
|
||||||
this.generateEntities();
|
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 = () => {
|
generateEntities = () => {
|
||||||
this.schemaNames.forEach(this.generateEntity);
|
this.schemaNames.forEach(this.generateEntity);
|
||||||
};
|
};
|
||||||
|
|
||||||
generateEntity = (sName: string) => {
|
generateEntity = (schemaName: string) => {
|
||||||
const { properties, type, oneOf } = this.schemas[sName];
|
const { properties, type, oneOf, enum: en } = this.schemas[schemaName];
|
||||||
const notAClass = !properties && TYPES[type as keyof typeof TYPES];
|
const notAClass = !properties && TYPES[type as keyof typeof TYPES];
|
||||||
|
|
||||||
if (oneOf) {
|
if (oneOf) {
|
||||||
this.generateOneOf(sName);
|
this.generateOneOf(schemaName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (en) {
|
||||||
|
this.generateEnum(schemaName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (notAClass) {
|
if (notAClass) {
|
||||||
this.generateEnum(sName);
|
this.generatePrimitive(schemaName)
|
||||||
} else {
|
} else {
|
||||||
this.generateClass(sName);
|
this.generateClass(schemaName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
generateEnum = (sName: string) => {
|
generatePrimitive = (schemaName: string) => {
|
||||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
|
||||||
entityFile.addStatements([
|
entityFile.addStatements([
|
||||||
'// This file was autogenerated. Please do not change.',
|
'// 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({
|
entityFile.addEnum({
|
||||||
name: sName,
|
name: schemaName,
|
||||||
members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })),
|
members: enumMembers!.map((e: string) => ({ name: e.toUpperCase(), value: e })),
|
||||||
isExported: true,
|
isExported: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.entities.push(entityFile);
|
this.entities.push(entityFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
generateOneOf = (sName: string) => {
|
generateOneOf = (schemaName: string) => {
|
||||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
|
||||||
entityFile.addStatements([
|
entityFile.addStatements([
|
||||||
'// This file was autogenerated. Please do not change.',
|
'// This file was autogenerated. Please do not change.',
|
||||||
'// All changes will be overwrited on commit.',
|
|
||||||
'',
|
'',
|
||||||
]);
|
]);
|
||||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||||
const entities = this.schemas[sName].oneOf.map((elem: any) => {
|
const entities = this.schemas[schemaName].oneOf.map((elem: any) => {
|
||||||
const [
|
const {
|
||||||
pType, isArray, isClass, isImport,
|
type: type, isArray, isClass, isImport,
|
||||||
] = schemaParamParser(elem, this.openapi);
|
} = schemaParamParser(elem, this.openapi);
|
||||||
importEntities.push({ type: pType, isClass });
|
importEntities.push({ type: type, isClass });
|
||||||
return { type: pType, isArray };
|
return { type: type, isArray };
|
||||||
});
|
});
|
||||||
entityFile.addTypeAlias({
|
entityFile.addTypeAlias({
|
||||||
name: sName,
|
name: schemaName,
|
||||||
isExported: true,
|
isExported: true,
|
||||||
type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
|
type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
|
||||||
})
|
})
|
||||||
|
|
||||||
// add import
|
// add import
|
||||||
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||||
const { type: pType, isClass } = ie;
|
const { type: type, isClass } = ie;
|
||||||
if (isClass) {
|
if (isClass) {
|
||||||
entityFile.addImportDeclaration({
|
entityFile.addImportDeclaration({
|
||||||
moduleSpecifier: `./${pType}`,
|
moduleSpecifier: `./${type}`,
|
||||||
namedImports: [`I${pType}`],
|
namedImports: [`I${type}`],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
entityFile.addImportDeclaration({
|
entityFile.addImportDeclaration({
|
||||||
moduleSpecifier: `./${pType}`,
|
moduleSpecifier: `./${type}`,
|
||||||
namedImports: [pType],
|
namedImports: [type],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.entities.push(entityFile);
|
this.entities.push(entityFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateClass = (sName: string) => {
|
generateClass = (schemaName: string) => {
|
||||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
const entityFile = this.project.createSourceFile(`${EntDir}/${schemaName}.ts`);
|
||||||
entityFile.addStatements([
|
entityFile.addStatements([
|
||||||
'// This file was autogenerated. Please do not change.',
|
'// 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) {
|
if ($ref) {
|
||||||
const temp = $ref.split('/');
|
const refLink = $ref.split('/').pop()!;
|
||||||
const importSchemaName = `${temp[temp.length - 1]}`;
|
|
||||||
entityFile.addImportDeclaration({
|
entityFile.addImportDeclaration({
|
||||||
defaultImport: importSchemaName,
|
defaultImport: refLink,
|
||||||
moduleSpecifier: `./${importSchemaName}`,
|
moduleSpecifier: `./${refLink}`,
|
||||||
namedImports: [`I${importSchemaName}`],
|
namedImports: [`I${refLink}`],
|
||||||
});
|
});
|
||||||
|
|
||||||
entityFile.addTypeAlias({
|
entityFile.addTypeAlias({
|
||||||
name: `I${sName}`,
|
name: `I${schemaName}`,
|
||||||
type: `I${importSchemaName}`,
|
type: `I${refLink}`,
|
||||||
isExported: true,
|
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);
|
this.entities.push(entityFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
|
||||||
const entityInterface = entityFile.addInterface({
|
const entityInterface = entityFile.addInterface({
|
||||||
name: `I${sName}`,
|
name: `I${schemaName}`,
|
||||||
isExported: true,
|
isExported: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedSProps = Object.keys(sProps || {}).sort();
|
const sortedProperties = Object.keys(properties || {}).sort();
|
||||||
const additionalPropsOnly = additionalProperties && sortedSProps.length === 0;
|
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
|
// add server response interface to entityFile
|
||||||
sortedSProps.forEach((sPropName) => {
|
sortedProperties.forEach((propertyName) => {
|
||||||
const [
|
const {
|
||||||
pType, isArray, isClass, isImport, isAdditional
|
type, isArray, isClass, isImport
|
||||||
] = schemaParamParser(sProps[sPropName], this.openapi);
|
} = sortedPropertiesTypes[propertyName];
|
||||||
|
|
||||||
if (isImport) {
|
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({
|
entityInterface.addProperty({
|
||||||
name: sPropName,
|
name: propertyName,
|
||||||
type: propertyType,
|
type: `${isClass ? 'I' : ''}${type}${isArray ? '[]' : ''}`,
|
||||||
hasQuestionToken: !(
|
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
|
// add import
|
||||||
const imports: { type: string, isClass: boolean }[] = [];
|
|
||||||
const types: string[] = [];
|
const types: string[] = [];
|
||||||
importEntities.forEach((i) => {
|
importEntities = importEntities.filter((i) => {
|
||||||
const { type } = i;
|
const { type } = i;
|
||||||
if (!types.includes(type)) {
|
if (!types.includes(type)) {
|
||||||
imports.push(i);
|
|
||||||
types.push(type);
|
types.push(type);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||||
const { type: pType, isClass } = ie;
|
const { type: type, isClass } = ie;
|
||||||
if (isClass) {
|
if (isClass) {
|
||||||
entityFile.addImportDeclaration({
|
entityFile.addImportDeclaration({
|
||||||
defaultImport: pType,
|
defaultImport: type,
|
||||||
moduleSpecifier: `./${pType}`,
|
moduleSpecifier: `./${type}`,
|
||||||
namedImports: [`I${pType}`],
|
namedImports: [`I${type}`],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
entityFile.addImportDeclaration({
|
entityFile.addImportDeclaration({
|
||||||
moduleSpecifier: `./${pType}`,
|
moduleSpecifier: `./${type}`,
|
||||||
namedImports: [pType],
|
namedImports: [type],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const entityClass = entityFile.addClass({
|
const entityClass = entityFile.addClass({
|
||||||
name: sName,
|
name: schemaName,
|
||||||
isDefaultExport: true,
|
isDefaultExport: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// addProperties to class;
|
// addProperties to class;
|
||||||
sortedSProps.forEach((sPropName) => {
|
sortedProperties.forEach((propertyName) => {
|
||||||
const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
const { type, isArray, isClass, isEnum, isRequired, computedType } = sortedPropertiesTypes[propertyName];
|
||||||
|
|
||||||
const isRequred = (required && required.includes(sPropName))
|
|
||||||
|| sProps[sPropName].required;
|
|
||||||
|
|
||||||
const propertyType = isAdditional
|
|
||||||
? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }`
|
|
||||||
: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`;
|
|
||||||
|
|
||||||
entityClass.addProperty({
|
entityClass.addProperty({
|
||||||
name: `_${sPropName}`,
|
name: `_${propertyName}`,
|
||||||
isReadonly: true,
|
isReadonly: true,
|
||||||
type: propertyType,
|
type: computedType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getter = entityClass.addGetAccessor({
|
const getter = entityClass.addGetAccessor({
|
||||||
name: toCamel(sPropName),
|
name: toCamel(propertyName),
|
||||||
returnType: propertyType,
|
returnType: computedType,
|
||||||
statements: [`return this._${sPropName};`],
|
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) {
|
if (description || example) {
|
||||||
getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
|
getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minItems) {
|
if (minItems) {
|
||||||
entityClass.addGetAccessor({
|
entityClass.addProperty({
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
name: `${toCamel(sPropName)}MinItems`,
|
isReadonly: true,
|
||||||
statements: [`return ${minItems};`],
|
name: `${capitalize(toCamel(propertyName))}MinItems`,
|
||||||
|
initializer: `${minItems}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (maxItems) {
|
if (maxItems) {
|
||||||
entityClass.addGetAccessor({
|
entityClass.addProperty({
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
name: `${toCamel(sPropName)}MaxItems`,
|
isReadonly: true,
|
||||||
statements: [`return ${maxItems};`],
|
name: `${capitalize(toCamel(propertyName))}MaxItems`,
|
||||||
|
initializer: `${maxItems}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (typeof minLength === 'number') {
|
if (typeof minLength === 'number') {
|
||||||
entityClass.addGetAccessor({
|
entityClass.addProperty({
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
name: `${toCamel(sPropName)}MinLength`,
|
isReadonly: true,
|
||||||
statements: [`return ${minLength};`],
|
name: `${capitalize(toCamel(propertyName))}MinLength`,
|
||||||
|
initializer: `${minLength}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (maxLength) {
|
if (maxLength) {
|
||||||
entityClass.addGetAccessor({
|
entityClass.addProperty({
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
name: `${toCamel(sPropName)}MaxLength`,
|
isReadonly: true,
|
||||||
statements: [`return ${maxLength};`],
|
name: `${capitalize(toCamel(propertyName))}MaxLength`,
|
||||||
|
initializer: `${maxLength}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (typeof minimum === 'number') {
|
if (typeof minimum === 'number') {
|
||||||
entityClass.addGetAccessor({
|
entityClass.addProperty({
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
name: `${toCamel(sPropName)}MinValue`,
|
isReadonly: true,
|
||||||
statements: [`return ${minimum};`],
|
name: `${capitalize(toCamel(propertyName))}MinValue`,
|
||||||
|
initializer: `${minimum}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (maximum) {
|
if (maximum) {
|
||||||
entityClass.addGetAccessor({
|
entityClass.addProperty({
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
name: `${toCamel(sPropName)}MaxValue`,
|
isReadonly: true,
|
||||||
statements: [`return ${maximum};`],
|
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;
|
// add constructor;
|
||||||
const ctor = entityClass.addConstructor({
|
const ctor = entityClass.addConstructor({
|
||||||
parameters: [{
|
parameters: [{
|
||||||
name: 'props',
|
name: 'props',
|
||||||
type: `I${sName}`,
|
type: `I${schemaName}`,
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
ctor.setBodyText((w) => {
|
ctor.setBodyText((w) => {
|
||||||
if (additionalProperties) {
|
sortedProperties.forEach((propertyName) => {
|
||||||
const [
|
const { type, isArray, isClass, isRequired } = sortedPropertiesTypes[propertyName];
|
||||||
pType, isArray, isClass, isImport, isAdditional
|
|
||||||
] = schemaParamParser(additionalProperties, this.openapi);
|
const indent = !isRequired ? ' ' : '';
|
||||||
w.writeLine(`this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`} = Object.entries(props).reduce<Record<string, ${pType}>>((prev, [key, value]) => {`);
|
if (!isRequired) {
|
||||||
if (isClass) {
|
if ((type === 'boolean' || type === 'number' || type ==='string') && !isClass && !isArray) {
|
||||||
w.writeLine(` prev[key] = new ${pType}(value!);`);
|
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 {
|
} else {
|
||||||
w.writeLine(' prev[key] = value!;')
|
if (type === 'string' && !isArray) {
|
||||||
}
|
w.writeLine(`${indent}this._${propertyName} = props.${propertyName}.trim();`);
|
||||||
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}') {`);
|
|
||||||
} else {
|
} else {
|
||||||
w.writeLine(`if (props.${sPropName}) {`);
|
w.writeLine(`${indent}this._${propertyName} = props.${propertyName};`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isAdditional) {
|
if (!isRequired) {
|
||||||
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) {
|
|
||||||
w.writeLine('}');
|
w.writeLine('}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// add serialize method;
|
// add serialize method;
|
||||||
const serialize = entityClass.addMethod({
|
const serialize = entityClass.addMethod({
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
name: 'serialize',
|
name: 'serialize',
|
||||||
returnType: `I${sName}`,
|
returnType: `I${schemaName}`,
|
||||||
});
|
});
|
||||||
serialize.setBodyText((w) => {
|
serialize.setBodyText((w) => {
|
||||||
if (additionalProperties) {
|
w.writeLine(`const data: I${schemaName} = {`);
|
||||||
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} = {`);
|
|
||||||
const unReqFields: string[] = [];
|
const unReqFields: string[] = [];
|
||||||
sortedSProps.forEach((sPropName) => {
|
|
||||||
const req = (required && required.includes(sPropName))
|
sortedProperties.forEach((propertyName) => {
|
||||||
|| sProps[sPropName].required;
|
const {isArray, isClass, isRequired } = sortedPropertiesTypes[propertyName];
|
||||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
if (!isRequired) {
|
||||||
if (!req) {
|
unReqFields.push(propertyName);
|
||||||
unReqFields.push(sPropName);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAdditional) {
|
if (isArray && isClass) {
|
||||||
if (isArray && isClass) {
|
w.writeLine(` ${propertyName}: this._${propertyName}.map((p) => p.serialize()),`);
|
||||||
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`);
|
} else if (isClass) {
|
||||||
} else if (isClass) {
|
w.writeLine(` ${propertyName}: this._${propertyName}.serialize(),`);
|
||||||
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] })),`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (isArray && isClass) {
|
w.writeLine(` ${propertyName}: this._${propertyName},`);
|
||||||
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('};');
|
w.writeLine('};');
|
||||||
unReqFields.forEach((sPropName) => {
|
|
||||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
unReqFields.forEach((propertyName) => {
|
||||||
w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`);
|
const { isArray, isClass } = sortedPropertiesTypes[propertyName];
|
||||||
if (isAdditional) {
|
w.writeLine(`if (typeof this._${propertyName} !== 'undefined') {`);
|
||||||
if (isArray && isClass) {
|
if (isArray && isClass) {
|
||||||
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`);
|
w.writeLine(` data.${propertyName} = this._${propertyName}.map((p) => p.serialize());`);
|
||||||
} else if (isClass) {
|
} else if (isClass) {
|
||||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`);
|
w.writeLine(` data.${propertyName} = this._${propertyName}.serialize();`);
|
||||||
} else {
|
|
||||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (isArray && isClass) {
|
w.writeLine(` data.${propertyName} = this._${propertyName};`);
|
||||||
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(`}`);
|
w.writeLine(`}`);
|
||||||
});
|
});
|
||||||
w.writeLine('return data;');
|
w.writeLine('return data;');
|
||||||
@@ -522,74 +510,55 @@ class EntitiesGenerator {
|
|||||||
returnType: `string[]`,
|
returnType: `string[]`,
|
||||||
})
|
})
|
||||||
validate.setBodyText((w) => {
|
validate.setBodyText((w) => {
|
||||||
if (additionalPropsOnly) {
|
w.writeLine('const validateRequired = {');
|
||||||
w.writeLine('return []')
|
Object.keys(properties || {}).forEach((propertyName) => {
|
||||||
return;
|
const { isArray, isClass, type, isRequired } = sortedPropertiesTypes[propertyName];
|
||||||
}
|
const { maxLength, minLength, maximum, minimum } = properties![propertyName];
|
||||||
w.writeLine('const validate = {');
|
|
||||||
Object.keys(sProps || {}).forEach((sPropName) => {
|
|
||||||
const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
|
||||||
|
|
||||||
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
const nonRequiredCall = isRequired ? `this._${propertyName}` : `!this._${propertyName} ? true : this._${propertyName}`;
|
||||||
|
|
||||||
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
|
|
||||||
const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`;
|
|
||||||
|
|
||||||
if (isArray && isClass) {
|
if (isArray && isClass) {
|
||||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`);
|
w.writeLine(` ${propertyName}: ${nonRequiredCall}.reduce<boolean>((result, p) => result && p.validate().length === 0, true),`);
|
||||||
} else if (isClass && !isAdditional) {
|
} else if (isClass) {
|
||||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`);
|
w.writeLine(` ${propertyName}: ${nonRequiredCall}.validate().length === 0,`);
|
||||||
} else {
|
} else {
|
||||||
if (pType === 'string') {
|
if (type === 'string') {
|
||||||
if (isArray) {
|
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 {
|
} else {
|
||||||
if (typeof minLength === 'number' && maxLength) {
|
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) {
|
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) {
|
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 {
|
} else {
|
||||||
if (typeof minimum === 'number' && maximum) {
|
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) {
|
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') {
|
} else if (type === 'boolean') {
|
||||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`);
|
w.writeLine(` ${propertyName}: ${isRequired ? `typeof this._${propertyName} === 'boolean'` : `!this._${propertyName} ? true : typeof this._${propertyName} === 'boolean'`},`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
w.writeLine('};');
|
w.writeLine('};');
|
||||||
w.writeLine('const isError: string[] = [];')
|
w.writeLine('const errorInFields: string[] = [];')
|
||||||
w.writeLine('Object.keys(validate).forEach((key) => {');
|
w.writeLine('Object.keys(validateRequired).forEach((key) => {');
|
||||||
w.writeLine(' if (!(validate as any)[key]) {');
|
w.writeLine(' if (!(validateRequired as any)[key]) {');
|
||||||
w.writeLine(' isError.push(key);');
|
w.writeLine(' errorInFields.push(key);');
|
||||||
w.writeLine(' }');
|
w.writeLine(' }');
|
||||||
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);
|
this.entities.push(entityFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,65 +19,154 @@ const TYPES = {
|
|||||||
boolean: 'boolean',
|
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 schemaProp: valueof shema.properties[key]
|
||||||
* @param openApi: openapi object
|
* @param openApi: openapi object
|
||||||
* @returns [propType - basicType or import one, isArray, isClass, isImport]
|
* @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 type = '';
|
||||||
let isImport = false;
|
let isImport = false;
|
||||||
let isClass = false;
|
let isClass = false;
|
||||||
let isArray = false;
|
let isArray = false;
|
||||||
let isAdditional = false;
|
let isAdditional = false;
|
||||||
|
let isEnum = false;
|
||||||
|
|
||||||
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
|
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) {
|
if (schemaProp.additionalProperties) {
|
||||||
isAdditional = true;
|
isAdditional = true;
|
||||||
}
|
}
|
||||||
|
const cl = openApi.components.schemas[type];
|
||||||
type = `${temp[temp.length - 1]}`;
|
|
||||||
|
if (cl.allOf) {
|
||||||
const cl = openApi ? openApi.components.schemas[type] : {};
|
const ref = cl.allOf.find((e) => !!e.$ref);
|
||||||
|
const link = schemaParamParser(ref, openApi);
|
||||||
|
return {...link, type};
|
||||||
|
}
|
||||||
|
|
||||||
if (cl.$ref) {
|
if (cl.$ref) {
|
||||||
const link = schemaParamParser(cl, openApi);
|
const link = schemaParamParser(cl, openApi);
|
||||||
link.shift();
|
return {...link, type};
|
||||||
return [type, ...link] as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cl.type === 'string' && cl.enum) {
|
if (cl.type === 'string' && cl.enum) {
|
||||||
isImport = true;
|
isImport = true;
|
||||||
|
isEnum = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cl.type === 'object' && !cl.oneOf) {
|
if (cl.type === 'object' && !cl.oneOf) {
|
||||||
isClass = true;
|
isClass = true;
|
||||||
isImport = true;
|
isImport = true;
|
||||||
} else if (cl.type === 'array') {
|
} else if (cl.type === 'array') {
|
||||||
const temp: any = schemaParamParser(cl.items, openApi);
|
const temp = schemaParamParser(cl.items!, openApi);
|
||||||
type = `${temp[0]}`;
|
type = temp.type;
|
||||||
isArray = true;
|
isArray = true;
|
||||||
isClass = isClass || temp[2];
|
isClass = isClass || temp.isClass;
|
||||||
isImport = isImport || temp[3];
|
isImport = isImport || temp.isImport;
|
||||||
|
isEnum = isEnum || temp.isEnum;
|
||||||
}
|
}
|
||||||
} else if (schemaProp.type === 'array') {
|
} else if (schemaProp.type === 'array') {
|
||||||
const temp: any = schemaParamParser(schemaProp.items, openApi);
|
const temp = schemaParamParser(schemaProp.items!, openApi);
|
||||||
type = `${temp[0]}`;
|
type = temp.type
|
||||||
isArray = true;
|
isArray = true;
|
||||||
isClass = isClass || temp[2];
|
isClass = isClass || temp.isClass;
|
||||||
isImport = isImport || temp[3];
|
isImport = isImport || temp.isImport;
|
||||||
|
isEnum = isEnum || temp.isEnum;
|
||||||
} else {
|
} else {
|
||||||
type = (TYPES as Record<any, string>)[schemaProp.type];
|
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 };
|
export { TYPES, toCamel, capitalize, uncapitalize, schemaParamParser };
|
||||||
|
|||||||
@@ -370,17 +370,16 @@
|
|||||||
remark "^13.0.0"
|
remark "^13.0.0"
|
||||||
unist-util-find-all-after "^3.0.2"
|
unist-util-find-all-after "^3.0.2"
|
||||||
|
|
||||||
"@ts-morph/common@~0.6.0":
|
"@ts-morph/common@~0.8.0":
|
||||||
version "0.6.0"
|
version "0.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.6.0.tgz#cbd4ee57c5ef971511b9c5778e0bb8eb27de4783"
|
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.8.0.tgz#ae7b292df8258040465c50b378108ec8f09a9516"
|
||||||
integrity sha512-pI35nZz5bs3tL3btSVX2cWkAE8rc80F+Fn4TwSC6bQvn7fgn9IyLXVcAfpG6X6NBY5wN9TkSWXn/QYUkBvR/Fw==
|
integrity sha512-YbjWiMXLMKxWxcMqP47nwZVWVBwoF5B65dtRz0lya2LetjldAPxTxRbRo1n4Iszr2tSvzXeaa+f1AbULmfc5uA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@dsherret/to-absolute-glob" "^2.0.2"
|
"@dsherret/to-absolute-glob" "^2.0.2"
|
||||||
fast-glob "^3.2.4"
|
fast-glob "^3.2.5"
|
||||||
fs-extra "^9.0.1"
|
|
||||||
is-negated-glob "^1.0.0"
|
is-negated-glob "^1.0.0"
|
||||||
multimatch "^4.0.0"
|
mkdirp "^1.0.4"
|
||||||
typescript "~4.0.2"
|
multimatch "^5.0.0"
|
||||||
|
|
||||||
"@types/anymatch@*":
|
"@types/anymatch@*":
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
@@ -1172,11 +1171,6 @@ async@^2.6.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.14"
|
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:
|
atob@^2.1.2:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||||
@@ -1593,7 +1587,7 @@ coa@^2.0.2:
|
|||||||
chalk "^2.4.1"
|
chalk "^2.4.1"
|
||||||
q "^1.1.2"
|
q "^1.1.2"
|
||||||
|
|
||||||
code-block-writer@^10.1.0:
|
code-block-writer@^10.1.1:
|
||||||
version "10.1.1"
|
version "10.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f"
|
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f"
|
||||||
integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==
|
integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==
|
||||||
@@ -2910,9 +2904,9 @@ fastest-levenshtein@^1.0.12:
|
|||||||
integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
|
integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
|
||||||
|
|
||||||
fastq@^1.6.0:
|
fastq@^1.6.0:
|
||||||
version "1.10.0"
|
version "1.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb"
|
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
|
||||||
integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==
|
integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
reusify "^1.0.4"
|
||||||
|
|
||||||
@@ -3084,16 +3078,6 @@ fs-extra@^8.1.0:
|
|||||||
jsonfile "^4.0.0"
|
jsonfile "^4.0.0"
|
||||||
universalify "^0.1.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:
|
fs-minipass@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||||
@@ -3267,11 +3251,16 @@ gonzales-pe@^4.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
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"
|
version "4.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
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:
|
handle-thing@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||||
@@ -4145,15 +4134,6 @@ jsonfile@^4.0.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs "^4.1.6"
|
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":
|
"jsx-ast-utils@^2.4.1 || ^3.0.0":
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82"
|
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"
|
dns-packet "^1.3.1"
|
||||||
thunky "^1.0.2"
|
thunky "^1.0.2"
|
||||||
|
|
||||||
multimatch@^4.0.0:
|
multimatch@^5.0.0:
|
||||||
version "4.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3"
|
resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6"
|
||||||
integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==
|
integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/minimatch" "^3.0.3"
|
"@types/minimatch" "^3.0.3"
|
||||||
array-differ "^3.0.0"
|
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"
|
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
|
||||||
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
|
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:
|
quick-lru@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
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"
|
glob "^7.1.3"
|
||||||
|
|
||||||
run-parallel@^1.1.9:
|
run-parallel@^1.1.9:
|
||||||
version "1.1.10"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef"
|
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||||
integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==
|
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:
|
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
@@ -7942,14 +7929,14 @@ ts-loader@^8.0.6:
|
|||||||
micromatch "^4.0.0"
|
micromatch "^4.0.0"
|
||||||
semver "^7.3.4"
|
semver "^7.3.4"
|
||||||
|
|
||||||
ts-morph@^8.1.2:
|
ts-morph@^10.0.1:
|
||||||
version "8.2.0"
|
version "10.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-8.2.0.tgz#41d83cd501cbd897eb029ac489d6d5b927555c57"
|
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-10.0.1.tgz#5a620cc4ef85e3e6d161989e690f44d0a0f723b0"
|
||||||
integrity sha512-NHHWu+7I2/AOZiTni5w3f+xCfIxrkzPCcQbTGa81Yk3pr23a4h9xLLEE6tIGuYIubWjkjr9QVC3ITqgmA5touQ==
|
integrity sha512-T1zufImtp5goTLTFhzi7XuKR1y/f+Jwz1gSULzB045LFjXuoqVlR87sfkpyWow8u2JwgusCJrhOnwmHCFNutTQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@dsherret/to-absolute-glob" "^2.0.2"
|
"@dsherret/to-absolute-glob" "^2.0.2"
|
||||||
"@ts-morph/common" "~0.6.0"
|
"@ts-morph/common" "~0.8.0"
|
||||||
code-block-writer "^10.1.0"
|
code-block-writer "^10.1.1"
|
||||||
|
|
||||||
ts-node@^9.0.0:
|
ts-node@^9.0.0:
|
||||||
version "9.1.1"
|
version "9.1.1"
|
||||||
@@ -8032,11 +8019,6 @@ typescript@^4.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
|
||||||
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
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:
|
unc-path-regex@^0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
|
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"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
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:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLimitReadCloser(t *testing.T) {
|
func TestLimitReadCloser(t *testing.T) {
|
||||||
@@ -78,11 +79,11 @@ func TestLimitedReadCloser_Read(t *testing.T) {
|
|||||||
buf := make([]byte, tc.limit+1)
|
buf := make([]byte, tc.limit+1)
|
||||||
|
|
||||||
lreader, err := LimitReadCloser(readCloser, tc.limit)
|
lreader, err := LimitReadCloser(readCloser, tc.limit)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
n, err := lreader.Read(buf)
|
n, err := lreader.Read(buf)
|
||||||
assert.Equal(t, n, tc.want)
|
require.Equal(t, tc.err, err)
|
||||||
assert.Equal(t, tc.err, err)
|
assert.Equal(t, tc.want, n)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
defaultDiscoverTime = time.Second * 3
|
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
|
var webHandlersRegistered = false
|
||||||
@@ -37,12 +42,24 @@ type Lease struct {
|
|||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface for *Lease.
|
// MarshalJSON implements the json.Marshaler interface for *Lease.
|
||||||
func (l *Lease) MarshalJSON() ([]byte, error) {
|
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
|
type lease Lease
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
HWAddr string `json:"mac"`
|
HWAddr string `json:"mac"`
|
||||||
|
Expiry string `json:"expires,omitempty"`
|
||||||
*lease
|
*lease
|
||||||
}{
|
}{
|
||||||
HWAddr: l.HWAddr.String(),
|
HWAddr: l.HWAddr.String(),
|
||||||
|
Expiry: expiryStr,
|
||||||
lease: (*lease)(l),
|
lease: (*lease)(l),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -117,14 +134,14 @@ type ServerInterface interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create - create object
|
// Create - create object
|
||||||
func Create(config ServerConfig) *Server {
|
func Create(conf ServerConfig) *Server {
|
||||||
s := &Server{}
|
s := &Server{}
|
||||||
|
|
||||||
s.conf.Enabled = config.Enabled
|
s.conf.Enabled = conf.Enabled
|
||||||
s.conf.InterfaceName = config.InterfaceName
|
s.conf.InterfaceName = conf.InterfaceName
|
||||||
s.conf.HTTPRegister = config.HTTPRegister
|
s.conf.HTTPRegister = conf.HTTPRegister
|
||||||
s.conf.ConfigModified = config.ConfigModified
|
s.conf.ConfigModified = conf.ConfigModified
|
||||||
s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename)
|
s.conf.DBFilePath = filepath.Join(conf.WorkDir, dbFilename)
|
||||||
|
|
||||||
if !webHandlersRegistered && s.conf.HTTPRegister != nil {
|
if !webHandlersRegistered && s.conf.HTTPRegister != nil {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@@ -145,7 +162,7 @@ func Create(config ServerConfig) *Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var err4, err6 error
|
var err4, err6 error
|
||||||
v4conf := config.Conf4
|
v4conf := conf.Conf4
|
||||||
v4conf.Enabled = s.conf.Enabled
|
v4conf.Enabled = s.conf.Enabled
|
||||||
if len(v4conf.RangeStart) == 0 {
|
if len(v4conf.RangeStart) == 0 {
|
||||||
v4conf.Enabled = false
|
v4conf.Enabled = false
|
||||||
@@ -154,7 +171,7 @@ func Create(config ServerConfig) *Server {
|
|||||||
v4conf.notify = s.onNotify
|
v4conf.notify = s.onNotify
|
||||||
s.srv4, err4 = v4Create(v4conf)
|
s.srv4, err4 = v4Create(v4conf)
|
||||||
|
|
||||||
v6conf := config.Conf6
|
v6conf := conf.Conf6
|
||||||
v6conf.Enabled = s.conf.Enabled
|
v6conf.Enabled = s.conf.Enabled
|
||||||
if len(v6conf.RangeStart) == 0 {
|
if len(v6conf.RangeStart) == 0 {
|
||||||
v6conf.Enabled = false
|
v6conf.Enabled = false
|
||||||
@@ -172,6 +189,9 @@ func Create(config ServerConfig) *Server {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.conf.Conf4 = conf.Conf4
|
||||||
|
s.conf.Conf6 = conf.Conf6
|
||||||
|
|
||||||
if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled {
|
if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled {
|
||||||
log.Error("Can't enable DHCP server because neither DHCPv4 nor DHCPv6 servers are configured")
|
log.Error("Can't enable DHCP server because neither DHCPv4 nor DHCPv6 servers are configured")
|
||||||
return nil
|
return nil
|
||||||
@@ -245,14 +265,10 @@ const (
|
|||||||
LeasesAll = LeasesDynamic | LeasesStatic
|
LeasesAll = LeasesDynamic | LeasesStatic
|
||||||
)
|
)
|
||||||
|
|
||||||
// Leases returns the list of current DHCP leases (thread-safe)
|
// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for
|
||||||
func (s *Server) Leases(flags int) []Lease {
|
// concurrent use.
|
||||||
result := s.srv4.GetLeases(flags)
|
func (s *Server) Leases(flags int) (leases []Lease) {
|
||||||
|
return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...)
|
||||||
v6leases := s.srv6.GetLeases(flags)
|
|
||||||
result = append(result, v6leases...)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
|
// 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 {
|
if err != nil {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ip":
|
case "ip":
|
||||||
ip := net.ParseIP(sval)
|
ip := net.ParseIP(sval)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return 0, 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:
|
default:
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -11,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@@ -20,116 +20,171 @@ func TestMain(m *testing.M) {
|
|||||||
func testNotify(flags uint32) {
|
func testNotify(flags uint32) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leases database store/load
|
// Leases database store/load.
|
||||||
func TestDB(t *testing.T) {
|
func TestDB(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
s := Server{}
|
s := Server{
|
||||||
s.conf.DBFilePath = dbFilename
|
conf: ServerConfig{
|
||||||
|
DBFilePath: dbFilename,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
conf := V4ServerConf{
|
s.srv4, err = v4Create(V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.IP{192, 168, 10, 100},
|
RangeStart: net.IP{192, 168, 10, 100},
|
||||||
RangeEnd: net.IP{192, 168, 10, 200},
|
RangeEnd: net.IP{192, 168, 10, 200},
|
||||||
GatewayIP: net.IP{192, 168, 10, 1},
|
GatewayIP: net.IP{192, 168, 10, 1},
|
||||||
SubnetMask: net.IP{255, 255, 255, 0},
|
SubnetMask: net.IP{255, 255, 255, 0},
|
||||||
notify: testNotify,
|
notify: testNotify,
|
||||||
}
|
})
|
||||||
s.srv4, err = v4Create(conf)
|
require.Nil(t, err)
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
s.srv6, err = v6Create(V6ServerConf{})
|
s.srv6, err = v6Create(V6ServerConf{})
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
l := Lease{}
|
leases := []Lease{{
|
||||||
l.IP = net.IP{192, 168, 10, 100}
|
IP: net.IP{192, 168, 10, 100},
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
exp1 := time.Now().Add(time.Hour)
|
Expiry: time.Now().Add(time.Hour),
|
||||||
l.Expiry = exp1
|
}, {
|
||||||
|
IP: net.IP{192, 168, 10, 101},
|
||||||
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB},
|
||||||
|
}}
|
||||||
|
|
||||||
srv4, ok := s.srv4.(*v4Server)
|
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()
|
s.dbStore()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
assert.Nil(t, os.Remove(dbFilename))
|
||||||
|
})
|
||||||
s.srv4.ResetLeases(nil)
|
s.srv4.ResetLeases(nil)
|
||||||
|
|
||||||
s.dbLoad()
|
s.dbLoad()
|
||||||
|
|
||||||
ll := s.srv4.GetLeases(LeasesAll)
|
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.Equal(t, leases[1].HWAddr, ll[0].HWAddr)
|
||||||
assert.True(t, net.IP{192, 168, 10, 101}.Equal(ll[0].IP))
|
assert.Equal(t, leases[1].IP, ll[0].IP)
|
||||||
assert.EqualValues(t, leaseExpireStatic, ll[0].Expiry.Unix())
|
assert.EqualValues(t, leaseExpireStatic, ll[0].Expiry.Unix())
|
||||||
|
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ll[1].HWAddr.String())
|
assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr)
|
||||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(ll[1].IP))
|
assert.Equal(t, leases[0].IP, ll[1].IP)
|
||||||
assert.Equal(t, exp1.Unix(), ll[1].Expiry.Unix())
|
assert.Equal(t, leases[0].Expiry.Unix(), ll[1].Expiry.Unix())
|
||||||
|
|
||||||
_ = os.Remove("leases.db")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidSubnetMask(t *testing.T) {
|
func TestIsValidSubnetMask(t *testing.T) {
|
||||||
assert.True(t, isValidSubnetMask([]byte{255, 255, 255, 0}))
|
testCases := []struct {
|
||||||
assert.True(t, isValidSubnetMask([]byte{255, 255, 254, 0}))
|
mask net.IP
|
||||||
assert.True(t, isValidSubnetMask([]byte{255, 255, 252, 0}))
|
want bool
|
||||||
assert.False(t, isValidSubnetMask([]byte{255, 255, 253, 0}))
|
}{{
|
||||||
assert.False(t, isValidSubnetMask([]byte{255, 255, 255, 1}))
|
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) {
|
func TestNormalizeLeases(t *testing.T) {
|
||||||
dynLeases := []*Lease{}
|
dynLeases := []*Lease{{
|
||||||
staticLeases := []*Lease{}
|
HWAddr: net.HardwareAddr{1, 2, 3, 4},
|
||||||
|
}, {
|
||||||
|
HWAddr: net.HardwareAddr{1, 2, 3, 5},
|
||||||
|
}}
|
||||||
|
|
||||||
lease := &Lease{}
|
staticLeases := []*Lease{{
|
||||||
lease.HWAddr = []byte{1, 2, 3, 4}
|
HWAddr: net.HardwareAddr{1, 2, 3, 4},
|
||||||
dynLeases = append(dynLeases, lease)
|
IP: net.IP{0, 2, 3, 4},
|
||||||
lease = new(Lease)
|
}, {
|
||||||
lease.HWAddr = []byte{1, 2, 3, 5}
|
HWAddr: net.HardwareAddr{2, 2, 3, 4},
|
||||||
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)
|
|
||||||
|
|
||||||
leases := normalizeLeases(staticLeases, dynLeases)
|
leases := normalizeLeases(staticLeases, dynLeases)
|
||||||
|
require.Len(t, leases, 3)
|
||||||
|
|
||||||
assert.Len(t, leases, 3)
|
assert.Equal(t, leases[0].HWAddr, dynLeases[0].HWAddr)
|
||||||
assert.True(t, bytes.Equal(leases[0].HWAddr, []byte{1, 2, 3, 4}))
|
assert.Equal(t, leases[0].IP, staticLeases[0].IP)
|
||||||
assert.True(t, bytes.Equal(leases[0].IP, []byte{0, 2, 3, 4}))
|
assert.Equal(t, leases[1].HWAddr, staticLeases[1].HWAddr)
|
||||||
assert.True(t, bytes.Equal(leases[1].HWAddr, []byte{2, 2, 3, 4}))
|
assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr)
|
||||||
assert.True(t, bytes.Equal(leases[2].HWAddr, []byte{1, 2, 3, 5}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOptions(t *testing.T) {
|
func TestOptions(t *testing.T) {
|
||||||
code, val := parseOptionString(" 12 hex abcdef ")
|
testCases := []struct {
|
||||||
assert.EqualValues(t, 12, code)
|
name string
|
||||||
assert.True(t, bytes.Equal([]byte{0xab, 0xcd, 0xef}, val))
|
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 ")
|
for _, tc := range testCases {
|
||||||
assert.EqualValues(t, 0, code)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
code, val := parseOptionString(tc.optStr)
|
||||||
code, val = parseOptionString("123 ip 1.2.3.4")
|
require.Equal(t, tc.wantCode, code)
|
||||||
assert.EqualValues(t, 123, code)
|
if tc.wantVal != nil {
|
||||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(net.IP(val)))
|
assert.Equal(t, tc.wantVal, 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dhcpd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
@@ -11,7 +12,6 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/sysutil"
|
"github.com/AdguardTeam/AdGuardHome/internal/sysutil"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/util"
|
"github.com/AdguardTeam/AdGuardHome/internal/util"
|
||||||
"github.com/AdguardTeam/golibs/jsonutil"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +29,11 @@ type v4ServerConfJSON struct {
|
|||||||
LeaseDuration uint32 `json:"lease_duration"`
|
LeaseDuration uint32 `json:"lease_duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func v4JSONToServerConf(j v4ServerConfJSON) V4ServerConf {
|
func v4JSONToServerConf(j *v4ServerConfJSON) V4ServerConf {
|
||||||
|
if j == nil {
|
||||||
|
return V4ServerConf{}
|
||||||
|
}
|
||||||
|
|
||||||
return V4ServerConf{
|
return V4ServerConf{
|
||||||
GatewayIP: j.GatewayIP,
|
GatewayIP: j.GatewayIP,
|
||||||
SubnetMask: j.SubnetMask,
|
SubnetMask: j.SubnetMask,
|
||||||
@@ -44,7 +48,11 @@ type v6ServerConfJSON struct {
|
|||||||
LeaseDuration uint32 `json:"lease_duration"`
|
LeaseDuration uint32 `json:"lease_duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func v6JSONToServerConf(j v6ServerConfJSON) V6ServerConf {
|
func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
|
||||||
|
if j == nil {
|
||||||
|
return V6ServerConf{}
|
||||||
|
}
|
||||||
|
|
||||||
return V6ServerConf{
|
return V6ServerConf{
|
||||||
RangeStart: j.RangeStart,
|
RangeStart: j.RangeStart,
|
||||||
LeaseDuration: j.LeaseDuration,
|
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) {
|
func (s *Server) enableDHCP(ifaceName string) (code int, err error) {
|
||||||
var hasStaticIP bool
|
var hasStaticIP bool
|
||||||
hasStaticIP, err = sysutil.IfaceHasStaticIP(ifaceName)
|
hasStaticIP, err = sysutil.IfaceHasStaticIP(ifaceName)
|
||||||
if err != nil {
|
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 {
|
if !hasStaticIP {
|
||||||
err = sysutil.IfaceSetStaticIP(ifaceName)
|
err = sysutil.IfaceSetStaticIP(ifaceName)
|
||||||
if err != nil {
|
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
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
type dhcpServerConfigJSON struct {
|
||||||
newconfig := dhcpServerConfigJSON{}
|
V4 *v4ServerConfJSON `json:"v4"`
|
||||||
newconfig.Enabled = s.conf.Enabled
|
V6 *v6ServerConfJSON `json:"v6"`
|
||||||
newconfig.InterfaceName = s.conf.InterfaceName
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
@@ -129,62 +165,72 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
v4Enabled := false
|
v4Enabled := false
|
||||||
v6Enabled := false
|
v6Enabled := false
|
||||||
|
|
||||||
if js.Exists("v4") {
|
if conf.V4 != nil {
|
||||||
v4conf := v4JSONToServerConf(newconfig.V4)
|
v4Conf := v4JSONToServerConf(conf.V4)
|
||||||
v4conf.Enabled = newconfig.Enabled
|
v4Conf.Enabled = conf.Enabled == nbTrue
|
||||||
if len(v4conf.RangeStart) == 0 {
|
if len(v4Conf.RangeStart) == 0 {
|
||||||
v4conf.Enabled = false
|
v4Conf.Enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
v4Enabled = v4conf.Enabled
|
v4Enabled = v4Conf.Enabled
|
||||||
v4conf.InterfaceName = newconfig.InterfaceName
|
v4Conf.InterfaceName = conf.InterfaceName
|
||||||
|
|
||||||
c4 := V4ServerConf{}
|
c4 := V4ServerConf{}
|
||||||
s.srv4.WriteDiskConfig4(&c4)
|
s.srv4.WriteDiskConfig4(&c4)
|
||||||
v4conf.notify = c4.notify
|
v4Conf.notify = c4.notify
|
||||||
v4conf.ICMPTimeout = c4.ICMPTimeout
|
v4Conf.ICMPTimeout = c4.ICMPTimeout
|
||||||
|
|
||||||
s4, err = v4Create(v4conf)
|
s4, err = v4Create(v4Conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(r, w, http.StatusBadRequest, "invalid dhcpv4 configuration: %s", err)
|
httpError(r, w, http.StatusBadRequest,
|
||||||
|
"invalid dhcpv4 configuration: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if js.Exists("v6") {
|
if conf.V6 != nil {
|
||||||
v6conf := v6JSONToServerConf(newconfig.V6)
|
v6Conf := v6JSONToServerConf(conf.V6)
|
||||||
v6conf.Enabled = newconfig.Enabled
|
v6Conf.Enabled = conf.Enabled == nbTrue
|
||||||
if len(v6conf.RangeStart) == 0 {
|
if len(v6Conf.RangeStart) == 0 {
|
||||||
v6conf.Enabled = false
|
v6Conf.Enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
v6Enabled = v6conf.Enabled
|
// Don't overwrite the RA/SLAAC settings from the config file.
|
||||||
v6conf.InterfaceName = newconfig.InterfaceName
|
//
|
||||||
v6conf.notify = s.onNotify
|
// 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 {
|
if err != nil {
|
||||||
httpError(r, w, http.StatusBadRequest, "invalid dhcpv6 configuration: %s", err)
|
httpError(r, w, http.StatusBadRequest,
|
||||||
|
"invalid dhcpv6 configuration: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newconfig.Enabled && !v4Enabled && !v6Enabled {
|
if conf.Enabled == nbTrue && !v4Enabled && !v6Enabled {
|
||||||
httpError(r, w, http.StatusBadRequest, "dhcpv4 or dhcpv6 configuration must be complete")
|
httpError(r, w, http.StatusBadRequest,
|
||||||
|
"dhcpv4 or dhcpv6 configuration must be complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Stop()
|
s.Stop()
|
||||||
|
|
||||||
if js.Exists("enabled") {
|
if conf.Enabled != nbNull {
|
||||||
s.conf.Enabled = newconfig.Enabled
|
s.conf.Enabled = conf.Enabled == nbTrue
|
||||||
}
|
}
|
||||||
|
|
||||||
if js.Exists("interface_name") {
|
if conf.InterfaceName != "" {
|
||||||
s.conf.InterfaceName = newconfig.InterfaceName
|
s.conf.InterfaceName = conf.InterfaceName
|
||||||
}
|
}
|
||||||
|
|
||||||
if s4 != nil {
|
if s4 != nil {
|
||||||
@@ -200,7 +246,7 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if s.conf.Enabled {
|
if s.conf.Enabled {
|
||||||
var code int
|
var code int
|
||||||
code, err = s.enableDHCP(newconfig.InterfaceName)
|
code, err = s.enableDHCP(conf.InterfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(r, w, code, "enabling dhcp: %s", err)
|
httpError(r, w, code, "enabling dhcp: %s", err)
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_notImplemented(t *testing.T) {
|
func TestServer_notImplemented(t *testing.T) {
|
||||||
@@ -14,7 +15,7 @@ func TestServer_notImplemented(t *testing.T) {
|
|||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r, err := http.NewRequest(http.MethodGet, "/unsupported", nil)
|
r, err := http.NewRequest(http.MethodGet, "/unsupported", nil)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
h(w, r)
|
h(w, r)
|
||||||
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
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 {
|
type raCtx struct {
|
||||||
raAllowSlaac bool // send RA packets without MO flags
|
raAllowSLAAC bool // send RA packets without MO flags
|
||||||
raSlaacOnly bool // send RA packets with MO flags
|
raSLAACOnly bool // send RA packets with MO flags
|
||||||
ipAddr net.IP // source IP address (link-local-unicast)
|
ipAddr net.IP // source IP address (link-local-unicast)
|
||||||
dnsIPAddr net.IP // IP address for DNS Server option
|
dnsIPAddr net.IP // IP address for DNS Server option
|
||||||
prefixIPAddr net.IP // IP address for Prefix option
|
prefixIPAddr net.IP // IP address for Prefix option
|
||||||
@@ -159,7 +159,7 @@ func createICMPv6RAPacket(params icmpv6RA) []byte {
|
|||||||
func (ra *raCtx) Init() error {
|
func (ra *raCtx) Init() error {
|
||||||
ra.stop.Store(0)
|
ra.stop.Store(0)
|
||||||
ra.conn = nil
|
ra.conn = nil
|
||||||
if !(ra.raAllowSlaac || ra.raSlaacOnly) {
|
if !(ra.raAllowSLAAC || ra.raSLAACOnly) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +167,8 @@ func (ra *raCtx) Init() error {
|
|||||||
ra.ipAddr, ra.dnsIPAddr)
|
ra.ipAddr, ra.dnsIPAddr)
|
||||||
|
|
||||||
params := icmpv6RA{
|
params := icmpv6RA{
|
||||||
managedAddressConfiguration: !ra.raSlaacOnly,
|
managedAddressConfiguration: !ra.raSLAACOnly,
|
||||||
otherConfiguration: !ra.raSlaacOnly,
|
otherConfiguration: !ra.raSLAACOnly,
|
||||||
mtu: uint32(ra.iface.MTU),
|
mtu: uint32(ra.iface.MTU),
|
||||||
prefixLen: 64,
|
prefixLen: 64,
|
||||||
recursiveDNSServer: ra.dnsIPAddr,
|
recursiveDNSServer: ra.dnsIPAddr,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestRA(t *testing.T) {
|
func TestRA(t *testing.T) {
|
||||||
ra := icmpv6RA{
|
data := createICMPv6RAPacket(icmpv6RA{
|
||||||
managedAddressConfiguration: false,
|
managedAddressConfiguration: false,
|
||||||
otherConfiguration: true,
|
otherConfiguration: true,
|
||||||
mtu: 1500,
|
mtu: 1500,
|
||||||
@@ -17,8 +16,7 @@ func TestRA(t *testing.T) {
|
|||||||
prefixLen: 64,
|
prefixLen: 64,
|
||||||
recursiveDNSServer: net.ParseIP("fe80::800:27ff:fe00:0"),
|
recursiveDNSServer: net.ParseIP("fe80::800:27ff:fe00:0"),
|
||||||
sourceLinkLayerAddress: []byte{0x0a, 0x00, 0x27, 0x00, 0x00, 0x00},
|
sourceLinkLayerAddress: []byte{0x0a, 0x00, 0x27, 0x00, 0x00, 0x00},
|
||||||
}
|
})
|
||||||
data := createICMPv6RAPacket(ra)
|
|
||||||
dataCorrect := []byte{
|
dataCorrect := []byte{
|
||||||
0x86, 0x00, 0x00, 0x00, 0x40, 0x40, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
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,
|
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,
|
0x19, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x10, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x08, 0x00, 0x27, 0xff, 0xfe, 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 first IP address for dynamic leases
|
||||||
// The last allowed IP address ends with 0xff byte
|
// 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
|
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds
|
||||||
|
|
||||||
RaSlaacOnly bool `yaml:"ra_slaac_only" json:"-"` // send ICMPv6.RA packets without 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
|
RAAllowSLAAC bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags
|
||||||
|
|
||||||
ipStart net.IP // starting IP address for dynamic leases
|
ipStart net.IP // starting IP address for dynamic leases
|
||||||
leaseTime time.Duration // the time during which a dynamic lease is considered valid
|
leaseTime time.Duration // the time during which a dynamic lease is considered valid
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ type v4Server struct {
|
|||||||
srv *server4.Server
|
srv *server4.Server
|
||||||
leasesLock sync.Mutex
|
leasesLock sync.Mutex
|
||||||
leases []*Lease
|
leases []*Lease
|
||||||
ipAddrs [256]byte
|
// TODO(e.burkov): This field type should be a normal bitmap.
|
||||||
|
ipAddrs [256]byte
|
||||||
|
|
||||||
conf V4ServerConf
|
conf V4ServerConf
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
|
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeIface struct {
|
type fakeIface struct {
|
||||||
@@ -79,8 +80,8 @@ func TestIfaceIPAddrs(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
got, gotErr := ifaceIPAddrs(tc.iface, tc.ipv)
|
got, gotErr := ifaceIPAddrs(tc.iface, tc.ipv)
|
||||||
|
require.True(t, errors.Is(gotErr, tc.wantErr))
|
||||||
assert.Equal(t, tc.want, got)
|
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,
|
want: nil,
|
||||||
wantErr: errTest,
|
wantErr: errTest,
|
||||||
}, {
|
}, {
|
||||||
name: "ipv4_wait",
|
name: "ipv4_wait",
|
||||||
iface: &waitingFakeIface{
|
iface: &waitingFakeIface{addrs: []net.Addr{addr4}, err: nil, n: 1},
|
||||||
addrs: []net.Addr{addr4},
|
|
||||||
err: nil,
|
|
||||||
n: 1,
|
|
||||||
},
|
|
||||||
ipv: ipVersion4,
|
ipv: ipVersion4,
|
||||||
want: []net.IP{ip4, ip4},
|
want: []net.IP{ip4, ip4},
|
||||||
wantErr: nil,
|
wantErr: nil,
|
||||||
@@ -168,12 +165,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) {
|
|||||||
want: nil,
|
want: nil,
|
||||||
wantErr: errTest,
|
wantErr: errTest,
|
||||||
}, {
|
}, {
|
||||||
name: "ipv6_wait",
|
name: "ipv6_wait",
|
||||||
iface: &waitingFakeIface{
|
iface: &waitingFakeIface{addrs: []net.Addr{addr6}, err: nil, n: 1},
|
||||||
addrs: []net.Addr{addr6},
|
|
||||||
err: nil,
|
|
||||||
n: 1,
|
|
||||||
},
|
|
||||||
ipv: ipVersion6,
|
ipv: ipVersion6,
|
||||||
want: []net.IP{ip6, ip6},
|
want: []net.IP{ip6, ip6},
|
||||||
wantErr: nil,
|
wantErr: nil,
|
||||||
@@ -182,8 +175,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
got, gotErr := ifaceDNSIPAddrs(tc.iface, tc.ipv, 2, 0)
|
got, gotErr := ifaceDNSIPAddrs(tc.iface, tc.ipv, 2, 0)
|
||||||
|
require.True(t, errors.Is(gotErr, tc.wantErr))
|
||||||
assert.Equal(t, tc.want, got)
|
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/insomniacslk/dhcp/dhcpv4"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notify4(flags uint32) {
|
func notify4(flags uint32) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV4StaticLeaseAddRemove(t *testing.T) {
|
func TestV4_AddRemove_static(t *testing.T) {
|
||||||
conf := V4ServerConf{
|
s, err := v4Create(V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.IP{192, 168, 10, 100},
|
RangeStart: net.IP{192, 168, 10, 100},
|
||||||
RangeEnd: net.IP{192, 168, 10, 200},
|
RangeEnd: net.IP{192, 168, 10, 200},
|
||||||
GatewayIP: net.IP{192, 168, 10, 1},
|
GatewayIP: net.IP{192, 168, 10, 1},
|
||||||
SubnetMask: net.IP{255, 255, 255, 0},
|
SubnetMask: net.IP{255, 255, 255, 0},
|
||||||
notify: notify4,
|
notify: notify4,
|
||||||
}
|
})
|
||||||
s, err := v4Create(conf)
|
require.Nil(t, err)
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
ls := s.GetLeases(LeasesStatic)
|
ls := s.GetLeases(LeasesStatic)
|
||||||
assert.Empty(t, ls)
|
assert.Empty(t, ls)
|
||||||
|
|
||||||
// add static lease
|
// Add static lease.
|
||||||
l := Lease{}
|
l := Lease{
|
||||||
l.IP = net.IP{192, 168, 10, 150}
|
IP: net.IP{192, 168, 10, 150},
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
assert.Nil(t, s.AddStaticLease(l))
|
}
|
||||||
|
require.Nil(t, s.AddStaticLease(l))
|
||||||
// try to add the same static lease - fail
|
|
||||||
assert.NotNil(t, s.AddStaticLease(l))
|
assert.NotNil(t, s.AddStaticLease(l))
|
||||||
|
|
||||||
// check
|
|
||||||
ls = s.GetLeases(LeasesStatic)
|
ls = s.GetLeases(LeasesStatic)
|
||||||
assert.Len(t, ls, 1)
|
require.Len(t, ls, 1)
|
||||||
assert.True(t, net.IP{192, 168, 10, 150}.Equal(ls[0].IP))
|
assert.True(t, l.IP.Equal(ls[0].IP))
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
|
||||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
||||||
|
|
||||||
// try to remove static lease - fail
|
// Try to remove static lease.
|
||||||
l.IP = net.IP{192, 168, 10, 110}
|
assert.NotNil(t, s.RemoveStaticLease(Lease{
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
IP: net.IP{192, 168, 10, 110},
|
||||||
assert.NotNil(t, s.RemoveStaticLease(l))
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
|
}))
|
||||||
|
|
||||||
// remove static lease
|
// Remove static lease.
|
||||||
l.IP = net.IP{192, 168, 10, 150}
|
require.Nil(t, s.RemoveStaticLease(l))
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
|
||||||
assert.Nil(t, s.RemoveStaticLease(l))
|
|
||||||
|
|
||||||
// check
|
|
||||||
ls = s.GetLeases(LeasesStatic)
|
ls = s.GetLeases(LeasesStatic)
|
||||||
assert.Empty(t, ls)
|
assert.Empty(t, ls)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV4StaticLeaseAddReplaceDynamic(t *testing.T) {
|
func TestV4_AddReplace(t *testing.T) {
|
||||||
conf := V4ServerConf{
|
sIface, err := v4Create(V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.IP{192, 168, 10, 100},
|
RangeStart: net.IP{192, 168, 10, 100},
|
||||||
RangeEnd: net.IP{192, 168, 10, 200},
|
RangeEnd: net.IP{192, 168, 10, 200},
|
||||||
GatewayIP: net.IP{192, 168, 10, 1},
|
GatewayIP: net.IP{192, 168, 10, 1},
|
||||||
SubnetMask: net.IP{255, 255, 255, 0},
|
SubnetMask: net.IP{255, 255, 255, 0},
|
||||||
notify: notify4,
|
notify: notify4,
|
||||||
}
|
})
|
||||||
sIface, err := v4Create(conf)
|
require.Nil(t, err)
|
||||||
|
|
||||||
s, ok := sIface.(*v4Server)
|
s, ok := sIface.(*v4Server)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
// add dynamic lease
|
dynLeases := []Lease{{
|
||||||
ld := Lease{}
|
IP: net.IP{192, 168, 10, 150},
|
||||||
ld.IP = net.IP{192, 168, 10, 150}
|
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa")
|
}, {
|
||||||
s.addLease(&ld)
|
IP: net.IP{192, 168, 10, 151},
|
||||||
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
|
}}
|
||||||
|
|
||||||
// add dynamic lease
|
for i := range dynLeases {
|
||||||
{
|
s.addLease(&dynLeases[i])
|
||||||
ld := Lease{}
|
|
||||||
ld.IP = net.IP{192, 168, 10, 151}
|
|
||||||
ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
|
||||||
s.addLease(&ld)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add static lease with the same IP
|
stLeases := []Lease{{
|
||||||
l := Lease{}
|
IP: net.IP{192, 168, 10, 150},
|
||||||
l.IP = net.IP{192, 168, 10, 150}
|
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa")
|
}, {
|
||||||
assert.Nil(t, s.AddStaticLease(l))
|
IP: net.IP{192, 168, 10, 152},
|
||||||
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
|
}}
|
||||||
|
|
||||||
// add static lease with the same MAC
|
for _, l := range stLeases {
|
||||||
l = Lease{}
|
require.Nil(t, s.AddStaticLease(l))
|
||||||
l.IP = net.IP{192, 168, 10, 152}
|
}
|
||||||
l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
|
||||||
assert.Nil(t, s.AddStaticLease(l))
|
|
||||||
|
|
||||||
// check
|
|
||||||
ls := s.GetLeases(LeasesStatic)
|
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))
|
for i, l := range ls {
|
||||||
assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
assert.True(t, stLeases[i].IP.Equal(l.IP))
|
||||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
||||||
|
assert.EqualValues(t, leaseExpireStatic, l.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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV4StaticLeaseGet(t *testing.T) {
|
func TestV4StaticLease_Get(t *testing.T) {
|
||||||
conf := V4ServerConf{
|
var err error
|
||||||
|
sIface, err := v4Create(V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.IP{192, 168, 10, 100},
|
RangeStart: net.IP{192, 168, 10, 100},
|
||||||
RangeEnd: net.IP{192, 168, 10, 200},
|
RangeEnd: net.IP{192, 168, 10, 200},
|
||||||
GatewayIP: net.IP{192, 168, 10, 1},
|
GatewayIP: net.IP{192, 168, 10, 1},
|
||||||
SubnetMask: net.IP{255, 255, 255, 0},
|
SubnetMask: net.IP{255, 255, 255, 0},
|
||||||
notify: notify4,
|
notify: notify4,
|
||||||
}
|
})
|
||||||
sIface, err := v4Create(conf)
|
require.Nil(t, err)
|
||||||
|
|
||||||
s, ok := sIface.(*v4Server)
|
s, ok := sIface.(*v4Server)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Nil(t, err)
|
|
||||||
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
||||||
|
|
||||||
l := Lease{}
|
l := Lease{
|
||||||
l.IP = net.IP{192, 168, 10, 150}
|
IP: net.IP{192, 168, 10, 150},
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
assert.Nil(t, s.AddStaticLease(l))
|
}
|
||||||
|
require.Nil(t, s.AddStaticLease(l))
|
||||||
|
|
||||||
// "Discover"
|
var req, resp *dhcpv4.DHCPv4
|
||||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
req, _ := dhcpv4.NewDiscovery(mac)
|
|
||||||
resp, _ := dhcpv4.NewReplyFromRequest(req)
|
|
||||||
assert.Equal(t, 1, s.process(req, resp))
|
|
||||||
|
|
||||||
// check "Offer"
|
t.Run("discover", func(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
var err error
|
||||||
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())
|
|
||||||
|
|
||||||
// "Request"
|
req, err = dhcpv4.NewDiscovery(mac)
|
||||||
req, _ = dhcpv4.NewRequestFromOffer(resp)
|
require.Nil(t, err)
|
||||||
resp, _ = dhcpv4.NewReplyFromRequest(req)
|
|
||||||
assert.Equal(t, 1, s.process(req, resp))
|
|
||||||
|
|
||||||
// check "Ack"
|
resp, err = dhcpv4.NewReplyFromRequest(req)
|
||||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
require.Nil(t, err)
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
|
assert.Equal(t, 1, s.process(req, resp))
|
||||||
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]))
|
require.Nil(t, err)
|
||||||
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())))
|
t.Run("offer", func(t *testing.T) {
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
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()
|
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]))
|
assert.True(t, s.conf.GatewayIP.Equal(dnsAddrs[0]))
|
||||||
|
|
||||||
// check lease
|
t.Run("check_lease", func(t *testing.T) {
|
||||||
ls := s.GetLeases(LeasesStatic)
|
ls := s.GetLeases(LeasesStatic)
|
||||||
assert.Len(t, ls, 1)
|
require.Len(t, ls, 1)
|
||||||
assert.True(t, net.IP{192, 168, 10, 150}.Equal(ls[0].IP))
|
assert.True(t, l.IP.Equal(ls[0].IP))
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
assert.Equal(t, mac, ls[0].HWAddr)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV4DynamicLeaseGet(t *testing.T) {
|
func TestV4DynamicLease_Get(t *testing.T) {
|
||||||
conf := V4ServerConf{
|
var err error
|
||||||
|
sIface, err := v4Create(V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.IP{192, 168, 10, 100},
|
RangeStart: net.IP{192, 168, 10, 100},
|
||||||
RangeEnd: net.IP{192, 168, 10, 200},
|
RangeEnd: net.IP{192, 168, 10, 200},
|
||||||
@@ -184,58 +194,97 @@ func TestV4DynamicLeaseGet(t *testing.T) {
|
|||||||
"81 hex 303132",
|
"81 hex 303132",
|
||||||
"82 ip 1.2.3.4",
|
"82 ip 1.2.3.4",
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
sIface, err := v4Create(conf)
|
require.Nil(t, err)
|
||||||
|
|
||||||
s, ok := sIface.(*v4Server)
|
s, ok := sIface.(*v4Server)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Nil(t, err)
|
|
||||||
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
||||||
|
|
||||||
// "Discover"
|
var req, resp *dhcpv4.DHCPv4
|
||||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
req, _ := dhcpv4.NewDiscovery(mac)
|
|
||||||
resp, _ := dhcpv4.NewReplyFromRequest(req)
|
|
||||||
assert.Equal(t, 1, s.process(req, resp))
|
|
||||||
|
|
||||||
// check "Offer"
|
t.Run("discover", func(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
req, err = dhcpv4.NewDiscovery(mac)
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
|
require.Nil(t, err)
|
||||||
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)])))
|
|
||||||
|
|
||||||
// "Request"
|
resp, err = dhcpv4.NewReplyFromRequest(req)
|
||||||
req, _ = dhcpv4.NewRequestFromOffer(resp)
|
require.Nil(t, err)
|
||||||
resp, _ = dhcpv4.NewReplyFromRequest(req)
|
assert.Equal(t, 1, s.process(req, resp))
|
||||||
assert.Equal(t, 1, s.process(req, resp))
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
// check "Ack"
|
t.Run("offer", func(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
|
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(resp.YourIPAddr))
|
assert.True(t, s.conf.RangeStart.Equal(resp.YourIPAddr))
|
||||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.Router()[0]))
|
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
|
||||||
assert.True(t, net.IP{192, 168, 10, 1}.Equal(resp.ServerIdentifier()))
|
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
||||||
assert.True(t, net.IP{255, 255, 255, 0}.Equal(net.IP(resp.SubnetMask())))
|
assert.Equal(t, s.conf.subnetMask, resp.SubnetMask())
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
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()
|
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]))
|
assert.True(t, net.IP{192, 168, 10, 1}.Equal(dnsAddrs[0]))
|
||||||
|
|
||||||
// check lease
|
// check lease
|
||||||
ls := s.GetLeases(LeasesDynamic)
|
t.Run("check_lease", func(t *testing.T) {
|
||||||
assert.Len(t, ls, 1)
|
ls := s.GetLeases(LeasesDynamic)
|
||||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(ls[0].IP))
|
assert.Len(t, ls, 1)
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
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}
|
start := net.IP{192, 168, 10, 100}
|
||||||
stop := net.IP{192, 168, 10, 200}
|
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}))
|
testCases := []struct {
|
||||||
assert.False(t, ip4InRange(start, stop, net.IP{192, 168, 11, 201}))
|
ip net.IP
|
||||||
assert.True(t, ip4InRange(start, stop, net.IP{192, 168, 10, 100}))
|
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.raAllowSLAAC = s.conf.RAAllowSLAAC
|
||||||
s.ra.raSlaacOnly = s.conf.RaSlaacOnly
|
s.ra.raSLAACOnly = s.conf.RASLAACOnly
|
||||||
s.ra.dnsIPAddr = s.ra.ipAddr
|
s.ra.dnsIPAddr = s.ra.ipAddr
|
||||||
s.ra.prefixIPAddr = s.conf.ipStart
|
s.ra.prefixIPAddr = s.conf.ipStart
|
||||||
s.ra.ifaceName = s.conf.InterfaceName
|
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
|
// 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")
|
log.Debug("DHCPv6: not starting DHCPv6 server due to ra_slaac_only=true")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,220 +9,283 @@ import (
|
|||||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||||
"github.com/insomniacslk/dhcp/iana"
|
"github.com/insomniacslk/dhcp/iana"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notify6(flags uint32) {
|
func notify6(flags uint32) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV6StaticLeaseAddRemove(t *testing.T) {
|
func TestV6_AddRemove_static(t *testing.T) {
|
||||||
conf := V6ServerConf{
|
s, err := v6Create(V6ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.ParseIP("2001::1"),
|
RangeStart: net.ParseIP("2001::1"),
|
||||||
notify: notify6,
|
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)
|
require.Nil(t, s.AddStaticLease(l))
|
||||||
assert.Nil(t, err)
|
|
||||||
|
// Try to add the same static lease.
|
||||||
|
require.NotNil(t, s.AddStaticLease(l))
|
||||||
|
|
||||||
ls := s.GetLeases(LeasesStatic)
|
ls := s.GetLeases(LeasesStatic)
|
||||||
assert.Empty(t, ls)
|
require.Len(t, ls, 1)
|
||||||
|
assert.Equal(t, l.IP, ls[0].IP)
|
||||||
// add static lease
|
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
|
||||||
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())
|
|
||||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
||||||
|
|
||||||
// try to remove static lease - fail
|
// Try to remove non-existent static lease.
|
||||||
l.IP = net.ParseIP("2001::2")
|
require.NotNil(t, s.RemoveStaticLease(Lease{
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
IP: net.ParseIP("2001::2"),
|
||||||
assert.NotNil(t, s.RemoveStaticLease(l))
|
HWAddr: l.HWAddr,
|
||||||
|
}))
|
||||||
|
|
||||||
// remove static lease
|
// Remove static lease.
|
||||||
l.IP = net.ParseIP("2001::1")
|
require.Nil(t, s.RemoveStaticLease(l))
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
|
||||||
assert.Nil(t, s.RemoveStaticLease(l))
|
|
||||||
|
|
||||||
// check
|
assert.Empty(t, s.GetLeases(LeasesStatic))
|
||||||
ls = s.GetLeases(LeasesStatic)
|
|
||||||
assert.Empty(t, ls)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV6StaticLeaseAddReplaceDynamic(t *testing.T) {
|
func TestV6_AddReplace(t *testing.T) {
|
||||||
conf := V6ServerConf{
|
sIface, err := v6Create(V6ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.ParseIP("2001::1"),
|
RangeStart: net.ParseIP("2001::1"),
|
||||||
notify: notify6,
|
notify: notify6,
|
||||||
}
|
})
|
||||||
sIface, err := v6Create(conf)
|
require.Nil(t, err)
|
||||||
s, ok := sIface.(*v6Server)
|
s, ok := sIface.(*v6Server)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
// add dynamic lease
|
// Add dynamic leases.
|
||||||
ld := Lease{}
|
dynLeases := []*Lease{{
|
||||||
ld.IP = net.ParseIP("2001::1")
|
IP: net.ParseIP("2001::1"),
|
||||||
ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa")
|
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
s.addLease(&ld)
|
}, {
|
||||||
|
IP: net.ParseIP("2001::2"),
|
||||||
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
|
}}
|
||||||
|
|
||||||
// add dynamic lease
|
for _, l := range dynLeases {
|
||||||
{
|
s.addLease(l)
|
||||||
ld := Lease{}
|
|
||||||
ld.IP = net.ParseIP("2001::2")
|
|
||||||
ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
|
||||||
s.addLease(&ld)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add static lease with the same IP
|
stLeases := []Lease{{
|
||||||
l := Lease{}
|
IP: net.ParseIP("2001::1"),
|
||||||
l.IP = net.ParseIP("2001::1")
|
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa")
|
}, {
|
||||||
assert.Nil(t, s.AddStaticLease(l))
|
IP: net.ParseIP("2001::3"),
|
||||||
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
|
}}
|
||||||
|
|
||||||
// add static lease with the same MAC
|
for _, l := range stLeases {
|
||||||
l = Lease{}
|
require.Nil(t, s.AddStaticLease(l))
|
||||||
l.IP = net.ParseIP("2001::3")
|
}
|
||||||
l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
|
|
||||||
assert.Nil(t, s.AddStaticLease(l))
|
|
||||||
|
|
||||||
// check
|
|
||||||
ls := s.GetLeases(LeasesStatic)
|
ls := s.GetLeases(LeasesStatic)
|
||||||
assert.Len(t, ls, 2)
|
require.Len(t, ls, 2)
|
||||||
|
|
||||||
assert.Equal(t, "2001::1", ls[0].IP.String())
|
for i, l := range ls {
|
||||||
assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
assert.True(t, stLeases[i].IP.Equal(l.IP))
|
||||||
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix())
|
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
||||||
|
assert.EqualValues(t, leaseExpireStatic, l.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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV6GetLease(t *testing.T) {
|
func TestV6GetLease(t *testing.T) {
|
||||||
conf := V6ServerConf{
|
var err error
|
||||||
|
sIface, err := v6Create(V6ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.ParseIP("2001::1"),
|
RangeStart: net.ParseIP("2001::1"),
|
||||||
notify: notify6,
|
notify: notify6,
|
||||||
}
|
})
|
||||||
sIface, err := v6Create(conf)
|
require.Nil(t, err)
|
||||||
s, ok := sIface.(*v6Server)
|
s, ok := sIface.(*v6Server)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Nil(t, err)
|
|
||||||
s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")}
|
dnsAddr := net.ParseIP("2000::1")
|
||||||
|
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
|
||||||
s.sid = dhcpv6.Duid{
|
s.sid = dhcpv6.Duid{
|
||||||
Type: dhcpv6.DUID_LLT,
|
Type: dhcpv6.DUID_LLT,
|
||||||
HwType: iana.HWTypeEthernet,
|
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 := Lease{
|
||||||
l.IP = net.ParseIP("2001::1")
|
IP: net.ParseIP("2001::1"),
|
||||||
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
assert.Nil(t, s.AddStaticLease(l))
|
}
|
||||||
|
require.Nil(t, s.AddStaticLease(l))
|
||||||
|
|
||||||
// "Solicit"
|
var req, resp, msg *dhcpv6.Message
|
||||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
req, _ := dhcpv6.NewSolicit(mac)
|
t.Run("solicit", func(t *testing.T) {
|
||||||
msg, _ := req.GetInnerMessage()
|
req, err = dhcpv6.NewSolicit(mac)
|
||||||
resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg)
|
require.Nil(t, err)
|
||||||
assert.True(t, s.process(msg, req, resp))
|
|
||||||
|
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))
|
resp.AddOption(dhcpv6.OptServerID(s.sid))
|
||||||
|
|
||||||
// check "Advertise"
|
var oia *dhcpv6.OptIANA
|
||||||
assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
var oiaAddr *dhcpv6.OptIAAddress
|
||||||
oia := resp.Options.OneIANA()
|
t.Run("advertise", func(t *testing.T) {
|
||||||
oiaAddr := oia.Options.OneAddress()
|
require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
||||||
assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String())
|
oia = resp.Options.OneIANA()
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
oiaAddr = oia.Options.OneAddress()
|
||||||
|
|
||||||
// "Request"
|
assert.Equal(t, l.IP, oiaAddr.IPv6Addr)
|
||||||
req, _ = dhcpv6.NewRequestFromAdvertise(resp)
|
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
||||||
msg, _ = req.GetInnerMessage()
|
})
|
||||||
resp, _ = dhcpv6.NewReplyFromMessage(msg)
|
|
||||||
assert.True(t, s.process(msg, req, resp))
|
|
||||||
|
|
||||||
// check "Reply"
|
t.Run("request", func(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
|
req, err = dhcpv6.NewRequestFromAdvertise(resp)
|
||||||
oia = resp.Options.OneIANA()
|
require.Nil(t, err)
|
||||||
oiaAddr = oia.Options.OneAddress()
|
|
||||||
assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String())
|
msg, err = req.GetInnerMessage()
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
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()
|
dnsAddrs := resp.Options.DNS()
|
||||||
assert.Len(t, dnsAddrs, 1)
|
require.Len(t, dnsAddrs, 1)
|
||||||
assert.Equal(t, "2000::1", dnsAddrs[0].String())
|
assert.Equal(t, dnsAddr, dnsAddrs[0])
|
||||||
|
|
||||||
// check lease
|
t.Run("lease", func(t *testing.T) {
|
||||||
ls := s.GetLeases(LeasesStatic)
|
ls := s.GetLeases(LeasesStatic)
|
||||||
assert.Len(t, ls, 1)
|
require.Len(t, ls, 1)
|
||||||
assert.Equal(t, "2001::1", ls[0].IP.String())
|
assert.Equal(t, l.IP, ls[0].IP)
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
assert.Equal(t, l.HWAddr, ls[0].HWAddr)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV6GetDynamicLease(t *testing.T) {
|
func TestV6GetDynamicLease(t *testing.T) {
|
||||||
conf := V6ServerConf{
|
sIface, err := v6Create(V6ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.ParseIP("2001::2"),
|
RangeStart: net.ParseIP("2001::2"),
|
||||||
notify: notify6,
|
notify: notify6,
|
||||||
}
|
})
|
||||||
sIface, err := v6Create(conf)
|
require.Nil(t, err)
|
||||||
s, ok := sIface.(*v6Server)
|
s, ok := sIface.(*v6Server)
|
||||||
assert.True(t, ok)
|
require.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")
|
|
||||||
|
|
||||||
// "Solicit"
|
dnsAddr := net.ParseIP("2000::1")
|
||||||
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
|
||||||
req, _ := dhcpv6.NewSolicit(mac)
|
s.sid = dhcpv6.Duid{
|
||||||
msg, _ := req.GetInnerMessage()
|
Type: dhcpv6.DUID_LLT,
|
||||||
resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg)
|
HwType: iana.HWTypeEthernet,
|
||||||
assert.True(t, s.process(msg, req, resp))
|
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))
|
resp.AddOption(dhcpv6.OptServerID(s.sid))
|
||||||
|
|
||||||
// check "Advertise"
|
var oia *dhcpv6.OptIANA
|
||||||
assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
var oiaAddr *dhcpv6.OptIAAddress
|
||||||
oia := resp.Options.OneIANA()
|
t.Run("advertise", func(t *testing.T) {
|
||||||
oiaAddr := oia.Options.OneAddress()
|
require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
|
||||||
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
|
oia = resp.Options.OneIANA()
|
||||||
|
oiaAddr = oia.Options.OneAddress()
|
||||||
|
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
|
||||||
|
})
|
||||||
|
|
||||||
// "Request"
|
t.Run("request", func(t *testing.T) {
|
||||||
req, _ = dhcpv6.NewRequestFromAdvertise(resp)
|
req, err = dhcpv6.NewRequestFromAdvertise(resp)
|
||||||
msg, _ = req.GetInnerMessage()
|
require.Nil(t, err)
|
||||||
resp, _ = dhcpv6.NewReplyFromMessage(msg)
|
|
||||||
assert.True(t, s.process(msg, req, resp))
|
|
||||||
|
|
||||||
// check "Reply"
|
msg, err = req.GetInnerMessage()
|
||||||
assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
|
require.Nil(t, err)
|
||||||
oia = resp.Options.OneIANA()
|
|
||||||
oiaAddr = oia.Options.OneAddress()
|
resp, err = dhcpv6.NewReplyFromMessage(msg)
|
||||||
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
|
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()
|
dnsAddrs := resp.Options.DNS()
|
||||||
assert.Len(t, dnsAddrs, 1)
|
require.Len(t, dnsAddrs, 1)
|
||||||
assert.Equal(t, "2000::1", dnsAddrs[0].String())
|
assert.Equal(t, dnsAddr, dnsAddrs[0])
|
||||||
|
|
||||||
// check lease
|
t.Run("lease", func(t *testing.T) {
|
||||||
ls := s.GetLeases(LeasesDynamic)
|
ls := s.GetLeases(LeasesDynamic)
|
||||||
assert.Len(t, ls, 1)
|
require.Len(t, ls, 1)
|
||||||
assert.Equal(t, "2001::2", ls[0].IP.String())
|
assert.Equal(t, "2001::2", ls[0].IP.String())
|
||||||
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
|
assert.Equal(t, mac, ls[0].HWAddr)
|
||||||
|
})
|
||||||
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")))
|
func TestIP6InRange(t *testing.T) {
|
||||||
assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::3")))
|
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/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/lucas-clemente/quic-go"
|
"github.com/lucas-clemente/quic-go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testTLSConn is a tlsConn for tests.
|
// testTLSConn is a tlsConn for tests.
|
||||||
@@ -53,6 +54,7 @@ func TestProcessClientID(t *testing.T) {
|
|||||||
wantClientID string
|
wantClientID string
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
wantRes resultCode
|
wantRes resultCode
|
||||||
|
strictSNI bool
|
||||||
}{{
|
}{{
|
||||||
name: "udp",
|
name: "udp",
|
||||||
proto: proxy.ProtoUDP,
|
proto: proxy.ProtoUDP,
|
||||||
@@ -61,6 +63,7 @@ func TestProcessClientID(t *testing.T) {
|
|||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
wantRes: resultCodeSuccess,
|
wantRes: resultCodeSuccess,
|
||||||
|
strictSNI: false,
|
||||||
}, {
|
}, {
|
||||||
name: "tls_no_client_id",
|
name: "tls_no_client_id",
|
||||||
proto: proxy.ProtoTLS,
|
proto: proxy.ProtoTLS,
|
||||||
@@ -69,6 +72,26 @@ func TestProcessClientID(t *testing.T) {
|
|||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
wantRes: resultCodeSuccess,
|
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",
|
name: "tls_client_id",
|
||||||
proto: proxy.ProtoTLS,
|
proto: proxy.ProtoTLS,
|
||||||
@@ -77,30 +100,39 @@ func TestProcessClientID(t *testing.T) {
|
|||||||
wantClientID: "cli",
|
wantClientID: "cli",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
wantRes: resultCodeSuccess,
|
wantRes: resultCodeSuccess,
|
||||||
|
strictSNI: true,
|
||||||
}, {
|
}, {
|
||||||
name: "tls_client_id_hostname_error",
|
name: "tls_client_id_hostname_error",
|
||||||
proto: proxy.ProtoTLS,
|
proto: proxy.ProtoTLS,
|
||||||
hostSrvName: "example.com",
|
hostSrvName: "example.com",
|
||||||
cliSrvName: "cli.example.net",
|
cliSrvName: "cli.example.net",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `client id check: client server name "cli.example.net" doesn't match host server name "example.com"`,
|
wantErrMsg: `client id check: client server name "cli.example.net" ` +
|
||||||
wantRes: resultCodeError,
|
`doesn't match host server name "example.com"`,
|
||||||
|
wantRes: resultCodeError,
|
||||||
|
strictSNI: true,
|
||||||
}, {
|
}, {
|
||||||
name: "tls_invalid_client_id",
|
name: "tls_invalid_client_id",
|
||||||
proto: proxy.ProtoTLS,
|
proto: proxy.ProtoTLS,
|
||||||
hostSrvName: "example.com",
|
hostSrvName: "example.com",
|
||||||
cliSrvName: "!!!.example.com",
|
cliSrvName: "!!!.example.com",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `client id check: invalid client id: invalid char '!' at index 0 in client id "!!!"`,
|
wantErrMsg: `client id check: invalid client id: invalid char '!' ` +
|
||||||
wantRes: resultCodeError,
|
`at index 0 in client id "!!!"`,
|
||||||
|
wantRes: resultCodeError,
|
||||||
|
strictSNI: true,
|
||||||
}, {
|
}, {
|
||||||
name: "tls_client_id_too_long",
|
name: "tls_client_id_too_long",
|
||||||
proto: proxy.ProtoTLS,
|
proto: proxy.ProtoTLS,
|
||||||
hostSrvName: "example.com",
|
hostSrvName: "example.com",
|
||||||
cliSrvName: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789.example.com",
|
cliSrvName: `abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmno` +
|
||||||
|
`pqrstuvwxyz0123456789.example.com`,
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `client id check: invalid client id: client id "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" is too long, max: 64`,
|
wantErrMsg: `client id check: invalid client id: client id "abcdefghijklmno` +
|
||||||
wantRes: resultCodeError,
|
`pqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" ` +
|
||||||
|
`is too long, max: 64`,
|
||||||
|
wantRes: resultCodeError,
|
||||||
|
strictSNI: true,
|
||||||
}, {
|
}, {
|
||||||
name: "quic_client_id",
|
name: "quic_client_id",
|
||||||
proto: proxy.ProtoQUIC,
|
proto: proxy.ProtoQUIC,
|
||||||
@@ -109,14 +141,17 @@ func TestProcessClientID(t *testing.T) {
|
|||||||
wantClientID: "cli",
|
wantClientID: "cli",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
wantRes: resultCodeSuccess,
|
wantRes: resultCodeSuccess,
|
||||||
|
strictSNI: true,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tlsConf := TLSConfig{
|
||||||
|
ServerName: tc.hostSrvName,
|
||||||
|
StrictSNICheck: tc.strictSNI,
|
||||||
|
}
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
conf: ServerConfig{
|
conf: ServerConfig{TLSConfig: tlsConf},
|
||||||
TLSConfig: TLSConfig{ServerName: tc.hostSrvName},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
@@ -146,10 +181,11 @@ func TestProcessClientID(t *testing.T) {
|
|||||||
assert.Equal(t, tc.wantRes, res)
|
assert.Equal(t, tc.wantRes, res)
|
||||||
assert.Equal(t, tc.wantClientID, dctx.clientID)
|
assert.Equal(t, tc.wantClientID, dctx.clientID)
|
||||||
|
|
||||||
if tc.wantErrMsg != "" && assert.NotNil(t, dctx.err) {
|
if tc.wantErrMsg == "" {
|
||||||
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
|
|
||||||
} else {
|
|
||||||
assert.Nil(t, dctx.err)
|
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",
|
name: "invalid_client_id",
|
||||||
path: "/dns-query/!!!",
|
path: "/dns-query/!!!",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `client id check: invalid client id: invalid char '!' at index 0 in client id "!!!"`,
|
wantErrMsg: `client id check: invalid client id: invalid char '!'` +
|
||||||
wantRes: resultCodeError,
|
` at index 0 in client id "!!!"`,
|
||||||
|
wantRes: resultCodeError,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -225,10 +262,11 @@ func TestProcessClientID_https(t *testing.T) {
|
|||||||
assert.Equal(t, tc.wantRes, res)
|
assert.Equal(t, tc.wantRes, res)
|
||||||
assert.Equal(t, tc.wantClientID, dctx.clientID)
|
assert.Equal(t, tc.wantClientID, dctx.clientID)
|
||||||
|
|
||||||
if tc.wantErrMsg != "" && assert.NotNil(t, dctx.err) {
|
if tc.wantErrMsg == "" {
|
||||||
assert.Equal(t, tc.wantErrMsg, dctx.err.Error())
|
|
||||||
} else {
|
|
||||||
assert.Nil(t, dctx.err)
|
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 {
|
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)
|
uc, err := proxy.ParseUpstreamsConfig(defaultDNS, s.conf.BootstrapDNS, DefaultTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("dns: failed to parse default upstreams: %v", err)
|
return fmt.Errorf("dns: failed to parse default upstreams: %v", err)
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package dnsforward
|
package dnsforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,7 +10,6 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/util"
|
"github.com/AdguardTeam/AdGuardHome/internal/util"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -234,154 +230,6 @@ func processInternalHosts(ctx *dnsContext) (rc resultCode) {
|
|||||||
return resultCodeSuccess
|
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
|
// Respond to PTR requests if the target IP address is leased by our DHCP server
|
||||||
func processInternalIPAddrs(ctx *dnsContext) (rc resultCode) {
|
func processInternalIPAddrs(ctx *dnsContext) (rc resultCode) {
|
||||||
s := ctx.srv
|
s := ctx.srv
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/utils"
|
"github.com/AdguardTeam/golibs/utils"
|
||||||
@@ -314,6 +315,11 @@ func ValidateUpstreams(upstreams []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := proxy.ParseUpstreamsConfig(upstreams, []string{}, DefaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var defaultUpstreamFound bool
|
var defaultUpstreamFound bool
|
||||||
for _, u := range upstreams {
|
for _, u := range upstreams {
|
||||||
d, err := validateUpstream(u)
|
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
|
// 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
|
// 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{
|
originURL := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Host: r.Host,
|
Host: r.Host,
|
||||||
}
|
}
|
||||||
w.Header().Set("Access-Control-Allow-Origin", originURL.String())
|
w.Header().Set("Access-Control-Allow-Origin", originURL.String())
|
||||||
|
w.Header().Set("Vary", "Origin")
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,15 +295,20 @@ func run(args options) {
|
|||||||
BindPort: config.BindPort,
|
BindPort: config.BindPort,
|
||||||
BetaBindPort: config.BetaBindPort,
|
BetaBindPort: config.BetaBindPort,
|
||||||
|
|
||||||
ReadTimeout: ReadTimeout,
|
ReadTimeout: readTimeout,
|
||||||
ReadHeaderTimeout: ReadHeaderTimeout,
|
ReadHeaderTimeout: readHdrTimeout,
|
||||||
WriteTimeout: WriteTimeout,
|
WriteTimeout: writeTimeout,
|
||||||
}
|
}
|
||||||
Context.web = CreateWeb(&webConf)
|
Context.web = CreateWeb(&webConf)
|
||||||
if Context.web == nil {
|
if Context.web == nil {
|
||||||
log.Fatalf("Can't initialize Web module")
|
log.Fatalf("Can't initialize Web module")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Context.ipDetector, err = newIPDetector()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
err := initDNSServer()
|
err := initDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -315,6 +320,7 @@ func run(args options) {
|
|||||||
go func() {
|
go func() {
|
||||||
err := startDNSServer()
|
err := startDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
closeDNSServer()
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -324,11 +330,6 @@ func run(args options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.ipDetector, err = newIPDetector()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
Context.web.Start()
|
Context.web.Start()
|
||||||
|
|
||||||
// wait indefinitely for other go-routines to complete their job
|
// wait indefinitely for other go-routines to complete their job
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIPDetector_detectSpecialNetwork(t *testing.T) {
|
func TestIPDetector_detectSpecialNetwork(t *testing.T) {
|
||||||
var ipd *ipDetector
|
var ipd *ipDetector
|
||||||
|
var err error
|
||||||
|
|
||||||
t.Run("newIPDetector", func(t *testing.T) {
|
ipd, err = newIPDetector()
|
||||||
var err error
|
require.Nil(t, err)
|
||||||
ipd, err = newIPDetector()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -22,15 +22,43 @@ func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Ha
|
|||||||
return wrapped
|
return wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestBodySizeLimit is maximum request body length in bytes.
|
// defaultReqBodySzLim is the default maximum request body size.
|
||||||
const RequestBodySizeLimit = 64 * 1024
|
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
|
// limitRequestBody wraps underlying handler h, making it's request's body Read
|
||||||
// method limited.
|
// method limited.
|
||||||
func limitRequestBody(h http.Handler) (limited http.Handler) {
|
func limitRequestBody(h http.Handler) (limited http.Handler) {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Error("limitRequestBody: %s", err)
|
log.Error("limitRequestBody: %s", err)
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLimitRequestBody(t *testing.T) {
|
func TestLimitRequestBody(t *testing.T) {
|
||||||
errReqLimitReached := &aghio.LimitReachedError{
|
errReqLimitReached := &aghio.LimitReachedError{
|
||||||
Limit: RequestBodySizeLimit,
|
Limit: defaultReqBodySzLim,
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -28,8 +29,8 @@ func TestLimitRequestBody(t *testing.T) {
|
|||||||
wantErr: nil,
|
wantErr: nil,
|
||||||
}, {
|
}, {
|
||||||
name: "so_big",
|
name: "so_big",
|
||||||
body: string(make([]byte, RequestBodySizeLimit+1)),
|
body: string(make([]byte, defaultReqBodySzLim+1)),
|
||||||
want: make([]byte, RequestBodySizeLimit),
|
want: make([]byte, defaultReqBodySzLim),
|
||||||
wantErr: errReqLimitReached,
|
wantErr: errReqLimitReached,
|
||||||
}, {
|
}, {
|
||||||
name: "empty",
|
name: "empty",
|
||||||
@@ -60,8 +61,8 @@ func TestLimitRequestBody(t *testing.T) {
|
|||||||
|
|
||||||
lim.ServeHTTP(res, req)
|
lim.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
require.Equal(t, tc.wantErr, err)
|
||||||
assert.Equal(t, tc.want, res.Body.Bytes())
|
assert.Equal(t, tc.want, res.Body.Bytes())
|
||||||
assert.Equal(t, tc.wantErr, err)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ReadTimeout is the maximum duration for reading the entire request,
|
// readTimeout is the maximum duration for reading the entire request,
|
||||||
// including the body.
|
// including the body.
|
||||||
ReadTimeout = 10 * time.Second
|
readTimeout = 60 * time.Second
|
||||||
|
// readHdrTimeout is the amount of time allowed to read request headers.
|
||||||
// ReadHeaderTimeout is the amount of time allowed to read request
|
readHdrTimeout = 60 * time.Second
|
||||||
// headers.
|
// writeTimeout is the maximum duration before timing out writes of the
|
||||||
ReadHeaderTimeout = 10 * time.Second
|
|
||||||
|
|
||||||
// WriteTimeout is the maximum duration before timing out writes of the
|
|
||||||
// response.
|
// response.
|
||||||
WriteTimeout = 10 * time.Second
|
writeTimeout = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type webConfig struct {
|
type webConfig struct {
|
||||||
@@ -191,7 +188,10 @@ func (web *Web) Start() {
|
|||||||
WriteTimeout: web.conf.WriteTimeout,
|
WriteTimeout: web.conf.WriteTimeout,
|
||||||
}
|
}
|
||||||
go func() {
|
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,
|
RootCAs: Context.tlsRoots,
|
||||||
CipherSuites: Context.tlsCiphers,
|
CipherSuites: Context.tlsCiphers,
|
||||||
},
|
},
|
||||||
Handler: Context.mux,
|
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
||||||
ReadTimeout: web.conf.ReadTimeout,
|
ReadTimeout: web.conf.ReadTimeout,
|
||||||
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
|
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
|
||||||
WriteTimeout: web.conf.WriteTimeout,
|
WriteTimeout: web.conf.WriteTimeout,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func NewClientProto(s string) (cp ClientProto, err error) {
|
|||||||
ClientProtoDOH,
|
ClientProtoDOH,
|
||||||
ClientProtoDOQ,
|
ClientProtoDOQ,
|
||||||
ClientProtoDOT,
|
ClientProtoDOT,
|
||||||
|
ClientProtoDNSCrypt,
|
||||||
ClientProtoPlain:
|
ClientProtoPlain:
|
||||||
|
|
||||||
return cp, nil
|
return cp, nil
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,10 +12,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO(a.garipov): Rewrite all of this. Add proper error handling and
|
||||||
|
// inspection. Improve logging. Decrease complexity.
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxDomains = 100 // max number of top domains to store in file or return via Get()
|
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()
|
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
|
TimeAvg uint32 // usec
|
||||||
}
|
}
|
||||||
|
|
||||||
func createObject(conf Config) (*statsCtx, error) {
|
func createObject(conf Config) (s *statsCtx, err error) {
|
||||||
s := statsCtx{}
|
s = &statsCtx{}
|
||||||
if !checkInterval(conf.LimitDays) {
|
if !checkInterval(conf.LimitDays) {
|
||||||
conf.LimitDays = 1
|
conf.LimitDays = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
s.conf = &Config{}
|
s.conf = &Config{}
|
||||||
*s.conf = conf
|
*s.conf = conf
|
||||||
s.conf.limit = conf.LimitDays * 24
|
s.conf.limit = conf.LimitDays * 24
|
||||||
@@ -84,27 +90,43 @@ func createObject(conf Config) (*statsCtx, error) {
|
|||||||
log.Tracef("Deleting old units...")
|
log.Tracef("Deleting old units...")
|
||||||
firstID := id - s.conf.limit - 1
|
firstID := id - s.conf.limit - 1
|
||||||
unitDel := 0
|
unitDel := 0
|
||||||
forEachBkt := func(name []byte, b *bolt.Bucket) error {
|
|
||||||
id := uint32(btoi(name))
|
// TODO(a.garipov): See if this is actually necessary. Looks
|
||||||
if id < firstID {
|
// like a rather bizarre solution.
|
||||||
err := tx.DeleteBucket(name)
|
errStop := agherr.Error("stop iteration")
|
||||||
if err != nil {
|
forEachBkt := func(name []byte, _ *bolt.Bucket) (cberr error) {
|
||||||
log.Debug("tx.DeleteBucket: %s", err)
|
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++
|
unitDel++
|
||||||
|
|
||||||
return nil
|
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)
|
udb = s.loadUnitFromDB(tx, id)
|
||||||
|
|
||||||
if unitDel != 0 {
|
if unitDel != 0 {
|
||||||
s.commitTxn(tx)
|
s.commitTxn(tx)
|
||||||
} else {
|
} 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
|
s.unit = &u
|
||||||
|
|
||||||
log.Debug("Stats: initialized")
|
log.Debug("stats: initialized")
|
||||||
return &s, nil
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statsCtx) Start() {
|
func (s *statsCtx) Start() {
|
||||||
@@ -133,7 +156,7 @@ func (s *statsCtx) dbOpen() bool {
|
|||||||
log.Tracef("db.Open...")
|
log.Tracef("db.Open...")
|
||||||
s.db, err = bolt.Open(s.conf.Filename, 0o644, nil)
|
s.db, err = bolt.Open(s.conf.Filename, 0o644, nil)
|
||||||
if err != 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" {
|
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")
|
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 {
|
func (s *statsCtx) deleteUnit(tx *bolt.Tx, id uint32) bool {
|
||||||
err := tx.DeleteBucket(unitName(id))
|
err := tx.DeleteBucket(unitName(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Tracef("bolt DeleteBucket: %s", err)
|
log.Tracef("stats: bolt DeleteBucket: %s", err)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
log.Debug("Stats: deleted unit %d", id)
|
|
||||||
|
log.Debug("stats: deleted unit %d", id)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +416,7 @@ func (s *statsCtx) setLimit(limitDays int) {
|
|||||||
conf := *s.conf
|
conf := *s.conf
|
||||||
conf.limit = uint32(limitDays) * 24
|
conf.limit = uint32(limitDays) * 24
|
||||||
s.conf = &conf
|
s.conf = &conf
|
||||||
log.Debug("Stats: set limit: %d", limitDays)
|
log.Debug("stats: set limit: %d", limitDays)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statsCtx) WriteDiskConfig(dc *DiskConfig) {
|
func (s *statsCtx) WriteDiskConfig(dc *DiskConfig) {
|
||||||
@@ -415,7 +441,7 @@ func (s *statsCtx) Close() {
|
|||||||
log.Tracef("db.Close")
|
log.Tracef("db.Close")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Stats: closed")
|
log.Debug("stats: closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset counters and clear database
|
// Reset counters and clear database
|
||||||
@@ -443,7 +469,7 @@ func (s *statsCtx) clear() {
|
|||||||
|
|
||||||
_ = s.dbOpen()
|
_ = s.dbOpen()
|
||||||
|
|
||||||
log.Debug("Stats: cleared")
|
log.Debug("stats: cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Client IP address
|
// Get Client IP address
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"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.
|
// 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) {
|
func IfaceHasStaticIP(ifaceName string) (has bool, err error) {
|
||||||
return ifaceHasStaticIP(ifaceName)
|
return ifaceHasStaticIP(ifaceName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import (
|
|||||||
const maxConfigFileSize = 1024 * 1024
|
const maxConfigFileSize = 1024 * 1024
|
||||||
|
|
||||||
func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
|
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 {
|
for _, check := range []struct {
|
||||||
checker func(io.Reader, string) (bool, error)
|
checker func(io.Reader, string) (bool, error)
|
||||||
filePath string
|
filePath string
|
||||||
@@ -32,28 +36,37 @@ func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
|
|||||||
checker: ifacesStaticConfig,
|
checker: ifacesStaticConfig,
|
||||||
filePath: "/etc/network/interfaces",
|
filePath: "/etc/network/interfaces",
|
||||||
}} {
|
}} {
|
||||||
|
var f *os.File
|
||||||
f, err = os.Open(check.filePath)
|
f, err = os.Open(check.filePath)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
fileReadCloser, err := aghio.LimitReadCloser(f, maxConfigFileSize)
|
var fileReadCloser io.ReadCloser
|
||||||
|
fileReadCloser, err = aghio.LimitReadCloser(f, maxConfigFileSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
defer fileReadCloser.Close()
|
defer fileReadCloser.Close()
|
||||||
|
|
||||||
has, err = check.checker(fileReadCloser, ifaceName)
|
has, err = check.checker(fileReadCloser, ifaceName)
|
||||||
if has || err != nil {
|
if err != nil {
|
||||||
break
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return has, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return has, err
|
return false, ErrNoStaticIPInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to
|
// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
|
|
||||||
## v0.105: API changes
|
## 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
|
### New `"dnscrypt"` `"client_proto"` value in `GET /querylog` response
|
||||||
|
|
||||||
* The field `"client_proto"` can now have the value `"dnscrypt"` when the
|
* The field `"client_proto"` can now have the value `"dnscrypt"` when the
|
||||||
@@ -69,6 +75,8 @@
|
|||||||
|
|
||||||
As well as other documentation fixes.
|
As well as other documentation fixes.
|
||||||
|
|
||||||
|
[Identifying clients]: https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#idclient
|
||||||
|
|
||||||
## v0.103: API changes
|
## v0.103: API changes
|
||||||
|
|
||||||
### API: replace settings in GET /control/dns_info & POST /control/dns_config
|
### 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"
|
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.
|
# Install AdGuard Home service and run it.
|
||||||
( cd "${AGH_DIR}" && ./AdGuardHome -s install || error_exit "Cannot install AdGuardHome as a service" )
|
( 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 channel="$CHANNEL"
|
||||||
readonly commit="$COMMIT"
|
readonly commit="$COMMIT"
|
||||||
readonly dist_dir="$DIST_DIR"
|
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.
|
# Allow users to use sudo.
|
||||||
readonly sudo_cmd="${SUDO:-}"
|
readonly sudo_cmd="${SUDO:-}"
|
||||||
|
|||||||
468
scripts/translations/package-lock.json
generated
468
scripts/translations/package-lock.json
generated
@@ -1,8 +1,466 @@
|
|||||||
{
|
{
|
||||||
"name": "translations",
|
"name": "translations",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"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": {
|
"dependencies": {
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "6.5.5",
|
"version": "6.5.5",
|
||||||
@@ -205,9 +663,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.15",
|
"version": "4.17.20",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||||
},
|
},
|
||||||
"mime-db": {
|
"mime-db": {
|
||||||
"version": "1.37.0",
|
"version": "1.37.0",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ initialisms = [
|
|||||||
, "MX"
|
, "MX"
|
||||||
, "PTR"
|
, "PTR"
|
||||||
, "QUIC"
|
, "QUIC"
|
||||||
|
, "RA"
|
||||||
, "SDNS"
|
, "SDNS"
|
||||||
|
, "SLAAC"
|
||||||
, "SVCB"
|
, "SVCB"
|
||||||
]
|
]
|
||||||
dot_import_whitelist = []
|
dot_import_whitelist = []
|
||||||
|
|||||||
Reference in New Issue
Block a user