Compare commits

...

39 Commits

Author SHA1 Message Date
Vlad
46698078a0 fix generator api parameters 2021-03-09 18:56:53 +03:00
Vlad
59a3045615 add api generator 2021-03-01 13:32:27 +03:00
Vlad
1453c27d87 wip: Update entities generator 2021-02-25 17:40:38 +03:00
Ainar Garipov
e31c0c456a Pull request: all: imp stalebot
Merge in DNS/adguard-home from imp-stalebot to master

Squashed commit of the following:

commit 8b72c1328030e2299e296a7ab6adb0148dcb03ad
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Feb 24 13:43:41 2021 +0300

    all: imp stalebot
2021-02-24 14:13:29 +03:00
Andrey Meshkov
836d0db563 Add links to discussions 2021-02-23 14:21:08 +03:00
Eugene Burkov
a0abad6644 Pull request: 2692 time format
Merge in DNS/adguard-home from 2692-time-format to master

Closes #2692.

Squashed commit of the following:

commit a77e3ffc1e2ca7ad1eb4b56641eee787595268e9
Merge: 5262c0b9 1122e71c
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Feb 18 13:58:29 2021 +0300

    Merge branch 'master' into 2692-time-format

commit 5262c0b95979a6c7a01bdd56e9476c4cdf217119
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Thu Feb 18 13:50:38 2021 +0300

    dhcpd: imp docs

commit 3744338d51dd003a0052672ceffa260c70f5738d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Feb 17 21:10:37 2021 +0300

    dhcpd: fix lease time format
2021-02-18 14:11:54 +03:00
Eugene Burkov
1122e71cf3 Pull request: 2674 fix upstreams validation
Merge in DNS/adguard-home from 2674-quic-upstream to master

Updates #2674.

Squashed commit of the following:

commit ce7643580cc9e19881689e7fd4933bc953f2884e
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 16 18:23:39 2021 +0300

    all: fix log

commit b53e4368dd9ca61965e73b1f1274388422142830
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 16 18:21:59 2021 +0300

    all: log changes

commit 1cdca48e31c272ccfbde955c2a3e560ca6ca6bcf
Merge: bc441dac e32c18fa
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 16 18:15:47 2021 +0300

    Merge branch 'master' into 2674-quic-upstream

commit bc441dac3cbe8070c8c1d672925b14d309f05b9f
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 16 17:06:47 2021 +0300

    dnsforward: fix error wrapping

commit 281c1b43233c2bb51e3a933588087a207b7eef3d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 16 17:00:35 2021 +0300

    all: fix validation, imp shutdown

commit 31a5ea7a081de4bcc3913bd04d62334fec1b59e1
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 16 14:53:10 2021 +0300

    dnsforward: fix upstreams validation
2021-02-16 18:46:49 +03:00
Ainar Garipov
e32c18faab Pull request: dhcpd: fix ip option parsing
Merge in DNS/adguard-home from 2688-dhcp-opt-ip to master

Updates #2688.

Squashed commit of the following:

commit e17e003a3a61c1f4ed55617bb61df721cbca12c1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 16 17:10:32 2021 +0300

    dhcpd: fix ip option parsing
2021-02-16 17:58:42 +03:00
Ainar Garipov
f68f6c9b37 Pull request: all: fix statip ip ck
Merge in DNS/adguard-home from fix-static-ip-check to master

Squashed commit of the following:

commit af365c106f3d620afc77492a06f5368611328f5f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 18:55:35 2021 +0300

    all: doc changes

commit 922afb262458fc488e03cad232430d90c504f2f3
Merge: 43fec5fb dbcc55f5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 18:53:31 2021 +0300

    Merge branch 'master' into fix-static-ip-check

commit 43fec5fb79f5c67b375da00aa73d11b3ed9ba3a4
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 18:37:16 2021 +0300

    all: fix statip ip ck
2021-02-15 19:07:08 +03:00
Artem Baskal
dbcc55f528 2641: Fix optical issue on custom rules
Close #2641

Squashed commit of the following:

commit 3d7280418e42c1607dd644fdbf5faab870470c93
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Mon Feb 15 18:37:11 2021 +0300

    Update changelog order

commit 98e46fe3285b294de5f0b5525611cfb18afb63f3
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Mon Feb 15 18:30:53 2021 +0300

    Update changelog

commit 5342d7c7bc5ca40888a4daeef1526464b861ef29
Author: Artem Baskal <a.baskal@adguard.com>
Date:   Mon Feb 15 18:03:14 2021 +0300

    2641: Fix optical issue on custom rules
2021-02-15 18:50:58 +03:00
Ainar Garipov
66b549a565 Pull request: scripts: fix shameful error
Merge in DNS/adguard-home from fix-edge to master

Squashed commit of the following:

commit fdb7bc5ff772aabf86063f18f3b7c4ec83e63028
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 16:26:39 2021 +0300

    scripts: fix shameful error
2021-02-15 16:52:16 +03:00
Ainar Garipov
a1c9e9d36b Pull request: all: prep release
Merge in DNS/adguard-home from prep-release to master

Squashed commit of the following:

commit 32d83fe663dfd7a585ed9d89d09d47fd7b4cb653
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 16:02:56 2021 +0300

    all: prep release
2021-02-15 16:25:44 +03:00
Ainar Garipov
ab81ff03f6 Pull request: all: upd translations
Merge in DNS/adguard-home from upd-locales to master

Updates #2643.

Squashed commit of the following:

commit d7f26aaa63bfc9307887301abfff4779be7eba41
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 16:18:54 2021 +0300

    all: upd translations
2021-02-15 15:53:24 +03:00
Ainar Garipov
d295621352 Pull request: home: inc http timeouts
Merge in DNS/adguard-home from 2671-timeout to master

Updates #2671.
Updates #2682.

Squashed commit of the following:

commit 79b1a36a79e3c7c26fc1a4b171feb050690f8c83
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 15:25:26 2021 +0300

    all: doc changes

commit 84229b782bde433faa4ed8b71dd092965787d30e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 15:12:33 2021 +0300

    home: imp names

commit b18d7b08473c99ddd37ecfa14be8d48838c2afab
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 15:04:27 2021 +0300

    home: inc http timeouts
2021-02-15 15:36:38 +03:00
Ainar Garipov
aebcd74efe Pull request: home: imp init
Merge in DNS/adguard-home from fix-init to master

Squashed commit of the following:

commit 551c143f6c3846f061b3118a06e1c756bc3e2ba1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 13:45:06 2021 +0300

    home: imp init
2021-02-15 14:20:23 +03:00
Ainar Garipov
7d1ca48ae4 Pull request #1005: home: imp large req handling
Merge in DNS/adguard-home from 2675-larger-requests to master

Updates #2675.

Squashed commit of the following:

commit 2b45c9bfdc817980204b11de768b425fb72a6488
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 13:38:44 2021 +0300

    home: imp names

commit dad39ae7ee35346ea91f15665acc93ba0b5653df
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 13:31:53 2021 +0300

    home: imp large req handling
2021-02-15 13:56:41 +03:00
Ainar Garipov
8a0bc5468b Pull request #1004: dhcpd: fix dhcpv6 status json
Merge in DNS/adguard-home from 2678-json-tag to master

Updates #2678.

Squashed commit of the following:

commit 7c272ae8830ac10b0e0154656cf472b003315349
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 13:14:16 2021 +0300

    all: doc changes

commit 3c964f814f2bc92e807aad3f5bad342eb455fe28
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 15 12:54:27 2021 +0300

    dhcpd: fix dhcpv6 status json
2021-02-15 13:32:16 +03:00
Ainar Garipov
e272e19516 Pull request: home: beta http server errorss are not fatal
Merge in DNS/adguard-home from http-beta-no-fatal to master

Squashed commit of the following:

commit 13dcf7bca6d156f387639906c778fd6b07f491f3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Feb 12 17:58:01 2021 +0300

    home: beta http server errorss are not fatal
2021-02-12 18:56:18 +03:00
Ainar Garipov
10f03b7527 Pull request: dhcpd: assume static ip on eperm
Merge in DNS/adguard-home from 2667-eperm-dhcp to master

Updates #2667.

Squashed commit of the following:

commit 7fad607ae0ae75419005707ee58312bc64fe78c5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Feb 12 16:27:59 2021 +0300

    dhcpd: assume static ip on eperm
2021-02-12 16:40:34 +03:00
Eugene Burkov
0d44822c43 Pull request: 2639 use testify require vol.3
Merge in DNS/adguard-home from 2639-testify-require-3 to master

Updates #2639.

Squashed commit of the following:

commit 83d7afcbb7e5393db5a0242f3eaca063710d36b7
Merge: ef154b6d e83b919d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Feb 12 13:07:58 2021 +0300

    Merge branch 'master' into 2639-testify-require-3

commit ef154b6d3c89f975ce28369372757a1205baa655
Merge: 5b46073a 2eb21ef4
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Feb 12 12:40:43 2021 +0300

    Merge branch 'master' into 2639-testify-require-3

commit 5b46073a09badef44c86a5f48c6bb874c8df2674
Merge: 7dd7b6e0 890f0322
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Feb 10 21:20:51 2021 +0300

    Merge branch 'master' into 2639-testify-require-3

commit 7dd7b6e00ead2bf507af541c801a9ac770106440
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Feb 10 21:19:36 2021 +0300

    dhcpd: fix comment

commit 9e74adbcf21dad58409c3dfc8e08b6470bfedc22
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Feb 10 15:13:40 2021 +0300

    all: imp tests drastically
2021-02-12 13:27:44 +03:00
Ainar Garipov
e83b919dbd Pull request: all: imp issue template
Merge in DNS/adguard-home from imp-issue-tmpl to master

Squashed commit of the following:

commit e6251d4e6db9498d26e555116dbb870625219ffa
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Feb 12 12:46:09 2021 +0300

    all: imp issue template more

commit f896c6076027c373bc8b62c600a82938ae3a813d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Feb 12 12:42:25 2021 +0300

    all: imp issue template
2021-02-12 12:59:08 +03:00
Ainar Garipov
2eb21ef409 Pull request: dhcpd: do not override ra-slaac settings
Merge in DNS/adguard-home from 2653-ra-slaac to master

Updates #2653.

Squashed commit of the following:

commit f261413a58dc813e37cc848606ed490b8c0ac9f3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 20:37:13 2021 +0300

    all: doc changes, rm debug

commit 4a8c6e4897579493c1ca242fb8f0f440c3b51a74
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 20:11:46 2021 +0300

    dhcpd: do not override ra-slaac settings
2021-02-11 20:49:03 +03:00
Ainar Garipov
7b014082ab Pull request: home: set vary hdr to origin
Merge in DNS/adguard-home from 2658-vary-origin to master

Updates #2658.

Squashed commit of the following:

commit b4bf6c16e19f1c0b04cc2e526e2b0968956cf56c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 18:18:32 2021 +0300

    all: doc changes

commit f2599c5b48759565e2f621c2fcf89440de56e4a4
Merge: 3eb08ac8 6b8a46ef
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 18:16:53 2021 +0300

    Merge branch 'master' into 2658-vary-origin

commit 3eb08ac889163d123b5ca638a83a9289b456d04e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 18:04:04 2021 +0300

    home: set vary hdr to origin
2021-02-11 18:40:14 +03:00
Andrey Meshkov
6b8a46ef3b Fix install script for darwin 2021-02-11 18:08:44 +03:00
Ainar Garipov
a623ac694b Pull request: stats: imp err handling, logs
Merge in DNS/adguard-home from 2661-imp-stats-logging to master

Updates #2661.

Squashed commit of the following:

commit 474735a5c6ab650973343a1323ebf3c00edd71cf
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 17:44:34 2021 +0300

    stats: imp err handling, logs
2021-02-11 17:55:37 +03:00
Ainar Garipov
7dd2d0af96 Pull request: all: doc make -j in readme
Merge in DNS/adguard-home from 2668-doc-make-j to master

Updates #2668.

Squashed commit of the following:

commit b52c2a18c46f8a6e5badf9db104c04a10765e96d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 16:21:54 2021 +0300

    all: doc make -j in readme
2021-02-11 16:32:27 +03:00
Ainar Garipov
7e08565212 Pull request: openapi: doc client id better
Merge in DNS/adguard-home from doc-client-id to master

Squashed commit of the following:

commit ea03887d505296e5033964e8227ed906b102d990
Merge: 693453b5 841bb9bc
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 15:20:56 2021 +0300

    Merge branch 'master' into doc-client-id

commit 693453b5eab40e201501d6881418ee42191a1bc5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 12:55:01 2021 +0300

    openapi: doc client id better
2021-02-11 15:41:03 +03:00
Ainar Garipov
841bb9bc35 Pull request: dnsforward: do not check client srv name unless asked
Merge in DNS/adguard-home from 2664-non-strict-sni to master

Updates #2664.

Squashed commit of the following:

commit e8d625fe3b1f06f97328809a3330b37e5bd578d7
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 14:46:52 2021 +0300

    all: imp doc

commit 10537b8bdf126eca9608353e57d92edba632232a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 14:30:25 2021 +0300

    dnsforward: do not check client srv name unless asked
2021-02-11 15:20:30 +03:00
Ainar Garipov
f016ae172c Pull request: home: inc req size for some apis
Merge in DNS/adguard-home from 2666-req-body-lim to master

Updates #2666.

Squashed commit of the following:

commit a525974aee54831963e3f95c8186d44f1752e9c7
Merge: 947703f3 44168292
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 13:48:06 2021 +0300

    Merge branch 'master' into 2666-req-body-lim

commit 947703f36e1ee0ab08f938850f76824b7899d7e1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 13:28:54 2021 +0300

    home: inc req size for some apis
2021-02-11 14:10:42 +03:00
Ainar Garipov
44168292d5 Pull request: 2662 dnscrypt logs
Merge in DNS/adguard-home from 2662-dnscrypt-logs to master

Closes #2662.

Squashed commit of the following:

commit 05f6742b5c73e1d150834965ae3a54ca06ef8e24
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 12:58:08 2021 +0300

    all: imp docs

commit ee0b8c574c1cb5302a5ffb62d2fec4126509b2e8
Merge: aaa8c6b8 e64df20e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 12:56:44 2021 +0300

    Merge branch 'master' into 2662-dnscrypt-logs

commit aaa8c6b8085679f4acd234527bd03cb0b2520b4f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 12:38:49 2021 +0300

    all: document changes

commit 57b6a4d8e95e87d928274d095dc2004f1591d940
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 12:36:22 2021 +0300

    all: fix dnscrypt in logs
2021-02-11 13:46:59 +03:00
Ainar Garipov
e64df20e74 Pull request #991: scripts: fix docker version handling
Merge in DNS/adguard-home from 2663-docker-version to master

Closes #2663.

Squashed commit of the following:

commit f9b03fd12543a3975ea6dc45115e4ec0f73eba1e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 12:40:06 2021 +0300

    all: document changes

commit 8bce414c9f25210420b6026cb4c21908c8b9c74f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Feb 11 12:22:45 2021 +0300

    scripts: fix docker version handling
2021-02-11 12:55:23 +03:00
Ainar Garipov
890f0322d8 Pull request: all: imp changelog
Merge in DNS/adguard-home from fix-chlog to master

Squashed commit of the following:

commit 9b6d68c6592ceeab50f30c5c9eb6363d0d772867
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Feb 10 17:19:14 2021 +0300

    all: imp changelog
2021-02-10 19:10:17 +03:00
Ainar Garipov
2bf2d5a19d Pull request: all: set release date
Merge in DNS/adguard-home from rel-date to master

Squashed commit of the following:

commit e9e03a408c70587fa28da94803b054a7557ac86b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Feb 10 16:44:52 2021 +0300

    all: set release date
2021-02-10 16:59:23 +03:00
Ainar Garipov
771a32cc9d Pull request: client: revert 5a1e04c2
Merge in DNS/adguard-home from revert-locales to master

Updates #2643.

Squashed commit of the following:

commit 7fb729edbeee681bdcdd0a295e530bc6f776cbac
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Feb 10 14:41:52 2021 +0300

    all: reupd locales

commit 28416bee63f5b318ad8c8fcfb86f531893b077d1
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Feb 10 14:33:41 2021 +0300

    scripts: imp translations downloading

commit d8c2bfe08d7f50597dedcb5b73059d16f329b71a
Merge: 00b19782 9df09357
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Feb 10 14:33:10 2021 +0300

    Merge branch 'master' into revert-locales

commit 00b19782e2d3311ac4cc1c4b79a886fdf0d386b9
Merge: 5e3d2b74 64715045
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 9 16:52:56 2021 +0300

    Merge branch 'master' into revert-locales

commit 5e3d2b74ca57637d6625a21fde1ffbdb28cc8b18
Merge: f34e2fe9 63e4adc0
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 9 13:17:30 2021 +0300

    Merge branch 'master' into revert-locales

commit f34e2fe9248feb9f1087be1b17ce187371896603
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 9 12:47:47 2021 +0300

    client: revert 5a1e04c2
2021-02-10 15:00:39 +03:00
Ainar Garipov
9df0935781 Pull request: all: imp changelog
Merge in DNS/adguard-home from imp-chlog to master

Squashed commit of the following:

commit 50805731db515d529ea583a591165b889a257a24
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 9 20:30:26 2021 +0300

    all: imp changelog
2021-02-09 20:44:41 +03:00
Eugene Burkov
a3dddd72c1 Pull request: 2639 use testify require vol.2
Merge in DNS/adguard-home from 2639-testify-require-2 to master

Updates #2639.

Squashed commit of the following:

commit 31cc29a166e2e48a73956853cbc6d6dd681ab6da
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 9 18:48:31 2021 +0300

    all: deal with t.Run

commit 484f477fbfedd03aca4d322bc1cc9e131f30e1ce
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 9 17:44:02 2021 +0300

    all: fix readability, imp tests

commit 1231a825b353c16e43eae1b660dbb4c87805f564
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 9 16:06:29 2021 +0300

    all: imp tests
2021-02-09 19:38:31 +03:00
Ainar Garipov
6471504555 Pull request: home: imp upgrade test
Merge in DNS/adguard-home from imp-upgrade-test to master

Updates #2639.
Updates #2646.

Squashed commit of the following:

commit f7bd8e020fa8fc285cdd6ecdf36711574f2e6e38
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 9 16:38:40 2021 +0300

    home: imp test more

commit 5b64131e04d568871cf401fa3fc2c7980d69bee0
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 9 16:09:22 2021 +0300

    home: imp upgrade test
2021-02-09 16:51:44 +03:00
Eugene Burkov
1fa4d55ae3 Pull request: 2639 use testify require vol.1
Merge in DNS/adguard-home from 2639-testify-require-1 to master

Updates #2639.

Squashed commit of the following:

commit da7d283c6b20b4dbbc0af4689fa812d14f022b52
Merge: c4af71b0 63e4adc0
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Feb 9 14:27:41 2021 +0300

    Merge branch 'master' into 2639-testify-require-1

commit c4af71b002dc68785106328f60946d7fa73fb933
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Feb 8 19:32:51 2021 +0300

    querylog: fix tests for windows

commit b616ea5de88a38550ffd42253d3054ea6f90cff9
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Feb 8 18:29:28 2021 +0300

    querylog: imp tests again

commit 091a698df5fbe6c3e572fde12da395f527c88b95
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Feb 8 15:49:38 2021 +0300

    querylog: imp tests
2021-02-09 15:17:02 +03:00
Ainar Garipov
63e4adc0e7 Pull request: all: upd urlfilter
Merge in DNS/adguard-home from upd-urlfilter to master

Squashed commit of the following:

commit 055cf6721f1412e8c63433bdcd7baf29f9afd699
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Feb 9 13:03:30 2021 +0300

    all: upd urlfilter
2021-02-09 13:17:19 +03:00
99 changed files with 3739 additions and 2455 deletions

5
.gitattributes vendored
View File

@@ -1,4 +1 @@
client/* linguist-vendored client/* linguist-vendored
client/src/__locales/*.json binary
# Make an exception for the english locale, as it is the base one.
client/src/__locales/en.json -binary

View File

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

View File

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

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

View File

@@ -10,9 +10,70 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
<!-- <!--
## [v0.105.0] - 2021-02-08 ## [v0.106.0] - 2021-04-26
--> -->
<!--
## [v0.105.2] - 2021-02-24
-->
### Fixed
- DHCP lease's `expired` field incorrect time format ([#2692]).
- Incomplete DNS upstreams validation ([#2674]).
- Wrong parsing of DHCP options of the `ip` type ([#2688]).
[#2674]: https://github.com/AdguardTeam/AdGuardHome/issues/2674
[#2688]: https://github.com/AdguardTeam/AdGuardHome/issues/2688
[#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692
## [v0.105.1] - 2021-02-15
### Changed
- Increased HTTP API timeouts ([#2671], [#2682]).
- "Permission denied" errors when checking if the machine has a static IP no
longer prevent the DHCP server from starting ([#2667]).
- The server name sent by clients of TLS APIs is not only checked when
`strict_sni_check` is enabled ([#2664]).
- HTTP API request body size limit for the `POST /control/access/set` and `POST
/control/filtering/set_rules` HTTP APIs is increased ([#2666], [#2675]).
### Fixed
- Error when enabling the DHCP server when AdGuard Home couldn't determine if
the machine has a static IP.
- Optical issue on custom rules ([#2641]).
- Occasional crashes during startup.
- The field `"range_start"` in the `GET /control/dhcp/status` HTTP API response
is now correctly named again ([#2678]).
- DHCPv6 server's `ra_slaac_only` and `ra_allow_slaac` settings aren't reset to
`false` on update any more ([#2653]).
- The `Vary` header is now added along with `Access-Control-Allow-Origin` to
prevent cache-related and other issues in browsers ([#2658]).
- The request body size limit is now set for HTTPS requests as well.
- Incorrect version tag in the Docker release ([#2663]).
- DNSCrypt queries weren't marked as such in logs ([#2662]).
[#2641]: https://github.com/AdguardTeam/AdGuardHome/issues/2641
[#2653]: https://github.com/AdguardTeam/AdGuardHome/issues/2653
[#2658]: https://github.com/AdguardTeam/AdGuardHome/issues/2658
[#2662]: https://github.com/AdguardTeam/AdGuardHome/issues/2662
[#2663]: https://github.com/AdguardTeam/AdGuardHome/issues/2663
[#2664]: https://github.com/AdguardTeam/AdGuardHome/issues/2664
[#2666]: https://github.com/AdguardTeam/AdGuardHome/issues/2666
[#2667]: https://github.com/AdguardTeam/AdGuardHome/issues/2667
[#2671]: https://github.com/AdguardTeam/AdGuardHome/issues/2671
[#2675]: https://github.com/AdguardTeam/AdGuardHome/issues/2675
[#2678]: https://github.com/AdguardTeam/AdGuardHome/issues/2678
[#2682]: https://github.com/AdguardTeam/AdGuardHome/issues/2682
## [v0.105.0] - 2021-02-10
### Added ### Added
- Added more services to the "Blocked services" list ([#2224], [#2401]). - Added more services to the "Blocked services" list ([#2224], [#2401]).
@@ -47,7 +108,7 @@ and this project adheres to
- Improved HTTP requests handling and timeouts ([#2343]). - Improved HTTP requests handling and timeouts ([#2343]).
- Our snap package now uses the `core20` image as its base ([#2306]). - Our snap package now uses the `core20` image as its base ([#2306]).
- New build system and various internal improvements ([#2271], [#2276], [#2297], - New build system and various internal improvements ([#2271], [#2276], [#2297],
[#2509], [#2552]). [#2509], [#2552], [#2639], [#2646]).
### Deprecated ### Deprecated
@@ -107,6 +168,8 @@ and this project adheres to
[#2552]: https://github.com/AdguardTeam/AdGuardHome/issues/2552 [#2552]: https://github.com/AdguardTeam/AdGuardHome/issues/2552
[#2589]: https://github.com/AdguardTeam/AdGuardHome/issues/2589 [#2589]: https://github.com/AdguardTeam/AdGuardHome/issues/2589
[#2630]: https://github.com/AdguardTeam/AdGuardHome/issues/2630 [#2630]: https://github.com/AdguardTeam/AdGuardHome/issues/2630
[#2639]: https://github.com/AdguardTeam/AdGuardHome/issues/2639
[#2646]: https://github.com/AdguardTeam/AdGuardHome/issues/2646
## [v0.104.3] - 2020-11-19 ## [v0.104.3] - 2020-11-19
@@ -147,9 +210,12 @@ and this project adheres to
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...HEAD [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

View File

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

View File

@@ -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,7 +276,7 @@
"source_label": "Крыніца", "source_label": "Крыніца",
"found_in_known_domain_db": "Знойдзены ў базе вядомых даменаў.", "found_in_known_domain_db": "Знойдзены ў базе вядомых даменаў.",
"category_label": "Катэгорыя", "category_label": "Катэгорыя",
"rule_label": "Правіла", "rule_label": "Правіла(ы)",
"list_label": "Спіс", "list_label": "Спіс",
"unknown_filter": "Невядомы фільтр {{filterId}}", "unknown_filter": "Невядомы фільтр {{filterId}}",
"known_tracker": "Вядомы трэкер", "known_tracker": "Вядомы трэкер",
@@ -430,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}}\" паспяхова выдалена",
@@ -529,7 +537,6 @@
"check_ip": "IP-адрасы: {{ip}}", "check_ip": "IP-адрасы: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Прычына: {{reason}}", "check_reason": "Прычына: {{reason}}",
"check_rule": "Правіла: {{rule}}",
"check_service": "Назва сэрвісу: {{service}}", "check_service": "Назва сэрвісу: {{service}}",
"service_name": "Назва сэрвіса", "service_name": "Назва сэрвіса",
"check_not_found": "Не знойдзена ў вашым спісе фільтраў", "check_not_found": "Не знойдзена ў вашым спісе фільтраў",

View File

@@ -144,7 +144,6 @@
"source_label": "Източник", "source_label": "Източник",
"found_in_known_domain_db": "Намерен в списъците с домейни.", "found_in_known_domain_db": "Намерен в списъците с домейни.",
"category_label": "Категория", "category_label": "Категория",
"rule_label": "Правило",
"unknown_filter": "Непознат филтър {{filterId}}", "unknown_filter": "Непознат филтър {{filterId}}",
"install_welcome_title": "Добре дошли в AdGuard Home!", "install_welcome_title": "Добре дошли в AdGuard Home!",
"install_welcome_desc": "AdGuard Home e мрежово решение за блокиране на реклами и тракери на DNS ниво. Създадено е за да ви даде пълен контрол над мрежата и всичките ви устройства, без да е необходимо допълнително инсталиране на друг софтуер.", "install_welcome_desc": "AdGuard Home e мрежово решение за блокиране на реклами и тракери на DNS ниво. Създадено е за да ви даде пълен контрол над мрежата и всичките ви устройства, без да е необходимо допълнително инсталиране на друг софтуер.",
@@ -202,7 +201,6 @@
"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": "Служи за автоматично пренасочване от HTTP към HTTPS на страницата за Администрация в AdGuard Home.", "encryption_redirect_desc": "Служи за автоматично пренасочване от HTTP към HTTPS на страницата за Администрация в AdGuard Home.",
"encryption_https": "HTTPS порт", "encryption_https": "HTTPS порт",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Neplatný formát IP", "form_error_ip_format": "Neplatný formát IP",
"form_error_mac_format": "Neplatný formát MAC", "form_error_mac_format": "Neplatný formát MAC",
"form_error_client_id_format": "Neplatný formát ID klienta", "form_error_client_id_format": "Neplatný formát ID klienta",
"form_error_server_name": "Neplatný název serveru",
"form_error_positive": "Musí být větší než 0", "form_error_positive": "Musí být větší než 0",
"form_error_negative": "Musí být rovno nebo větší než 0", "form_error_negative": "Musí být rovno nebo větší než 0",
"range_end_error": "Musí být větší než začátek rozsahu", "range_end_error": "Musí být větší než začátek rozsahu",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID klienta",
"client_id_placeholder": "Zadejte ID klienta",
"client_id_desc": "Různé klienty lze identifikovat pomocí speciálního ID klienta. <a>Zde</a> se můžete dozvědět více o tom, jak klienty identifikovat.",
"download_mobileconfig_doh": "Stáhnout .mobileconfig pro DNS skrze HTTPS", "download_mobileconfig_doh": "Stáhnout .mobileconfig pro DNS skrze HTTPS",
"download_mobileconfig_dot": "Stáhnout .mobileconfig pro DNS skrze TLS", "download_mobileconfig_dot": "Stáhnout .mobileconfig pro DNS skrze TLS",
"download_mobileconfig": "Stáhnout konfigurační soubor",
"plain_dns": "Čisté DNS", "plain_dns": "Čisté DNS",
"form_enter_rate_limit": "Zadejte rychlostní limit", "form_enter_rate_limit": "Zadejte rychlostní limit",
"rate_limit": "Rychlostní limit", "rate_limit": "Rychlostní limit",
@@ -269,7 +276,7 @@
"source_label": "Zdroj", "source_label": "Zdroj",
"found_in_known_domain_db": "Nalezeno v databázi známých domén", "found_in_known_domain_db": "Nalezeno v databázi známých domén",
"category_label": "Kategorie", "category_label": "Kategorie",
"rule_label": "Pravidlo", "rule_label": "Pravidla",
"list_label": "Seznam", "list_label": "Seznam",
"unknown_filter": "Neznámý filtr {{filterId}}", "unknown_filter": "Neznámý filtr {{filterId}}",
"known_tracker": "Známý slídič", "known_tracker": "Známý slídič",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Konfigurace šifrování byla uložena", "encryption_config_saved": "Konfigurace šifrování byla uložena",
"encryption_server": "Název serveru", "encryption_server": "Název serveru",
"encryption_server_enter": "Zadejte název domény", "encryption_server_enter": "Zadejte název domény",
"encryption_server_desc": "Abyste mohli používat protokol HTTPS, musíte zadat název serveru, který odpovídá vašemu certifikátu SSL.", "encryption_server_desc": "Abyste mohli používat HTTPS, musíte zadat název serveru, který odpovídá vašemu certifikátu SSL nebo zástupnému certifikátu. Pokud není pole nastaveno, bude přijímat připojení TLS pro libovolnou doménu.",
"encryption_redirect": "Automaticky přesměrovat na HTTPS", "encryption_redirect": "Automaticky přesměrovat na HTTPS",
"encryption_redirect_desc": "Pokud je zaškrtnuto, AdGuard Home vás automaticky přesměruje z adres HTTP na HTTPS.", "encryption_redirect_desc": "Pokud je zaškrtnuto, AdGuard Home vás automaticky přesměruje z adres HTTP na HTTPS.",
"encryption_https": "HTTPS port", "encryption_https": "HTTPS port",
@@ -386,7 +393,7 @@
"client_edit": "Upravit klienta", "client_edit": "Upravit klienta",
"client_identifier": "Identifikátor", "client_identifier": "Identifikátor",
"ip_address": "IP adresa", "ip_address": "IP adresa",
"client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy, CIDR nebo MAC adresy. Upozorňujeme, že použití MAC jako identifikátoru je možné pouze v případě, že je AdGuard Home také <0>DHCP server</0>", "client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy, CIDR, MAC adresy nebo speciálního ID klienta (může být použito pro DoT/DoH/DoQ). <0>Zde</0> se můžete dozvědět více o tom, jak klienty identifikovat.",
"form_enter_ip": "Zadejte IP", "form_enter_ip": "Zadejte IP",
"form_enter_mac": "Zadejte MAC", "form_enter_mac": "Zadejte MAC",
"form_enter_id": "Zadejte identifikátor", "form_enter_id": "Zadejte identifikátor",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podporuje <1>DNS-přes-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podporuje <1>DNS-přes-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podporuje <1>DNS-přes-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podporuje <1>DNS-přes-HTTPS</1>.",
"setup_dns_privacy_other_5": "Další implementace naleznete <0>zde</0> a <1>zde</1>.", "setup_dns_privacy_other_5": "Další implementace naleznete <0>zde</0> a <1>zde</1>.",
"setup_dns_privacy_ioc_mac": "Konfigurace pro iOS a macOS",
"setup_dns_notice": "Pro použití <1>DNS-přes-HTTPS</1> nebo <1>DNS-přes-TLS</1> potřebujete v nastaveních AdGuard Home <0>nakonfigurovat šifrování</0>.", "setup_dns_notice": "Pro použití <1>DNS-přes-HTTPS</1> nebo <1>DNS-přes-TLS</1> potřebujete v nastaveních AdGuard Home <0>nakonfigurovat šifrování</0>.",
"rewrite_added": "Přesměrování DNS pro „{{key}}“ úspěšně přidáno", "rewrite_added": "Přesměrování DNS pro „{{key}}“ úspěšně přidáno",
"rewrite_deleted": "Přesměrování DNS pro „{{key}}“ úspěšně smazáno", "rewrite_deleted": "Přesměrování DNS pro „{{key}}“ úspěšně smazáno",
@@ -529,7 +537,6 @@
"check_ip": "IP adresy: {{ip}}", "check_ip": "IP adresy: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Důvod: {{reason}}", "check_reason": "Důvod: {{reason}}",
"check_rule": "Pravidlo: {{rule}}",
"check_service": "Název služby: {{service}}", "check_service": "Název služby: {{service}}",
"service_name": "Název služby", "service_name": "Název služby",
"check_not_found": "Nenalezeno ve Vašich seznamech filtrů", "check_not_found": "Nenalezeno ve Vašich seznamech filtrů",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Ugyldigt IP-format", "form_error_ip_format": "Ugyldigt IP-format",
"form_error_mac_format": "Ugyldigt MAC-format", "form_error_mac_format": "Ugyldigt MAC-format",
"form_error_client_id_format": "Ugyldigt klient-ID-format", "form_error_client_id_format": "Ugyldigt klient-ID-format",
"form_error_server_name": "Ugyldigt servernavn",
"form_error_positive": "Skal være større end 0", "form_error_positive": "Skal være større end 0",
"form_error_negative": "Skal være lig med 0 eller større", "form_error_negative": "Skal være lig med 0 eller større",
"range_end_error": "Skal være større end starten af intervallet", "range_end_error": "Skal være større end starten af intervallet",
@@ -247,10 +248,16 @@
"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",
"client_id": "Klient-ID",
"client_id_placeholder": "Indtast klient-ID",
"client_id_desc": "Forskellige klienter kan identificeres ved hjælp af et specielt klient-ID. <a>Her</a> kan du lære mere om, hvordan du identificerer klienter.",
"download_mobileconfig_doh": "Download .mobileconfig til DNS-over-HTTPS", "download_mobileconfig_doh": "Download .mobileconfig til DNS-over-HTTPS",
"download_mobileconfig_dot": "Download .mobileconfig til DNS-over-TLS", "download_mobileconfig_dot": "Download .mobileconfig til DNS-over-TLS",
"download_mobileconfig": "Download konfigurationsfil",
"plain_dns": "Almindelig DNS", "plain_dns": "Almindelig DNS",
"form_enter_rate_limit": "Indtast hyppighedsgrænse", "form_enter_rate_limit": "Indtast hyppighedsgrænse",
"rate_limit": "Hyppighedsgrænse", "rate_limit": "Hyppighedsgrænse",
@@ -269,7 +276,7 @@
"source_label": "Kilde", "source_label": "Kilde",
"found_in_known_domain_db": "Fundet i databasen med kendte domæner.", "found_in_known_domain_db": "Fundet i databasen med kendte domæner.",
"category_label": "Kategori", "category_label": "Kategori",
"rule_label": "Regel", "rule_label": "Regel(regler)",
"list_label": "Liste", "list_label": "Liste",
"unknown_filter": "Ukendt filter {{filterId}}", "unknown_filter": "Ukendt filter {{filterId}}",
"known_tracker": "Kendt tracker", "known_tracker": "Kendt tracker",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Krypteringskonfiguration gemt", "encryption_config_saved": "Krypteringskonfiguration gemt",
"encryption_server": "Servernavn", "encryption_server": "Servernavn",
"encryption_server_enter": "Indtast dit domænenavn", "encryption_server_enter": "Indtast dit domænenavn",
"encryption_server_desc": "For at kunne bruge HTTPS skal du indtaste servernavnet, der matcher dit SSL-certifikat.", "encryption_server_desc": "For at kunne bruge HTTPS skal du indtaste det servernavn, der matcher dit SSL-certifikat eller wildcard-certifikat. Hvis feltet ikke er indstillet, accepterer det TLS-forbindelser til ethvert domæne.",
"encryption_redirect": "Omdiriger automatisk til HTTPS", "encryption_redirect": "Omdiriger automatisk til HTTPS",
"encryption_redirect_desc": "Hvis afkrydset, vil AdGuard Home automatisk omdirigere dig fra HTTP til HTTPS-adresser.", "encryption_redirect_desc": "Hvis afkrydset, vil AdGuard Home automatisk omdirigere dig fra HTTP til HTTPS-adresser.",
"encryption_https": "HTTPS-port", "encryption_https": "HTTPS-port",
@@ -386,7 +393,7 @@
"client_edit": "Rediger Klient", "client_edit": "Rediger Klient",
"client_identifier": "Identifikator", "client_identifier": "Identifikator",
"ip_address": "IP-adresse", "ip_address": "IP-adresse",
"client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen, CIDR eller MAC-adressen. Bemærk, at det kun er muligt at bruge MAC som identifikator, hvis AdGuard Home også er en <0>DHCP-server</0>", "client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen, CIDR eller MAC-adressen eller et specielt klient-ID (kan bruges til DoT/DoH/DoQ). <0>Her</0> kan du lære mere om, hvordan du identificerer klienter.",
"form_enter_ip": "Indtast IP", "form_enter_ip": "Indtast IP",
"form_enter_mac": "Indtast MAC", "form_enter_mac": "Indtast MAC",
"form_enter_id": "Indtast identifikator", "form_enter_id": "Indtast identifikator",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> understøtter <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> understøtter <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> understøtter <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> understøtter <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Du kan finde flere implementeringer <0>her</0> og <1>her</1>.", "setup_dns_privacy_other_5": "Du kan finde flere implementeringer <0>her</0> og <1>her</1>.",
"setup_dns_privacy_ioc_mac": "iOS- og macOS-konfiguration",
"setup_dns_notice": "For at kunne bruge <1>DNS-over-HTTPS</1> eller <1>DNS-over-TLS</1>, skal du <0>konfigurere Krypteringen</0> i indstillingerne i AdGuard Home.", "setup_dns_notice": "For at kunne bruge <1>DNS-over-HTTPS</1> eller <1>DNS-over-TLS</1>, skal du <0>konfigurere Krypteringen</0> i indstillingerne i AdGuard Home.",
"rewrite_added": "DNS-omskrivning for \"{{key}}\" blev tilføjet", "rewrite_added": "DNS-omskrivning for \"{{key}}\" blev tilføjet",
"rewrite_deleted": "DNS-omskrivning for \"{{key}}\" blev slettet", "rewrite_deleted": "DNS-omskrivning for \"{{key}}\" blev slettet",
@@ -529,7 +537,6 @@
"check_ip": "IP-adresser: {{ip}}", "check_ip": "IP-adresser: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Årsag: {{reason}}", "check_reason": "Årsag: {{reason}}",
"check_rule": "Regel: {{rule}}",
"check_service": "Servicenavn: {{service}}", "check_service": "Servicenavn: {{service}}",
"service_name": "Navn på tjeneste", "service_name": "Navn på tjeneste",
"check_not_found": "Ikke fundet i dine filterlister", "check_not_found": "Ikke fundet i dine filterlister",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Ungültiges IPv4-Format", "form_error_ip_format": "Ungültiges IPv4-Format",
"form_error_mac_format": "Ungültiges MAC-Format", "form_error_mac_format": "Ungültiges MAC-Format",
"form_error_client_id_format": "Ungültiges Client-ID-Format", "form_error_client_id_format": "Ungültiges Client-ID-Format",
"form_error_server_name": "Ungültiger Servername",
"form_error_positive": "Muss größer als 0 sein.", "form_error_positive": "Muss größer als 0 sein.",
"form_error_negative": "Muss gleich oder größer als 0 (Null) sein", "form_error_negative": "Muss gleich oder größer als 0 (Null) sein",
"range_end_error": "Muss größer als der Bereichsbeginn sein", "range_end_error": "Muss größer als der Bereichsbeginn sein",
@@ -247,10 +248,16 @@
"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",
"client_id": "Client-ID",
"client_id_placeholder": "Client-ID eingeben",
"client_id_desc": "Verschiedene Clients können durch eine spezielle Client-ID identifiziert werden. <a>Hier</a> können Sie mehr darüber erfahren, wie Sie Clients identifizieren können.",
"download_mobileconfig_doh": ".mobileconfig für DNS-über-HTTPS herunterladen", "download_mobileconfig_doh": ".mobileconfig für DNS-über-HTTPS herunterladen",
"download_mobileconfig_dot": ".mobileconfig für DNS-über-TLS herunterladen", "download_mobileconfig_dot": ".mobileconfig für DNS-über-TLS herunterladen",
"download_mobileconfig": "Konfigurationsdatei herunterladen",
"plain_dns": "Einfaches DNS", "plain_dns": "Einfaches DNS",
"form_enter_rate_limit": "Begrenzungswert eingeben", "form_enter_rate_limit": "Begrenzungswert eingeben",
"rate_limit": "Begrenzungswert", "rate_limit": "Begrenzungswert",
@@ -269,7 +276,7 @@
"source_label": "Quelle", "source_label": "Quelle",
"found_in_known_domain_db": "In der Datenbank der bekannten Domains gefunden.", "found_in_known_domain_db": "In der Datenbank der bekannten Domains gefunden.",
"category_label": "Kategorie", "category_label": "Kategorie",
"rule_label": "Regel", "rule_label": "Regel(n)",
"list_label": "Liste", "list_label": "Liste",
"unknown_filter": "Unbekannter Filter {{filterId}}", "unknown_filter": "Unbekannter Filter {{filterId}}",
"known_tracker": "Bekannte Tracker", "known_tracker": "Bekannte Tracker",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> unterstützt <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> unterstützt <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> unterstützt <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> unterstützt <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Weitere Umsetzungen finden Sie <0>hier</0> und <1>hier</1>.", "setup_dns_privacy_other_5": "Weitere Umsetzungen finden Sie <0>hier</0> und <1>hier</1>.",
"setup_dns_privacy_ioc_mac": "Konfiguration für iOS und macOS",
"setup_dns_notice": "Um <1>DNS-over-HTTTPS</1> oder <1>DNS-over-TLS</1> verwenden zu können, müssen Sie in den AdGuard Home Einstellungen die <0>Verschlüsselung konfigurieren</0>.", "setup_dns_notice": "Um <1>DNS-over-HTTTPS</1> oder <1>DNS-over-TLS</1> verwenden zu können, müssen Sie in den AdGuard Home Einstellungen die <0>Verschlüsselung konfigurieren</0>.",
"rewrite_added": "DNS-Umschreibung für „{{key}}” erfolgreich hinzugefügt", "rewrite_added": "DNS-Umschreibung für „{{key}}” erfolgreich hinzugefügt",
"rewrite_deleted": "DNS-Umschreibung für „{{key}}” erfolgreich entfernt", "rewrite_deleted": "DNS-Umschreibung für „{{key}}” erfolgreich entfernt",
@@ -529,7 +537,6 @@
"check_ip": "IP-Adressen: {{ip}}", "check_ip": "IP-Adressen: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Grund: {{reason}}", "check_reason": "Grund: {{reason}}",
"check_rule": "Regel: {{rule}}",
"check_service": "Dienstname: {{service}}", "check_service": "Dienstname: {{service}}",
"service_name": "Name des Dienstes", "service_name": "Name des Dienstes",
"check_not_found": "Nicht in Ihren Filterlisten enthalten", "check_not_found": "Nicht in Ihren Filterlisten enthalten",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Invalid IP format", "form_error_ip_format": "Invalid IP format",
"form_error_mac_format": "Invalid MAC format", "form_error_mac_format": "Invalid MAC format",
"form_error_client_id_format": "Invalid client ID format", "form_error_client_id_format": "Invalid client ID format",
"form_error_server_name": "Invalid server name",
"form_error_positive": "Must be greater than 0", "form_error_positive": "Must be greater than 0",
"form_error_negative": "Must be equal to 0 or greater", "form_error_negative": "Must be equal to 0 or greater",
"range_end_error": "Must be greater than range start", "range_end_error": "Must be greater than range start",
@@ -247,10 +248,16 @@
"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",
"client_id": "Client ID",
"client_id_placeholder": "Enter client ID",
"client_id_desc": "Different clients can be identified by a special client ID. <a>Here</a> you can learn more about how to identify clients.",
"download_mobileconfig_doh": "Download .mobileconfig for DNS-over-HTTPS", "download_mobileconfig_doh": "Download .mobileconfig for DNS-over-HTTPS",
"download_mobileconfig_dot": "Download .mobileconfig for DNS-over-TLS", "download_mobileconfig_dot": "Download .mobileconfig for DNS-over-TLS",
"download_mobileconfig": "Download configuration file",
"plain_dns": "Plain DNS", "plain_dns": "Plain DNS",
"form_enter_rate_limit": "Enter rate limit", "form_enter_rate_limit": "Enter rate limit",
"rate_limit": "Rate limit", "rate_limit": "Rate limit",
@@ -269,7 +276,7 @@
"source_label": "Source", "source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.", "found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category", "category_label": "Category",
"rule_label": "Rule", "rule_label": "Rule(s)",
"list_label": "List", "list_label": "List",
"unknown_filter": "Unknown filter {{filterId}}", "unknown_filter": "Unknown filter {{filterId}}",
"known_tracker": "Known tracker", "known_tracker": "Known tracker",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Encryption config saved", "encryption_config_saved": "Encryption config saved",
"encryption_server": "Server name", "encryption_server": "Server name",
"encryption_server_enter": "Enter your domain name", "encryption_server_enter": "Enter your domain name",
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.", "encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate or wildcard certificate. If the field is not set, it will accept TLS connections for any domain.",
"encryption_redirect": "Redirect to HTTPS automatically", "encryption_redirect": "Redirect to HTTPS automatically",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.", "encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "HTTPS port", "encryption_https": "HTTPS port",
@@ -386,7 +393,7 @@
"client_edit": "Edit Client", "client_edit": "Edit Client",
"client_identifier": "Identifier", "client_identifier": "Identifier",
"ip_address": "IP address", "ip_address": "IP address",
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address. Please note that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>", "client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address or a special client ID (can be used for DoT/DoH/DoQ). <0>Here</0> you can learn more about how to identify clients.",
"form_enter_ip": "Enter IP", "form_enter_ip": "Enter IP",
"form_enter_mac": "Enter MAC", "form_enter_mac": "Enter MAC",
"form_enter_id": "Enter identifier", "form_enter_id": "Enter identifier",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.", "setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.",
"setup_dns_privacy_ioc_mac": "iOS and macOS configuration",
"setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings.", "setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings.",
"rewrite_added": "DNS rewrite for \"{{key}}\" successfully added", "rewrite_added": "DNS rewrite for \"{{key}}\" successfully added",
"rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted", "rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted",
@@ -529,7 +537,6 @@
"check_ip": "IP addresses: {{ip}}", "check_ip": "IP addresses: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Reason: {{reason}}", "check_reason": "Reason: {{reason}}",
"check_rule": "Rule: {{rule}}",
"check_service": "Service name: {{service}}", "check_service": "Service name: {{service}}",
"service_name": "Service name", "service_name": "Service name",
"check_not_found": "Not found in your filter lists", "check_not_found": "Not found in your filter lists",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Formato IP no válido", "form_error_ip_format": "Formato IP no válido",
"form_error_mac_format": "Formato MAC no válido", "form_error_mac_format": "Formato MAC no válido",
"form_error_client_id_format": "Formato de ID de cliente no válido", "form_error_client_id_format": "Formato de ID de cliente no válido",
"form_error_server_name": "Nombre de servidor no válido",
"form_error_positive": "Debe ser mayor que 0", "form_error_positive": "Debe ser mayor que 0",
"form_error_negative": "Debe ser igual o mayor que 0", "form_error_negative": "Debe ser igual o mayor que 0",
"range_end_error": "Debe ser mayor que el inicio de rango", "range_end_error": "Debe ser mayor que el inicio de rango",
@@ -247,10 +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 mediante QUIC",
"client_id": "ID de cliente",
"client_id_placeholder": "Ingresa el ID del cliente",
"client_id_desc": "Diferentes clientes pueden ser identificados por un ID de cliente especial. <a>Aquí</a> puedes obtener más información sobre cómo identificar clientes.",
"download_mobileconfig_doh": "Descargar .mobileconfig para DNS mediante HTTPS", "download_mobileconfig_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 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",
@@ -330,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.", "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",
@@ -386,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 y CIDR. Ten en cuenta que el uso de MAC como identificador solo es posible si AdGuard Home también es un <0>servidor DHCP</0>", "client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP, MAC, CIDR o un ID de cliente especial (puede ser utilizado para DoT/DoH/DoQ). <0>Aquí</0> puedes obtener más información sobre cómo identificar clientes.",
"form_enter_ip": "Ingresa la IP", "form_enter_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",
@@ -430,6 +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": "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",
@@ -529,7 +537,6 @@
"check_ip": "Direcciones IP: {{ip}}", "check_ip": "Direcciones IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Razón: {{reason}}", "check_reason": "Razón: {{reason}}",
"check_rule": "Regla: {{rule}}",
"check_service": "Nombre del servicio: {{service}}", "check_service": "Nombre del servicio: {{service}}",
"service_name": "Nombre del servicio", "service_name": "Nombre del servicio",
"check_not_found": "No se ha encontrado en tus listas de filtros", "check_not_found": "No se ha encontrado en tus listas de filtros",

View File

@@ -239,7 +239,6 @@
"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": "ردیاب های شناخته شده",
@@ -300,7 +299,6 @@
"encryption_config_saved": "پیکربندی رمزگذاری ذخیره شد", "encryption_config_saved": "پیکربندی رمزگذاری ذخیره شد",
"encryption_server": "نام سرور", "encryption_server": "نام سرور",
"encryption_server_enter": "نام دامنه خود را وارد کنید", "encryption_server_enter": "نام دامنه خود را وارد کنید",
"encryption_server_desc": "به منظور استفاده از HTTPS،شما باید نام سرور مطابق با گواهینامه اِس اِس اِل را وارد کنید.",
"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",
@@ -354,7 +352,6 @@
"client_edit": "ویرایش کلاینت", "client_edit": "ویرایش کلاینت",
"client_identifier": "احراز با", "client_identifier": "احراز با",
"ip_address": "آدرس آی پی", "ip_address": "آدرس آی پی",
"client_identifier_desc": "کلاینت میتواند با آدرس آی پی یا آدرس مَک احراز شود. لطفا توجه کنید،که استفاده از مَک بعنوان عامل احراز زمانی امکان دارد که AdGuard Home نیز <0>سرور DHCP </0> باشد",
"form_enter_ip": "آی پی را وارد کنید", "form_enter_ip": "آی پی را وارد کنید",
"form_enter_mac": "مَک را وارد کنید", "form_enter_mac": "مَک را وارد کنید",
"form_enter_id": "خطای احرازکننده", "form_enter_id": "خطای احرازکننده",
@@ -487,7 +484,6 @@
"check_ip": "آدرس آی پی: {{ip}}", "check_ip": "آدرس آی پی: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "علت: {{reason}}", "check_reason": "علت: {{reason}}",
"check_rule": "دستور: {{rule}}",
"check_service": "نام سرویس: {{service}}", "check_service": "نام سرویس: {{service}}",
"check_not_found": "در لیست فیلترهای شما یافت نشد", "check_not_found": "در لیست فیلترهای شما یافت نشد",
"client_confirm_block": "آیا واقعا میخواهید کلاینت \"{{ip}}\" را مسدود کنید؟", "client_confirm_block": "آیا واقعا میخواهید کلاینت \"{{ip}}\" را مسدود کنید؟",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Format IPv4 invalide", "form_error_ip_format": "Format IPv4 invalide",
"form_error_mac_format": "Format MAC invalide", "form_error_mac_format": "Format MAC invalide",
"form_error_client_id_format": "Format d'ID client non valide", "form_error_client_id_format": "Format d'ID client non valide",
"form_error_server_name": "Nom de serveur invalide",
"form_error_positive": "Doit être supérieur à 0", "form_error_positive": "Doit être supérieur à 0",
"form_error_negative": "Doit être égal à 0 ou supérieur", "form_error_negative": "Doit être égal à 0 ou supérieur",
"range_end_error": "Doit être supérieur au début de la gamme", "range_end_error": "Doit être supérieur au début de la gamme",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID du client",
"client_id_placeholder": "Saisissez le ID du client",
"client_id_desc": "Les clients différents peuvent être identifiés par aide d'un ID client spécial. Vous trouverez plus d'information sur l'identification des clients <a>ici</a> .",
"download_mobileconfig_doh": "Télécharger .mobileconfig pour DNS-sur-HTTPS", "download_mobileconfig_doh": "Télécharger .mobileconfig pour DNS-sur-HTTPS",
"download_mobileconfig_dot": "Télécharger .mobileconfig pour DNS-sur-TLS", "download_mobileconfig_dot": "Télécharger .mobileconfig pour DNS-sur-TLS",
"download_mobileconfig": "Télécharger le fichier de configuration",
"plain_dns": "DNS brut", "plain_dns": "DNS brut",
"form_enter_rate_limit": "Entrez la limite de taux", "form_enter_rate_limit": "Entrez la limite de taux",
"rate_limit": "Limite de taux", "rate_limit": "Limite de taux",
@@ -269,7 +276,7 @@
"source_label": "Source", "source_label": "Source",
"found_in_known_domain_db": "Trouvé dans la base de données des domaines connus", "found_in_known_domain_db": "Trouvé dans la base de données des domaines connus",
"category_label": "Catégorie", "category_label": "Catégorie",
"rule_label": "Règle", "rule_label": "Règle(s)",
"list_label": "Liste", "list_label": "Liste",
"unknown_filter": "Filtre inconnu {{filterId}}", "unknown_filter": "Filtre inconnu {{filterId}}",
"known_tracker": "Pisteur connu", "known_tracker": "Pisteur connu",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Configuration de chiffrement enregistrée", "encryption_config_saved": "Configuration de chiffrement enregistrée",
"encryption_server": "Nom du serveur", "encryption_server": "Nom du serveur",
"encryption_server_enter": "Entrez votre nom de domaine", "encryption_server_enter": "Entrez votre nom de domaine",
"encryption_server_desc": "Pour utiliser HTTPS, vous devez entrer le nom du serveur qui correspond à votre certificat SSL.", "encryption_server_desc": "Pour utiliser HTTPS, vous devez saisir le nom du serveur qui correspond à votre certificat SSL ou wildcard. Si le champ n'est pas configuré, les connexions TLS pour tous les domaines seront acceptées.",
"encryption_redirect": "Redirection automatiquement vers HTTPS", "encryption_redirect": "Redirection automatiquement vers HTTPS",
"encryption_redirect_desc": "Si coché, AdGuard Home vous redirigera automatiquement d'adresses HTTP vers HTTPS.", "encryption_redirect_desc": "Si coché, AdGuard Home vous redirigera automatiquement d'adresses HTTP vers HTTPS.",
"encryption_https": "Port HTTPS", "encryption_https": "Port HTTPS",
@@ -386,7 +393,7 @@
"client_edit": "Modifier le client", "client_edit": "Modifier le client",
"client_identifier": "Identifiant", "client_identifier": "Identifiant",
"ip_address": "Adresse IP", "ip_address": "Adresse IP",
"client_identifier_desc": "Les clients peuvent être identifiés par les adresses IP ou MAC. Veuillez noter que l'utilisation de l'adresse MAC comme identifiant est possible uniquement si AdGuard Home est aussi un <0>serveur DHCP</0>", "client_identifier_desc": "Les clients peuvent être identifiés par les adresses IP, CIDR, MAC ou un ID client spécial (qui peut être utilisé pour DoT/DoH/DoQ). Vous trouverez plus d'information sur l'identification des clients <0>ici</0> .",
"form_enter_ip": "Saisissez l'IP", "form_enter_ip": "Saisissez l'IP",
"form_enter_mac": "Saisissez MAC", "form_enter_mac": "Saisissez MAC",
"form_enter_id": "Entrer identifiant", "form_enter_id": "Entrer identifiant",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supporte le <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supporte le <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supporte le <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supporte le <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Vous trouverez plus d'implémentations <0>ici</0> et <1>ici</1>.", "setup_dns_privacy_other_5": "Vous trouverez plus d'implémentations <0>ici</0> et <1>ici</1>.",
"setup_dns_privacy_ioc_mac": "Configuration sur iOS et macOS",
"setup_dns_notice": "Pour utiliser le <1>DNS-over-HTTPS</1> ou le <1>DNS-over-TLS</1>, vous devez <0>configurer le Chiffrement</0> dans les paramètres de AdGuard Home.", "setup_dns_notice": "Pour utiliser le <1>DNS-over-HTTPS</1> ou le <1>DNS-over-TLS</1>, vous devez <0>configurer le Chiffrement</0> dans les paramètres de AdGuard Home.",
"rewrite_added": "Réécriture DNS pour \"{{key}}\" ajoutée", "rewrite_added": "Réécriture DNS pour \"{{key}}\" ajoutée",
"rewrite_deleted": "Réécriture DNS pour \"{{key}}\" supprimée", "rewrite_deleted": "Réécriture DNS pour \"{{key}}\" supprimée",
@@ -529,7 +537,6 @@
"check_ip": "Adresses IP : {{ip}}", "check_ip": "Adresses IP : {{ip}}",
"check_cname": "CNAME : {{cname}}", "check_cname": "CNAME : {{cname}}",
"check_reason": "Raison : {{reason}}", "check_reason": "Raison : {{reason}}",
"check_rule": "Règle : {{rule}}",
"check_service": "Nom du service : {{service}}", "check_service": "Nom du service : {{service}}",
"service_name": "Nom du service", "service_name": "Nom du service",
"check_not_found": "Introuvable dans vos listes de filtres", "check_not_found": "Introuvable dans vos listes de filtres",

View File

@@ -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",
@@ -430,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",
@@ -529,7 +537,6 @@
"check_ip": "IP adrese: {{ip}}", "check_ip": "IP adrese: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Razlog: {{reason}}", "check_reason": "Razlog: {{reason}}",
"check_rule": "Pravilo: {{rule}}",
"check_service": "Naziv usluge: {{service}}", "check_service": "Naziv usluge: {{service}}",
"service_name": "Naziv usluge", "service_name": "Naziv usluge",
"check_not_found": "Nije pronađeno na vašoj listi filtara", "check_not_found": "Nije pronađeno na vašoj listi filtara",

View File

@@ -269,7 +269,6 @@
"source_label": "Forrás", "source_label": "Forrás",
"found_in_known_domain_db": "Benne van az ismert domainek listájában.", "found_in_known_domain_db": "Benne van az ismert domainek listájában.",
"category_label": "Kategória", "category_label": "Kategória",
"rule_label": "Szabály",
"list_label": "Lista", "list_label": "Lista",
"unknown_filter": "Ismeretlen szűrő: {{filterId}}", "unknown_filter": "Ismeretlen szűrő: {{filterId}}",
"known_tracker": "Ismert követő", "known_tracker": "Ismert követő",
@@ -330,7 +329,6 @@
"encryption_config_saved": "Titkosítási beállítások mentve", "encryption_config_saved": "Titkosítási beállítások mentve",
"encryption_server": "Szerver neve", "encryption_server": "Szerver neve",
"encryption_server_enter": "Adja meg az Ön domain címét", "encryption_server_enter": "Adja meg az Ön domain címét",
"encryption_server_desc": "A HTTPS használatához be kell írnia egy, az SSL-tanúsítvánnyal megegyező kiszolgálónevet.",
"encryption_redirect": "Automatikus átirányítás HTTPS kapcsolatra", "encryption_redirect": "Automatikus átirányítás HTTPS kapcsolatra",
"encryption_redirect_desc": "Ha be van jelölve, az AdGuard Home automatikusan átirányítja a HTTP kapcsolatokat a biztonságos HTTPS protokollra.", "encryption_redirect_desc": "Ha be van jelölve, az AdGuard Home automatikusan átirányítja a HTTP kapcsolatokat a biztonságos HTTPS protokollra.",
"encryption_https": "HTTPS port", "encryption_https": "HTTPS port",
@@ -386,7 +384,6 @@
"client_edit": "Kliens módosítása", "client_edit": "Kliens módosítása",
"client_identifier": "Azonosító", "client_identifier": "Azonosító",
"ip_address": "IP cím", "ip_address": "IP cím",
"client_identifier_desc": "A klienseket be lehet azonosítani IP-cím, CIDR, valamint MAC-cím alapján. Kérjük, vegye figyelembe, hogy a MAC-cím alapján történő azonosítás csak akkor működik, ha az AdGuard Home egyben <0>DHCP szerverként</0> is funkcionál",
"form_enter_ip": "IP-cím megadása", "form_enter_ip": "IP-cím megadása",
"form_enter_mac": "MAC-cím megadása", "form_enter_mac": "MAC-cím megadása",
"form_enter_id": "Azonosító megadása", "form_enter_id": "Azonosító megadása",
@@ -529,7 +526,6 @@
"check_ip": "IP-címek: {{ip}}", "check_ip": "IP-címek: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Indok: {{reason}}", "check_reason": "Indok: {{reason}}",
"check_rule": "Szabály: {{rule}}",
"check_service": "Szolgáltatás neve: {{service}}", "check_service": "Szolgáltatás neve: {{service}}",
"service_name": "Szolgáltatás neve", "service_name": "Szolgáltatás neve",
"check_not_found": "Nem található az Ön szűrőlistái között", "check_not_found": "Nem található az Ön szűrőlistái között",

View File

@@ -269,7 +269,6 @@
"source_label": "Sumber", "source_label": "Sumber",
"found_in_known_domain_db": "Ditemukan di database domain dikenal", "found_in_known_domain_db": "Ditemukan di database domain dikenal",
"category_label": "Kategori", "category_label": "Kategori",
"rule_label": "Aturan",
"list_label": "Daftar", "list_label": "Daftar",
"unknown_filter": "Penyaringan {{filterId}} tidak dikenal", "unknown_filter": "Penyaringan {{filterId}} tidak dikenal",
"known_tracker": "Pelacak yang dikenal", "known_tracker": "Pelacak yang dikenal",
@@ -330,7 +329,6 @@
"encryption_config_saved": "Pengaturan enkripsi telah tersimpan", "encryption_config_saved": "Pengaturan enkripsi telah tersimpan",
"encryption_server": "Nama server", "encryption_server": "Nama server",
"encryption_server_enter": "Masukkan nama domain anda", "encryption_server_enter": "Masukkan nama domain anda",
"encryption_server_desc": "Untuk menggunakan HTTPS, Anda harus memasukkan nama server yang cocok dengan sertifikat SSL Anda.",
"encryption_redirect": "Alihkan ke HTTPS secara otomatis", "encryption_redirect": "Alihkan ke HTTPS secara otomatis",
"encryption_redirect_desc": "Jika dicentang, AdGuard Home akan secara otomatis mengarahkan anda dari HTTP ke alamat HTTPS.", "encryption_redirect_desc": "Jika dicentang, AdGuard Home akan secara otomatis mengarahkan anda dari HTTP ke alamat HTTPS.",
"encryption_https": "Port HTTPS", "encryption_https": "Port HTTPS",
@@ -386,7 +384,6 @@
"client_edit": "Ubah Klien", "client_edit": "Ubah Klien",
"client_identifier": "Identifikasi", "client_identifier": "Identifikasi",
"ip_address": "Alamat IP", "ip_address": "Alamat IP",
"client_identifier_desc": "Klien dapat diidentifikasi dengan alamat IP atau alamat MAC. Harap dicatat bahwa menggunakan MAC sebagai pengidentifikasi hanya dimungkinkan jika AdGuard Home juga merupakan <0>server DHCP</0>",
"form_enter_ip": "Masukkan IP", "form_enter_ip": "Masukkan IP",
"form_enter_mac": "Masukkan MAC", "form_enter_mac": "Masukkan MAC",
"form_enter_id": "Masukkan pengenal", "form_enter_id": "Masukkan pengenal",
@@ -529,7 +526,6 @@
"check_ip": "Alamat IP: {{ip}}", "check_ip": "Alamat IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Alasan: {{reason}}", "check_reason": "Alasan: {{reason}}",
"check_rule": "Aturan: {{rule}}",
"check_service": "Nama layanan: {{service}}", "check_service": "Nama layanan: {{service}}",
"service_name": "Nama layanan", "service_name": "Nama layanan",
"check_not_found": "Tidak di temukan di daftar penyaringan anda", "check_not_found": "Tidak di temukan di daftar penyaringan anda",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Formato IPv4 non valido", "form_error_ip_format": "Formato IPv4 non valido",
"form_error_mac_format": "Formato MAC non valido", "form_error_mac_format": "Formato MAC non valido",
"form_error_client_id_format": "Formato ID cliente non valido", "form_error_client_id_format": "Formato ID cliente non valido",
"form_error_server_name": "Nome server non valido",
"form_error_positive": "Deve essere maggiore di 0", "form_error_positive": "Deve essere maggiore di 0",
"form_error_negative": "Deve essere maggiore o uguale a 0 (zero)", "form_error_negative": "Deve essere maggiore o uguale a 0 (zero)",
"range_end_error": "Deve essere maggiore dell'intervallo di inizio", "range_end_error": "Deve essere maggiore dell'intervallo di inizio",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID client",
"client_id_placeholder": "Inserisci ID client",
"client_id_desc": "Client differenti possono essere identificati da uno speciale ID. <a>Qui</a> potrai saperne di più sui metodi per identificarli.",
"download_mobileconfig_doh": "Scarica .mobileconfig per DNS su HTTPS", "download_mobileconfig_doh": "Scarica .mobileconfig per DNS su HTTPS",
"download_mobileconfig_dot": "Scarica .mobileconfig per DNS su TLS", "download_mobileconfig_dot": "Scarica .mobileconfig per DNS su TLS",
"download_mobileconfig": "Scarica file di configurazione",
"plain_dns": "DNS semplice", "plain_dns": "DNS semplice",
"form_enter_rate_limit": "Imposta limite delle richieste", "form_enter_rate_limit": "Imposta limite delle richieste",
"rate_limit": "Limite delle richieste", "rate_limit": "Limite delle richieste",
@@ -269,7 +276,7 @@
"source_label": "Fonte", "source_label": "Fonte",
"found_in_known_domain_db": "Trovato nel database dei domini conosciuti.", "found_in_known_domain_db": "Trovato nel database dei domini conosciuti.",
"category_label": "Categoria", "category_label": "Categoria",
"rule_label": "Regola", "rule_label": "Regola(e)",
"list_label": "Lista", "list_label": "Lista",
"unknown_filter": "Filtro sconosciuto {{filterId}}", "unknown_filter": "Filtro sconosciuto {{filterId}}",
"known_tracker": "Tracker conosciuto", "known_tracker": "Tracker conosciuto",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Configurazione della crittografia salvata", "encryption_config_saved": "Configurazione della crittografia salvata",
"encryption_server": "Nome server", "encryption_server": "Nome server",
"encryption_server_enter": "Inserisci il tuo nome di dominio", "encryption_server_enter": "Inserisci il tuo nome di dominio",
"encryption_server_desc": "Per utilizzare HTTPS, è necessario inserire il nome del server che corrisponde al certificato SSL.", "encryption_server_desc": "Per utilizzare HTTPS, è necessario immettere il nome del server che corrisponde al certificato SSL o al certificato wildcard. Se il campo risulterà vuoto, accetterà connessioni TLS per qualsiasi dominio.",
"encryption_redirect": "Reindirizza automaticamente a HTTPS", "encryption_redirect": "Reindirizza automaticamente a HTTPS",
"encryption_redirect_desc": "Se selezionato, AdGuard Home ti reindirizzerà automaticamente da indirizzi HTTP a HTTPS.", "encryption_redirect_desc": "Se selezionato, AdGuard Home ti reindirizzerà automaticamente da indirizzi HTTP a HTTPS.",
"encryption_https": "Porta HTTPS", "encryption_https": "Porta HTTPS",
@@ -386,7 +393,7 @@
"client_edit": "Modifica Client", "client_edit": "Modifica Client",
"client_identifier": "Identificatore", "client_identifier": "Identificatore",
"ip_address": "Indirizzo IP", "ip_address": "Indirizzo IP",
"client_identifier_desc": "I client possono essere identificati dall indirizzo IP o dall' indirizzo MAC. Nota che l' utilizzo dell' indirizzo MAC come identificatore è consentito solo se AdGuard Home è anche il <0>server DHCP</0>", "client_identifier_desc": "I client possono essere identificati dall'indirizzo IP, CIDR, indirizzo MAC o un ID speciale (che può essere utilizzato per DoT/DoH/DoQ). <0>Qui</0> potrai saperne di più sui metodi per identificarli.",
"form_enter_ip": "Inserisci IP", "form_enter_ip": "Inserisci IP",
"form_enter_mac": "Inserisci MAC", "form_enter_mac": "Inserisci MAC",
"form_enter_id": "Inserisci identificatore", "form_enter_id": "Inserisci identificatore",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supporta <1>DNS su HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supporta <1>DNS su HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supporta <1>DNS su HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supporta <1>DNS su HTTPS</1>.",
"setup_dns_privacy_other_5": "Troverai più implementazioni <0>qui</0> e <1>qui</1>.", "setup_dns_privacy_other_5": "Troverai più implementazioni <0>qui</0> e <1>qui</1>.",
"setup_dns_privacy_ioc_mac": "configurazione iOS e macOS",
"setup_dns_notice": "Per utilizzare <1>DNS su HTTPS</1> o <1>DNS su TLS</1>, è necessario <0>configurare la crittografia</0> nelle impostazioni di AdGuard Home.", "setup_dns_notice": "Per utilizzare <1>DNS su HTTPS</1> o <1>DNS su TLS</1>, è necessario <0>configurare la crittografia</0> nelle impostazioni di AdGuard Home.",
"rewrite_added": "Riscrittura DNS per \"{{key}}\" aggiunta correttamente", "rewrite_added": "Riscrittura DNS per \"{{key}}\" aggiunta correttamente",
"rewrite_deleted": "La riscrittura DNS per \"{{key}}\" è stata eliminata correttamente", "rewrite_deleted": "La riscrittura DNS per \"{{key}}\" è stata eliminata correttamente",
@@ -529,7 +537,6 @@
"check_ip": "Indirizzi IP: {{ip}}", "check_ip": "Indirizzi IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Motivo: {{reason}}", "check_reason": "Motivo: {{reason}}",
"check_rule": "Regola: {{rule}}",
"check_service": "Nome servizio: {{service}}", "check_service": "Nome servizio: {{service}}",
"service_name": "Nome servizio", "service_name": "Nome servizio",
"check_not_found": "Non trovato negli elenchi dei filtri", "check_not_found": "Non trovato negli elenchi dei filtri",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "IPv4フォーマットではありません", "form_error_ip_format": "IPv4フォーマットではありません",
"form_error_mac_format": "MACフォーマットではありません", "form_error_mac_format": "MACフォーマットではありません",
"form_error_client_id_format": "Client IDの形式が無効です", "form_error_client_id_format": "Client 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クライアントID",
"client_id_placeholder": "クライアントIDを入力してください",
"client_id_desc": "それぞれのクライアントは、特別なクライアントIDで識別できます。 <a>ここ</a>では、クライアントを特定する方法について詳しく知ることができます。",
"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": "頻度制限",
@@ -330,7 +337,7 @@
"encryption_config_saved": "暗号化の設定を保存しました", "encryption_config_saved": "暗号化の設定を保存しました",
"encryption_server": "サーバ名", "encryption_server": "サーバ名",
"encryption_server_enter": "ドメイン名を入力してください", "encryption_server_enter": "ドメイン名を入力してください",
"encryption_server_desc": "HTTPSを使用するには、SSL証明書と一致するサーバ名を入力する必要があります。", "encryption_server_desc": "HTTPSを使用するには、SSL証明書またはワイルドカード証明書と一致するサーバ名を入力する必要があります。このフィールドが設定されていない場合は、任意のドメインのTLS接続を受け入れます。",
"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 ポート",
@@ -386,7 +393,7 @@
"client_edit": "クライアントの編集", "client_edit": "クライアントの編集",
"client_identifier": "識別子", "client_identifier": "識別子",
"ip_address": "IPアドレス", "ip_address": "IPアドレス",
"client_identifier_desc": "クライアントはIPアドレスまたはMACアドレスで識別できます。AdGuard Homeが<0>DHCPサーバ</0>でもある場合にのみ、識別子としてMACを使用することが可能であることにご注意ください。", "client_identifier_desc": "クライアントはIPアドレス、CIDR、MACアドレス、または特別なクライアントID(DoT/DoH/DoQで使用可能)によって識別することができます。<0>ここ</0>では、クライアントの識別方法についてより詳しく説明しています。",
"form_enter_ip": "IPアドレスを入力してください", "form_enter_ip": "IPアドレスを入力してください",
"form_enter_mac": "MACアドレスを入力してください", "form_enter_mac": "MACアドレスを入力してください",
"form_enter_id": "識別子を入力してください", "form_enter_id": "識別子を入力してください",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0>は<1>DNS-over-HTTPS</1>をサポートします。", "setup_dns_privacy_other_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書き換え情報を削除完了しました",
@@ -529,7 +537,6 @@
"check_ip": "IPアドレス: {{ip}}", "check_ip": "IPアドレス: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "理由: {{reason}}", "check_reason": "理由: {{reason}}",
"check_rule": "ルール: {{rule}}",
"check_service": "サービス名: {{service}}", "check_service": "サービス名: {{service}}",
"service_name": "サービス名", "service_name": "サービス名",
"check_not_found": "フィルタ一覧には見つかりません", "check_not_found": "フィルタ一覧には見つかりません",

View File

@@ -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를 기반으로 구분됩니다. <a>여기</a>에서 클라이언트를 구분하는 방법을 자세히 알아보세요.",
"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,7 +393,7 @@
"client_edit": "클라이언트 수정", "client_edit": "클라이언트 수정",
"client_identifier": "식별자", "client_identifier": "식별자",
"ip_address": "IP 주소", "ip_address": "IP 주소",
"client_identifier_desc": "사용자는 IP 주소 또는 MAC 주소로 식별할 수 있지만 AdGuard Home이 <0>DHCP 서버인 </0> 경우에만 사용자는 MAC 주소로 식별할 수 있습니다.", "client_identifier_desc": "클라이언트는 IP 주소, CIDR, MAC 주소 또는 특수 클라이언트 ID로 식별할 수 있습니다 (DoT/DoH/DoQ에 사용 가능). <0>여기에서</0> 클라이언트를 식별하는 방법에 대한 자세한 내용은 확인하실 수 있습니다.",
"form_enter_ip": "IP 입력", "form_enter_ip": "IP 입력",
"form_enter_mac": "MAC 입력", "form_enter_mac": "MAC 입력",
"form_enter_id": "식별자 입력", "form_enter_id": "식별자 입력",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> <1>DNS-over-HTTPS</1> 지원합니다.", "setup_dns_privacy_other_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 수정 정보를 성공적으로 삭제 됩니다.",
@@ -529,7 +537,6 @@
"check_ip": "IP 주소: {{ip}}", "check_ip": "IP 주소: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "이유: {{reason}}", "check_reason": "이유: {{reason}}",
"check_rule": "규칙: {{rule}}",
"check_service": "서비스 이름: {{service}}", "check_service": "서비스 이름: {{service}}",
"service_name": "서비스 이름", "service_name": "서비스 이름",
"check_not_found": "필터 목록에서 찾을 수 없음", "check_not_found": "필터 목록에서 찾을 수 없음",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Ongeldig IPv4 formaat", "form_error_ip_format": "Ongeldig IPv4 formaat",
"form_error_mac_format": "Ongeldig MAC formaat.", "form_error_mac_format": "Ongeldig MAC formaat.",
"form_error_client_id_format": "Opmaak cliënt-ID is ongeldig", "form_error_client_id_format": "Opmaak cliënt-ID is ongeldig",
"form_error_server_name": "Ongeldige servernaam",
"form_error_positive": "Moet groter zijn dan 0", "form_error_positive": "Moet groter zijn dan 0",
"form_error_negative": "Moet 0 of hoger dan 0 zijn", "form_error_negative": "Moet 0 of hoger dan 0 zijn",
"range_end_error": "Moet groter zijn dan het startbereik", "range_end_error": "Moet groter zijn dan het startbereik",
@@ -247,10 +248,16 @@
"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",
"client_id": "Apparaat-ID",
"client_id_placeholder": "Apparaat-ID invoeren",
"client_id_desc": "Verschillende apparaten kunnen worden geïdentificeerd door hun specifiek apparaat-ID. <a>Hier</a> vind je meer informatie over het identificeren van apparaten.",
"download_mobileconfig_doh": ".mobileconfig voor DNS-via-HTTPS downloaden", "download_mobileconfig_doh": ".mobileconfig voor DNS-via-HTTPS downloaden",
"download_mobileconfig_dot": ".mobileconfig voor DNS-via-TLS downloaden", "download_mobileconfig_dot": ".mobileconfig voor DNS-via-TLS downloaden",
"download_mobileconfig": "Configuratiebestand downloaden",
"plain_dns": "Gewone DNS", "plain_dns": "Gewone DNS",
"form_enter_rate_limit": "Voer ratio limiet in", "form_enter_rate_limit": "Voer ratio limiet in",
"rate_limit": "Ratio limiet", "rate_limit": "Ratio limiet",
@@ -269,7 +276,7 @@
"source_label": "Bron", "source_label": "Bron",
"found_in_known_domain_db": "Gevonden in de bekende domeingegevensbank.", "found_in_known_domain_db": "Gevonden in de bekende domeingegevensbank.",
"category_label": "Categorie", "category_label": "Categorie",
"rule_label": "Regel", "rule_label": "Regel(s)",
"list_label": "Lijst", "list_label": "Lijst",
"unknown_filter": "Onbekend filter {{filterId}}", "unknown_filter": "Onbekend filter {{filterId}}",
"known_tracker": "Bekende volger", "known_tracker": "Bekende volger",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Encryptie configuratie opgeslagen", "encryption_config_saved": "Encryptie configuratie opgeslagen",
"encryption_server": "Server naam", "encryption_server": "Server naam",
"encryption_server_enter": "Voer domein naam in", "encryption_server_enter": "Voer domein naam in",
"encryption_server_desc": "Om HTTPS te gebruiken, voer de naam in van de server overeenkomstig met het SSL certificaat.", "encryption_server_desc": "Om HTTPS te gebruiken, moet je de servernaam invoeren die overeenkomt met je SSL-certificaat of jokerteken-certificaat. Als het veld niet is ingesteld, accepteert het TLS-verbindingen voor elk domein.",
"encryption_redirect": "Herleid automatisch naar HTTPS", "encryption_redirect": "Herleid automatisch naar HTTPS",
"encryption_redirect_desc": "Indien ingeschakeld, zal AdGuard Home je automatisch herleiden van HTTP naar HTTPS.", "encryption_redirect_desc": "Indien ingeschakeld, zal AdGuard Home je automatisch herleiden van HTTP naar HTTPS.",
"encryption_https": "HTTPS poort", "encryption_https": "HTTPS poort",
@@ -386,7 +393,7 @@
"client_edit": "Wijzig gebruiker", "client_edit": "Wijzig gebruiker",
"client_identifier": "Identificeer via", "client_identifier": "Identificeer via",
"ip_address": "IP adres", "ip_address": "IP adres",
"client_identifier_desc": "Gebruikers kunnen worden geïdentificeerd door het IP-adres, CIDR of MAC-adres. Hou er rekening mee dat het gebruik van MAC als ID alleen mogelijk is als AdGuard Home ook een <0>DHCP-server</0> is", "client_identifier_desc": "Apparaten kunnen worden geïdentificeerd door hun IP-adres, CIDR, MAC-adres of een speciaal apparaat-ID (kan gebruikt worden voor DoT/DoH/DoQ). <0>Hier</0> kan je meer lezen over het identificeren van apparaten.",
"form_enter_ip": "Vul IP in", "form_enter_ip": "Vul IP in",
"form_enter_mac": "Vul MAC in", "form_enter_mac": "Vul MAC in",
"form_enter_id": "ID invoeren", "form_enter_id": "ID invoeren",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> ondersteunt <1>DNS-via-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> ondersteunt <1>DNS-via-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> ondersteunt <1>DNS-via-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> ondersteunt <1>DNS-via-HTTPS</1>.",
"setup_dns_privacy_other_5": "U vindt meer implementaties <0> hier </0> en <1> hier </1>.", "setup_dns_privacy_other_5": "U vindt meer implementaties <0> hier </0> en <1> hier </1>.",
"setup_dns_privacy_ioc_mac": "iOS en macOS configuratie",
"setup_dns_notice": "Om <1>DNS-via-HTTPS</1> of <1>DNS-via-TLS</1> te gebruiken, moet je <0>Versleuteling configureren</0> in de AdGuard Home instellingen.", "setup_dns_notice": "Om <1>DNS-via-HTTPS</1> of <1>DNS-via-TLS</1> te gebruiken, moet je <0>Versleuteling configureren</0> in de AdGuard Home instellingen.",
"rewrite_added": "DNS-herschrijving voor \"{{key}}\" met succes toegevoegd", "rewrite_added": "DNS-herschrijving voor \"{{key}}\" met succes toegevoegd",
"rewrite_deleted": "DNS-herschrijving voor \"{{key}}\" met succes verwijderd", "rewrite_deleted": "DNS-herschrijving voor \"{{key}}\" met succes verwijderd",
@@ -529,7 +537,6 @@
"check_ip": "IP-adressen: {{ip}}", "check_ip": "IP-adressen: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Reden: {{reason}}", "check_reason": "Reden: {{reason}}",
"check_rule": "Regel: {{rule}}",
"check_service": "Servicenaam: {{service}}", "check_service": "Servicenaam: {{service}}",
"service_name": "Naam service", "service_name": "Naam service",
"check_not_found": "Niet in je lijst met filters gevonden", "check_not_found": "Niet in je lijst met filters gevonden",

View File

@@ -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",
@@ -269,7 +273,6 @@
"source_label": "Kilde", "source_label": "Kilde",
"found_in_known_domain_db": "Funnet i databasen over kjente domener.", "found_in_known_domain_db": "Funnet i databasen over kjente domener.",
"category_label": "Kategori", "category_label": "Kategori",
"rule_label": "Oppføring",
"list_label": "Liste", "list_label": "Liste",
"unknown_filter": "Ukjent filter {{filterId}}", "unknown_filter": "Ukjent filter {{filterId}}",
"known_tracker": "Kjent sporer", "known_tracker": "Kjent sporer",
@@ -330,7 +333,6 @@
"encryption_config_saved": "Krypteringsoppsettet ble lagret", "encryption_config_saved": "Krypteringsoppsettet ble lagret",
"encryption_server": "Tjenerens navn", "encryption_server": "Tjenerens navn",
"encryption_server_enter": "Skriv inn domenenavnet ditt", "encryption_server_enter": "Skriv inn domenenavnet ditt",
"encryption_server_desc": "For å kunne bruke HTTPS, må du skrive inn tjenernavnet som samsvarer med ditt SSL-sertifikat.",
"encryption_redirect": "Automatisk omdiriger til HTTPS", "encryption_redirect": "Automatisk omdiriger til HTTPS",
"encryption_redirect_desc": "Dersom dette er valgt, vil AdGuard Home automatisk omdirigere deg fra HTTP til HTTPS-adresser.", "encryption_redirect_desc": "Dersom dette er valgt, vil AdGuard Home automatisk omdirigere deg fra HTTP til HTTPS-adresser.",
"encryption_https": "HTTPS-port", "encryption_https": "HTTPS-port",
@@ -386,7 +388,6 @@
"client_edit": "Rediger klienten", "client_edit": "Rediger klienten",
"client_identifier": "Identifikator", "client_identifier": "Identifikator",
"ip_address": "IP-adresse", "ip_address": "IP-adresse",
"client_identifier_desc": "Klienter kan bli identifisert gjennom IP-adressen eller MAC-adressen. Vennligst bemerk at å bruke MAC som en identifikator, bare er mulig dersom AdGuard Home også er en <0>DHCP-tjener</0>",
"form_enter_ip": "Skriv inn IP", "form_enter_ip": "Skriv inn IP",
"form_enter_mac": "Skriv inn MAC", "form_enter_mac": "Skriv inn MAC",
"form_enter_id": "Skriv inn identifikator", "form_enter_id": "Skriv inn identifikator",
@@ -528,7 +529,6 @@
"check_ip": "IP-adresser: {{ip}}", "check_ip": "IP-adresser: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Årsak: {{reason}}", "check_reason": "Årsak: {{reason}}",
"check_rule": "Oppføring: {{rule}}",
"check_service": "Tjenestenavn: {{service}}", "check_service": "Tjenestenavn: {{service}}",
"service_name": "Tjenestenavn", "service_name": "Tjenestenavn",
"check_not_found": "Ikke funnet i filterlistene dine", "check_not_found": "Ikke funnet i filterlistene dine",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Nieprawidłowy format IP", "form_error_ip_format": "Nieprawidłowy format IP",
"form_error_mac_format": "Nieprawidłowy format MAC", "form_error_mac_format": "Nieprawidłowy format MAC",
"form_error_client_id_format": "Nieprawidłowy format identyfikatora klienta", "form_error_client_id_format": "Nieprawidłowy format identyfikatora klienta",
"form_error_server_name": "Nieprawidłowa nazwa serwera",
"form_error_positive": "Musi być większa niż 0", "form_error_positive": "Musi być większa niż 0",
"form_error_negative": "Musi być równy 0 lub większy", "form_error_negative": "Musi być równy 0 lub większy",
"range_end_error": "Zakres musi być większy niż początkowy", "range_end_error": "Zakres musi być większy niż początkowy",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID klienta",
"client_id_placeholder": "Wpisz ID klienta",
"client_id_desc": "Różnych klientów można zidentyfikować za pomocą specjalnego ID klienta. <a>Tutaj</a> możesz dowiedzieć się więcej o tym, jak identyfikować klientów.",
"download_mobileconfig_doh": "Pobierz plik .mobileconfig dla DNS-over-HTTPS", "download_mobileconfig_doh": "Pobierz plik .mobileconfig dla DNS-over-HTTPS",
"download_mobileconfig_dot": "Pobierz plik .mobileconfig dla DNS-over-TLS", "download_mobileconfig_dot": "Pobierz plik .mobileconfig dla DNS-over-TLS",
"download_mobileconfig": "Pobierz plik konfiguracyjny",
"plain_dns": "Zwykły DNS", "plain_dns": "Zwykły DNS",
"form_enter_rate_limit": "Wpisz limit ilościowy", "form_enter_rate_limit": "Wpisz limit ilościowy",
"rate_limit": "Limit ilościowy", "rate_limit": "Limit ilościowy",
@@ -269,7 +276,7 @@
"source_label": "Źródło", "source_label": "Źródło",
"found_in_known_domain_db": "Znaleziono w bazie danych znanych domen.", "found_in_known_domain_db": "Znaleziono w bazie danych znanych domen.",
"category_label": "Kategoria", "category_label": "Kategoria",
"rule_label": "Reguła", "rule_label": "Reguła(y)",
"list_label": "Lista", "list_label": "Lista",
"unknown_filter": "Nieznany filtr {{filterId}}", "unknown_filter": "Nieznany filtr {{filterId}}",
"known_tracker": "Znany element śledzący", "known_tracker": "Znany element śledzący",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Zapisano konfigurację szyfrowania", "encryption_config_saved": "Zapisano konfigurację szyfrowania",
"encryption_server": "Nazwa serwera", "encryption_server": "Nazwa serwera",
"encryption_server_enter": "Wpisz swoją nazwę domeny", "encryption_server_enter": "Wpisz swoją nazwę domeny",
"encryption_server_desc": "Aby korzystać z protokołu HTTPS, musisz wprowadzić nazwę serwera zgodną z certyfikatem SSL.", "encryption_server_desc": "Aby korzystać z protokołu HTTPS, musisz wprowadzić nazwę serwera, która jest zgodna z certyfikatem SSL lub certyfikatem typu wildcard. Jeśli pole nie jest ustawione, będzie akceptować połączenia TLS dla dowolnej domeny.",
"encryption_redirect": "Przekieruj automatycznie do HTTPS", "encryption_redirect": "Przekieruj automatycznie do HTTPS",
"encryption_redirect_desc": "Jeśli zaznaczone, AdGuard Home automatycznie przekieruje Cię z adresów HTTP na HTTPS.", "encryption_redirect_desc": "Jeśli zaznaczone, AdGuard Home automatycznie przekieruje Cię z adresów HTTP na HTTPS.",
"encryption_https": "Port HTTPS", "encryption_https": "Port HTTPS",
@@ -386,7 +393,7 @@
"client_edit": "Edytuj klienta", "client_edit": "Edytuj klienta",
"client_identifier": "Identyfikator", "client_identifier": "Identyfikator",
"ip_address": "Adres IP", "ip_address": "Adres IP",
"client_identifier_desc": "Klienci mogą być identyfikowani na podstawie adresu IP, CIDR, adresu MAC. Pamiętaj, że użycie MAC jako identyfikatora jest możliwe tylko wtedy, gdy AdGuard Home jest również <0>serwerem DHCP</0>", "client_identifier_desc": "Klientów można zidentyfikować po adresie IP, CIDR, adresie MAC lub specjalnym identyfikatorze klienta (może służyć do DoT/DoH/DoQ). <0>Tutaj</0> możesz dowiedzieć się więcej o tym, jak identyfikować klientów.",
"form_enter_ip": "Wpisz adres IP", "form_enter_ip": "Wpisz adres IP",
"form_enter_mac": "Wpisz adres MAC", "form_enter_mac": "Wpisz adres MAC",
"form_enter_id": "Wpisz identyfikator", "form_enter_id": "Wpisz identyfikator",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> obsługuje <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> obsługuje <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> obsługuje <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> obsługuje <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Znajdziesz więcej implementacji <0>tutaj</0> i <1>tutaj</1>.", "setup_dns_privacy_other_5": "Znajdziesz więcej implementacji <0>tutaj</0> i <1>tutaj</1>.",
"setup_dns_privacy_ioc_mac": "Konfiguracja iOS i macOS",
"setup_dns_notice": "Aby skorzystać z <1>DNS-over-HTTPS</1> lub <1>DNS-over-TLS</1>, musisz w ustawieniach AdGuard Home <0>skonfigurować szyfrowanie</0>.", "setup_dns_notice": "Aby skorzystać z <1>DNS-over-HTTPS</1> lub <1>DNS-over-TLS</1>, musisz w ustawieniach AdGuard Home <0>skonfigurować szyfrowanie</0>.",
"rewrite_added": "Pomyślnie dodano przepisanie DNS dla „{{key}}”", "rewrite_added": "Pomyślnie dodano przepisanie DNS dla „{{key}}”",
"rewrite_deleted": "Przepisanie DNS dla „{{key}}” zostało pomyślnie usunięte", "rewrite_deleted": "Przepisanie DNS dla „{{key}}” zostało pomyślnie usunięte",
@@ -529,7 +537,6 @@
"check_ip": "Adresy IP: {{ip}}", "check_ip": "Adresy IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Powód: {{reason}}", "check_reason": "Powód: {{reason}}",
"check_rule": "Reguła: {{rule}}",
"check_service": "Nazwa usługi: {{service}}", "check_service": "Nazwa usługi: {{service}}",
"service_name": "Nazwa usługi", "service_name": "Nazwa usługi",
"check_not_found": "Nie znaleziono na Twoich listach filtrów", "check_not_found": "Nie znaleziono na Twoich listach filtrów",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Formato de endereço IPv inválido", "form_error_ip_format": "Formato de endereço IPv inválido",
"form_error_mac_format": "Formato do endereço MAC inválido", "form_error_mac_format": "Formato do endereço MAC inválido",
"form_error_client_id_format": "Formato do ID de cliente inválido", "form_error_client_id_format": "Formato do ID de cliente inválido",
"form_error_server_name": "Nome de servidor inválido",
"form_error_positive": "Deve ser maior que 0", "form_error_positive": "Deve ser maior que 0",
"form_error_negative": "Deve ser igual ou superior a 0", "form_error_negative": "Deve ser igual ou superior a 0",
"range_end_error": "Deve ser maior que o início do intervalo", "range_end_error": "Deve ser maior que o início do intervalo",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID do cliente",
"client_id_placeholder": "Digite o ID do cliente",
"client_id_desc": "Diferentes clientes podem ser identificados por um ID de cliente especial. <a>Aqui</a> você pode aprender mais sobre como identificar clientes.",
"download_mobileconfig_doh": "BAixar .mobileconfig para DNS-sobre-HTTPS", "download_mobileconfig_doh": "BAixar .mobileconfig para DNS-sobre-HTTPS",
"download_mobileconfig_dot": "BAixar .mobileconfig para DNS-sobre-TLS", "download_mobileconfig_dot": "BAixar .mobileconfig para DNS-sobre-TLS",
"download_mobileconfig": "Baixar arquivo de configuração",
"plain_dns": "DNS simples", "plain_dns": "DNS simples",
"form_enter_rate_limit": "Insira a taxa limite", "form_enter_rate_limit": "Insira a taxa limite",
"rate_limit": "Taxa limite", "rate_limit": "Taxa limite",
@@ -269,7 +276,7 @@
"source_label": "Fonte", "source_label": "Fonte",
"found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecidos.", "found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecidos.",
"category_label": "Categoria", "category_label": "Categoria",
"rule_label": "Regra", "rule_label": "Regra(s)",
"list_label": "Lista", "list_label": "Lista",
"unknown_filter": "Filtro desconhecido {{filterId}}", "unknown_filter": "Filtro desconhecido {{filterId}}",
"known_tracker": "Rastreador conhecido", "known_tracker": "Rastreador conhecido",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Configuração de criptografia salva", "encryption_config_saved": "Configuração de criptografia salva",
"encryption_server": "Nome do servidor", "encryption_server": "Nome do servidor",
"encryption_server_enter": "Digite seu nome de domínio", "encryption_server_enter": "Digite seu nome de domínio",
"encryption_server_desc": "Para usar o protocolo HTTPS, você precisa digitar o nome do servidor que corresponde ao seu certificado SSL.", "encryption_server_desc": "Para usar HTTPS, você precisa inserir o nome do servidor que corresponda ao seu certificado SSL ou certificado curinga. Se o campo não estiver definido, ele aceitará conexões TLS para qualquer domínio.",
"encryption_redirect": "Redirecionar automaticamente para HTTPS", "encryption_redirect": "Redirecionar automaticamente para HTTPS",
"encryption_redirect_desc": "Se marcado, o AdGuard Home irá redirecionar automaticamente os endereços HTTP para HTTPS.", "encryption_redirect_desc": "Se marcado, o AdGuard Home irá redirecionar automaticamente os endereços HTTP para HTTPS.",
"encryption_https": "Porta HTTPS", "encryption_https": "Porta HTTPS",
@@ -386,7 +393,7 @@
"client_edit": "Editar cliente", "client_edit": "Editar cliente",
"client_identifier": "Identificador", "client_identifier": "Identificador",
"ip_address": "Endereço de IP", "ip_address": "Endereço de IP",
"client_identifier_desc": "Clientes podem ser identificados pelo endereço de IP ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>", "client_identifier_desc": "Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). <0>Aqui</0> você pode aprender mais sobre como identificar clientes.",
"form_enter_ip": "Digite o endereço de IP", "form_enter_ip": "Digite o endereço de IP",
"form_enter_mac": "Digite o endereço MAC", "form_enter_mac": "Digite o endereço MAC",
"form_enter_id": "Inserir identificador", "form_enter_id": "Inserir identificador",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.",
"setup_dns_privacy_other_5": "Você encontrará mais implementações <0>aqui</0> e <1>aqui</1>.", "setup_dns_privacy_other_5": "Você encontrará mais implementações <0>aqui</0> e <1>aqui</1>.",
"setup_dns_privacy_ioc_mac": "configuração para iOS e macOS",
"setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, você precisa <0>configurar a criptografia</0> nas configurações do AdGuard Home.", "setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, você precisa <0>configurar a criptografia</0> nas configurações do AdGuard Home.",
"rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso", "rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso",
"rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso", "rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso",
@@ -529,7 +537,6 @@
"check_ip": "Endereços de IP: {{ip}}", "check_ip": "Endereços de IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Motivo: {{reason}}", "check_reason": "Motivo: {{reason}}",
"check_rule": "Regra: {{rule}}",
"check_service": "Nome do serviço: {{service}}", "check_service": "Nome do serviço: {{service}}",
"service_name": "Nome do serviço", "service_name": "Nome do serviço",
"check_not_found": "Não encontrado em suas listas de filtros", "check_not_found": "Não encontrado em suas listas de filtros",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Formato de endereço IPv4 inválido", "form_error_ip_format": "Formato de endereço IPv4 inválido",
"form_error_mac_format": "Formato do endereço MAC inválido", "form_error_mac_format": "Formato do endereço MAC inválido",
"form_error_client_id_format": "Formato inválido", "form_error_client_id_format": "Formato inválido",
"form_error_server_name": "Nome de servidor inválido",
"form_error_positive": "Deve ser maior que 0", "form_error_positive": "Deve ser maior que 0",
"form_error_negative": "Deve ser igual ou superior a 0", "form_error_negative": "Deve ser igual ou superior a 0",
"range_end_error": "Deve ser maior que o início do intervalo", "range_end_error": "Deve ser maior que o início do intervalo",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID do cliente",
"client_id_placeholder": "Insira o ID do cliente",
"client_id_desc": "Diferentes clientes podem ser identificados por um ID de cliente especial. <a>Aqui</a> você pode aprender mais sobre como identificar clientes.",
"download_mobileconfig_doh": "Transferir .mobileconfig para DNS-sobre-HTTPS", "download_mobileconfig_doh": "Transferir .mobileconfig para DNS-sobre-HTTPS",
"download_mobileconfig_dot": "Transferir .mobileconfig para DNS-sobre-TLS", "download_mobileconfig_dot": "Transferir .mobileconfig para DNS-sobre-TLS",
"download_mobileconfig": "Transferir arquivo de configuração",
"plain_dns": "DNS simples", "plain_dns": "DNS simples",
"form_enter_rate_limit": "Insira o limite de taxa", "form_enter_rate_limit": "Insira o limite de taxa",
"rate_limit": "Limite de taxa", "rate_limit": "Limite de taxa",
@@ -269,7 +276,7 @@
"source_label": "Fonte", "source_label": "Fonte",
"found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecido.", "found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecido.",
"category_label": "Categoria", "category_label": "Categoria",
"rule_label": "Regra", "rule_label": "Regra(s)",
"list_label": "Lista", "list_label": "Lista",
"unknown_filter": "Filtro desconhecido {{filterId}}", "unknown_filter": "Filtro desconhecido {{filterId}}",
"known_tracker": "Rastreador conhecido", "known_tracker": "Rastreador conhecido",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Definição de criptografia guardada", "encryption_config_saved": "Definição de criptografia guardada",
"encryption_server": "Nome do servidor", "encryption_server": "Nome do servidor",
"encryption_server_enter": "Insira o seu nome de domínio", "encryption_server_enter": "Insira o seu nome de domínio",
"encryption_server_desc": "Para usar o protocolo HTTPS, precisa de inserir o nome do servidor que corresponde ao seu certificado SSL.", "encryption_server_desc": "Para usar HTTPS, você precisa inserir o nome do servidor que corresponda ao seu certificado SSL ou certificado curinga. Se o campo não estiver definido, ele aceitará conexões TLS para qualquer domínio.",
"encryption_redirect": "Redireccionar automaticamente para HTTPS", "encryption_redirect": "Redireccionar automaticamente para HTTPS",
"encryption_redirect_desc": "Se marcado, o AdGuard Home irá redireccionar automaticamente os endereços HTTP para HTTPS.", "encryption_redirect_desc": "Se marcado, o AdGuard Home irá redireccionar automaticamente os endereços HTTP para HTTPS.",
"encryption_https": "Porta HTTPS", "encryption_https": "Porta HTTPS",
@@ -386,7 +393,7 @@
"client_edit": "Editar cliente", "client_edit": "Editar cliente",
"client_identifier": "Identificador", "client_identifier": "Identificador",
"ip_address": "Endereço de IP", "ip_address": "Endereço de IP",
"client_identifier_desc": "Os clientes podem ser identificados pelo endereço de IP, CIDR, ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP</0>", "client_identifier_desc": "Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). <0>Aqui</0> você pode aprender mais sobre como identificar clientes.",
"form_enter_ip": "Insira IP", "form_enter_ip": "Insira IP",
"form_enter_mac": "Insira o endereço MAC", "form_enter_mac": "Insira o endereço MAC",
"form_enter_id": "Inserir identificador", "form_enter_id": "Inserir identificador",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.",
"setup_dns_privacy_other_5": "Encontrará mais implementações <0>aqui</0> e <1>aqui</1>.", "setup_dns_privacy_other_5": "Encontrará mais implementações <0>aqui</0> e <1>aqui</1>.",
"setup_dns_privacy_ioc_mac": "configuração para iOS e macOS",
"setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, precisa de <0>configurar a criptografia</0> nas configurações do AdGuard Home.", "setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, precisa de <0>configurar a criptografia</0> nas configurações do AdGuard Home.",
"rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso", "rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso",
"rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso", "rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso",
@@ -529,7 +537,6 @@
"check_ip": "Endereços de IP: {{ip}}", "check_ip": "Endereços de IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Motivo: {{reason}}", "check_reason": "Motivo: {{reason}}",
"check_rule": "Regra: {{rule}}",
"check_service": "Nome do serviço: {{service}}", "check_service": "Nome do serviço: {{service}}",
"service_name": "Nome do serviço", "service_name": "Nome do serviço",
"check_not_found": "Não encontrado nas tuas listas de filtros", "check_not_found": "Não encontrado nas tuas listas de filtros",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Format IP invalid", "form_error_ip_format": "Format IP invalid",
"form_error_mac_format": "Format MAC invalid", "form_error_mac_format": "Format MAC invalid",
"form_error_client_id_format": "Format ID de client invalid", "form_error_client_id_format": "Format ID de client invalid",
"form_error_server_name": "Nume de server nevalid",
"form_error_positive": "Trebuie să fie mai mare de 0", "form_error_positive": "Trebuie să fie mai mare de 0",
"form_error_negative": "Trebuie să fie egală cu 0 sau mai mare", "form_error_negative": "Trebuie să fie egală cu 0 sau mai mare",
"range_end_error": "Trebuie să fie mai mare decât începutul intervalului", "range_end_error": "Trebuie să fie mai mare decât începutul intervalului",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID Client",
"client_id_placeholder": "Introduceți ID client",
"client_id_desc": "Diferiți clienți pot fi identificați printr-un ID special al clientului. <a>Aici</a> puteți afla mai multe despre cum să identificați clienții.",
"download_mobileconfig_doh": "Descărcați .mobileconfig pentru DNS-over-HTTPS", "download_mobileconfig_doh": "Descărcați .mobileconfig pentru DNS-over-HTTPS",
"download_mobileconfig_dot": "Descărcați .mobileconfig pentru DNS-over-TLS", "download_mobileconfig_dot": "Descărcați .mobileconfig pentru DNS-over-TLS",
"download_mobileconfig": "Descărcați fișierul de configurare",
"plain_dns": "DNS simplu", "plain_dns": "DNS simplu",
"form_enter_rate_limit": "Introduceți limita ratei", "form_enter_rate_limit": "Introduceți limita ratei",
"rate_limit": "Limita ratei", "rate_limit": "Limita ratei",
@@ -269,7 +276,7 @@
"source_label": "Sursă", "source_label": "Sursă",
"found_in_known_domain_db": "Găsit în baza de date de domenii cunoscută.", "found_in_known_domain_db": "Găsit în baza de date de domenii cunoscută.",
"category_label": "Categorie", "category_label": "Categorie",
"rule_label": "Regulă", "rule_label": "Regulă(reguli)",
"list_label": "Listă", "list_label": "Listă",
"unknown_filter": "Filtru necunoscut {{filterId}}", "unknown_filter": "Filtru necunoscut {{filterId}}",
"known_tracker": "Tracker cunoscut", "known_tracker": "Tracker cunoscut",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Configurația de criptare salvată", "encryption_config_saved": "Configurația de criptare salvată",
"encryption_server": "Nume de server", "encryption_server": "Nume de server",
"encryption_server_enter": "Introduceți numele domeniului", "encryption_server_enter": "Introduceți numele domeniului",
"encryption_server_desc": "Pentru a utiliza HTTPS, trebuie introdus numele serverului care corespunde certificatului SSL.", "encryption_server_desc": "Pentru a utiliza HTTPS, trebuie introduceți numele serverului care se potrivește cu certificatul SSL sau certificatul wildcard al dvs. În cazul în care câmpul nu este setat, va accepta conexiuni TLS pentru orice domeniu.",
"encryption_redirect": "Redirecționați automat la HTTPS", "encryption_redirect": "Redirecționați automat la HTTPS",
"encryption_redirect_desc": "Dacă este bifat, AdGuard Home vă va redirecționa automat de la adrese HTTP la HTTPS.", "encryption_redirect_desc": "Dacă este bifat, AdGuard Home vă va redirecționa automat de la adrese HTTP la HTTPS.",
"encryption_https": "Port HTTPS", "encryption_https": "Port HTTPS",
@@ -386,7 +393,7 @@
"client_edit": "Editare client", "client_edit": "Editare client",
"client_identifier": "Identificator", "client_identifier": "Identificator",
"ip_address": "Adresa IP", "ip_address": "Adresa IP",
"client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC. Vă rugăm să rețineți că utilizarea MAC ca identificator este posibilă numai dacă AdGuard Home este și un <0>server DHCP</0>", "client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC sau un ID special al clientului (poate fi folosit pentru DoT/DoH/DoQ). <0>Aici</0> puteți afla mai multe despre cum să identificați clienții.",
"form_enter_ip": "Introduceți IP", "form_enter_ip": "Introduceți IP",
"form_enter_mac": "Introduceți MAC", "form_enter_mac": "Introduceți MAC",
"form_enter_id": "Introduceți identificator", "form_enter_id": "Introduceți identificator",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> acceptă <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> acceptă <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> acceptă <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> acceptă <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Veți găsi mai multe implementări <0>aici</0> și <1>aici</1>.", "setup_dns_privacy_other_5": "Veți găsi mai multe implementări <0>aici</0> și <1>aici</1>.",
"setup_dns_privacy_ioc_mac": "Configurarea iOS și macOS",
"setup_dns_notice": "Pentru a utiliza <1>DNS-over-HTTPS</1> sau <1>DNS-over-TLS</1>, trebuie să <0>configurați Criptarea</0> în setările AdGuard Home.", "setup_dns_notice": "Pentru a utiliza <1>DNS-over-HTTPS</1> sau <1>DNS-over-TLS</1>, trebuie să <0>configurați Criptarea</0> în setările AdGuard Home.",
"rewrite_added": "Rescriere DNS pentru \"{{key}}\" adăugată cu succes", "rewrite_added": "Rescriere DNS pentru \"{{key}}\" adăugată cu succes",
"rewrite_deleted": "Rescriere DNS pentru \"{{key}}\" ștearsă cu succes", "rewrite_deleted": "Rescriere DNS pentru \"{{key}}\" ștearsă cu succes",
@@ -529,7 +537,6 @@
"check_ip": "Adrese IP: {{ip}}", "check_ip": "Adrese IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Cauza: {{reason}}", "check_reason": "Cauza: {{reason}}",
"check_rule": "Regula: {{rule}}",
"check_service": "Nume servici: {{service}}", "check_service": "Nume servici: {{service}}",
"service_name": "Numele serviciului", "service_name": "Numele serviciului",
"check_not_found": "Nu se găsește în listele de filtre", "check_not_found": "Nu se găsește în listele de filtre",

View File

@@ -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", "rate_limit": "Rate limit",
@@ -269,7 +276,7 @@
"source_label": "Источник", "source_label": "Источник",
"found_in_known_domain_db": "Найден в базе известных доменов.", "found_in_known_domain_db": "Найден в базе известных доменов.",
"category_label": "Категория", "category_label": "Категория",
"rule_label": "Правило", "rule_label": "Правило(-а)",
"list_label": "Список", "list_label": "Список",
"unknown_filter": "Неизвестный фильтр {{filterId}}", "unknown_filter": "Неизвестный фильтр {{filterId}}",
"known_tracker": "Известный трекер", "known_tracker": "Известный трекер",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Настройки шифрования сохранены", "encryption_config_saved": "Настройки шифрования сохранены",
"encryption_server": "Имя сервера", "encryption_server": "Имя сервера",
"encryption_server_enter": "Введите ваше доменное имя", "encryption_server_enter": "Введите ваше доменное имя",
"encryption_server_desc": "Для использования HTTPS вам необходимо ввести имя сервера, которое подходит вашему SSL-сертификату.", "encryption_server_desc": "Для использования HTTPS вам необходимо ввести имя сервера, которое подходит вашему SSL-сертификату или сертификату с поддержкой поддоменов. Если это поле не задано, сервер будет принимать TLS-соединения для любого домена.",
"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",
@@ -386,7 +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>", "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": "Введите идентификатор",
@@ -430,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}}\" успешно удалено",
@@ -529,7 +537,6 @@
"check_ip": "IP-адреса: {{ip}}", "check_ip": "IP-адреса: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Причина: {{reason}}", "check_reason": "Причина: {{reason}}",
"check_rule": "Правило: {{rule}}",
"check_service": "Название сервиса: {{service}}", "check_service": "Название сервиса: {{service}}",
"service_name": "Имя сервиса", "service_name": "Имя сервиса",
"check_not_found": "Не найдено в вашем списке фильтров", "check_not_found": "Не найдено в вашем списке фильтров",

View File

@@ -72,7 +72,7 @@
"dns_query": "ව.නා.ප. (DNS) විමසුම්", "dns_query": "ව.නා.ප. (DNS) විමසුම්",
"blocked_by": "<0>පෙරහන් මගින් අවහිර කරන ලද</0>", "blocked_by": "<0>පෙරහන් මගින් අවහිර කරන ලද</0>",
"stats_malware_phishing": "අවහිර කළ ද්වේශාංග/තතුබෑම්", "stats_malware_phishing": "අවහිර කළ ද්වේශාංග/තතුබෑම්",
"stats_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි", "stats_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි",
"stats_query_domain": "ජනප්‍රිය විමසන ලද වසම්", "stats_query_domain": "ජනප්‍රිය විමසන ලද වසම්",
"for_last_24_hours": "පසුගිය පැය 24 සඳහා", "for_last_24_hours": "පසුගිය පැය 24 සඳහා",
"for_last_days": "පසුගිය දින {{count}} සඳහා", "for_last_days": "පසුගිය දින {{count}} සඳහා",
@@ -83,22 +83,22 @@
"top_clients": "ජනප්‍රිය අනුග්‍රාහකයන්", "top_clients": "ජනප්‍රිය අනුග්‍රාහකයන්",
"general_statistics": "පොදු සංඛ්‍යාලේඛන", "general_statistics": "පොදු සංඛ්‍යාලේඛන",
"number_of_dns_query_blocked_24_hours": "දැන්වීම් වාරණ පෙරහන් සහ ධාරක වාරණ ලැයිස්තු මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන", "number_of_dns_query_blocked_24_hours": "දැන්වීම් වාරණ පෙරහන් සහ ධාරක වාරණ ලැයිස්තු මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන",
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard browsing security ඒකකය මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන", "number_of_dns_query_blocked_24_hours_by_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ ඒකකය මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන",
"number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි ගණන", "number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි ගණන",
"enforced_save_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන ලද", "enforced_save_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන ලද",
"number_of_dns_query_to_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්‍ර සඳහා ව.නා.ප. ඉල්ලීම් ගණන", "number_of_dns_query_to_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්‍ර සඳහා ව.නා.ප. ඉල්ලීම් ගණන",
"average_processing_time": "සාමාන්‍ය සැකසුම් කාලය", "average_processing_time": "සාමාන්‍ය සැකසුම් කාලය",
"average_processing_time_hint": "ව.නා.ප. ඉල්ලීමක් සැකසීමේ සාමාන්‍ය කාලය මිලි තත්පර වලින්", "average_processing_time_hint": "ව.නා.ප. ඉල්ලීමක් සැකසීමේ සාමාන්‍ය කාලය මිලි තත්පර වලින්",
"block_domain_use_filters_and_hosts": "පෙරහන් සහ ධාරක ගොනු භාවිතා කරමින් වසම් අවහිර කරන්න", "block_domain_use_filters_and_hosts": "පෙරහන් සහ ධාරක ගොනු භාවිතා කරමින් වසම් අවහිර කරන්න",
"filters_block_toggle_hint": "ඔබට අවහිර කිරීමේ නීති <a>පෙරහන්</a> තුළ පිහිටුවිය හැකිය.", "filters_block_toggle_hint": "ඔබට අවහිර කිරීමේ නීති <a>පෙරහන්</a> තුළ පිහිටුවිය හැකිය.",
"use_adguard_browsing_sec": "AdGuard browsing security වෙබ් සේවාව භාවිතා කරන්න", "use_adguard_browsing_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ වියමන සේවාව භාවිතා කරන්න",
"use_adguard_parental": "AdGuard parental control වෙබ් සේවාව භාවිතා කරන්න", "use_adguard_parental": "ඇඩ්ගාර්ඩ් දෙමාපිය පාලන වියමන සේවාව භාවිතා කරන්න",
"use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය බ්‍රව්සින් සෙකියුරිටි වෙබ් සේවාව මෙන් රහස්‍යතා හිතකාමී යෙ.ක්‍ර. අ.මු. (API) භාවිතා කරයි.", "use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය පිරික්සුම් ආරක්ෂණ වියමන සේවාව මෙන් රහස්‍යතා හිතකාමී යෙ.ක්‍ර. අ.මු. (API) භාවිතා කරයි.",
"enforce_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන්න", "enforce_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන්න",
"enforce_save_search_hint": "ඇඩ්ගාර්ඩ් හෝම් හට පහත සෙවුම් යන්ත්‍ර තුළ ආරක්ෂිත සෙවීම බලාත්මක කළ හැකිය: ගූගල්, යූටියුබ්, බින්ග්, ඩක්ඩක්ගෝ, යාන්ඩෙක්ස් සහ පික්සාබේ.", "enforce_save_search_hint": "ඇඩ්ගාර්ඩ් හෝම් හට පහත සෙවුම් යන්ත්‍ර තුළ ආරක්ෂිත සෙවීම බලාත්මක කළ හැකිය: ගූගල්, යූටියුබ්, බින්ග්, ඩක්ඩක්ගෝ, යාන්ඩෙක්ස් සහ පික්සාබේ.",
"no_servers_specified": "සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත", "no_servers_specified": "සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත",
"general_settings": "පොදු සැකසුම්", "general_settings": "පොදු සැකසුම්",
"dns_settings": "DNS සැකසුම්", "dns_settings": "ව.නා.ප. සැකසුම්",
"dns_blocklists": "ව.නා.ප. අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු", "dns_blocklists": "ව.නා.ප. අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු",
"dns_allowlists": "ව.නා.ප. අවසර දීමේ ලැයිස්තු", "dns_allowlists": "ව.නා.ප. අවසර දීමේ ලැයිස්තු",
"dns_blocklists_desc": "ඇඩ්ගාර්ඩ් හෝම් විසින් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු වලට ගැලපෙන වසම් අවහිර කරනු ඇත.", "dns_blocklists_desc": "ඇඩ්ගාර්ඩ් හෝම් විසින් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු වලට ගැලපෙන වසම් අවහිර කරනු ඇත.",
@@ -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": "උදාහරණ",
@@ -156,11 +156,11 @@
"example_comment_meaning": "විස්තර කිරීමක්", "example_comment_meaning": "විස්තර කිරීමක්",
"example_comment_hash": "# එසේම අදහස් දැක්වීමක්", "example_comment_hash": "# එසේම අදහස් දැක්වීමක්",
"example_regex_meaning": "නිශ්චිතව දක්වා ඇති නිත්‍ය වාක්‍යවිධියට ගැලපෙන වසම් වෙත පිවිසීම අවහිර කරයි", "example_regex_meaning": "නිශ්චිතව දක්වා ඇති නිත්‍ය වාක්‍යවිධියට ගැලපෙන වසම් වෙත පිවිසීම අවහිර කරයි",
"example_upstream_regular": "සාමාන්‍ය DNS (UDP හරහා)", "example_upstream_regular": "සාමාන්‍ය ව.නා.ප. (UDP හරහා)",
"example_upstream_dot": "සංකේතාංකනය කළ <0>DNS-over-TLS</0>", "example_upstream_dot": "සංකේතාංකනය කළ <0>DNS-over-TLS</0>",
"example_upstream_doh": "සංකේතාංකනය කළ <0>DNS-over-HTTPS</0>", "example_upstream_doh": "සංකේතාංකනය කළ <0>DNS-over-HTTPS</0>",
"example_upstream_doq": "සංකේතාංකනය කළ <0>DNS-over-QUIC</0>", "example_upstream_doq": "සංකේතාංකනය කළ <0>DNS-over-QUIC</0>",
"example_upstream_tcp": "සාමාන්‍ය DNS (TCP හරහා)", "example_upstream_tcp": "සාමාන්‍ය ව.නා.ප. (TCP/ස.පා.කෙ. හරහා) ",
"all_lists_up_to_date_toast": "සියලුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි", "all_lists_up_to_date_toast": "සියලුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි",
"dns_test_ok_toast": "සඳහන් කළ ව.නා.ප. සේවාදායකයන් නිවැරදිව ක්‍රියා කරයි", "dns_test_ok_toast": "සඳහන් කළ ව.නා.ප. සේවාදායකයන් නිවැරදිව ක්‍රියා කරයි",
"dns_test_not_ok_toast": "සේවාදායක \"{{key}}\": භාවිතා කළ නොහැකි විය, කරුණාකර ඔබ එය නිවැරදිව ලියා ඇත්දැයි පරීක්ෂා කරන්න", "dns_test_not_ok_toast": "සේවාදායක \"{{key}}\": භාවිතා කළ නොහැකි විය, කරුණාකර ඔබ එය නිවැරදිව ලියා ඇත්දැයි පරීක්ෂා කරන්න",
@@ -227,22 +227,21 @@
"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": "දැනුවත් ලුහුබැඳීමක්",
"install_welcome_title": "ඇඩ්ගාර්ඩ් හෝම් වෙත සාදරයෙන් පිළිගනිමු!", "install_welcome_title": "ඇඩ්ගාර්ඩ් හෝම් වෙත සාදරයෙන් පිළිගනිමු!",
"install_welcome_desc": "ඇඩ්ගාර්ඩ් හෝම් යනු ජාලය පුරා ඇති දැන්වීම් සහ ලුහුබැඳීම අවහිර කරන ව.නා.ප. සේවාදායකි. ඔබගේ මුළු ජාලය සහ සියලුම උපාංග පාලනය කිරීමට ඉඩ සලසා දීම එහි පරමාර්ථය යි, එයට අනුග්‍රාහක පාර්ශවීය වැඩසටහනක් භාවිතා කිරීම අවශ්‍ය නොවේ.", "install_welcome_desc": "ඇඩ්ගාර්ඩ් හෝම් යනු ජාලය පුරා ඇති දැන්වීම් සහ ලුහුබැඳීම අවහිර කරන ව.නා.ප. සේවාදායකි. ඔබගේ මුළු ජාලය සහ සියලුම උපාංග පාලනය කිරීමට ඉඩ සලසා දීම එහි පරමාර්ථය යි, එයට අනුග්‍රාහක පාර්ශවීය වැඩසටහනක් භාවිතා කිරීම අවශ්‍ය නොවේ.",
"install_settings_title": "පරිපාලක වෙබ් අතුරු මුහුණත", "install_settings_title": "පරිපාලක වියමන අතුරු මුහුණත",
"install_settings_listen": "සවන් දෙන අතුරු මුහුණත", "install_settings_listen": "සවන් දෙන අතුරු මුහුණත",
"install_settings_port": "කවුළුව", "install_settings_port": "කවුළුව",
"install_settings_interface_link": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වෙබ් අතුරු මුහුණත පහත ලිපිනයන්ගෙන් ප්‍රවේශ විය හැකිය:", "install_settings_interface_link": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වියමන අතුරු මුහුණත පහත ලිපිනයන්ගෙන් ප්‍රවේශ විය හැකිය:",
"form_error_port": "වලංගු කවුළුවක අගයක් ඇතුළත් කරන්න", "form_error_port": "වලංගු කවුළුවක අගයක් ඇතුළත් කරන්න",
"install_settings_dns": "ව.නා.ප. සේවාදායකය", "install_settings_dns": "ව.නා.ප. සේවාදායකය",
"install_settings_dns_desc": "පහත ලිපිනයන්හි ව.නා.ප. සේවාදායකය භාවිතා කිරීම සඳහා ඔබගේ උපාංග හෝ මාර්ගකාරකය වින්‍යාසගත කිරීමට අවශ්‍ය වනු ඇත:", "install_settings_dns_desc": "පහත ලිපිනයන්හි ව.නා.ප. සේවාදායකය භාවිතා කිරීම සඳහා ඔබගේ උපාංග හෝ මාර්ගකාරකය වින්‍යාසගත කිරීමට අවශ්‍ය වනු ඇත:",
"install_settings_all_interfaces": "සියලුම අතුරුමුහුණත්", "install_settings_all_interfaces": "සියලුම අතුරුමුහුණත්",
"install_auth_title": "සත්‍යාපනය", "install_auth_title": "සත්‍යාපනය",
"install_auth_desc": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වෙබ් අතුරු මුහුණතට මුරපද සත්‍යාපනය වින්‍යාසගත කිරීම අතිශයින් නිර්දේශ කෙරේ. එය ඔබගේ ස්ථානීය ජාල‌යෙන් පමණක් ප්‍රවේශ විය හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්‍රවේශයකින් ආරක්ෂා කර ගැනීම වැදගත් ය.", "install_auth_desc": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වියමන අතුරු මුහුණතට මුරපද සත්‍යාපනය වින්‍යාසගත කිරීම අතිශයින් නිර්දේශ කෙරේ. එය ඔබගේ ස්ථානීය ජාල‌යෙන් පමණක් ප්‍රවේශ විය හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්‍රවේශයකින් ආරක්ෂා කර ගැනීම වැදගත් ය.",
"install_auth_username": "පරිශීලක නාමය", "install_auth_username": "පරිශීලක නාමය",
"install_auth_password": "මුරපදය", "install_auth_password": "මුරපදය",
"install_auth_confirm": "මුරපදය තහවුරු කරන්න", "install_auth_confirm": "මුරපදය තහවුරු කරන්න",
@@ -284,7 +283,7 @@
"open_dashboard": "උපකරණ පුවරුව විවෘත කරන්න", "open_dashboard": "උපකරණ පුවරුව විවෘත කරන්න",
"install_saved": "සාර්ථකව සුරකින ලදි", "install_saved": "සාර්ථකව සුරකින ලදි",
"encryption_title": "සංකේතාංකනය", "encryption_title": "සංකේතාංකනය",
"encryption_desc": "ගුප්තකේතනය (HTTPS/TLS) සඳහා ව.නා.ප. සහ පරිපාලක වෙබ් අතුරු මුහුණත සහය දක්වයි", "encryption_desc": "ගුප්තකේතනය (HTTPS/TLS) සඳහා ව.නා.ප. සහ පරිපාලක වියමන අතුරු මුහුණත සහය දක්වයි",
"encryption_config_saved": "සංකේතාංකන වින්‍යාසය සුරකින ලදි", "encryption_config_saved": "සංකේතාංකන වින්‍යාසය සුරකින ලදි",
"encryption_server": "සේවාදායක‌‌‌‌යේ නම", "encryption_server": "සේවාදායක‌‌‌‌යේ නම",
"encryption_server_enter": "ඔබගේ වසම් නාමය ඇතුළත් කරන්න", "encryption_server_enter": "ඔබගේ වසම් නාමය ඇතුළත් කරන්න",
@@ -340,7 +339,6 @@
"client_edit": "අනුග්‍රාහකය සංස්කරණය කරන්න", "client_edit": "අනුග්‍රාහකය සංස්කරණය කරන්න",
"client_identifier": "හඳුන්වනය", "client_identifier": "හඳුන්වනය",
"ip_address": "අ.ජා. කෙ. (IP) ලිපිනය", "ip_address": "අ.ජා. කෙ. (IP) ලිපිනය",
"client_identifier_desc": "අනුග්‍රාහකයන් අ.ජා. කෙ. (IP) ලිපිනයක් හෝ මා.ප්‍ර.පා. (MAC) ලිපිනයක් මගින් හඳුනාගත හැකිය. මා.ප්‍ර.පා. හඳුන්වනයක් ලෙස භාවිතා කළ හැක්කේ ඇඩ්ගාර්ඩ් හෝම් ද <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": "හඳුන්වනය ඇතුළත් කරන්න",
@@ -462,7 +460,6 @@
"check_ip": "අ.ජා. කෙ. (IP) ලිපින: {{ip}}", "check_ip": "අ.ජා. කෙ. (IP) ලිපින: {{ip}}",
"check_cname": "අන්. නාමය (CNAME): {{cname}}", "check_cname": "අන්. නාමය (CNAME): {{cname}}",
"check_reason": "හේතුව: {{reason}}", "check_reason": "හේතුව: {{reason}}",
"check_rule": "නීතිය: {{rule}}",
"check_service": "සේවාවෙහි නම: {{service}}", "check_service": "සේවාවෙහි නම: {{service}}",
"service_name": "සේවාවේ නම", "service_name": "සේවාවේ නම",
"check_not_found": "ඔබගේ පෙරහන් ලැයිස්තු තුළ සොයා ගත නොහැක", "check_not_found": "ඔබගේ පෙරහන් ලැයිස්තු තුළ සොයා ගත නොහැක",
@@ -483,7 +480,7 @@
"show_blocked_responses": "අවහිර කර ඇත", "show_blocked_responses": "අවහිර කර ඇත",
"show_whitelisted_responses": "සුදු ලැයිස්තුගත කර ඇත", "show_whitelisted_responses": "සුදු ලැයිස්තුගත කර ඇත",
"blocked_safebrowsing": "ආරක්ෂිත සෙවීම මගින් අවහිර කරන ලද", "blocked_safebrowsing": "ආරක්ෂිත සෙවීම මගින් අවහිර කරන ලද",
"blocked_adult_websites": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි", "blocked_adult_websites": "අවහිර කළ වැඩිහිටි වියමන අඩවි",
"blocked_threats": "අවහිර කළ තර්ජන", "blocked_threats": "අවහිර කළ තර්ජන",
"allowed": "අවසර ලත්", "allowed": "අවසර ලත්",
"filtered": "පෙරහන් කරන ලද", "filtered": "පෙරහන් කරන ලද",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Nesprávny formát IPv4", "form_error_ip_format": "Nesprávny formát IPv4",
"form_error_mac_format": "Nesprávny MAC formát", "form_error_mac_format": "Nesprávny MAC formát",
"form_error_client_id_format": "Neplatný formát client ID", "form_error_client_id_format": "Neplatný formát client ID",
"form_error_server_name": "Neplatné meno servera",
"form_error_positive": "Musí byť väčšie ako 0", "form_error_positive": "Musí byť väčšie ako 0",
"form_error_negative": "Musí byť číslo 0 alebo viac", "form_error_negative": "Musí byť číslo 0 alebo viac",
"range_end_error": "Musí byť väčšie ako začiatok rozsahu", "range_end_error": "Musí byť väčšie ako začiatok rozsahu",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID klienta",
"client_id_placeholder": "Zadať ID klienta",
"client_id_desc": "Rôznych klientov možno identifikovať podľa špeciálneho ID klienta. <a>Tu</a> sa dozviete viac o tom, ako identifikovať klientov.",
"download_mobileconfig_doh": "Prevziať .mobileconfig pre DNS-over-HTTPS", "download_mobileconfig_doh": "Prevziať .mobileconfig pre DNS-over-HTTPS",
"download_mobileconfig_dot": "Prevziať .mobileconfig pre DNS-over-TLS", "download_mobileconfig_dot": "Prevziať .mobileconfig pre DNS-over-TLS",
"download_mobileconfig": "Stiahnuť konfiguračný súbor",
"plain_dns": "Obyčajné DNS", "plain_dns": "Obyčajné DNS",
"form_enter_rate_limit": "Zadajte rýchlostný limit", "form_enter_rate_limit": "Zadajte rýchlostný limit",
"rate_limit": "Rýchlostný limit", "rate_limit": "Rýchlostný limit",
@@ -269,7 +276,6 @@
"source_label": "Zdroj", "source_label": "Zdroj",
"found_in_known_domain_db": "Nájdené v databáze známych domén.", "found_in_known_domain_db": "Nájdené v databáze známych domén.",
"category_label": "Kategória", "category_label": "Kategória",
"rule_label": "Pravidlo",
"list_label": "Zoznam", "list_label": "Zoznam",
"unknown_filter": "Neznámy filter {{filterId}}", "unknown_filter": "Neznámy filter {{filterId}}",
"known_tracker": "Známy sledovač", "known_tracker": "Známy sledovač",
@@ -330,7 +336,6 @@
"encryption_config_saved": "Konfigurácia šifrovania uložená", "encryption_config_saved": "Konfigurácia šifrovania uložená",
"encryption_server": "Meno servera", "encryption_server": "Meno servera",
"encryption_server_enter": "Zadajte meno Vašej domény", "encryption_server_enter": "Zadajte meno Vašej domény",
"encryption_server_desc": "Ak chcete používať HTTPS, musíte zadať meno servera, ktoré zodpovedá Vášmu SSL certifikátu.",
"encryption_redirect": "Automaticky presmerovať na HTTPS", "encryption_redirect": "Automaticky presmerovať na HTTPS",
"encryption_redirect_desc": "Ak je táto možnosť začiarknutá, služba AdGuard Home Vás automaticky presmeruje z adresy HTTP na adresy HTTPS.", "encryption_redirect_desc": "Ak je táto možnosť začiarknutá, služba AdGuard Home Vás automaticky presmeruje z adresy HTTP na adresy HTTPS.",
"encryption_https": "HTTPS port", "encryption_https": "HTTPS port",
@@ -386,7 +391,6 @@
"client_edit": "Upraviť klienta", "client_edit": "Upraviť klienta",
"client_identifier": "Identifikátor", "client_identifier": "Identifikátor",
"ip_address": "IP adresa", "ip_address": "IP adresa",
"client_identifier_desc": "Klienti môžu byť identifikovaní podľa IP adresy, CIDR alebo MAC adresy. Upozorňujeme, že používanie MAC ako identifikátora je možné len vtedy, ak je AdGuard Home tiež <0>DHCP server</0>",
"form_enter_ip": "Zadajte IP adresu", "form_enter_ip": "Zadajte IP adresu",
"form_enter_mac": "Zadajte MAC adresu", "form_enter_mac": "Zadajte MAC adresu",
"form_enter_id": "Zadajte identifikátor", "form_enter_id": "Zadajte identifikátor",
@@ -430,6 +434,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podporuje <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podporuje <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podporuje <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podporuje <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Viac implementácií nájdete <0>tu</0> a <1>tu</1>.", "setup_dns_privacy_other_5": "Viac implementácií nájdete <0>tu</0> a <1>tu</1>.",
"setup_dns_privacy_ioc_mac": "Konfigurácia iOS a macOS",
"setup_dns_notice": "Pre použitie <1>DNS-over-HTTPS</1> alebo <1>DNS-over-TLS</1>, potrebujete v nastaveniach AdGuard Home <0>nakonfigurovať šifrovanie</0>.", "setup_dns_notice": "Pre použitie <1>DNS-over-HTTPS</1> alebo <1>DNS-over-TLS</1>, potrebujete v nastaveniach AdGuard Home <0>nakonfigurovať šifrovanie</0>.",
"rewrite_added": "DNS prepísanie pre \"{{key}}\" bolo úspešne pridané", "rewrite_added": "DNS prepísanie pre \"{{key}}\" bolo úspešne pridané",
"rewrite_deleted": "DNS prepísanie pre \"{{key}}\" bolo úspešne vymazané", "rewrite_deleted": "DNS prepísanie pre \"{{key}}\" bolo úspešne vymazané",
@@ -529,7 +534,6 @@
"check_ip": "IP adresy: {{ip}}", "check_ip": "IP adresy: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Dôvod: {{reason}}", "check_reason": "Dôvod: {{reason}}",
"check_rule": "Pravidlo: {{rule}}",
"check_service": "Meno služby: {{service}}", "check_service": "Meno služby: {{service}}",
"service_name": "Názov služby", "service_name": "Názov služby",
"check_not_found": "Nenašlo sa vo Vašom zozname filtrov", "check_not_found": "Nenašlo sa vo Vašom zozname filtrov",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Neveljaven format IP", "form_error_ip_format": "Neveljaven format IP",
"form_error_mac_format": "Neveljaven MAC format", "form_error_mac_format": "Neveljaven MAC format",
"form_error_client_id_format": "Neveljaven format ID odjemalca", "form_error_client_id_format": "Neveljaven format ID odjemalca",
"form_error_server_name": "Neveljavno ime strežnika",
"form_error_positive": "Mora biti večja od 0", "form_error_positive": "Mora biti večja od 0",
"form_error_negative": "Mora biti enako ali več kot 0", "form_error_negative": "Mora biti enako ali več kot 0",
"range_end_error": "Mora biti večji od začtka razpona", "range_end_error": "Mora biti večji od začtka razpona",
@@ -247,10 +248,16 @@
"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",
"client_id": "ID odjemalca",
"client_id_placeholder": "Vnesite ID odjemalca",
"client_id_desc": "Različne odjemalce je mogoče prepoznati s posebnim ID-jem odjemalca. <a>Tukaj</a> lahko izveste več o prepoznavanju odjemalcev.",
"download_mobileconfig_doh": "Prenos .mobileconfig za DNS-preko-HTTPS", "download_mobileconfig_doh": "Prenos .mobileconfig za DNS-preko-HTTPS",
"download_mobileconfig_dot": "Prenos .mobileconfig za DNS-preko-TLS", "download_mobileconfig_dot": "Prenos .mobileconfig za DNS-preko-TLS",
"download_mobileconfig": "Prenesi nastavitveno datoteko",
"plain_dns": "Navadni DNS", "plain_dns": "Navadni DNS",
"form_enter_rate_limit": "Vnesite omejitev hitrosti", "form_enter_rate_limit": "Vnesite omejitev hitrosti",
"rate_limit": "Omejitev hitrosti", "rate_limit": "Omejitev hitrosti",
@@ -269,7 +276,7 @@
"source_label": "Vir", "source_label": "Vir",
"found_in_known_domain_db": "Najdeno v zbirki podatkov znanih domen.", "found_in_known_domain_db": "Najdeno v zbirki podatkov znanih domen.",
"category_label": "Kategorija", "category_label": "Kategorija",
"rule_label": "Pravilo", "rule_label": "Pravila",
"list_label": "Seznam", "list_label": "Seznam",
"unknown_filter": "Neznan filter {{filterId}}", "unknown_filter": "Neznan filter {{filterId}}",
"known_tracker": "Znan sledilec", "known_tracker": "Znan sledilec",
@@ -386,7 +393,7 @@
"client_edit": "Uredi odjemalca", "client_edit": "Uredi odjemalca",
"client_identifier": "Identifikator", "client_identifier": "Identifikator",
"ip_address": "IP naslov", "ip_address": "IP naslov",
"client_identifier_desc": "Odjemalce je mogoče identificirati po naslovu IP, CIDR, MAC naslovu. Upoštevajte, da je uporaba MAC kot identifikatorja mogoča le, če je AdGuard Home tudi <0>strežnik DHCP</0>", "client_identifier_desc": "Odjemalce je mogoče prepoznati po naslovu IP, CIDR, naslovu MAC ali posebnem ID-ju odjemalca (lahko se uporablja za DoT/DoH/DoQ). <0>Tukaj</0> lahko izveste več o prepoznavanju odjemalcev.",
"form_enter_ip": "Vnesite IP", "form_enter_ip": "Vnesite IP",
"form_enter_mac": "Vnesite MAC", "form_enter_mac": "Vnesite MAC",
"form_enter_id": "Vnesi identifikatorja", "form_enter_id": "Vnesi identifikatorja",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podpira <1>DNS-prek-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> podpira <1>DNS-prek-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podpira <1>DNS-prek-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> podpira <1>DNS-prek-HTTPS</1>.",
"setup_dns_privacy_other_5": "Našli boste več izvedb <0>tukaj</0> in <1>tukaj</1>.", "setup_dns_privacy_other_5": "Našli boste več izvedb <0>tukaj</0> in <1>tukaj</1>.",
"setup_dns_privacy_ioc_mac": "Nastavitve iOS in macOS",
"setup_dns_notice": "Za uporabo <1>DNS-prek-HTTPS</1> ali <1>DNS-prek-TLS</1>, morate <0>konfigurirati šifriranje</0> v nastavitvah AdGuard Home.", "setup_dns_notice": "Za uporabo <1>DNS-prek-HTTPS</1> ali <1>DNS-prek-TLS</1>, morate <0>konfigurirati šifriranje</0> v nastavitvah AdGuard Home.",
"rewrite_added": "Uspešno je dodano DNS prepisovanje za \"{{key}}\"", "rewrite_added": "Uspešno je dodano DNS prepisovanje za \"{{key}}\"",
"rewrite_deleted": "Uspešno je izbrisano DNS prepisovanje za \"{{key}}\"", "rewrite_deleted": "Uspešno je izbrisano DNS prepisovanje za \"{{key}}\"",
@@ -529,7 +537,6 @@
"check_ip": "IP naslovi: {{ip}}", "check_ip": "IP naslovi: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Razlog: {{reason}}", "check_reason": "Razlog: {{reason}}",
"check_rule": "Pravilo: {{rule}}",
"check_service": "Ime storitve: {{service}}", "check_service": "Ime storitve: {{service}}",
"service_name": "Ime storitve", "service_name": "Ime storitve",
"check_not_found": "Ni najdeno na vašem seznamu filtrov", "check_not_found": "Ni najdeno na vašem seznamu filtrov",

View File

@@ -269,7 +269,6 @@
"source_label": "Izvor", "source_label": "Izvor",
"found_in_known_domain_db": "Pronađeno u poznatim bazama podataka domena.", "found_in_known_domain_db": "Pronađeno u poznatim bazama podataka domena.",
"category_label": "Kategorija", "category_label": "Kategorija",
"rule_label": "Pravilo",
"list_label": "Lista", "list_label": "Lista",
"unknown_filter": "Nepoznat filter {{filterId}}", "unknown_filter": "Nepoznat filter {{filterId}}",
"known_tracker": "Poznato praćenje", "known_tracker": "Poznato praćenje",
@@ -330,7 +329,6 @@
"encryption_config_saved": "Konfiguracija šifrovanja je sačuvana", "encryption_config_saved": "Konfiguracija šifrovanja je sačuvana",
"encryption_server": "Ime servera", "encryption_server": "Ime servera",
"encryption_server_enter": "Unesite vaše ime domena", "encryption_server_enter": "Unesite vaše ime domena",
"encryption_server_desc": "Kako biste koristili HTTPS, potrebno je da unesete ime servera koje se podudara sa SSL sertifikatom.",
"encryption_redirect": "Automatski preusmeri na HTTPS", "encryption_redirect": "Automatski preusmeri na HTTPS",
"encryption_redirect_desc": "Ako je označeno, AdGuard Home će vas automatski preusmeravati sa HTTP na HTTPS adrese.", "encryption_redirect_desc": "Ako je označeno, AdGuard Home će vas automatski preusmeravati sa HTTP na HTTPS adrese.",
"encryption_https": "HTTPS port", "encryption_https": "HTTPS port",
@@ -386,7 +384,6 @@
"client_edit": "Izmeni klijent", "client_edit": "Izmeni klijent",
"client_identifier": "Identifikator", "client_identifier": "Identifikator",
"ip_address": "IP adresa", "ip_address": "IP adresa",
"client_identifier_desc": "Klijenti mogu da budu prepoznati po IP adresi ili MAC adresi. Imajte na umu da je korišćenje MAC adrese kao identifikatora moguće samo ako je AdGuard Home takođe a <0>DHCP server</0>",
"form_enter_ip": "Unesite IP", "form_enter_ip": "Unesite IP",
"form_enter_mac": "Unesite MAC", "form_enter_mac": "Unesite MAC",
"form_enter_id": "Unesite identifikator", "form_enter_id": "Unesite identifikator",
@@ -529,7 +526,6 @@
"check_ip": "IP adrese: {{ip}}", "check_ip": "IP adrese: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Razlog: {{reason}}", "check_reason": "Razlog: {{reason}}",
"check_rule": "Pravilo: {{rule}}",
"check_service": "Ime usluge: {{service}}", "check_service": "Ime usluge: {{service}}",
"service_name": "Ime usluge", "service_name": "Ime usluge",
"check_not_found": "Nije pronađeno na vašoj listi filtera", "check_not_found": "Nije pronađeno na vašoj listi filtera",

View File

@@ -177,7 +177,6 @@
"source_label": "Källa", "source_label": "Källa",
"found_in_known_domain_db": "Hittad i domändatabas.", "found_in_known_domain_db": "Hittad i domändatabas.",
"category_label": "Kategori", "category_label": "Kategori",
"rule_label": "Regel",
"unknown_filter": "Okänt filter {{filterId}}", "unknown_filter": "Okänt filter {{filterId}}",
"install_welcome_title": "Välkommen till AdGuard Home!", "install_welcome_title": "Välkommen till AdGuard Home!",
"install_welcome_desc": "AdGuard Home är en DNS-server för nätverkstäckande annons- och spårningsblockering. Dess syfte är att de dig kontroll över hela nätverket och alla dina enheter, utan behov av att använda klientbaserade program.", "install_welcome_desc": "AdGuard Home är en DNS-server för nätverkstäckande annons- och spårningsblockering. Dess syfte är att de dig kontroll över hela nätverket och alla dina enheter, utan behov av att använda klientbaserade program.",
@@ -235,7 +234,6 @@
"encryption_config_saved": "Krypteringsinställningar sparade", "encryption_config_saved": "Krypteringsinställningar sparade",
"encryption_server": "Servernamn", "encryption_server": "Servernamn",
"encryption_server_enter": "Skriv in ditt domännamn", "encryption_server_enter": "Skriv in ditt domännamn",
"encryption_server_desc": "För att använda HTTPS behöver du skriva in servernamnet som stämmer överens med ditt SSL-certifikat.",
"encryption_redirect": "Omdirigera till HTTPS automatiskt", "encryption_redirect": "Omdirigera till HTTPS automatiskt",
"encryption_redirect_desc": "Om bockad kommer AdGuard Home automatiskt att omdirigera dig från HTTP till HTTPS-adresser.", "encryption_redirect_desc": "Om bockad kommer AdGuard Home automatiskt att omdirigera dig från HTTP till HTTPS-adresser.",
"encryption_https": "HTTPS-port", "encryption_https": "HTTPS-port",
@@ -287,7 +285,6 @@
"client_edit": "Redigera klient", "client_edit": "Redigera klient",
"client_identifier": "Identifikator", "client_identifier": "Identifikator",
"ip_address": "IP-adress", "ip_address": "IP-adress",
"client_identifier_desc": "Klienter kan identifieras genom IP-adresser eller MAC-adresser. Notera att användning av MAC som identifierare bara är möjligt om AdGuard Home också är en <0>DHCP-server</0>",
"form_enter_ip": "Skriv in IP", "form_enter_ip": "Skriv in IP",
"form_enter_mac": "Skriv in MAC", "form_enter_mac": "Skriv in MAC",
"form_client_name": "Skriv in klientnamn", "form_client_name": "Skriv in klientnamn",

View File

@@ -200,7 +200,6 @@
"source_label": "ที่มา", "source_label": "ที่มา",
"found_in_known_domain_db": "พบในฐานข้อมูลโดเมนที่รู้จัก", "found_in_known_domain_db": "พบในฐานข้อมูลโดเมนที่รู้จัก",
"category_label": "ประเภท", "category_label": "ประเภท",
"rule_label": "กฎ",
"unknown_filter": "ตัวกรองที่ไม่รู้จัก {{filterId}}", "unknown_filter": "ตัวกรองที่ไม่รู้จัก {{filterId}}",
"install_welcome_title": "ยินดีต้อนรับสู่ AdGuard Home", "install_welcome_title": "ยินดีต้อนรับสู่ AdGuard Home",
"install_welcome_desc": "AdGuard Home เป็นเซิร์ฟเวอร์ DNS ปิดกั้นโฆษณาและติดตามทั่วทั้งเครือข่าย วัตถุประสงค์คือเพื่อให้คุณควบคุมเครือข่ายทั้งหมดและอุปกรณ์ทั้งหมดของคุณและไม่จำเป็นต้องใช้โปรแกรมฝั่งไคลเอ็นต์", "install_welcome_desc": "AdGuard Home เป็นเซิร์ฟเวอร์ DNS ปิดกั้นโฆษณาและติดตามทั่วทั้งเครือข่าย วัตถุประสงค์คือเพื่อให้คุณควบคุมเครือข่ายทั้งหมดและอุปกรณ์ทั้งหมดของคุณและไม่จำเป็นต้องใช้โปรแกรมฝั่งไคลเอ็นต์",
@@ -258,7 +257,6 @@
"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",
@@ -312,7 +310,6 @@
"client_edit": "แก้ไขเครื่องลูกข่าย", "client_edit": "แก้ไขเครื่องลูกข่าย",
"client_identifier": "ตรวจสอบโดย", "client_identifier": "ตรวจสอบโดย",
"ip_address": "IP addresses", "ip_address": "IP addresses",
"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": "ป้อนตัวระบุ",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Geçersiz IP biçimi", "form_error_ip_format": "Geçersiz IP biçimi",
"form_error_mac_format": "Geçersiz MAC biçimi", "form_error_mac_format": "Geçersiz MAC biçimi",
"form_error_client_id_format": "Geçersiz istemci kimliği biçimi", "form_error_client_id_format": "Geçersiz istemci kimliği biçimi",
"form_error_server_name": "Geçersiz sunucu adı",
"form_error_positive": "0'dan büyük olmalı", "form_error_positive": "0'dan büyük olmalı",
"form_error_negative": "0 veya daha büyük olmalıdır", "form_error_negative": "0 veya daha büyük olmalıdır",
"range_end_error": "Başlangıç aralığından daha büyük olmalı", "range_end_error": "Başlangıç aralığından daha büyük olmalı",
@@ -247,10 +248,16 @@
"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",
"client_id": "İstemci Kimliği",
"client_id_placeholder": "İstemci Kimliği girin",
"client_id_desc": "Farklı istemciler, özel bir istemci kimliği ile tanımlanabilir. <a>Burada</a> istemcileri nasıl belirleyeceğiniz hakkında daha fazla bilgi edinebilirsiniz.",
"download_mobileconfig_doh": "DNS-over-HTTPS için .mobileconfig dosyasını indir", "download_mobileconfig_doh": "DNS-over-HTTPS için .mobileconfig dosyasını indir",
"download_mobileconfig_dot": "DNS-over-TLS için .mobileconfig dosyasını indir", "download_mobileconfig_dot": "DNS-over-TLS için .mobileconfig dosyasını indir",
"download_mobileconfig": "Yapılandırma dosyasını indir",
"plain_dns": "Sade DNS", "plain_dns": "Sade DNS",
"form_enter_rate_limit": "Sıklık limitini girin", "form_enter_rate_limit": "Sıklık limitini girin",
"rate_limit": "Sıklık limiti", "rate_limit": "Sıklık limiti",
@@ -330,7 +337,7 @@
"encryption_config_saved": "Şifreleme ayarı kaydedildi", "encryption_config_saved": "Şifreleme ayarı kaydedildi",
"encryption_server": "Sunucu adı", "encryption_server": "Sunucu adı",
"encryption_server_enter": "Alan adınızı girin", "encryption_server_enter": "Alan adınızı girin",
"encryption_server_desc": "HTTPS kullanmak için SSL sertifikanızla eşleşen sunucu adını girmeniz gerekir", "encryption_server_desc": "HTTPS kullanmak için, SSL sertifikanız veya joker karakter sertifikanızla eşleşen sunucu adını girmeniz gerekir. Alan ayarlanmazsa, herhangi bir alan adı için TKG bağlantılarını kabul eder.",
"encryption_redirect": "Otomatik olarak HTTPS'e yönlendir", "encryption_redirect": "Otomatik olarak HTTPS'e yönlendir",
"encryption_redirect_desc": "Etkinleştirirseniz AdGuard Home sizi HTTP yerine HTTPS adreslerine yönlendirir.", "encryption_redirect_desc": "Etkinleştirirseniz AdGuard Home sizi HTTP yerine HTTPS adreslerine yönlendirir.",
"encryption_https": "HTTPS bağlantı noktası", "encryption_https": "HTTPS bağlantı noktası",
@@ -386,7 +393,7 @@
"client_edit": "İstemciyi düzenle", "client_edit": "İstemciyi düzenle",
"client_identifier": "Tanımlayıcı", "client_identifier": "Tanımlayıcı",
"ip_address": "IP adresi", "ip_address": "IP adresi",
"client_identifier_desc": "İstemciler IP adresleri veya MAC adresleri ile tanımlanabilir. Lütfen not edin, MAC adresi ile tanımlamayı kullanmak için AdGuard Home'un <0>DHCP Sunucusu</0> olması gerekir.", "client_identifier_desc": "İstemciler IP adresi, CIDR, MAC adresi veya özel bir istemci kimliği ile tanımlanabilir (DoT/DoH/DoQ için kullanılabilir). <0>Burada</0> istemcileri nasıl belirleyeceğiniz hakkında daha fazla bilgi edinebilirsiniz.",
"form_enter_ip": "IP Girin", "form_enter_ip": "IP Girin",
"form_enter_mac": "MAC Girin", "form_enter_mac": "MAC Girin",
"form_enter_id": "Tanımlayıcı girin", "form_enter_id": "Tanımlayıcı girin",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0>, <1>DNS-over-HTTPS</1> destekler.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0>, <1>DNS-over-HTTPS</1> destekler.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0>, <1>DNS-over-HTTPS</1> desteklemektedir.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0>, <1>DNS-over-HTTPS</1> desteklemektedir.",
"setup_dns_privacy_other_5": "<0>Burada</0> ve <1>burada</1> daha fazla uygulama bulacaksınız.", "setup_dns_privacy_other_5": "<0>Burada</0> ve <1>burada</1> daha fazla uygulama bulacaksınız.",
"setup_dns_privacy_ioc_mac": "iOS ve macOS yapılandırması",
"setup_dns_notice": "<1>DNS-over-HTTPS</1> veya <1>DNS-over-TLS</1> kullanmak için, AdGuard Home ayarlarında <0>Şifreleme yapılandırmasını</0> yapmanız gerekir.", "setup_dns_notice": "<1>DNS-over-HTTPS</1> veya <1>DNS-over-TLS</1> kullanmak için, AdGuard Home ayarlarında <0>Şifreleme yapılandırmasını</0> yapmanız gerekir.",
"rewrite_added": "\"{{key}}\" için DNS yeniden yazımı başarıyla eklendi", "rewrite_added": "\"{{key}}\" için DNS yeniden yazımı başarıyla eklendi",
"rewrite_deleted": "\"{{key}}\" için DNS yeniden yazımı başarıyla silindi", "rewrite_deleted": "\"{{key}}\" için DNS yeniden yazımı başarıyla silindi",
@@ -529,7 +537,6 @@
"check_ip": "IP adresleri: {{ip}}", "check_ip": "IP adresleri: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Sebep: {{reason}}", "check_reason": "Sebep: {{reason}}",
"check_rule": "Kural: {{rule}}",
"check_service": "Hizmet adı: {{service}}", "check_service": "Hizmet adı: {{service}}",
"service_name": "Servis adı", "service_name": "Servis adı",
"check_not_found": "Filtre listelerinizde bulunamadı", "check_not_found": "Filtre listelerinizde bulunamadı",

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "Định dạng IPv4 không hợp lệ", "form_error_ip_format": "Định dạng IPv4 không hợp lệ",
"form_error_mac_format": "Định dạng MAC không hợp lệ", "form_error_mac_format": "Định dạng MAC không hợp lệ",
"form_error_client_id_format": "Định dạng client ID không hợp lệ", "form_error_client_id_format": "Định dạng client ID không hợp lệ",
"form_error_server_name": "Tên máy chủ không hợp lệ",
"form_error_positive": "Phải lớn hơn 0", "form_error_positive": "Phải lớn hơn 0",
"form_error_negative": "Phải lớn hơn hoặc bằng 0", "form_error_negative": "Phải lớn hơn hoặc bằng 0",
"range_end_error": "Phải lớn hơn khoảng bắt đầu", "range_end_error": "Phải lớn hơn khoảng bắt đầu",
@@ -133,6 +134,7 @@
"encryption_settings": "Cài đặt mã hóa", "encryption_settings": "Cài đặt mã hóa",
"dhcp_settings": "Cài đặt DHCP", "dhcp_settings": "Cài đặt DHCP",
"upstream_dns": "Máy chủ DNS tìm kiếm", "upstream_dns": "Máy chủ DNS tìm kiếm",
"upstream_dns_help": "Nhập địa chỉ máy chủ một trên mỗi dòng. <a>Tìm hiểu thêm</a> về cách định cấu hình máy chủ DNS ngược dòng.",
"upstream_dns_configured_in_file": "Cấu hình tại {{path}}", "upstream_dns_configured_in_file": "Cấu hình tại {{path}}",
"test_upstream_btn": "Kiểm tra", "test_upstream_btn": "Kiểm tra",
"upstreams": "Nguồn", "upstreams": "Nguồn",
@@ -197,6 +199,9 @@
"unblock": "Bỏ chặn", "unblock": "Bỏ chặn",
"block": "Chặn", "block": "Chặn",
"disallow_this_client": "Không cho phép client này", "disallow_this_client": "Không cho phép client này",
"allow_this_client": "Cho phép ứng dụng khách này",
"block_for_this_client_only": "Chỉ chặn ứng dụng khách này",
"unblock_for_this_client_only": "Chỉ hủy chặn ứng dụng khách này",
"time_table_header": "Thời gian", "time_table_header": "Thời gian",
"date": "Ngày", "date": "Ngày",
"domain_name_table_header": "Tên miền", "domain_name_table_header": "Tên miền",
@@ -243,8 +248,16 @@
"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",
"client_id": "ID khách hàng",
"client_id_placeholder": "Nhập ID khách hàng",
"client_id_desc": "Các khách hàng khác nhau có thể được xác định bằng một ID khách hàng đặc biệt. <a>Tại đây</a> bạn có thể tìm hiểu thêm về cách xác định khách hàng.",
"download_mobileconfig_doh": "Tải xuống .mobileconfig cho DNS-over-HTTPS",
"download_mobileconfig_dot": "Tải xuống .mobileconfig cho DNS-over-TLS",
"download_mobileconfig": "Tải xuống tệp cấu hình",
"plain_dns": "DNS thuần", "plain_dns": "DNS thuần",
"form_enter_rate_limit": "Nhập giới hạn yêu cầu", "form_enter_rate_limit": "Nhập giới hạn yêu cầu",
"rate_limit": "Giới hạn yêu cầu", "rate_limit": "Giới hạn yêu cầu",
@@ -254,6 +267,7 @@
"blocking_ipv4_desc": "Địa chỉ IP được trả lại cho một yêu cầu A bị chặn", "blocking_ipv4_desc": "Địa chỉ IP được trả lại cho một yêu cầu A bị chặn",
"blocking_ipv6_desc": "Địa chỉ IP được trả lại cho một yêu cầu AAA bị chặn", "blocking_ipv6_desc": "Địa chỉ IP được trả lại cho một yêu cầu AAA bị chặn",
"blocking_mode_default": "Mặc định: Trả lời với NXDOMAIN khi bị chặn bởi quy tắc kiểu Adblock; phản hồi với địa chỉ IP được chỉ định trong quy tắc khi bị chặn bởi quy tắc / etc / hosts-style", "blocking_mode_default": "Mặc định: Trả lời với NXDOMAIN khi bị chặn bởi quy tắc kiểu Adblock; phản hồi với địa chỉ IP được chỉ định trong quy tắc khi bị chặn bởi quy tắc / etc / hosts-style",
"blocking_mode_refused": "REFUSED: Trả lời bằng mã REFUSED",
"blocking_mode_nxdomain": "NXDOMAIN: Phản hổi với mã NXDOMAIN", "blocking_mode_nxdomain": "NXDOMAIN: Phản hổi với mã NXDOMAIN",
"blocking_mode_null_ip": "Null IP: Trả lời bằng không địa chỉ IP (0.0.0.0 cho A; :: cho AAAA)", "blocking_mode_null_ip": "Null IP: Trả lời bằng không địa chỉ IP (0.0.0.0 cho A; :: cho AAAA)",
"blocking_mode_custom_ip": "IP tùy chỉnh: Phản hồi với địa chỉ IP đã được tiết lập", "blocking_mode_custom_ip": "IP tùy chỉnh: Phản hồi với địa chỉ IP đã được tiết lập",
@@ -262,7 +276,6 @@
"source_label": "Nguồn", "source_label": "Nguồn",
"found_in_known_domain_db": "Tìm thấy trong cơ sở dữ liệu tên miền", "found_in_known_domain_db": "Tìm thấy trong cơ sở dữ liệu tên miền",
"category_label": "Thể loại", "category_label": "Thể loại",
"rule_label": "Quy tắc",
"list_label": "Danh sách", "list_label": "Danh sách",
"unknown_filter": "Bộ lọc không rõ {{filterId}}", "unknown_filter": "Bộ lọc không rõ {{filterId}}",
"known_tracker": "Theo dõi đã biết", "known_tracker": "Theo dõi đã biết",
@@ -323,13 +336,14 @@
"encryption_config_saved": "Đã lưu cấu hình mã hóa", "encryption_config_saved": "Đã lưu cấu hình mã hóa",
"encryption_server": "Tên máy chủ", "encryption_server": "Tên máy chủ",
"encryption_server_enter": "Nhập tên miền của bạn", "encryption_server_enter": "Nhập tên miền của bạn",
"encryption_server_desc": "Để sử dụng HTTPS, bạn cần nhập tên máy chủ phù hợp với chứng chỉ SSL của bạn.",
"encryption_redirect": "Tự động chuyển hướng đến HTTPS", "encryption_redirect": "Tự động chuyển hướng đến HTTPS",
"encryption_redirect_desc": "Nếu được chọn, AdGuard Home sẽ tự động chuyển hướng bạn từ địa chỉ HTTP sang địa chỉ HTTPS.", "encryption_redirect_desc": "Nếu được chọn, AdGuard Home sẽ tự động chuyển hướng bạn từ địa chỉ HTTP sang địa chỉ HTTPS.",
"encryption_https": "Cổng HTTPS", "encryption_https": "Cổng HTTPS",
"encryption_https_desc": "Nếu cổng HTTPS được định cấu hình, giao diện quản trị viên AdGuard Home sẽ có thể truy cập thông qua HTTPS và nó cũng sẽ cung cấp DNS-over-HTTPS trên vị trí '/dns-query'.", "encryption_https_desc": "Nếu cổng HTTPS được định cấu hình, giao diện quản trị viên AdGuard Home sẽ có thể truy cập thông qua HTTPS và nó cũng sẽ cung cấp DNS-over-HTTPS trên vị trí '/dns-query'.",
"encryption_dot": "Cổng DNS-over-TLS", "encryption_dot": "Cổng DNS-over-TLS",
"encryption_dot_desc": "Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS-over-TLS trên cổng này.", "encryption_dot_desc": "Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS-over-TLS trên cổng này.",
"encryption_doq": "Cổng DNS-over-QUIC",
"encryption_doq_desc": "Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS qua QUIC trên cổng này. Đó là thử nghiệm và có thể không đáng tin cậy. Ngoài ra, không có quá nhiều khách hàng hỗ trợ nó vào lúc này.",
"encryption_certificates": "Chứng chỉ", "encryption_certificates": "Chứng chỉ",
"encryption_certificates_desc": "Để sử dụng mã hóa, bạn cần cung cấp chuỗi chứng chỉ SSL hợp lệ cho miền của mình. Bạn có thể nhận chứng chỉ miễn phí trên <0>{{link}}</0> hoặc bạn có thể mua chứng chỉ từ một trong các Cơ Quan Chứng Nhận tin cậy.", "encryption_certificates_desc": "Để sử dụng mã hóa, bạn cần cung cấp chuỗi chứng chỉ SSL hợp lệ cho miền của mình. Bạn có thể nhận chứng chỉ miễn phí trên <0>{{link}}</0> hoặc bạn có thể mua chứng chỉ từ một trong các Cơ Quan Chứng Nhận tin cậy.",
"encryption_certificates_input": "Sao chép/dán chứng chỉ được mã hóa PEM của bạn tại đây.", "encryption_certificates_input": "Sao chép/dán chứng chỉ được mã hóa PEM của bạn tại đây.",
@@ -377,7 +391,6 @@
"client_edit": "Chỉnh Sửa Máy Khách", "client_edit": "Chỉnh Sửa Máy Khách",
"client_identifier": "Định danh", "client_identifier": "Định danh",
"ip_address": "Địa chỉ IP", "ip_address": "Địa chỉ IP",
"client_identifier_desc": "Các máy khách có thể được xác định bằng địa chỉ IP hoặc địa chỉ MAC. Xin lưu ý rằng chỉ có thể sử dụng MAC làm định danh nếu AdGuard Home cũng là <0>máy chủ DHCP</0>",
"form_enter_ip": "Nhập IP", "form_enter_ip": "Nhập IP",
"form_enter_mac": "Nhập MAC", "form_enter_mac": "Nhập MAC",
"form_enter_id": "Nhập định danh", "form_enter_id": "Nhập định danh",
@@ -408,6 +421,7 @@
"dns_privacy": "DNS Riêng Tư", "dns_privacy": "DNS Riêng Tư",
"setup_dns_privacy_1": "<0>DNS-over-TLS:</0> Sử dụng chuỗi <1>{{address}}</1>.", "setup_dns_privacy_1": "<0>DNS-over-TLS:</0> Sử dụng chuỗi <1>{{address}}</1>.",
"setup_dns_privacy_2": "<0>DNS-over-HTTPS:</0> Sử dụng chuỗi <1>{{address}}</1>.", "setup_dns_privacy_2": "<0>DNS-over-HTTPS:</0> Sử dụng chuỗi <1>{{address}}</1>.",
"setup_dns_privacy_3": "<0>Đây là danh sách phần mềm bạn có thể sử dụng.</0>",
"setup_dns_privacy_4": "Trên thiết bị chạy iOS 14 hoặc macOS Big Sur bạn có thể tải tệp '.mobileconfig' đặc biệt có chứa máy chủ <highlight>DNS-over-HTTPS</highlight> hoặc <highlight>DNS-over-TLS</highlight> trong thiết lập DNS.", "setup_dns_privacy_4": "Trên thiết bị chạy iOS 14 hoặc macOS Big Sur bạn có thể tải tệp '.mobileconfig' đặc biệt có chứa máy chủ <highlight>DNS-over-HTTPS</highlight> hoặc <highlight>DNS-over-TLS</highlight> trong thiết lập DNS.",
"setup_dns_privacy_android_1": "Android 9 hỗ trợ DNS trên TLS nguyên bản. Để định cấu hình, hãy đi tới Cài đặt → Mạng & internet → Nâng cao → DNS Riêng Tư và nhập tên miền của bạn vào đó.", "setup_dns_privacy_android_1": "Android 9 hỗ trợ DNS trên TLS nguyên bản. Để định cấu hình, hãy đi tới Cài đặt → Mạng & internet → Nâng cao → DNS Riêng Tư và nhập tên miền của bạn vào đó.",
"setup_dns_privacy_android_2": "<0>AdGuard for Android</0> hỗ trợ <1>DNS-over-HTTPS</1> và <1>DNS-over-TLS</1>.", "setup_dns_privacy_android_2": "<0>AdGuard for Android</0> hỗ trợ <1>DNS-over-HTTPS</1> và <1>DNS-over-TLS</1>.",
@@ -420,6 +434,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> hỗ trợ <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> hỗ trợ <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> hỗ trợ <1>DNS-over-HTTPS</1>.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> hỗ trợ <1>DNS-over-HTTPS</1>.",
"setup_dns_privacy_other_5": "Bạn sẽ tìm thấy nhiều triển khai hơn <0>tại đây</0> và <1>tại đây</1>.", "setup_dns_privacy_other_5": "Bạn sẽ tìm thấy nhiều triển khai hơn <0>tại đây</0> và <1>tại đây</1>.",
"setup_dns_privacy_ioc_mac": "Cấu hình iOS và macOS",
"setup_dns_notice": "Để sử dụng <1>DNS-over-HTTPS</1> hoặc <1>DNS-over-TLS</1>, bạn cần <0>định cấu hình Mã hóa</0> trong cài đặt AdGuard Home.", "setup_dns_notice": "Để sử dụng <1>DNS-over-HTTPS</1> hoặc <1>DNS-over-TLS</1>, bạn cần <0>định cấu hình Mã hóa</0> trong cài đặt AdGuard Home.",
"rewrite_added": "DNS viết lại cho \"{{key}}\" đã thêm thành công", "rewrite_added": "DNS viết lại cho \"{{key}}\" đã thêm thành công",
"rewrite_deleted": "DNS viết lại cho \"{{key}}\" đã xóa thành công", "rewrite_deleted": "DNS viết lại cho \"{{key}}\" đã xóa thành công",
@@ -519,8 +534,8 @@
"check_ip": "Địa chỉ IP: {{ip}}", "check_ip": "Địa chỉ IP: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Lý do: {{reason}}", "check_reason": "Lý do: {{reason}}",
"check_rule": "Quy tắc: {{rule}}",
"check_service": "Tên dịch vụ: {{service}}", "check_service": "Tên dịch vụ: {{service}}",
"service_name": "Tên dịch vụ",
"check_not_found": "Không tìm thấy trong danh sách bộ lọc của bạn", "check_not_found": "Không tìm thấy trong danh sách bộ lọc của bạn",
"client_confirm_block": "Bạn có muốn chặn người dùng {{ip}}?", "client_confirm_block": "Bạn có muốn chặn người dùng {{ip}}?",
"client_confirm_unblock": "Bạn có muốn bỏ chặn người dùng {{ip}}?", "client_confirm_unblock": "Bạn có muốn bỏ chặn người dùng {{ip}}?",
@@ -555,6 +570,12 @@
"cache_size_desc": "Kích thước cache DNS (bytes)", "cache_size_desc": "Kích thước cache DNS (bytes)",
"cache_ttl_min_override": "Ghi đè TTL tối thiểu", "cache_ttl_min_override": "Ghi đè TTL tối thiểu",
"cache_ttl_max_override": "Ghi đè TTL tối đa", "cache_ttl_max_override": "Ghi đè TTL tối đa",
"enter_cache_size": "Nhập kích thước bộ nhớ cache (byte)",
"enter_cache_ttl_min_override": "Nhập TTL tối thiểu (giây)",
"enter_cache_ttl_max_override": "Nhập TTL tối đa (giây)",
"cache_ttl_min_override_desc": "Mở rộng giá trị thời gian tồn tại ngắn (giây) nhận được từ máy chủ ngược dòng khi phản hồi DNS vào bộ nhớ đệm",
"cache_ttl_max_override_desc": "Đặt giá trị thời gian tồn tại tối đa (giây) cho các mục nhập trong bộ nhớ cache DNS",
"ttl_cache_validation": "Giá trị TTL trong bộ nhớ cache tối thiểu phải nhỏ hơn hoặc bằng giá trị lớn nhất",
"filter_category_general": "Chung", "filter_category_general": "Chung",
"filter_category_security": "Bảo mật", "filter_category_security": "Bảo mật",
"filter_category_regional": "Khu vực", "filter_category_regional": "Khu vực",
@@ -566,5 +587,8 @@
"setup_config_to_enable_dhcp_server": "Thiết lập cấu hình để bật máy chủ DHCP", "setup_config_to_enable_dhcp_server": "Thiết lập cấu hình để bật máy chủ DHCP",
"original_response": "Phản hồi gốc", "original_response": "Phản hồi gốc",
"click_to_view_queries": "Nhấp để xem truy xuất", "click_to_view_queries": "Nhấp để xem truy xuất",
"port_53_faq_link": "Cổng 53 thường được sử dụng \"DNSStubListener\" hoặc \"systemd-resolved\". Vui lòng đọc <0>hướng dẫn</0> để giải quyết vấn đề này." "port_53_faq_link": "Cổng 53 thường được sử dụng \"DNSStubListener\" hoặc \"systemd-resolved\". Vui lòng đọc <0>hướng dẫn</0> để giải quyết vấn đề này.",
"adg_will_drop_dns_queries": "AdGuard Home sẽ loại bỏ tất cả các truy vấn DNS từ ứng dụng khách này.",
"client_not_in_allowed_clients": "Ứng dụng khách không được phép vì nó không có trong danh sách \"Ứng dụng khách được phép\".",
"experimental": "Thử nghiệm"
} }

View File

@@ -32,6 +32,7 @@
"form_error_ip_format": "无效的 IPv4 格式", "form_error_ip_format": "无效的 IPv4 格式",
"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 识别不同客户端。在 <a>这里</a>你可以了解到更多关于如何识别客户端的信息。",
"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": "速度限制",
@@ -330,7 +337,7 @@
"encryption_config_saved": "加密配置已保存", "encryption_config_saved": "加密配置已保存",
"encryption_server": "服务器名称", "encryption_server": "服务器名称",
"encryption_server_enter": "输入您的域名", "encryption_server_enter": "输入您的域名",
"encryption_server_desc": "若要使用 HTTPS ,您需要输入与 SSL 证书相匹配的服务器名称。", "encryption_server_desc": "为了使用 HTTPS,请您输入与 SSL 证书或通配证书相匹配的服务器名称。如此字段未设置,服务器将要为所有域名接受 TLS 连接。",
"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 端口",
@@ -386,7 +393,7 @@
"client_edit": "编辑客户端", "client_edit": "编辑客户端",
"client_identifier": "标识符", "client_identifier": "标识符",
"ip_address": "IP 地址", "ip_address": "IP 地址",
"client_identifier_desc": "客户端可通过 IP 地址或 MAC 地址识别。请注意,如 AdGuard Home 也是 <0>DHCP 服务器</0>,则仅能将 MAC 用作标识符", "client_identifier_desc": "客户端可通过 IP MAC 地址、CIDR 或特殊 ID可用于 DoT/DoH/DoQ被识别。<0>这里</0>您可多了解如何识别客户端。",
"form_enter_ip": "输入 IP", "form_enter_ip": "输入 IP",
"form_enter_mac": "输入 MAC", "form_enter_mac": "输入 MAC",
"form_enter_id": "输入标识符", "form_enter_id": "输入标识符",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> 支持 <1>DNS-over-HTTPS</1>。", "setup_dns_privacy_other_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 重写",
@@ -529,7 +537,6 @@
"check_ip": "IP地址{{ip}}", "check_ip": "IP地址{{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "原因:{{reason}}", "check_reason": "原因:{{reason}}",
"check_rule": "规则:{{rule}}",
"check_service": "服务名称:{{service}}", "check_service": "服务名称:{{service}}",
"service_name": "服务名称", "service_name": "服务名称",
"check_not_found": "未在您的筛选列表中找到", "check_not_found": "未在您的筛选列表中找到",

View File

@@ -269,7 +269,6 @@
"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": "已知追蹤器",
@@ -330,7 +329,6 @@
"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 連接埠",
@@ -386,7 +384,6 @@
"client_edit": "編輯用戶端", "client_edit": "編輯用戶端",
"client_identifier": "識別碼", "client_identifier": "識別碼",
"ip_address": "IP 位址", "ip_address": "IP 位址",
"client_identifier_desc": "可通過 IP 地址、CIDR、MAC 地址來辨識使用者裝置。注意:必須使用 AdGuard Home 內建 <0>DHCP 伺服器</0> 才能偵測 MAC 地址。",
"form_enter_ip": "輸入 IP", "form_enter_ip": "輸入 IP",
"form_enter_mac": "輸入 MAC 地址", "form_enter_mac": "輸入 MAC 地址",
"form_enter_id": "輸入識別碼", "form_enter_id": "輸入識別碼",
@@ -529,7 +526,6 @@
"check_ip": "IP 位址:{{ip}}", "check_ip": "IP 位址:{{ip}}",
"check_cname": "CNAME{{cname}}", "check_cname": "CNAME{{cname}}",
"check_reason": "原因:{{reason}}", "check_reason": "原因:{{reason}}",
"check_rule": "規則:{{rule}}",
"check_service": "服務名稱:{{service}}", "check_service": "服務名稱:{{service}}",
"service_name": "服務名稱", "service_name": "服務名稱",
"check_not_found": "未在您的過濾清單中找到", "check_not_found": "未在您的過濾清單中找到",

View File

@@ -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": "速率限制",
@@ -330,7 +337,7 @@
"encryption_config_saved": "加密配置被儲存", "encryption_config_saved": "加密配置被儲存",
"encryption_server": "伺服器名稱", "encryption_server": "伺服器名稱",
"encryption_server_enter": "輸入您的域名", "encryption_server_enter": "輸入您的域名",
"encryption_server_desc": "為了使用 HTTPS您需要輸入與您的安全通訊端層SSL憑證相符的伺服器名稱。", "encryption_server_desc": "為了使用 HTTPS您需要輸入與您的安全通訊端層SSL憑證或萬用字元憑證相符的伺服器名稱。如果此欄位未被設定它將接受向任何網域的傳輸層安全性協定TLS連線。",
"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 連接埠",
@@ -386,7 +393,7 @@
"client_edit": "編輯用戶端", "client_edit": "編輯用戶端",
"client_identifier": "識別碼", "client_identifier": "識別碼",
"ip_address": "IP 位址", "ip_address": "IP 位址",
"client_identifier_desc": "用戶端可 IP 位址、無類別網域間路由CIDR媒體存取控制MAC位址識別。請注意,只要 AdGuard Home 也是<0>動態主機設定協定DHCP伺服器</0>,使用 MAC 作為識別碼是可能的", "client_identifier_desc": "用戶端可根據 IP 位址、無類別網域間路由CIDR媒體存取控制MAC位址或特殊的用戶端 ID可被用於 DoT/DoH/DoQ被識別。<0>於此</0>,您可了解更多關於如何識別用戶端。",
"form_enter_ip": "輸入 IP", "form_enter_ip": "輸入 IP",
"form_enter_mac": "輸入媒體存取控制MAC", "form_enter_mac": "輸入媒體存取控制MAC",
"form_enter_id": "輸入識別碼", "form_enter_id": "輸入識別碼",
@@ -430,6 +437,7 @@
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> 支援 <1>DNS-over-HTTPS</1>。", "setup_dns_privacy_other_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 改寫被成功地刪除",
@@ -529,7 +537,6 @@
"check_ip": "IP 位址:{{ip}}", "check_ip": "IP 位址:{{ip}}",
"check_cname": "正規名稱CNAME{{cname}}", "check_cname": "正規名稱CNAME{{cname}}",
"check_reason": "原因:{{reason}}", "check_reason": "原因:{{reason}}",
"check_rule": "規則:{{rule}}",
"check_service": "服務名稱:{{service}}", "check_service": "服務名稱:{{service}}",
"service_name": "服務名稱", "service_name": "服務名稱",
"check_not_found": "未在您的過濾器中被找到", "check_not_found": "未在您的過濾器中被找到",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.14
require ( require (
github.com/AdguardTeam/dnsproxy v0.33.9 github.com/AdguardTeam/dnsproxy v0.33.9
github.com/AdguardTeam/golibs v0.4.4 github.com/AdguardTeam/golibs v0.4.4
github.com/AdguardTeam/urlfilter v0.14.2 github.com/AdguardTeam/urlfilter v0.14.3
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.0.1 github.com/ameshkov/dnscrypt/v2 v2.0.1
github.com/digineo/go-ipset/v2 v2.2.1 github.com/digineo/go-ipset/v2 v2.2.1

4
go.sum
View File

@@ -26,8 +26,8 @@ github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw= github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw=
github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU= github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.14.2 h1:k26vEYz0mT/liDGZ0JGIBLYLMHaisIGX1UR0qaVnO4k= github.com/AdguardTeam/urlfilter v0.14.3 h1:MBaLEXS0LRQNbHtLkDCYhHINDPtkevPrYWGiOUuLJU4=
github.com/AdguardTeam/urlfilter v0.14.2/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U= github.com/AdguardTeam/urlfilter v0.14.3/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=

View File

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

45
internal/aghtest/os.go Normal file
View File

@@ -0,0 +1,45 @@
package aghtest
import (
"io/ioutil"
"os"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// PrepareTestDir returns the full path to temporary created directory and
// registers the appropriate cleanup for *t.
func PrepareTestDir(t *testing.T) (dir string) {
t.Helper()
wd, err := os.Getwd()
require.Nil(t, err)
dir, err = ioutil.TempDir(wd, "agh-test")
require.Nil(t, err)
require.NotEmpty(t, dir)
t.Cleanup(func() {
// TODO(e.burkov): Replace with t.TempDir methods after updating
// go version to 1.15.
start := time.Now()
for {
err := os.RemoveAll(dir)
if err == nil {
break
}
if runtime.GOOS != "windows" || time.Since(start) >= 500*time.Millisecond {
break
}
time.Sleep(5 * time.Millisecond)
}
assert.Nil(t, err)
})
return dir
}

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,183 +3,108 @@ package home
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestUpgrade1to2(t *testing.T) { // any is a convenient alias for interface{}.
// let's create test config for 1 schema version type any = interface{}
diskConfig := createTestDiskConfig(1)
// update config // object is a convenient alias for map[string]interface{}.
err := upgradeSchema1to2(&diskConfig) type object = map[string]any
if err != nil {
t.Fatalf("Can't upgrade schema version from 1 to 2")
}
// ensure that schema version was bumped func TestUpgradeSchema1to2(t *testing.T) {
compareSchemaVersion(t, diskConfig["schema_version"], 2) diskConf := testDiskConf(1)
// old coredns entry should be removed err := upgradeSchema1to2(&diskConf)
_, ok := diskConfig["coredns"] require.Nil(t, err)
if ok {
t.Fatalf("Core DNS config was not removed after upgrade schema version from 1 to 2")
}
// pull out new dns config require.Equal(t, diskConf["schema_version"], 2)
dnsMap, ok := diskConfig["dns"]
if !ok {
t.Fatalf("No DNS config after upgrade schema version from 1 to 2")
}
// cast dns configurations to maps and compare them _, ok := diskConf["coredns"]
oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(1)) require.False(t, ok)
newDNSConfig := castInterfaceToMap(t, dnsMap)
compareConfigs(t, &oldDNSConfig, &newDNSConfig) dnsMap, ok := diskConf["dns"]
require.True(t, ok)
oldDNSConf := convertToObject(t, testDNSConf(1))
newDNSConf := convertToObject(t, dnsMap)
assert.Equal(t, oldDNSConf, newDNSConf)
// exclude dns config and schema version from disk config comparison
oldExcludedEntries := []string{"coredns", "schema_version"} oldExcludedEntries := []string{"coredns", "schema_version"}
newExcludedEntries := []string{"dns", "schema_version"} newExcludedEntries := []string{"dns", "schema_version"}
oldDiskConfig := createTestDiskConfig(1) oldDiskConf := testDiskConf(1)
compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, oldExcludedEntries, newExcludedEntries) assertEqualExcept(t, oldDiskConf, diskConf, oldExcludedEntries, newExcludedEntries)
} }
func TestUpgrade2to3(t *testing.T) { func TestUpgradeSchema2to3(t *testing.T) {
// let's create test config diskConf := testDiskConf(2)
diskConfig := createTestDiskConfig(2)
// upgrade schema from 2 to 3 err := upgradeSchema2to3(&diskConf)
err := upgradeSchema2to3(&diskConfig) require.Nil(t, err)
if err != nil {
t.Fatalf("Can't update schema version from 2 to 3: %s", err)
}
// check new schema version require.Equal(t, diskConf["schema_version"], 3)
compareSchemaVersion(t, diskConfig["schema_version"], 3)
// pull out new dns configuration dnsMap, ok := diskConf["dns"]
dnsMap, ok := diskConfig["dns"] require.True(t, ok)
if !ok {
t.Fatalf("No dns config in new configuration")
}
// cast dns configuration to map newDNSConf := convertToObject(t, dnsMap)
newDNSConfig := castInterfaceToMap(t, dnsMap) bootstrapDNS := newDNSConf["bootstrap_dns"]
// check if bootstrap DNS becomes an array
bootstrapDNS := newDNSConfig["bootstrap_dns"]
switch v := bootstrapDNS.(type) { switch v := bootstrapDNS.(type) {
case []string: case []string:
if len(v) != 1 { require.Len(t, v, 1)
t.Fatalf("Wrong count of bootsrap DNS servers: %d", len(v)) require.Equal(t, "8.8.8.8:53", v[0])
}
if v[0] != "8.8.8.8:53" {
t.Fatalf("Bootsrap DNS server is not 8.8.8.8:53 : %s", v[0])
}
default: default:
t.Fatalf("Wrong type for bootsrap DNS: %T", v) t.Fatalf("wrong type for bootsrap dns: %T", v)
} }
// exclude bootstrap DNS from DNS configs comparison
excludedEntries := []string{"bootstrap_dns"} excludedEntries := []string{"bootstrap_dns"}
oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(2)) oldDNSConf := convertToObject(t, testDNSConf(2))
compareConfigsWithoutEntries(t, &oldDNSConfig, &newDNSConfig, excludedEntries, excludedEntries) assertEqualExcept(t, oldDNSConf, newDNSConf, excludedEntries, excludedEntries)
// excluded dns config and schema version from disk config comparison
excludedEntries = []string{"dns", "schema_version"} excludedEntries = []string{"dns", "schema_version"}
oldDiskConfig := createTestDiskConfig(2) oldDiskConf := testDiskConf(2)
compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, excludedEntries, excludedEntries) assertEqualExcept(t, oldDiskConf, diskConf, excludedEntries, excludedEntries)
} }
func castInterfaceToMap(t *testing.T, oldConfig interface{}) (newConfig map[string]interface{}) { func convertToObject(t *testing.T, oldConf any) (newConf object) {
newConfig = make(map[string]interface{}) t.Helper()
switch v := oldConfig.(type) {
case map[interface{}]interface{}: switch v := oldConf.(type) {
case map[any]any:
newConf = make(object, len(v))
for key, value := range v { for key, value := range v {
newConfig[fmt.Sprint(key)] = value newConf[fmt.Sprint(key)] = value
} }
case map[string]interface{}: case object:
newConf = make(object, len(v))
for key, value := range v { for key, value := range v {
newConfig[key] = value newConf[key] = value
} }
default: default:
t.Fatalf("DNS configuration is not a map") t.Fatalf("dns configuration is not a map, got %T", oldConf)
} }
return
return newConf
} }
// compareConfigsWithoutEntry removes entries from configs and returns result of compareConfigs // assertEqualExcept removes entries from configs and compares them.
func compareConfigsWithoutEntries(t *testing.T, oldConfig, newConfig *map[string]interface{}, oldKey, newKey []string) { func assertEqualExcept(t *testing.T, oldConf, newConf object, oldKeys, newKeys []string) {
for _, k := range oldKey { t.Helper()
delete(*oldConfig, k)
for _, k := range oldKeys {
delete(oldConf, k)
} }
for _, k := range newKey { for _, k := range newKeys {
delete(*newConfig, k) delete(newConf, k)
} }
compareConfigs(t, oldConfig, newConfig)
assert.Equal(t, oldConf, newConf)
} }
// compares configs before and after schema upgrade func testDiskConf(schemaVersion int) (diskConf object) {
func compareConfigs(t *testing.T, oldConfig, newConfig *map[string]interface{}) { filters := []filter{
if len(*oldConfig) != len(*newConfig) {
t.Fatalf("wrong config entries count! Before upgrade: %d; After upgrade: %d", len(*oldConfig), len(*oldConfig))
}
// Check old and new entries
for k, v := range *newConfig {
switch value := v.(type) {
case string:
if value != (*oldConfig)[k] {
t.Fatalf("wrong value for string %s. Before update: %s; After update: %s", k, (*oldConfig)[k], value)
}
case int:
if value != (*oldConfig)[k] {
t.Fatalf("wrong value for int %s. Before update: %d; After update: %d", k, (*oldConfig)[k], value)
}
case []string:
for i, line := range value {
if len((*oldConfig)[k].([]string)) != len(value) {
t.Fatalf("wrong array length for %s. Before update: %d; After update: %d", k, len((*oldConfig)[k].([]string)), len(value))
}
if (*oldConfig)[k].([]string)[i] != line {
t.Fatalf("wrong data for string array %s. Before update: %s; After update: %s", k, (*oldConfig)[k].([]string)[i], line)
}
}
case bool:
if v != (*oldConfig)[k].(bool) {
t.Fatalf("wrong boolean value for %s", k)
}
case []filter:
if len((*oldConfig)[k].([]filter)) != len(value) {
t.Fatalf("wrong filters count. Before update: %d; After update: %d", len((*oldConfig)[k].([]filter)), len(value))
}
for i, newFilter := range value {
oldFilter := (*oldConfig)[k].([]filter)[i]
if oldFilter.Enabled != newFilter.Enabled || oldFilter.Name != newFilter.Name || oldFilter.RulesCount != newFilter.RulesCount {
t.Fatalf("old filter %s not equals new filter %s", oldFilter.Name, newFilter.Name)
}
}
default:
t.Fatalf("uknown data type for %s: %T", k, value)
}
}
}
// compareSchemaVersion check if newSchemaVersion equals schemaVersion
func compareSchemaVersion(t *testing.T, newSchemaVersion interface{}, schemaVersion int) {
switch v := newSchemaVersion.(type) {
case int:
if v != schemaVersion {
t.Fatalf("Wrong schema version in new config file")
}
default:
t.Fatalf("Schema version is not an integer after update")
}
}
func createTestDiskConfig(schemaVersion int) (diskConfig map[string]interface{}) {
diskConfig = make(map[string]interface{})
diskConfig["language"] = "en"
diskConfig["filters"] = []filter{
{ {
URL: "https://filters.adtidy.org/android/filters/111_optimized.txt", URL: "https://filters.adtidy.org/android/filters/111_optimized.txt",
Name: "Latvian filter", Name: "Latvian filter",
@@ -191,40 +116,51 @@ func createTestDiskConfig(schemaVersion int) (diskConfig map[string]interface{})
RulesCount: 200, RulesCount: 200,
}, },
} }
diskConfig["user_rules"] = []string{} diskConf = object{
diskConfig["schema_version"] = schemaVersion "language": "en",
diskConfig["bind_host"] = "0.0.0.0" "filters": filters,
diskConfig["bind_port"] = 80 "user_rules": []string{},
diskConfig["auth_name"] = "name" "schema_version": schemaVersion,
diskConfig["auth_pass"] = "pass" "bind_host": "0.0.0.0",
dnsConfig := createTestDNSConfig(schemaVersion) "bind_port": 80,
if schemaVersion > 1 { "auth_name": "name",
diskConfig["dns"] = dnsConfig "auth_pass": "pass",
} else {
diskConfig["coredns"] = dnsConfig
} }
return diskConfig
dnsConf := testDNSConf(schemaVersion)
if schemaVersion > 1 {
diskConf["dns"] = dnsConf
} else {
diskConf["coredns"] = dnsConf
}
return diskConf
} }
func createTestDNSConfig(schemaVersion int) map[interface{}]interface{} { // testDNSConf creates a DNS config for test the way gopkg.in/yaml.v2 would
dnsConfig := make(map[interface{}]interface{}) // unmarshal it. In YAML, keys aren't guaranteed to always only be strings.
dnsConfig["port"] = 53 func testDNSConf(schemaVersion int) (dnsConf map[any]any) {
dnsConfig["blocked_response_ttl"] = 10 dnsConf = map[any]any{
dnsConfig["querylog_enabled"] = true "port": 53,
dnsConfig["ratelimit"] = 20 "blocked_response_ttl": 10,
dnsConfig["bootstrap_dns"] = "8.8.8.8:53" "querylog_enabled": true,
if schemaVersion > 2 { "ratelimit": 20,
dnsConfig["bootstrap_dns"] = []string{"8.8.8.8:53"} "bootstrap_dns": "8.8.8.8:53",
"parental_sensitivity": 13,
"ratelimit_whitelist": []string{},
"upstream_dns": []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"},
"filtering_enabled": true,
"refuse_any": true,
"parental_enabled": true,
"bind_host": "0.0.0.0",
"protection_enabled": true,
"safesearch_enabled": true,
"safebrowsing_enabled": true,
} }
dnsConfig["parental_sensitivity"] = 13
dnsConfig["ratelimit_whitelist"] = []string{} if schemaVersion > 2 {
dnsConfig["upstream_dns"] = []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"} dnsConf["bootstrap_dns"] = []string{"8.8.8.8:53"}
dnsConfig["filtering_enabled"] = true }
dnsConfig["refuse_any"] = true
dnsConfig["parental_enabled"] = true return dnsConf
dnsConfig["bind_host"] = "0.0.0.0"
dnsConfig["protection_enabled"] = true
dnsConfig["safesearch_enabled"] = true
dnsConfig["safebrowsing_enabled"] = true
return dnsConfig
} }

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
package querylog package querylog
import ( import (
"fmt"
"math/rand" "math/rand"
"net" "net"
"os"
"sort" "sort"
"testing" "testing"
"time" "time"
@@ -14,226 +14,260 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
"github.com/miekg/dns" "github.com/miekg/dns"
"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) {
aghtest.DiscardLogOutput(m) aghtest.DiscardLogOutput(m)
} }
func prepareTestDir() string { // TestQueryLog tests adding and loading (with filtering) entries from disk and
const dir = "./agh-test" // memory.
_ = os.RemoveAll(dir)
_ = os.MkdirAll(dir, 0o755)
return dir
}
// Check adding and loading (with filtering) entries from disk and memory
func TestQueryLog(t *testing.T) { func TestQueryLog(t *testing.T) {
conf := Config{ l := newQueryLog(Config{
Enabled: true, Enabled: true,
FileEnabled: true, FileEnabled: true,
Interval: 1, Interval: 1,
MemSize: 100, MemSize: 100,
} BaseDir: aghtest.PrepareTestDir(t),
conf.BaseDir = prepareTestDir() })
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
l := newQueryLog(conf)
// add disk entries // Add disk entries.
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
// write to disk (first file) // Write to disk (first file).
_ = l.flushLogBuffer(true) require.Nil(t, l.flushLogBuffer(true))
// start writing to the second file // Start writing to the second file.
_ = l.rotate() require.Nil(t, l.rotate())
// add disk entries // Add disk entries.
addEntry(l, "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2)) addEntry(l, "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
// write to disk // Write to disk.
_ = l.flushLogBuffer(true) require.Nil(t, l.flushLogBuffer(true))
// add memory entries // Add memory entries.
addEntry(l, "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3)) addEntry(l, "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
addEntry(l, "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4)) addEntry(l, "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
// get all entries type tcAssertion struct {
params := newSearchParams() num int
entries, _ := l.search(params) host string
assert.Len(t, entries, 4) answer, client net.IP
assertLogEntry(t, entries[0], "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4)) }
assertLogEntry(t, entries[1], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
assertLogEntry(t, entries[2], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
assertLogEntry(t, entries[3], "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
// search by domain (strict) testCases := []struct {
params = newSearchParams() name string
params.searchCriteria = append(params.searchCriteria, searchCriteria{ sCr []searchCriteria
criteriaType: ctDomainOrClient, want []tcAssertion
strict: true, }{{
value: "TEST.example.org", name: "all",
}) sCr: []searchCriteria{},
entries, _ = l.search(params) want: []tcAssertion{
assert.Len(t, entries, 1) {num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
assertLogEntry(t, entries[0], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3)) {num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
},
}, {
name: "by_domain_strict",
sCr: []searchCriteria{{
criteriaType: ctDomainOrClient,
strict: true,
value: "TEST.example.org",
}},
want: []tcAssertion{{
num: 0, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3),
}},
}, {
name: "by_domain_non-strict",
sCr: []searchCriteria{{
criteriaType: ctDomainOrClient,
strict: false,
value: "example.ORG",
}},
want: []tcAssertion{
{num: 0, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 1, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
},
}, {
name: "by_client_ip_strict",
sCr: []searchCriteria{{
criteriaType: ctDomainOrClient,
strict: true,
value: "2.2.2.2",
}},
want: []tcAssertion{{
num: 0, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2),
}},
}, {
name: "by_client_ip_non-strict",
sCr: []searchCriteria{{
criteriaType: ctDomainOrClient,
strict: false,
value: "2.2.2",
}},
want: []tcAssertion{
{num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
{num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
},
}}
// search by domain (not strict) for _, tc := range testCases {
params = newSearchParams() t.Run(tc.name, func(t *testing.T) {
params.searchCriteria = append(params.searchCriteria, searchCriteria{ params := newSearchParams()
criteriaType: ctDomainOrClient, params.searchCriteria = tc.sCr
strict: false,
value: "example.ORG",
})
entries, _ = l.search(params)
assert.Len(t, entries, 3)
assertLogEntry(t, entries[0], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
assertLogEntry(t, entries[1], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
assertLogEntry(t, entries[2], "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
// search by client IP (strict) entries, _ := l.search(params)
params = newSearchParams() require.Len(t, entries, len(tc.want))
params.searchCriteria = append(params.searchCriteria, searchCriteria{ for _, want := range tc.want {
criteriaType: ctDomainOrClient, assertLogEntry(t, entries[want.num], want.host, want.answer, want.client)
strict: true, }
value: "2.2.2.2", })
}) }
entries, _ = l.search(params)
assert.Len(t, entries, 1)
assertLogEntry(t, entries[0], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
// search by client IP (part of)
params = newSearchParams()
params.searchCriteria = append(params.searchCriteria, searchCriteria{
criteriaType: ctDomainOrClient,
strict: false,
value: "2.2.2",
})
entries, _ = l.search(params)
assert.Len(t, entries, 4)
assertLogEntry(t, entries[0], "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
assertLogEntry(t, entries[1], "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
assertLogEntry(t, entries[2], "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
assertLogEntry(t, entries[3], "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
} }
func TestQueryLogOffsetLimit(t *testing.T) { func TestQueryLogOffsetLimit(t *testing.T) {
conf := Config{ l := newQueryLog(Config{
Enabled: true, Enabled: true,
Interval: 1, Interval: 1,
MemSize: 100, MemSize: 100,
} BaseDir: aghtest.PrepareTestDir(t),
conf.BaseDir = prepareTestDir() })
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
l := newQueryLog(conf)
// add 10 entries to the log const (
for i := 0; i < 10; i++ { entNum = 10
addEntry(l, "second.example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) firstPageDomain = "first.example.org"
secondPageDomain = "second.example.org"
)
// Add entries to the log.
for i := 0; i < entNum; i++ {
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
} }
// write them to disk (first file) // Write them to the first file.
_ = l.flushLogBuffer(true) require.Nil(t, l.flushLogBuffer(true))
// add 10 more entries to the log (memory) // Add more to the in-memory part of log.
for i := 0; i < 10; i++ { for i := 0; i < entNum; i++ {
addEntry(l, "first.example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
} }
// First page
params := newSearchParams() params := newSearchParams()
params.offset = 0
params.limit = 10
entries, _ := l.search(params)
assert.Len(t, entries, 10)
assert.Equal(t, entries[0].QHost, "first.example.org")
assert.Equal(t, entries[9].QHost, "first.example.org")
// Second page testCases := []struct {
params.offset = 10 name string
params.limit = 10 offset int
entries, _ = l.search(params) limit int
assert.Len(t, entries, 10) wantLen int
assert.Equal(t, entries[0].QHost, "second.example.org") want string
assert.Equal(t, entries[9].QHost, "second.example.org") }{{
name: "page_1",
offset: 0,
limit: 10,
wantLen: 10,
want: firstPageDomain,
}, {
name: "page_2",
offset: 10,
limit: 10,
wantLen: 10,
want: secondPageDomain,
}, {
name: "page_2.5",
offset: 15,
limit: 10,
wantLen: 5,
want: secondPageDomain,
}, {
name: "page_3",
offset: 20,
limit: 10,
wantLen: 0,
}}
// Second and a half page for _, tc := range testCases {
params.offset = 15 t.Run(tc.name, func(t *testing.T) {
params.limit = 10 params.offset = tc.offset
entries, _ = l.search(params) params.limit = tc.limit
assert.Len(t, entries, 5) entries, _ := l.search(params)
assert.Equal(t, entries[0].QHost, "second.example.org")
assert.Equal(t, entries[4].QHost, "second.example.org")
// Third page require.Len(t, entries, tc.wantLen)
params.offset = 20
params.limit = 10 if tc.wantLen > 0 {
entries, _ = l.search(params) assert.Equal(t, entries[0].QHost, tc.want)
assert.Empty(t, entries) assert.Equal(t, entries[tc.wantLen-1].QHost, tc.want)
}
})
}
} }
func TestQueryLogMaxFileScanEntries(t *testing.T) { func TestQueryLogMaxFileScanEntries(t *testing.T) {
conf := Config{ l := newQueryLog(Config{
Enabled: true, Enabled: true,
FileEnabled: true, FileEnabled: true,
Interval: 1, Interval: 1,
MemSize: 100, MemSize: 100,
} BaseDir: aghtest.PrepareTestDir(t),
conf.BaseDir = prepareTestDir() })
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
l := newQueryLog(conf)
// add 10 entries to the log const entNum = 10
for i := 0; i < 10; i++ { // Add entries to the log.
for i := 0; i < entNum; i++ {
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
} }
// write them to disk (first file) // Write them to disk.
_ = l.flushLogBuffer(true) require.Nil(t, l.flushLogBuffer(true))
params := newSearchParams() params := newSearchParams()
params.maxFileScanEntries = 5 // do not scan more than 5 records
entries, _ := l.search(params)
assert.Len(t, entries, 5)
params.maxFileScanEntries = 0 // disable the limit for _, maxFileScanEntries := range []int{5, 0} {
entries, _ = l.search(params) t.Run(fmt.Sprintf("limit_%d", maxFileScanEntries), func(t *testing.T) {
assert.Len(t, entries, 10) params.maxFileScanEntries = maxFileScanEntries
entries, _ := l.search(params)
assert.Len(t, entries, entNum-maxFileScanEntries)
})
}
} }
func TestQueryLogFileDisabled(t *testing.T) { func TestQueryLogFileDisabled(t *testing.T) {
conf := Config{ l := newQueryLog(Config{
Enabled: true, Enabled: true,
FileEnabled: false, FileEnabled: false,
Interval: 1, Interval: 1,
MemSize: 2, MemSize: 2,
} BaseDir: aghtest.PrepareTestDir(t),
conf.BaseDir = prepareTestDir() })
defer func() { _ = os.RemoveAll(conf.BaseDir) }()
l := newQueryLog(conf)
addEntry(l, "example1.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) addEntry(l, "example1.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
addEntry(l, "example2.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) addEntry(l, "example2.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
// The oldest entry is going to be removed from memory buffer.
addEntry(l, "example3.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) addEntry(l, "example3.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
// the oldest entry is now removed from mem buffer
params := newSearchParams() params := newSearchParams()
ll, _ := l.search(params) ll, _ := l.search(params)
assert.Len(t, ll, 2) require.Len(t, ll, 2)
assert.Equal(t, "example3.org", ll[0].QHost) assert.Equal(t, "example3.org", ll[0].QHost)
assert.Equal(t, "example2.org", ll[1].QHost) assert.Equal(t, "example2.org", ll[1].QHost)
} }
func addEntry(l *queryLog, host string, answerStr, client net.IP) { func addEntry(l *queryLog, host string, answerStr, client net.IP) {
q := dns.Msg{} q := dns.Msg{
q.Question = append(q.Question, dns.Question{ Question: []dns.Question{{
Name: host + ".", Name: host + ".",
Qtype: dns.TypeA, Qtype: dns.TypeA,
Qclass: dns.ClassINET, Qclass: dns.ClassINET,
}) }},
}
a := dns.Msg{}
a.Question = append(a.Question, q.Question[0]) a := dns.Msg{
answer := new(dns.A) Question: q.Question,
answer.Hdr = dns.RR_Header{ Answer: []dns.RR{&dns.A{
Name: q.Question[0].Name, Hdr: dns.RR_Header{
Rrtype: dns.TypeA, Name: q.Question[0].Name,
Class: dns.ClassINET, Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: answerStr,
}},
} }
answer.A = answerStr
a.Answer = append(a.Answer, answer)
res := dnsfilter.Result{ res := dnsfilter.Result{
IsFiltered: true, IsFiltered: true,
Reason: dnsfilter.Rewritten, Reason: dnsfilter.Rewritten,
@@ -254,19 +288,22 @@ func addEntry(l *queryLog, host string, answerStr, client net.IP) {
l.Add(params) l.Add(params)
} }
func assertLogEntry(t *testing.T, entry *logEntry, host string, answer, client net.IP) bool { func assertLogEntry(t *testing.T, entry *logEntry, host string, answer, client net.IP) {
t.Helper()
require.NotNil(t, entry)
assert.Equal(t, host, entry.QHost) assert.Equal(t, host, entry.QHost)
assert.Equal(t, client, entry.IP) assert.Equal(t, client, entry.IP)
assert.Equal(t, "A", entry.QType) assert.Equal(t, "A", entry.QType)
assert.Equal(t, "IN", entry.QClass) assert.Equal(t, "IN", entry.QClass)
msg := new(dns.Msg) msg := &dns.Msg{}
assert.Nil(t, msg.Unpack(entry.Answer)) require.Nil(t, msg.Unpack(entry.Answer))
assert.Len(t, msg.Answer, 1) require.Len(t, msg.Answer, 1)
ip := proxyutil.GetIPFromDNSRecord(msg.Answer[0]).To16() ip := proxyutil.GetIPFromDNSRecord(msg.Answer[0]).To16()
assert.NotNil(t, ip)
assert.Equal(t, answer, ip) assert.Equal(t, answer, ip)
return true
} }
func testEntries() (entries []*logEntry) { func testEntries() (entries []*logEntry) {
@@ -332,8 +369,8 @@ func TestLogEntriesByTime_sort(t *testing.T) {
entries := testEntries() entries := testEntries()
sort.Sort(logEntriesByTimeDesc(entries)) sort.Sort(logEntriesByTimeDesc(entries))
for i := 1; i < len(entries); i++ { for i := range entries[1:] {
assert.False(t, entries[i].Time.After(entries[i-1].Time), assert.False(t, entries[i+1].Time.After(entries[i].Time),
"%s %s", entries[i].Time, entries[i-1].Time) "%s %s", entries[i+1].Time, entries[i].Time)
} }
} }

View File

@@ -2,347 +2,347 @@ package querylog
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math" "math"
"net" "net"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestQLogFileEmpty(t *testing.T) { // prepareTestFiles prepares several test query log files, each with the
testDir := prepareTestDir() // specified lines count.
defer func() { _ = os.RemoveAll(testDir) }() func prepareTestFiles(t *testing.T, filesNum, linesNum int) []string {
testFile := prepareTestFile(testDir, 0) t.Helper()
// create the new QLogFile instance if filesNum == 0 {
q, err := NewQLogFile(testFile) return []string{}
assert.Nil(t, err)
assert.NotNil(t, q)
defer q.Close()
// seek to the start
pos, err := q.SeekStart()
assert.Nil(t, err)
assert.EqualValues(t, 0, pos)
// try reading anyway
line, err := q.ReadNext()
assert.Equal(t, io.EOF, err)
assert.Empty(t, line)
}
func TestQLogFileLarge(t *testing.T) {
// should be large enough
count := 50000
testDir := prepareTestDir()
defer func() { _ = os.RemoveAll(testDir) }()
testFile := prepareTestFile(testDir, count)
// create the new QLogFile instance
q, err := NewQLogFile(testFile)
assert.Nil(t, err)
assert.NotNil(t, q)
defer q.Close()
// seek to the start
pos, err := q.SeekStart()
assert.Nil(t, err)
assert.NotEqualValues(t, 0, pos)
read := 0
var line string
for err == nil {
line, err = q.ReadNext()
if err == nil {
assert.NotZero(t, len(line))
read++
}
} }
assert.Equal(t, count, read) const strV = "\"%s\""
assert.Equal(t, io.EOF, err) const nl = "\n"
} const format = `{"IP":` + strV + `,"T":` + strV + `,` +
`"QH":"example.org","QT":"A","QC":"IN",` +
func TestQLogFileSeekLargeFile(t *testing.T) { `"Answer":"AAAAAAABAAEAAAAAB2V4YW1wbGUDb3JnAAABAAEHZXhhbXBsZQNvcmcAAAEAAQAAAAAABAECAwQ=",` +
// more or less big file `"Result":{},"Elapsed":0,"Upstream":"upstream"}` + nl
count := 10000
testDir := prepareTestDir()
defer func() { _ = os.RemoveAll(testDir) }()
testFile := prepareTestFile(testDir, count)
// create the new QLogFile instance
q, err := NewQLogFile(testFile)
assert.Nil(t, err)
assert.NotNil(t, q)
defer q.Close()
// CASE 1: NOT TOO OLD LINE
testSeekLineQLogFile(t, q, 300)
// CASE 2: OLD LINE
testSeekLineQLogFile(t, q, count-300)
// CASE 3: FIRST LINE
testSeekLineQLogFile(t, q, 0)
// CASE 4: LAST LINE
testSeekLineQLogFile(t, q, count)
// CASE 5: Seek non-existent (too low)
_, _, err = q.SeekTS(123)
assert.NotNil(t, err)
// CASE 6: Seek non-existent (too high)
ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
_, _, err = q.SeekTS(ts.UnixNano())
assert.NotNil(t, err)
// CASE 7: "Almost" found
line, err := getQLogFileLine(q, count/2)
assert.Nil(t, err)
// ALMOST the record we need
timestamp := readQLogTimestamp(line) - 1
assert.NotEqualValues(t, 0, timestamp)
_, depth, err := q.SeekTS(timestamp)
assert.NotNil(t, err)
assert.LessOrEqual(t, depth, int(math.Log2(float64(count))+3))
}
func TestQLogFileSeekSmallFile(t *testing.T) {
// more or less big file
count := 10
testDir := prepareTestDir()
defer func() { _ = os.RemoveAll(testDir) }()
testFile := prepareTestFile(testDir, count)
// create the new QLogFile instance
q, err := NewQLogFile(testFile)
assert.Nil(t, err)
assert.NotNil(t, q)
defer q.Close()
// CASE 1: NOT TOO OLD LINE
testSeekLineQLogFile(t, q, 2)
// CASE 2: OLD LINE
testSeekLineQLogFile(t, q, count-2)
// CASE 3: FIRST LINE
testSeekLineQLogFile(t, q, 0)
// CASE 4: LAST LINE
testSeekLineQLogFile(t, q, count)
// CASE 5: Seek non-existent (too low)
_, _, err = q.SeekTS(123)
assert.NotNil(t, err)
// CASE 6: Seek non-existent (too high)
ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
_, _, err = q.SeekTS(ts.UnixNano())
assert.NotNil(t, err)
// CASE 7: "Almost" found
line, err := getQLogFileLine(q, count/2)
assert.Nil(t, err)
// ALMOST the record we need
timestamp := readQLogTimestamp(line) - 1
assert.NotEqualValues(t, 0, timestamp)
_, depth, err := q.SeekTS(timestamp)
assert.NotNil(t, err)
assert.LessOrEqual(t, depth, int(math.Log2(float64(count))+3))
}
func testSeekLineQLogFile(t *testing.T, q *QLogFile, lineNumber int) {
line, err := getQLogFileLine(q, lineNumber)
assert.Nil(t, err)
ts := readQLogTimestamp(line)
assert.NotEqualValues(t, 0, ts)
// try seeking to that line now
pos, _, err := q.SeekTS(ts)
assert.Nil(t, err)
assert.NotEqualValues(t, 0, pos)
testLine, err := q.ReadNext()
assert.Nil(t, err)
assert.Equal(t, line, testLine)
}
func getQLogFileLine(q *QLogFile, lineNumber int) (string, error) {
_, err := q.SeekStart()
if err != nil {
return "", err
}
for i := 1; i < lineNumber; i++ {
_, err := q.ReadNext()
if err != nil {
return "", err
}
}
return q.ReadNext()
}
// Check adding and loading (with filtering) entries from disk and memory
func TestQLogFile(t *testing.T) {
testDir := prepareTestDir()
defer func() { _ = os.RemoveAll(testDir) }()
testFile := prepareTestFile(testDir, 2)
// create the new QLogFile instance
q, err := NewQLogFile(testFile)
assert.Nil(t, err)
assert.NotNil(t, q)
defer q.Close()
// seek to the start
pos, err := q.SeekStart()
assert.Nil(t, err)
assert.Greater(t, pos, int64(0))
// read first line
line, err := q.ReadNext()
assert.Nil(t, err)
assert.Contains(t, line, "0.0.0.2")
assert.True(t, strings.HasPrefix(line, "{"), line)
assert.True(t, strings.HasSuffix(line, "}"), line)
// read second line
line, err = q.ReadNext()
assert.Nil(t, err)
assert.EqualValues(t, 0, q.position)
assert.Contains(t, line, "0.0.0.1")
assert.True(t, strings.HasPrefix(line, "{"), line)
assert.True(t, strings.HasSuffix(line, "}"), line)
// try reading again (there's nothing to read anymore)
line, err = q.ReadNext()
assert.Equal(t, io.EOF, err)
assert.Empty(t, line)
}
// prepareTestFile - prepares a test query log file with the specified number of lines
func prepareTestFile(dir string, linesCount int) string {
return prepareTestFiles(dir, 1, linesCount)[0]
}
// prepareTestFiles - prepares several test query log files
// each of them -- with the specified linesCount
func prepareTestFiles(dir string, filesCount, linesCount int) []string {
format := `{"IP":"${IP}","T":"${TIMESTAMP}","QH":"example.org","QT":"A","QC":"IN","Answer":"AAAAAAABAAEAAAAAB2V4YW1wbGUDb3JnAAABAAEHZXhhbXBsZQNvcmcAAAEAAQAAAAAABAECAwQ=","Result":{},"Elapsed":0,"Upstream":"upstream"}`
lineTime, _ := time.Parse(time.RFC3339Nano, "2020-02-18T22:36:35.920973+03:00") lineTime, _ := time.Parse(time.RFC3339Nano, "2020-02-18T22:36:35.920973+03:00")
lineIP := uint32(0) lineIP := uint32(0)
files := make([]string, filesCount) dir := aghtest.PrepareTestDir(t)
for j := 0; j < filesCount; j++ {
f, _ := ioutil.TempFile(dir, "*.txt")
files[filesCount-j-1] = f.Name()
for i := 0; i < linesCount; i++ { files := make([]string, filesNum)
for j := range files {
f, err := ioutil.TempFile(dir, "*.txt")
require.Nil(t, err)
files[filesNum-j-1] = f.Name()
for i := 0; i < linesNum; i++ {
lineIP++ lineIP++
lineTime = lineTime.Add(time.Second) lineTime = lineTime.Add(time.Second)
ip := make(net.IP, 4) ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, lineIP) binary.BigEndian.PutUint32(ip, lineIP)
line := format line := fmt.Sprintf(format, ip, lineTime.Format(time.RFC3339Nano))
line = strings.ReplaceAll(line, "${IP}", ip.String())
line = strings.ReplaceAll(line, "${TIMESTAMP}", lineTime.Format(time.RFC3339Nano))
_, _ = f.WriteString(line) _, err = f.WriteString(line)
_, _ = f.WriteString("\n") require.Nil(t, err)
} }
} }
return files return files
} }
func TestQLogSeek(t *testing.T) { // prepareTestFile prepares a test query log file with the specified number of
testDir := prepareTestDir() // lines.
defer func() { _ = os.RemoveAll(testDir) }() func prepareTestFile(t *testing.T, linesCount int) string {
t.Helper()
d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"} return prepareTestFiles(t, 1, linesCount)[0]
{"T":"2020-08-31T18:44:25.376690873+03:00"}
{"T":"2020-08-31T18:44:25.382540454+03:00"}`
f, _ := ioutil.TempFile(testDir, "*.txt")
_, _ = f.WriteString(d)
defer f.Close()
q, err := NewQLogFile(f.Name())
assert.Nil(t, err)
defer q.Close()
target, _ := time.Parse(time.RFC3339, "2020-08-31T18:44:25.376690873+03:00")
_, depth, err := q.SeekTS(target.UnixNano())
assert.Nil(t, err)
assert.Equal(t, 1, depth)
} }
func TestQLogSeek_ErrTSTooLate(t *testing.T) { // newTestQLogFile creates new *QLogFile for tests and registers the required
testDir := prepareTestDir() // cleanup functions.
func newTestQLogFile(t *testing.T, linesNum int) (file *QLogFile) {
t.Helper()
testFile := prepareTestFile(t, linesNum)
// Create the new QLogFile instance.
file, err := NewQLogFile(testFile)
require.Nil(t, err)
assert.NotNil(t, file)
t.Cleanup(func() { t.Cleanup(func() {
_ = os.RemoveAll(testDir) assert.Nil(t, file.Close())
}) })
d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"} return file
{"T":"2020-08-31T18:44:25.376690873+03:00"}
{"T":"2020-08-31T18:44:25.382540454+03:00"}
`
f, err := ioutil.TempFile(testDir, "*.txt")
assert.Nil(t, err)
defer f.Close()
_, err = f.WriteString(d)
assert.Nil(t, err)
q, err := NewQLogFile(f.Name())
assert.Nil(t, err)
defer q.Close()
target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:25.382540454+03:00")
assert.Nil(t, err)
_, depth, err := q.SeekTS(target.UnixNano() + int64(time.Second))
assert.Equal(t, ErrTSTooLate, err)
assert.Equal(t, 2, depth)
} }
func TestQLogSeek_ErrTSTooEarly(t *testing.T) { func TestQLogFile_ReadNext(t *testing.T) {
testDir := prepareTestDir() testCases := []struct {
name string
linesNum int
}{{
name: "empty",
linesNum: 0,
}, {
name: "large",
linesNum: 50000,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
q := newTestQLogFile(t, tc.linesNum)
// Calculate the expected position.
fileInfo, err := q.file.Stat()
require.Nil(t, err)
var expPos int64
if expPos = fileInfo.Size(); expPos > 0 {
expPos--
}
// Seek to the start.
pos, err := q.SeekStart()
require.Nil(t, err)
require.EqualValues(t, expPos, pos)
var read int
var line string
for err == nil {
line, err = q.ReadNext()
if err == nil {
assert.NotEmpty(t, line)
read++
}
}
require.Equal(t, io.EOF, err)
assert.Equal(t, tc.linesNum, read)
})
}
}
func TestQLogFile_SeekTS_good(t *testing.T) {
linesCases := []struct {
name string
num int
}{{
name: "large",
num: 10000,
}, {
name: "small",
num: 10,
}}
for _, l := range linesCases {
testCases := []struct {
name string
linesNum int
line int
}{{
name: "not_too_old",
line: 2,
}, {
name: "old",
line: l.num - 2,
}, {
name: "first",
line: 0,
}, {
name: "last",
line: l.num,
}}
q := newTestQLogFile(t, l.num)
for _, tc := range testCases {
t.Run(l.name+"_"+tc.name, func(t *testing.T) {
line, err := getQLogFileLine(q, tc.line)
require.Nil(t, err)
ts := readQLogTimestamp(line)
assert.NotEqualValues(t, 0, ts)
// Try seeking to that line now.
pos, _, err := q.SeekTS(ts)
require.Nil(t, err)
assert.NotEqualValues(t, 0, pos)
testLine, err := q.ReadNext()
require.Nil(t, err)
assert.Equal(t, line, testLine)
})
}
}
}
func TestQLogFile_SeekTS_bad(t *testing.T) {
linesCases := []struct {
name string
num int
}{{
name: "large",
num: 10000,
}, {
name: "small",
num: 10,
}}
for _, l := range linesCases {
testCases := []struct {
name string
ts int64
leq bool
}{{
name: "non-existent_long_ago",
}, {
name: "non-existent_far_ahead",
}, {
name: "almost",
leq: true,
}}
q := newTestQLogFile(t, l.num)
testCases[0].ts = 123
lateTS, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
testCases[1].ts = lateTS.UnixNano()
line, err := getQLogFileLine(q, l.num/2)
require.Nil(t, err)
testCases[2].ts = readQLogTimestamp(line) - 1
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.NotEqualValues(t, 0, tc.ts)
_, depth, err := q.SeekTS(tc.ts)
assert.NotEmpty(t, l.num)
require.NotNil(t, err)
if tc.leq {
assert.LessOrEqual(t, depth, int(math.Log2(float64(l.num))+3))
}
})
}
}
}
func getQLogFileLine(q *QLogFile, lineNumber int) (line string, err error) {
if _, err = q.SeekStart(); err != nil {
return line, err
}
for i := 1; i < lineNumber; i++ {
if _, err = q.ReadNext(); err != nil {
return line, err
}
}
return q.ReadNext()
}
// Check adding and loading (with filtering) entries from disk and memory.
func TestQLogFile(t *testing.T) {
// Create the new QLogFile instance.
q := newTestQLogFile(t, 2)
// Seek to the start.
pos, err := q.SeekStart()
require.Nil(t, err)
assert.Greater(t, pos, int64(0))
// Read first line.
line, err := q.ReadNext()
require.Nil(t, err)
assert.Contains(t, line, "0.0.0.2")
assert.True(t, strings.HasPrefix(line, "{"), line)
assert.True(t, strings.HasSuffix(line, "}"), line)
// Read second line.
line, err = q.ReadNext()
require.Nil(t, err)
assert.EqualValues(t, 0, q.position)
assert.Contains(t, line, "0.0.0.1")
assert.True(t, strings.HasPrefix(line, "{"), line)
assert.True(t, strings.HasSuffix(line, "}"), line)
// Try reading again (there's nothing to read anymore).
line, err = q.ReadNext()
require.Equal(t, io.EOF, err)
assert.Empty(t, line)
}
func NewTestQLogFileData(t *testing.T, data string) (file *QLogFile) {
f, err := ioutil.TempFile(aghtest.PrepareTestDir(t), "*.txt")
require.Nil(t, err)
t.Cleanup(func() { t.Cleanup(func() {
_ = os.RemoveAll(testDir) assert.Nil(t, f.Close())
}) })
d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"} _, err = f.WriteString(data)
{"T":"2020-08-31T18:44:25.376690873+03:00"} require.Nil(t, err)
{"T":"2020-08-31T18:44:25.382540454+03:00"}
`
f, err := ioutil.TempFile(testDir, "*.txt")
assert.Nil(t, err)
defer f.Close()
_, err = f.WriteString(d) file, err = NewQLogFile(f.Name())
assert.Nil(t, err) require.Nil(t, err)
t.Cleanup(func() {
assert.Nil(t, file.Close())
})
q, err := NewQLogFile(f.Name()) return file
assert.Nil(t, err) }
defer q.Close()
func TestQLog_Seek(t *testing.T) {
target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:23.911246629+03:00") const nl = "\n"
assert.Nil(t, err) const strV = "%s"
const recs = `{"T":"` + strV + `","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"}` + nl +
_, depth, err := q.SeekTS(target.UnixNano() - int64(time.Second)) `{"T":"` + strV + `"}` + nl +
assert.Equal(t, ErrTSTooEarly, err) `{"T":"` + strV + `"}` + nl
assert.Equal(t, 1, depth) timestamp, _ := time.Parse(time.RFC3339Nano, "2020-08-31T18:44:25.376690873+03:00")
testCases := []struct {
name string
delta int
wantErr error
wantDepth int
}{{
name: "ok",
delta: 0,
wantErr: nil,
wantDepth: 2,
}, {
name: "too_late",
delta: 2,
wantErr: ErrTSTooLate,
wantDepth: 2,
}, {
name: "too_early",
delta: -2,
wantErr: ErrTSTooEarly,
wantDepth: 1,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data := fmt.Sprintf(recs,
timestamp.Add(-time.Second).Format(time.RFC3339Nano),
timestamp.Format(time.RFC3339Nano),
timestamp.Add(time.Second).Format(time.RFC3339Nano),
)
q := NewTestQLogFileData(t, data)
_, depth, err := q.SeekTS(timestamp.Add(time.Second * time.Duration(tc.delta)).UnixNano())
require.Truef(t, errors.Is(err, tc.wantErr), "%v", err)
assert.Equal(t, tc.wantDepth, depth)
})
}
} }

View File

@@ -3,110 +3,77 @@ package querylog
import ( import (
"errors" "errors"
"io" "io"
"os"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestQLogReaderEmpty(t *testing.T) { // newTestQLogReader creates new *QLogReader for tests and registers the
r, err := NewQLogReader([]string{}) // required cleanup functions.
assert.Nil(t, err) func newTestQLogReader(t *testing.T, filesNum, linesNum int) (reader *QLogReader) {
assert.NotNil(t, r) t.Helper()
defer r.Close()
// seek to the start testFiles := prepareTestFiles(t, filesNum, linesNum)
err = r.SeekStart()
assert.Nil(t, err)
line, err := r.ReadNext() // Create the new QLogReader instance.
assert.Empty(t, line) reader, err := NewQLogReader(testFiles)
assert.Equal(t, io.EOF, err) require.Nil(t, err)
assert.NotNil(t, reader)
t.Cleanup(func() {
assert.Nil(t, reader.Close())
})
return reader
} }
func TestQLogReaderOneFile(t *testing.T) { func TestQLogReader(t *testing.T) {
// let's do one small file testCases := []struct {
count := 10 name string
filesCount := 1 filesNum int
linesNum int
}{{
name: "empty",
filesNum: 0,
linesNum: 0,
}, {
name: "one_file",
filesNum: 1,
linesNum: 10,
}, {
name: "multiple_files",
filesNum: 5,
linesNum: 10000,
}}
testDir := prepareTestDir() for _, tc := range testCases {
defer func() { _ = os.RemoveAll(testDir) }() t.Run(tc.name, func(t *testing.T) {
testFiles := prepareTestFiles(testDir, filesCount, count) r := newTestQLogReader(t, tc.filesNum, tc.linesNum)
r, err := NewQLogReader(testFiles) // Seek to the start.
assert.Nil(t, err) err := r.SeekStart()
assert.NotNil(t, r) require.Nil(t, err)
defer r.Close()
// seek to the start // Read everything.
err = r.SeekStart() var read int
assert.Nil(t, err) var line string
for err == nil {
line, err = r.ReadNext()
if err == nil {
assert.NotEmpty(t, line)
read++
}
}
// read everything require.Equal(t, io.EOF, err)
read := 0 assert.Equal(t, tc.filesNum*tc.linesNum, read)
var line string })
for err == nil {
line, err = r.ReadNext()
if err == nil {
assert.True(t, len(line) > 0)
read++
}
} }
assert.Equal(t, count*filesCount, read)
assert.Equal(t, io.EOF, err)
}
func TestQLogReaderMultipleFiles(t *testing.T) {
// should be large enough
count := 10000
filesCount := 5
testDir := prepareTestDir()
defer func() { _ = os.RemoveAll(testDir) }()
testFiles := prepareTestFiles(testDir, filesCount, count)
r, err := NewQLogReader(testFiles)
assert.Nil(t, err)
assert.NotNil(t, r)
defer r.Close()
// seek to the start
err = r.SeekStart()
assert.Nil(t, err)
// read everything
read := 0
var line string
for err == nil {
line, err = r.ReadNext()
if err == nil {
assert.True(t, len(line) > 0)
read++
}
}
assert.Equal(t, count*filesCount, read)
assert.Equal(t, io.EOF, err)
} }
func TestQLogReader_Seek(t *testing.T) { func TestQLogReader_Seek(t *testing.T) {
count := 10000 r := newTestQLogReader(t, 2, 10000)
filesCount := 2
testDir := prepareTestDir()
t.Cleanup(func() {
_ = os.RemoveAll(testDir)
})
testFiles := prepareTestFiles(testDir, filesCount, count)
r, err := NewQLogReader(testFiles)
assert.Nil(t, err)
assert.NotNil(t, r)
t.Cleanup(func() {
_ = r.Close()
})
testCases := []struct { testCases := []struct {
name string name string
@@ -114,7 +81,7 @@ func TestQLogReader_Seek(t *testing.T) {
want error want error
}{{ }{{
name: "not_too_old", name: "not_too_old",
time: "2020-02-19T04:04:56.920973+03:00", time: "2020-02-18T22:39:35.920973+03:00",
want: nil, want: nil,
}, { }, {
name: "old", name: "old",
@@ -122,7 +89,7 @@ func TestQLogReader_Seek(t *testing.T) {
want: nil, want: nil,
}, { }, {
name: "first", name: "first",
time: "2020-02-19T04:09:55.920973+03:00", time: "2020-02-18T22:36:36.920973+03:00",
want: nil, want: nil,
}, { }, {
name: "last", name: "last",
@@ -147,28 +114,20 @@ func TestQLogReader_Seek(t *testing.T) {
timestamp, err := time.Parse(time.RFC3339Nano, tc.time) timestamp, err := time.Parse(time.RFC3339Nano, tc.time)
assert.Nil(t, err) assert.Nil(t, err)
if tc.name == "first" {
assert.True(t, true)
}
err = r.SeekTS(timestamp.UnixNano()) err = r.SeekTS(timestamp.UnixNano())
assert.True(t, errors.Is(err, tc.want), err) assert.True(t, errors.Is(err, tc.want))
}) })
} }
} }
func TestQLogReader_ReadNext(t *testing.T) { func TestQLogReader_ReadNext(t *testing.T) {
count := 10 const linesNum = 10
filesCount := 1 const filesNum = 1
r := newTestQLogReader(t, filesNum, linesNum)
testDir := prepareTestDir()
t.Cleanup(func() {
_ = os.RemoveAll(testDir)
})
testFiles := prepareTestFiles(testDir, filesCount, count)
r, err := NewQLogReader(testFiles)
assert.Nil(t, err)
assert.NotNil(t, r)
t.Cleanup(func() {
_ = r.Close()
})
testCases := []struct { testCases := []struct {
name string name string
@@ -180,7 +139,7 @@ func TestQLogReader_ReadNext(t *testing.T) {
want: nil, want: nil,
}, { }, {
name: "too_big", name: "too_big",
start: count + 1, start: linesNum + 1,
want: io.EOF, want: io.EOF,
}} }}
@@ -199,70 +158,3 @@ func TestQLogReader_ReadNext(t *testing.T) {
}) })
} }
} }
// TODO(e.burkov): Remove the tests below. Make tests above more compelling.
func TestQLogReaderSeek(t *testing.T) {
// more or less big file
count := 10000
filesCount := 2
testDir := prepareTestDir()
defer func() { _ = os.RemoveAll(testDir) }()
testFiles := prepareTestFiles(testDir, filesCount, count)
r, err := NewQLogReader(testFiles)
assert.Nil(t, err)
assert.NotNil(t, r)
defer r.Close()
// CASE 1: NOT TOO OLD LINE
testSeekLineQLogReader(t, r, 300)
// CASE 2: OLD LINE
testSeekLineQLogReader(t, r, count-300)
// CASE 3: FIRST LINE
testSeekLineQLogReader(t, r, 0)
// CASE 4: LAST LINE
testSeekLineQLogReader(t, r, count)
// CASE 5: Seek non-existent (too low)
err = r.SeekTS(123)
assert.NotNil(t, err)
// CASE 6: Seek non-existent (too high)
ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00")
err = r.SeekTS(ts.UnixNano())
assert.NotNil(t, err)
}
func testSeekLineQLogReader(t *testing.T, r *QLogReader, lineNumber int) {
line, err := getQLogReaderLine(r, lineNumber)
assert.Nil(t, err)
ts := readQLogTimestamp(line)
assert.NotEqualValues(t, 0, ts)
// try seeking to that line now
err = r.SeekTS(ts)
assert.Nil(t, err)
testLine, err := r.ReadNext()
assert.Nil(t, err)
assert.Equal(t, line, testLine)
}
func getQLogReaderLine(r *QLogReader, lineNumber int) (string, error) {
err := r.SeekStart()
if err != nil {
return "", err
}
for i := 1; i < lineNumber; i++ {
_, err := r.ReadNext()
if err != nil {
return "", err
}
}
return r.ReadNext()
}

View File

@@ -9,6 +9,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) {
@@ -34,28 +35,30 @@ func TestStats(t *testing.T) {
Filename: "./stats.db", Filename: "./stats.db",
LimitDays: 1, LimitDays: 1,
} }
s, err := createObject(conf)
require.Nil(t, err)
t.Cleanup(func() { t.Cleanup(func() {
s.clear()
s.Close()
assert.Nil(t, os.Remove(conf.Filename)) assert.Nil(t, os.Remove(conf.Filename))
}) })
s, _ := createObject(conf) s.Update(Entry{
Domain: "domain",
e := Entry{} Client: "127.0.0.1",
Result: RFiltered,
e.Domain = "domain" Time: 123456,
e.Client = "127.0.0.1" })
e.Result = RFiltered s.Update(Entry{
e.Time = 123456 Domain: "domain",
s.Update(e) Client: "127.0.0.1",
Result: RNotFiltered,
e.Domain = "domain" Time: 123456,
e.Client = "127.0.0.1" })
e.Result = RNotFiltered
e.Time = 123456
s.Update(e)
d, ok := s.getData() d, ok := s.getData()
assert.True(t, ok) require.True(t, ok)
a := []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} a := []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
assert.True(t, UIntArrayEquals(d.DNSQueries, a)) assert.True(t, UIntArrayEquals(d.DNSQueries, a))
@@ -70,12 +73,15 @@ func TestStats(t *testing.T) {
assert.True(t, UIntArrayEquals(d.ReplacedParental, a)) assert.True(t, UIntArrayEquals(d.ReplacedParental, a))
m := d.TopQueried m := d.TopQueried
require.NotEmpty(t, m)
assert.EqualValues(t, 1, m[0]["domain"]) assert.EqualValues(t, 1, m[0]["domain"])
m = d.TopBlocked m = d.TopBlocked
require.NotEmpty(t, m)
assert.EqualValues(t, 1, m[0]["domain"]) assert.EqualValues(t, 1, m[0]["domain"])
m = d.TopClients m = d.TopClients
require.NotEmpty(t, m)
assert.EqualValues(t, 2, m[0]["127.0.0.1"]) assert.EqualValues(t, 2, m[0]["127.0.0.1"])
assert.EqualValues(t, 2, d.NumDNSQueries) assert.EqualValues(t, 2, d.NumDNSQueries)
@@ -86,81 +92,69 @@ func TestStats(t *testing.T) {
assert.EqualValues(t, 0.123456, d.AvgProcessingTime) assert.EqualValues(t, 0.123456, d.AvgProcessingTime)
topClients := s.GetTopClientsIP(2) topClients := s.GetTopClientsIP(2)
require.NotEmpty(t, topClients)
assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0])) assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0]))
s.clear()
s.Close()
} }
func TestLargeNumbers(t *testing.T) { func TestLargeNumbers(t *testing.T) {
var hour int32 = 1 var hour int32 = 0
newID := func() uint32 { newID := func() uint32 {
// use "atomic" to make Go race detector happy // Use "atomic" to make go race detector happy.
return uint32(atomic.LoadInt32(&hour)) return uint32(atomic.LoadInt32(&hour))
} }
// log.SetLevel(log.DEBUG)
conf := Config{ conf := Config{
Filename: "./stats.db", Filename: "./stats.db",
LimitDays: 1, LimitDays: 1,
UnitID: newID, UnitID: newID,
} }
s, err := createObject(conf)
require.Nil(t, err)
t.Cleanup(func() { t.Cleanup(func() {
s.Close()
assert.Nil(t, os.Remove(conf.Filename)) assert.Nil(t, os.Remove(conf.Filename))
}) })
s, _ := createObject(conf) // Number of distinct clients and domains every hour.
e := Entry{} const n = 1000
n := 1000 // number of distinct clients and domains every hour for h := 0; h < 12; h++ {
for h := 0; h != 12; h++ { atomic.AddInt32(&hour, 1)
if h != 0 { for i := 0; i < n; i++ {
atomic.AddInt32(&hour, 1) s.Update(Entry{
} Domain: fmt.Sprintf("domain%d", i),
for i := 0; i != n; i++ { Client: net.IP{
e.Domain = fmt.Sprintf("domain%d", i) 127,
ip := net.IP{127, 0, 0, 1} 0,
ip[2] = byte((i & 0xff00) >> 8) byte((i & 0xff00) >> 8),
ip[3] = byte(i & 0xff) byte(i & 0xff),
e.Client = ip.String() }.String(),
e.Result = RNotFiltered Result: RNotFiltered,
e.Time = 123456 Time: 123456,
s.Update(e) })
} }
} }
d, ok := s.getData() d, ok := s.getData()
assert.True(t, ok) require.True(t, ok)
assert.EqualValues(t, int(hour)*n, d.NumDNSQueries) assert.EqualValues(t, hour*n, d.NumDNSQueries)
s.Close()
} }
// this code is a chunk copied from getData() that generates aggregate data per day func TestStatsCollector(t *testing.T) {
func aggregateDataPerDay(firstID uint32) int { ng := func(_ *unitDB) uint64 {
firstDayID := (firstID + 24 - 1) / 24 * 24 // align_ceil(24) return 0
a := []uint64{} }
var sum uint64 units := make([]*unitDB, 720)
id := firstDayID
nextDayID := firstDayID + 24 t.Run("hours", func(t *testing.T) {
for i := firstDayID - firstID; int(i) != 720; i++ { statsData := statsCollector(units, 0, Hours, ng)
sum++ assert.Len(t, statsData, 720)
if id == nextDayID { })
a = append(a, sum)
sum = 0 t.Run("days", func(t *testing.T) {
nextDayID += 24 for i := 0; i != 25; i++ {
statsData := statsCollector(units, uint32(i), Days, ng)
require.Lenf(t, statsData, 30, "i=%d", i)
} }
id++ })
}
if id <= nextDayID {
a = append(a, sum)
}
return len(a)
}
func TestAggregateDataPerTimeUnit(t *testing.T) {
for i := 0; i != 25; i++ {
alen := aggregateDataPerDay(uint32(i))
assert.Equalf(t, 30, alen, "i=%d", i)
}
} }

View File

@@ -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
@@ -528,6 +554,57 @@ func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) {
return units, firstID return units, firstID
} }
// numsGetter is a signature for statsCollector argument.
type numsGetter func(u *unitDB) (num uint64)
// statsCollector collects statisctics for the given *unitDB slice by specified
// timeUnit using ng to retrieve data.
func statsCollector(units []*unitDB, firstID uint32, timeUnit TimeUnit, ng numsGetter) (nums []uint64) {
if timeUnit == Hours {
for _, u := range units {
nums = append(nums, ng(u))
}
} else {
// Per time unit counters: 720 hours may span 31 days, so we
// skip data for the first day in this case.
// align_ceil(24)
firstDayID := (firstID + 24 - 1) / 24 * 24
var sum uint64
id := firstDayID
nextDayID := firstDayID + 24
for i := int(firstDayID - firstID); i != len(units); i++ {
sum += ng(units[i])
if id == nextDayID {
nums = append(nums, sum)
sum = 0
nextDayID += 24
}
id++
}
if id <= nextDayID {
nums = append(nums, sum)
}
}
return nums
}
// pairsGetter is a signature for topsCollector argument.
type pairsGetter func(u *unitDB) (pairs []countPair)
// topsCollector collects statistics about highest values fro the given *unitDB
// slice using pg to retrieve data.
func topsCollector(units []*unitDB, max int, pg pairsGetter) []map[string]uint64 {
m := map[string]uint64{}
for _, u := range units {
for _, it := range pg(u) {
m[it.Name] += it.Count
}
}
a2 := convertMapToSlice(m, max)
return convertTopSlice(a2)
}
/* Algorithm: /* Algorithm:
. Prepare array of N units, where N is the value of "limit" configuration setting . Prepare array of N units, where N is the value of "limit" configuration setting
. Load data for the most recent units from file . Load data for the most recent units from file
@@ -568,65 +645,25 @@ func (s *statsCtx) getData() (statsResponse, bool) {
return statsResponse{}, false return statsResponse{}, false
} }
// per time unit counters: dnsQueries := statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NTotal })
// 720 hours may span 31 days, so we skip data for the first day in this case
firstDayID := (firstID + 24 - 1) / 24 * 24 // align_ceil(24)
statsCollector := func(numsGetter func(u *unitDB) (num uint64)) (nums []uint64) {
if timeUnit == Hours {
for _, u := range units {
nums = append(nums, numsGetter(u))
}
} else {
var sum uint64
id := firstDayID
nextDayID := firstDayID + 24
for i := int(firstDayID - firstID); i != len(units); i++ {
sum += numsGetter(units[i])
if id == nextDayID {
nums = append(nums, sum)
sum = 0
nextDayID += 24
}
id++
}
if id <= nextDayID {
nums = append(nums, sum)
}
}
return nums
}
topsCollector := func(max int, pairsGetter func(u *unitDB) (pairs []countPair)) []map[string]uint64 {
m := map[string]uint64{}
for _, u := range units {
for _, it := range pairsGetter(u) {
m[it.Name] += it.Count
}
}
a2 := convertMapToSlice(m, max)
return convertTopSlice(a2)
}
dnsQueries := statsCollector(func(u *unitDB) (num uint64) { return u.NTotal })
if timeUnit != Hours && len(dnsQueries) != int(limit/24) { if timeUnit != Hours && len(dnsQueries) != int(limit/24) {
log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit) log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit)
} }
data := statsResponse{ data := statsResponse{
DNSQueries: dnsQueries, DNSQueries: dnsQueries,
BlockedFiltering: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }), BlockedFiltering: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }),
ReplacedSafebrowsing: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }), ReplacedSafebrowsing: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }),
ReplacedParental: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }), ReplacedParental: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RParental] }),
TopQueried: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }), TopQueried: topsCollector(units, maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }),
TopBlocked: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), TopBlocked: topsCollector(units, maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }),
TopClients: topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }), TopClients: topsCollector(units, maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }),
} }
// total counters: // Total counters:
sum := unitDB{
sum := unitDB{} NResult: make([]uint64, rLast),
sum.NResult = make([]uint64, rLast) }
timeN := 0 timeN := 0
for _, u := range units { for _, u := range units {
sum.NTotal += u.NTotal sum.NTotal += u.NTotal

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const nl = "\n" const nl = "\n"
@@ -48,7 +49,7 @@ func TestDHCPCDStaticConfig(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := bytes.NewReader(tc.data) r := bytes.NewReader(tc.data)
has, err := dhcpcdStaticConfig(r, "wlan0") has, err := dhcpcdStaticConfig(r, "wlan0")
assert.Nil(t, err) require.Nil(t, err)
assert.Equal(t, tc.want, has) assert.Equal(t, tc.want, has)
}) })
} }
@@ -85,26 +86,36 @@ func TestIfacesStaticConfig(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := bytes.NewReader(tc.data) r := bytes.NewReader(tc.data)
has, err := ifacesStaticConfig(r, "enp0s3") has, err := ifacesStaticConfig(r, "enp0s3")
assert.Nil(t, err) require.Nil(t, err)
assert.Equal(t, tc.want, has) assert.Equal(t, tc.want, has)
}) })
} }
} }
func TestSetStaticIPdhcpcdConf(t *testing.T) { func TestSetStaticIPdhcpcdConf(t *testing.T) {
dhcpcdConf := nl + `interface wlan0` + nl + testCases := []struct {
`static ip_address=192.168.0.2/24` + nl + name string
`static routers=192.168.0.1` + nl + dhcpcdConf string
`static domain_name_servers=192.168.0.2` + nl + nl routers net.IP
}{{
name: "with_gateway",
dhcpcdConf: nl + `interface wlan0` + nl +
`static ip_address=192.168.0.2/24` + nl +
`static routers=192.168.0.1` + nl +
`static domain_name_servers=192.168.0.2` + nl + nl,
routers: net.IP{192, 168, 0, 1},
}, {
name: "without_gateway",
dhcpcdConf: nl + `interface wlan0` + nl +
`static ip_address=192.168.0.2/24` + nl +
`static domain_name_servers=192.168.0.2` + nl + nl,
routers: nil,
}}
s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 2}) for _, tc := range testCases {
assert.Equal(t, dhcpcdConf, s) t.Run(tc.name, func(t *testing.T) {
s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", tc.routers, net.IP{192, 168, 0, 2})
// without gateway assert.Equal(t, tc.dhcpcdConf, s)
dhcpcdConf = nl + `interface wlan0` + nl + })
`static ip_address=192.168.0.2/24` + nl + }
`static domain_name_servers=192.168.0.2` + nl + nl
s = updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", nil, net.IP{192, 168, 0, 2})
assert.Equal(t, dhcpcdConf, s)
} }

View File

@@ -11,114 +11,162 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/miekg/dns" "github.com/miekg/dns"
"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) {
aghtest.DiscardLogOutput(m) aghtest.DiscardLogOutput(m)
} }
func prepareTestDir() string { func prepareTestFile(t *testing.T) (f *os.File) {
const dir = "./agh-test" t.Helper()
_ = os.RemoveAll(dir)
_ = os.MkdirAll(dir, 0o755) dir := aghtest.PrepareTestDir(t)
return dir
f, err := ioutil.TempFile(dir, "")
require.Nil(t, err)
require.NotNil(t, f)
t.Cleanup(func() {
assert.Nil(t, f.Close())
})
return f
}
func assertWriting(t *testing.T, f *os.File, strs ...string) {
t.Helper()
for _, str := range strs {
n, err := f.WriteString(str)
require.Nil(t, err)
assert.Equal(t, n, len(str))
}
} }
func TestAutoHostsResolution(t *testing.T) { func TestAutoHostsResolution(t *testing.T) {
ah := AutoHosts{} ah := &AutoHosts{}
dir := prepareTestDir() f := prepareTestFile(t)
defer func() { _ = os.RemoveAll(dir) }()
f, _ := ioutil.TempFile(dir, "")
defer func() { _ = os.Remove(f.Name()) }()
defer f.Close()
_, _ = f.WriteString(" 127.0.0.1 host localhost # comment \n")
_, _ = f.WriteString(" ::1 localhost#comment \n")
assertWriting(t, f,
" 127.0.0.1 host localhost # comment \n",
" ::1 localhost#comment \n",
)
ah.Init(f.Name()) ah.Init(f.Name())
// Existing host t.Run("existing_host", func(t *testing.T) {
ips := ah.Process("localhost", dns.TypeA) ips := ah.Process("localhost", dns.TypeA)
assert.NotNil(t, ips) require.Len(t, ips, 1)
assert.Len(t, ips, 1) assert.Equal(t, net.IPv4(127, 0, 0, 1), ips[0])
assert.Equal(t, net.ParseIP("127.0.0.1"), ips[0]) })
// Unknown host t.Run("unknown_host", func(t *testing.T) {
ips = ah.Process("newhost", dns.TypeA) ips := ah.Process("newhost", dns.TypeA)
assert.Nil(t, ips) assert.Nil(t, ips)
// Unknown host (comment) // Comment.
ips = ah.Process("comment", dns.TypeA) ips = ah.Process("comment", dns.TypeA)
assert.Nil(t, ips) assert.Nil(t, ips)
})
// Test hosts file t.Run("hosts_file", func(t *testing.T) {
table := ah.List() names, ok := ah.List()["127.0.0.1"]
names, ok := table["127.0.0.1"] require.True(t, ok)
assert.True(t, ok) assert.Equal(t, []string{"host", "localhost"}, names)
assert.Equal(t, []string{"host", "localhost"}, names) })
// Test PTR t.Run("ptr", func(t *testing.T) {
a, _ := dns.ReverseAddr("127.0.0.1") testCases := []struct {
a = strings.TrimSuffix(a, ".") wantIP string
hosts := ah.ProcessReverse(a, dns.TypePTR) wantLen int
if assert.Len(t, hosts, 2) { wantHost string
assert.Equal(t, hosts[0], "host") }{
} {wantIP: "127.0.0.1", wantLen: 2, wantHost: "host"},
{wantIP: "::1", wantLen: 1, wantHost: "localhost"},
}
a, _ = dns.ReverseAddr("::1") for _, tc := range testCases {
a = strings.TrimSuffix(a, ".") a, err := dns.ReverseAddr(tc.wantIP)
hosts = ah.ProcessReverse(a, dns.TypePTR) require.Nil(t, err)
if assert.Len(t, hosts, 1) {
assert.Equal(t, hosts[0], "localhost") a = strings.TrimSuffix(a, ".")
} hosts := ah.ProcessReverse(a, dns.TypePTR)
require.Len(t, hosts, tc.wantLen)
assert.Equal(t, tc.wantHost, hosts[0])
}
})
} }
func TestAutoHostsFSNotify(t *testing.T) { func TestAutoHostsFSNotify(t *testing.T) {
ah := AutoHosts{} ah := &AutoHosts{}
dir := prepareTestDir() f := prepareTestFile(t)
defer func() { _ = os.RemoveAll(dir) }()
f, _ := ioutil.TempFile(dir, "") assertWriting(t, f, " 127.0.0.1 host localhost \n")
defer func() { _ = os.Remove(f.Name()) }()
defer f.Close()
// Init
_, _ = f.WriteString(" 127.0.0.1 host localhost \n")
ah.Init(f.Name()) ah.Init(f.Name())
// Unknown host t.Run("unknown_host", func(t *testing.T) {
ips := ah.Process("newhost", dns.TypeA) ips := ah.Process("newhost", dns.TypeA)
assert.Nil(t, ips) assert.Nil(t, ips)
})
// Stat monitoring for changes // Start monitoring for changes.
ah.Start() ah.Start()
defer ah.Close() t.Cleanup(ah.Close)
// Update file assertWriting(t, f, "127.0.0.2 newhost\n")
_, _ = f.WriteString("127.0.0.2 newhost\n") require.Nil(t, f.Sync())
_ = f.Sync()
// wait until fsnotify has triggerred and processed the file-modification event // Wait until fsnotify has triggerred and processed the
// file-modification event.
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
// Check if we are notified about changes t.Run("notified", func(t *testing.T) {
ips = ah.Process("newhost", dns.TypeA) ips := ah.Process("newhost", dns.TypeA)
assert.NotNil(t, ips) assert.NotNil(t, ips)
assert.Len(t, ips, 1) require.Len(t, ips, 1)
assert.True(t, net.IP{127, 0, 0, 2}.Equal(ips[0])) assert.True(t, net.IP{127, 0, 0, 2}.Equal(ips[0]))
})
} }
func TestIP(t *testing.T) { func TestDNSReverseAddr(t *testing.T) {
assert.True(t, net.IP{127, 0, 0, 1}.Equal(DNSUnreverseAddr("1.0.0.127.in-addr.arpa"))) testCases := []struct {
assert.Equal(t, "::abcd:1234", DNSUnreverseAddr("4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa").String()) name string
assert.Equal(t, "::abcd:1234", DNSUnreverseAddr("4.3.2.1.d.c.B.A.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa").String()) have string
want net.IP
}{{
name: "good_ipv4",
have: "1.0.0.127.in-addr.arpa",
want: net.IP{127, 0, 0, 1},
}, {
name: "good_ipv6",
have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
want: net.ParseIP("::abcd:1234"),
}, {
name: "good_ipv6_case",
have: "4.3.2.1.d.c.B.A.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
want: net.ParseIP("::abcd:1234"),
}, {
name: "bad_ipv4_dot",
have: "1.0.0.127.in-addr.arpa.",
}, {
name: "wrong_ipv4",
have: ".0.0.127.in-addr.arpa",
}, {
name: "wrong_ipv6",
have: ".3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
}, {
name: "bad_ipv6_dot",
have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0..ip6.arpa",
}, {
name: "bad_ipv6_space",
have: "4.3.2.1.d.c.b. .0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
}}
assert.Nil(t, DNSUnreverseAddr("1.0.0.127.in-addr.arpa.")) for _, tc := range testCases {
assert.Nil(t, DNSUnreverseAddr(".0.0.127.in-addr.arpa")) t.Run(tc.name, func(t *testing.T) {
assert.Nil(t, DNSUnreverseAddr(".3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa")) ip := DNSUnreverseAddr(tc.have)
assert.Nil(t, DNSUnreverseAddr("4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0..ip6.arpa")) assert.True(t, tc.want.Equal(ip))
assert.Nil(t, DNSUnreverseAddr("4.3.2.1.d.c.b. .0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa")) })
}
} }

View File

@@ -4,12 +4,14 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestSplitNext(t *testing.T) { func TestSplitNext(t *testing.T) {
s := " a,b , c " s := " a,b , c "
assert.Equal(t, "a", SplitNext(&s, ',')) assert.Equal(t, "a", SplitNext(&s, ','))
assert.Equal(t, "b", SplitNext(&s, ',')) assert.Equal(t, "b", SplitNext(&s, ','))
assert.Equal(t, "c", SplitNext(&s, ',')) assert.Equal(t, "c", SplitNext(&s, ','))
assert.Empty(t, s) require.Empty(t, s)
} }

View File

@@ -2,22 +2,15 @@ package util
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require"
) )
func TestGetValidNetInterfacesForWeb(t *testing.T) { func TestGetValidNetInterfacesForWeb(t *testing.T) {
ifaces, err := GetValidNetInterfacesForWeb() ifaces, err := GetValidNetInterfacesForWeb()
if err != nil { require.Nilf(t, err, "Cannot get net interfaces: %s", err)
t.Fatalf("Cannot get net interfaces: %s", err) require.NotEmpty(t, ifaces, "No net interfaces found")
}
if len(ifaces) == 0 {
t.Fatalf("No net interfaces found")
}
for _, iface := range ifaces { for _, iface := range ifaces {
if len(iface.Addresses) == 0 { require.NotEmptyf(t, iface.Addresses, "No addresses found for %s", iface.Name)
t.Fatalf("No addresses found for %s", iface.Name)
}
t.Logf("%v", iface)
} }
} }

View File

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

View File

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

View File

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

View File

@@ -80,10 +80,18 @@ const request = (url, locale) => (
return `${locale} - Not OK`; return `${locale} - Not OK`;
})); }));
/**
* Sleep.
* @param {number} ms
*/
const sleep = (ms) => new Promise((resolve) => {
setTimeout(resolve, ms);
});
/** /**
* Download locales * Download locales
*/ */
const download = () => { const download = async () => {
const locales = LOCALES_LIST; const locales = LOCALES_LIST;
if (!TWOSKY_URI) { if (!TWOSKY_URI) {
@@ -91,10 +99,16 @@ const download = () => {
return; return;
} }
const requests = locales.map((locale) => { const requests = [];
for (let i = 0; i < locales.length; i++) {
const locale = locales[i];
const url = getRequestUrl(locale, TWOSKY_URI, TWOSKY_PROJECT_ID); const url = getRequestUrl(locale, TWOSKY_URI, TWOSKY_PROJECT_ID);
return request(url, locale); requests.push(request(url, locale));
});
// Don't request the Crowdin API too aggressively to prevent spurious
// 400 errors.
await sleep(200);
}
Promise Promise
.all(requests) .all(requests)

View File

@@ -1,8 +1,466 @@
{ {
"name": "translations", "name": "translations",
"version": "0.2.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",

View File

@@ -10,7 +10,9 @@ initialisms = [
, "MX" , "MX"
, "PTR" , "PTR"
, "QUIC" , "QUIC"
, "RA"
, "SDNS" , "SDNS"
, "SLAAC"
, "SVCB" , "SVCB"
] ]
dot_import_whitelist = [] dot_import_whitelist = []