Compare commits
694 commits
Author | SHA1 | Date | |
---|---|---|---|
25d162db91 | |||
![]() |
50e8d2ed00 | ||
![]() |
d6da6b878d | ||
![]() |
33a86eb556 | ||
![]() |
10d3561353 | ||
![]() |
803f77404d | ||
![]() |
590d7d91fc | ||
![]() |
41170b9c08 | ||
![]() |
63bf3512a4 | ||
![]() |
3b10e37607 | ||
![]() |
115f23dead | ||
![]() |
009033d476 | ||
![]() |
d18b1cdb27 | ||
![]() |
b31e5e2f91 | ||
![]() |
3b73350541 | ||
![]() |
6bb80cbdaa | ||
![]() |
6fd9a6f106 | ||
![]() |
1c178d4c09 | ||
![]() |
ae01ba26c1 | ||
![]() |
17fc4c0a35 | ||
![]() |
8dcea0d779 | ||
![]() |
8883641d97 | ||
![]() |
741a2345ea | ||
![]() |
8cf322e162 | ||
![]() |
ddeaedc136 | ||
![]() |
9ab038412f | ||
![]() |
ecaa05abd3 | ||
![]() |
6408be6ccc | ||
![]() |
06c47695fc | ||
![]() |
c89a2d6186 | ||
![]() |
3f3b8cf825 | ||
![]() |
8decfc4b77 | ||
![]() |
660bb11c02 | ||
![]() |
fee71b46be | ||
![]() |
7248341ad6 | ||
![]() |
60bedd0fab | ||
![]() |
56f88e3bab | ||
![]() |
8ffbedd436 | ||
![]() |
678b76f7e8 | ||
![]() |
e4920373ee | ||
![]() |
4008ccfa2d | ||
![]() |
cf4bad127d | ||
![]() |
76fccba151 | ||
![]() |
d2b1a4dfa6 | ||
![]() |
d1f81dc9e4 | ||
![]() |
2de77f17f7 | ||
![]() |
a2e818d6d3 | ||
![]() |
8030a46ca3 | ||
![]() |
59f6c2959a | ||
![]() |
0a687d505b | ||
![]() |
3da4259a41 | ||
![]() |
87a919a715 | ||
![]() |
59495e99d2 | ||
![]() |
07289d5c48 | ||
![]() |
4d7d6ae48e | ||
![]() |
54b6d0cb0d | ||
![]() |
9f2d6279d2 | ||
![]() |
af4ea14fda | ||
![]() |
a12398c315 | ||
![]() |
6dfcede81a | ||
![]() |
ceced7e094 | ||
![]() |
8fbf9ed4c8 | ||
![]() |
8aedcf47db | ||
![]() |
d3e793bf21 | ||
![]() |
b200e0c4e3 | ||
![]() |
aba1df3e6b | ||
![]() |
eb48bb55ae | ||
![]() |
d9365359bd | ||
![]() |
1c0ba9a126 | ||
![]() |
ad3cd11446 | ||
![]() |
c71f6f6eae | ||
![]() |
f3678ce119 | ||
![]() |
5d545aae5c | ||
![]() |
490dc16d33 | ||
![]() |
5ed43a2e4c | ||
![]() |
62f3759c54 | ||
![]() |
9c7c0e55c1 | ||
![]() |
dd7ad1ccf4 | ||
![]() |
d38fcbddb8 | ||
![]() |
d0eb899fc8 | ||
![]() |
e8d79d842c | ||
![]() |
7653f60058 | ||
![]() |
c768f1350b | ||
![]() |
e9029b85d5 | ||
![]() |
bd1e42ac6c | ||
![]() |
d7861b6d61 | ||
![]() |
6c33ccaa25 | ||
![]() |
2ccdd3b19e | ||
![]() |
8b7581287c | ||
![]() |
b6ac0e6d05 | ||
![]() |
f32f7fc29a | ||
![]() |
a7abfcb715 | ||
![]() |
695c3c4be8 | ||
![]() |
76afbb6673 | ||
![]() |
0f1ea65fd7 | ||
![]() |
ac67c04f13 | ||
![]() |
a18efcbe32 | ||
![]() |
1e3bebc60d | ||
![]() |
2da08cceb9 | ||
![]() |
273af1c821 | ||
![]() |
803621a9ee | ||
![]() |
268369a05e | ||
![]() |
0348ded46b | ||
![]() |
e478117d4e | ||
![]() |
1a748e7a86 | ||
![]() |
7660ca52bf | ||
![]() |
2927f205ea | ||
![]() |
974bba4d93 | ||
![]() |
75552f80f7 | ||
![]() |
25fac765a0 | ||
![]() |
5256a1d02c | ||
![]() |
a178d40633 | ||
![]() |
bf83ba032c | ||
![]() |
c5df774b7e | ||
![]() |
20439bc130 | ||
![]() |
cb66870019 | ||
![]() |
78be40fe2c | ||
![]() |
499318fbe0 | ||
![]() |
94ce6367ec | ||
![]() |
c64e432bf1 | ||
![]() |
f2c9ef6641 | ||
![]() |
70e2b51377 | ||
![]() |
8359eff6ea | ||
![]() |
989f8be8c3 | ||
![]() |
c9cdb96086 | ||
![]() |
fbd7167b94 | ||
![]() |
35cbc8d200 | ||
![]() |
31740006d0 | ||
![]() |
65d2473213 | ||
![]() |
ce1bcaa68b | ||
![]() |
e8b3d9168b | ||
![]() |
c943d7c0d9 | ||
![]() |
0a9ee106e4 | ||
![]() |
7181152c78 | ||
![]() |
4b5f28b2f0 | ||
![]() |
cf54da50e4 | ||
![]() |
c2db690efb | ||
![]() |
3dafdbf604 | ||
![]() |
de5d894c91 | ||
![]() |
4f89492dc0 | ||
![]() |
a21e215ada | ||
![]() |
acd8dfe47f | ||
![]() |
f024bcce34 | ||
![]() |
46bd2f1771 | ||
![]() |
f23070a114 | ||
![]() |
603a59ffe3 | ||
![]() |
533e4735cd | ||
![]() |
b9ec2d42a3 | ||
![]() |
555359dc98 | ||
![]() |
80bbf1dc43 | ||
![]() |
1631b465d5 | ||
![]() |
ed1d480617 | ||
![]() |
12ff5bfbdc | ||
![]() |
442eac96c7 | ||
![]() |
3be5b91601 | ||
![]() |
a06c532394 | ||
![]() |
42f720df86 | ||
![]() |
967bf2f6e8 | ||
![]() |
564b315bfa | ||
![]() |
18bd312216 | ||
![]() |
e8f0358bbb | ||
![]() |
2b65aff56b | ||
![]() |
912bc6291a | ||
![]() |
5a66efe79e | ||
![]() |
478f517d53 | ||
![]() |
7fde55c188 | ||
![]() |
fe1768316a | ||
![]() |
775b7fcbfe | ||
![]() |
bbf98dd031 | ||
![]() |
270a82dd58 | ||
![]() |
4c7634855b | ||
![]() |
19848852a4 | ||
![]() |
ed2afde72d | ||
![]() |
2f8a4ba00a | ||
![]() |
4d3dcdc7de | ||
![]() |
70858e659f | ||
![]() |
05dbe7a984 | ||
![]() |
9e659a18eb | ||
![]() |
c83dc67039 | ||
![]() |
b4c4b5dc54 | ||
![]() |
bd688e9750 | ||
![]() |
ab2e0d7999 | ||
![]() |
0b79e3bc95 | ||
![]() |
0c094f6ee8 | ||
![]() |
a136ba4cdc | ||
![]() |
598dee50ca | ||
![]() |
959b5ddc37 | ||
![]() |
d497422bf9 | ||
![]() |
2330543cc8 | ||
![]() |
13a66c79bb | ||
![]() |
e4d43f0292 | ||
![]() |
ab27df6f79 | ||
![]() |
eb281ea47b | ||
![]() |
3d345ff08b | ||
![]() |
622abfca2c | ||
![]() |
4f369a3b0b | ||
![]() |
2239b57101 | ||
![]() |
7bee2d7c82 | ||
![]() |
143630c7fd | ||
![]() |
a99d093eca | ||
![]() |
60d1c53a36 | ||
![]() |
43ea691e0c | ||
![]() |
f4248d0617 | ||
![]() |
56f8c83d3a | ||
![]() |
0f094ac121 | ||
![]() |
15db76f739 | ||
![]() |
f36c2f45aa | ||
![]() |
439b0fd0e1 | ||
![]() |
1bdd65e46e | ||
![]() |
dff4cd4854 | ||
![]() |
37504fe6f2 | ||
![]() |
2e59e86df6 | ||
![]() |
e036fd0cf6 | ||
![]() |
9e45aecf20 | ||
![]() |
23bc8cdac3 | ||
![]() |
3262dd0952 | ||
![]() |
015600d72f | ||
![]() |
42d635c2df | ||
![]() |
706ba713e0 | ||
![]() |
f5c59c2024 | ||
![]() |
0c2c97123f | ||
![]() |
71dc1f92e4 | ||
![]() |
9dce53ea4a | ||
![]() |
af65dd86cf | ||
![]() |
9c5160a514 | ||
![]() |
5620127c71 | ||
![]() |
96ada0c79e | ||
![]() |
32f95526f9 | ||
![]() |
d380e17aba | ||
![]() |
3f0fd0f37b | ||
![]() |
bb65b64e39 | ||
![]() |
2715743ee3 | ||
![]() |
0b30df4b69 | ||
![]() |
06c3dd5825 | ||
![]() |
6f505e6538 | ||
![]() |
5e52f728ad | ||
![]() |
a890b08935 | ||
![]() |
b1ddaa0ce8 | ||
![]() |
12d5539abc | ||
![]() |
325eb10536 | ||
![]() |
2ccdefff93 | ||
![]() |
15595d01ac | ||
![]() |
26d7aa500a | ||
![]() |
0fa7e132b1 | ||
![]() |
54d381a18e | ||
![]() |
61cc5d66ae | ||
![]() |
ee0940175e | ||
![]() |
f410b915ce | ||
![]() |
228efa7927 | ||
![]() |
fc453a0de3 | ||
![]() |
536c7c87a2 | ||
![]() |
d8a23ff9a4 | ||
![]() |
e1c8b26f7b | ||
![]() |
0c31681d35 | ||
![]() |
16b15ea089 | ||
![]() |
c65d5c1254 | ||
![]() |
bafd5a8715 | ||
![]() |
45d832145f | ||
![]() |
038b31cf77 | ||
![]() |
630a2d5d49 | ||
![]() |
4273580bdf | ||
![]() |
c6bcfd4644 | ||
![]() |
1020145fdf | ||
![]() |
3f740c3e19 | ||
![]() |
4b5b8ab62d | ||
![]() |
3dae16457a | ||
![]() |
2eb0398cf2 | ||
![]() |
b07aa91ed5 | ||
![]() |
971fe438a3 | ||
![]() |
ca28694dd7 | ||
![]() |
11876498d5 | ||
![]() |
c79d12263e | ||
![]() |
07800a4586 | ||
![]() |
f42583c0cf | ||
![]() |
6ac5b41a20 | ||
![]() |
d79ef268bd | ||
![]() |
8b58f7bd99 | ||
![]() |
2823e47c58 | ||
![]() |
fe502abcd8 | ||
![]() |
7a43920b99 | ||
![]() |
17a002cbd6 | ||
![]() |
e155e1bf2c | ||
![]() |
d62495c41e | ||
![]() |
1195a40c45 | ||
![]() |
3ece2017e9 | ||
![]() |
a252ff5ebe | ||
![]() |
9343ebec89 | ||
![]() |
c53f40d205 | ||
![]() |
b3006dd6c6 | ||
![]() |
62154f9869 | ||
![]() |
f8bdc48e42 | ||
![]() |
7bdb554e36 | ||
![]() |
1eccfb8c77 | ||
![]() |
91fd9e3842 | ||
![]() |
971e88452d | ||
![]() |
8a334fd9cf | ||
![]() |
98ed129b20 | ||
![]() |
b80fe1b505 | ||
![]() |
459970c5e3 | ||
![]() |
f807ba58ac | ||
![]() |
58c6caa5ff | ||
![]() |
ef8bf634fe | ||
![]() |
61fff1c344 | ||
![]() |
d391f41074 | ||
![]() |
b4e08ae3ae | ||
![]() |
231601ae54 | ||
![]() |
f0edd7f781 | ||
![]() |
2534375cfd | ||
![]() |
02c80fdf09 | ||
![]() |
95a10e2595 | ||
![]() |
2d60183e93 | ||
![]() |
c0b28f344f | ||
![]() |
5b433c3cd5 | ||
![]() |
27143db56e | ||
![]() |
1c94ed6063 | ||
![]() |
4a394f4562 | ||
![]() |
56f0d931a4 | ||
![]() |
e5b00216ec | ||
![]() |
073fe5a51d | ||
![]() |
3d894364bf | ||
![]() |
2ac61250e5 | ||
![]() |
962abfbbc3 | ||
![]() |
e272caa385 | ||
![]() |
b563e9c2fd | ||
![]() |
08626482c3 | ||
![]() |
d48b482269 | ||
![]() |
b1752c2622 | ||
![]() |
6aa68f72a7 | ||
![]() |
bd437a0abf | ||
![]() |
8262f112ea | ||
![]() |
1ad9b565bd | ||
![]() |
1054e162fa | ||
![]() |
0392c5e725 | ||
![]() |
e891f53345 | ||
![]() |
5d68b11d78 | ||
![]() |
2530adb39e | ||
![]() |
b9d372c12d | ||
![]() |
0ea2f06513 | ||
![]() |
ccc205301a | ||
![]() |
6ddecb4ecc | ||
![]() |
356c3354bd | ||
![]() |
89d7193f69 | ||
![]() |
38a4e9eeef | ||
![]() |
0ffffb1400 | ||
![]() |
5a8bee1e4d | ||
![]() |
2a9abc9d4c | ||
![]() |
59ff497c1b | ||
![]() |
bcfdf70c34 | ||
![]() |
482be5ce46 | ||
![]() |
9996c1b7d4 | ||
![]() |
ddfa8663ad | ||
![]() |
cf078017c2 | ||
![]() |
3c68abe551 | ||
![]() |
a7feb95091 | ||
![]() |
d8c74169ee | ||
![]() |
a724084114 | ||
![]() |
435357ac50 | ||
![]() |
da26fe76e0 | ||
![]() |
7a4b96e04e | ||
![]() |
fb990208b3 | ||
![]() |
6992a34028 | ||
![]() |
7a2625b7a7 | ||
![]() |
94c304601e | ||
![]() |
e7ad0e8e6e | ||
![]() |
c57f4b0d56 | ||
![]() |
2792689c35 | ||
![]() |
e142d6a5ac | ||
![]() |
3a57ca1374 | ||
![]() |
dab67eae69 | ||
![]() |
dfef6b2e99 | ||
![]() |
12b2c0d03d | ||
![]() |
bdc69d879f | ||
![]() |
b9144c01c2 | ||
![]() |
4b2155a43c | ||
![]() |
6e98c0cdb2 | ||
![]() |
b1c0029604 | ||
![]() |
3258ea34c0 | ||
![]() |
56f4a2afe2 | ||
![]() |
e01ed55a58 | ||
![]() |
c63eb0f060 | ||
![]() |
f82d2af0f2 | ||
![]() |
53b373fc9e | ||
![]() |
5ab15b1d53 | ||
![]() |
83ef1fa99a | ||
![]() |
f6e13f8003 | ||
![]() |
30a7c5ad78 | ||
![]() |
26f57bf36a | ||
![]() |
adfd68d5e0 | ||
![]() |
1e73f4a51a | ||
![]() |
90de2f9606 | ||
![]() |
88f140d470 | ||
![]() |
8a667e3f57 | ||
![]() |
db3472a7ce | ||
![]() |
0892655fd6 | ||
![]() |
d88e6438ef | ||
![]() |
23dad564be | ||
![]() |
9256096e64 | ||
![]() |
2f4b0859bd | ||
![]() |
af0035d266 | ||
![]() |
c6581b03f2 | ||
![]() |
f0de73e8c4 | ||
![]() |
13369804a0 | ||
![]() |
430d9026f8 | ||
![]() |
6284133b1c | ||
![]() |
5931a7150c | ||
![]() |
4c6842e569 | ||
![]() |
7754c65103 | ||
![]() |
0ed2970852 | ||
![]() |
469c5a072e | ||
![]() |
6fbb7eb3dc | ||
![]() |
c31668b413 | ||
![]() |
a7fef2e1eb | ||
![]() |
4d5a416725 | ||
![]() |
efa487bfb3 | ||
![]() |
0973e9d83c | ||
![]() |
ecf935a4e2 | ||
![]() |
9eff7404e3 | ||
![]() |
d489cea344 | ||
![]() |
203bf12245 | ||
![]() |
ad4e3769eb | ||
![]() |
0882712ec2 | ||
![]() |
45e3603918 | ||
![]() |
60f931e7da | ||
![]() |
3ffcdf8317 | ||
![]() |
8a65264841 | ||
![]() |
b8df93febe | ||
![]() |
08ccc41650 | ||
![]() |
64af205cfc | ||
![]() |
5d2a1e864a | ||
![]() |
216c9c6010 | ||
![]() |
5ae0fd3024 | ||
![]() |
45677c0403 | ||
![]() |
8e20185323 | ||
![]() |
40e4aee74f | ||
![]() |
e8a6d1479f | ||
![]() |
5db77f7c31 | ||
![]() |
217bc998fc | ||
![]() |
c8ee25ef82 | ||
![]() |
1faa315794 | ||
![]() |
39e3322fc0 | ||
![]() |
161c623557 | ||
![]() |
b488cb2235 | ||
![]() |
4d9d0646cf | ||
![]() |
2e26a63c2f | ||
![]() |
fa0bfde3cb | ||
![]() |
6af76afde9 | ||
![]() |
01d2db06c1 | ||
![]() |
6e7a4fb460 | ||
![]() |
7b6f640c9b | ||
![]() |
ab9ac65f46 | ||
![]() |
f5b369a7ef | ||
![]() |
89c84f9f07 | ||
![]() |
1f31b0e570 | ||
![]() |
d8317a730d | ||
![]() |
04bdd68415 | ||
![]() |
be3c2060eb | ||
![]() |
de39ac7bcc | ||
![]() |
ae7a9dce2a | ||
![]() |
76900c708c | ||
![]() |
49f5551764 | ||
![]() |
eab72ef6d7 | ||
![]() |
399f8a8b32 | ||
![]() |
88eb2ed4fe | ||
![]() |
ba6a279186 | ||
![]() |
b8a0a26441 | ||
![]() |
be9e305e73 | ||
![]() |
e32b9436fb | ||
![]() |
66bb07450f | ||
![]() |
5757f7e07d | ||
![]() |
f4c4d974d2 | ||
![]() |
948567c456 | ||
![]() |
9ba583175a | ||
![]() |
7d99da77cc | ||
![]() |
8e24c92b1e | ||
![]() |
d2f0e042f4 | ||
![]() |
29e86d9a91 | ||
![]() |
a5dedeed3c | ||
![]() |
ac9f937c88 | ||
![]() |
bab9d9483e | ||
![]() |
134e47b61d | ||
![]() |
e0d9bcc36d | ||
![]() |
9d49a33ac6 | ||
![]() |
0cde2e3f96 | ||
![]() |
61577d29ae | ||
![]() |
32bf975bfa | ||
![]() |
99dfd7f84d | ||
![]() |
b154d8ef98 | ||
![]() |
dafde8becb | ||
![]() |
7ac6eda7cc | ||
![]() |
c7c8c5f097 | ||
![]() |
27b50a3b93 | ||
![]() |
e1e8d5711a | ||
![]() |
b363fb48a5 | ||
![]() |
61539105bd | ||
![]() |
1be8438c70 | ||
![]() |
b426b370fd | ||
![]() |
49bd1b7347 | ||
![]() |
1718ceab70 | ||
![]() |
3d73e7c231 | ||
![]() |
9a5500a667 | ||
![]() |
ab60675660 | ||
![]() |
0d712f7bbc | ||
![]() |
5c38af2ed5 | ||
![]() |
160344514f | ||
![]() |
288a30ab1e | ||
![]() |
afa6db8129 | ||
![]() |
0c42478ea7 | ||
![]() |
1ee64537df | ||
![]() |
2d4a93d5e7 | ||
![]() |
211d59fccc | ||
![]() |
7fe7fd0e18 | ||
![]() |
adbac91be7 | ||
![]() |
b58a10b3e3 | ||
![]() |
a486d4f976 | ||
![]() |
bb658d763a | ||
![]() |
1401ff4aea | ||
![]() |
1e1e100d7f | ||
![]() |
a0240345bf | ||
![]() |
11be757d54 | ||
![]() |
1c1642acfd | ||
![]() |
31dbd8e4ed | ||
![]() |
8e901c3db6 | ||
![]() |
09d8d0426e | ||
![]() |
09ce262c82 | ||
![]() |
73a67b728d | ||
![]() |
11d0c84639 | ||
![]() |
216741c9ce | ||
![]() |
ec2d5f7f69 | ||
![]() |
282bb01e17 | ||
![]() |
a0e119c2f2 | ||
![]() |
e60e6e804b | ||
![]() |
f4802fc534 | ||
![]() |
343fcff625 | ||
![]() |
ce0a362fd0 | ||
![]() |
ddb04075be | ||
![]() |
f976b771d4 | ||
![]() |
f7f4856b93 | ||
![]() |
24a22092ca | ||
![]() |
d28c8ea7ad | ||
![]() |
7b95b379aa | ||
![]() |
86ec02a9b6 | ||
![]() |
2a47b17541 | ||
![]() |
d8a1449a19 | ||
![]() |
6e5e2ab63f | ||
![]() |
eebb1b8a47 | ||
![]() |
b3951e407a | ||
![]() |
3c84f7a1b5 | ||
![]() |
5b7400ae7d | ||
![]() |
46bca54393 | ||
![]() |
da9f39917f | ||
![]() |
6c89eaf4ac | ||
![]() |
2bf6d348b0 | ||
![]() |
4804e15c12 | ||
![]() |
7c4fe28bab | ||
![]() |
40d1bc8e51 | ||
![]() |
18007dda8a | ||
![]() |
61d34a9157 | ||
![]() |
df81075e49 | ||
![]() |
ed7f4a68a4 | ||
![]() |
3e91fd02bf | ||
![]() |
9b1a785c6d | ||
![]() |
3a5e86b4d2 | ||
![]() |
6cdf5da9f4 | ||
![]() |
63989d96fb | ||
![]() |
0040fc9608 | ||
![]() |
a91ca7a199 | ||
![]() |
d1068bede1 | ||
![]() |
61b979c49e | ||
![]() |
d8a9d9d089 | ||
![]() |
65fb4db6cd | ||
![]() |
9c6e5fdda4 | ||
![]() |
ff39ce3874 | ||
![]() |
bcd57b486b | ||
![]() |
d6693e0175 | ||
![]() |
065b227711 | ||
![]() |
f9dafa35a1 | ||
![]() |
66ec57a902 | ||
![]() |
8ef7b13cb0 | ||
![]() |
ba18535c51 | ||
![]() |
f212613526 | ||
![]() |
c60aa225a1 | ||
![]() |
dc92f16eb2 | ||
![]() |
baec50d134 | ||
![]() |
8b0c038d63 | ||
![]() |
9573051e3e | ||
![]() |
8269021d7b | ||
![]() |
0d85dfd044 | ||
![]() |
6320e6c395 | ||
![]() |
7b18e4bce4 | ||
![]() |
86031edd2d | ||
![]() |
6d2dba3aee | ||
![]() |
f2c9158da4 | ||
![]() |
dd7a8aeb10 | ||
![]() |
08c914c660 | ||
![]() |
c0ba4b7d91 | ||
![]() |
b32619892f | ||
![]() |
545d5e10d8 | ||
![]() |
23b368f5ff | ||
![]() |
27b5c535bc | ||
![]() |
4d1b3439ea | ||
![]() |
6da30367d0 | ||
![]() |
5e3e10d32e | ||
![]() |
52864f2bb1 | ||
![]() |
58152b03de | ||
![]() |
03ad24829c | ||
![]() |
af50f7f69d | ||
![]() |
6b7bf29e56 | ||
![]() |
d02a9cf6db | ||
![]() |
ee5bb2de90 | ||
![]() |
474cc76587 | ||
![]() |
e2919873ba | ||
![]() |
21de3cbc96 | ||
![]() |
509ea8745a | ||
![]() |
c0a1431f78 | ||
![]() |
48daf8a5d7 | ||
![]() |
5cad38a047 | ||
![]() |
2eacc71acc | ||
![]() |
50b7e3d94b | ||
![]() |
066b19af8f | ||
![]() |
2764cd8a10 | ||
![]() |
498df75790 | ||
![]() |
542bb28a13 | ||
![]() |
8ac575125b | ||
![]() |
12222ff912 | ||
![]() |
e35be25010 | ||
![]() |
05304622ea | ||
![]() |
a911f2bc0e | ||
![]() |
16fd4d948d | ||
![]() |
07c4e4ad4c | ||
![]() |
dfb2196499 | ||
![]() |
0806363b57 | ||
![]() |
c9cec591f0 | ||
![]() |
5eb6a6d755 | ||
![]() |
58d7be4e83 | ||
![]() |
fe06a19742 | ||
![]() |
281b7307a8 | ||
![]() |
fedf0cbf40 | ||
![]() |
f6f19c962e | ||
![]() |
d525e28c20 | ||
![]() |
b7dd590300 | ||
![]() |
57f15bcb97 | ||
![]() |
01d1d5e142 | ||
![]() |
97397db294 | ||
![]() |
908b728503 | ||
![]() |
09b42a78bd | ||
![]() |
6ac1ee45b0 | ||
![]() |
fc4daae0cd | ||
![]() |
77c9cb5512 | ||
![]() |
dac72a344c | ||
![]() |
640a6f08d7 | ||
![]() |
9885d55a37 | ||
![]() |
e6e0072e8d | ||
![]() |
981dd5f333 | ||
![]() |
09ee38f694 | ||
![]() |
b803b308fa | ||
![]() |
12e9a7c47b | ||
![]() |
9b7714c39c | ||
![]() |
5aa6530c84 | ||
![]() |
9c3fc230ba | ||
![]() |
9ba82e5472 | ||
![]() |
0be0cc6953 | ||
![]() |
28bb7d076a | ||
![]() |
eb7e9c5e2e | ||
![]() |
8246c65ba8 | ||
![]() |
e47e63d91e | ||
![]() |
a57cb3b9ff | ||
![]() |
4e33dd754f | ||
![]() |
5b104ad116 | ||
![]() |
34cc8fc70c | ||
![]() |
330ddc6bd2 | ||
![]() |
ae7f92772b | ||
![]() |
5e7609ea2a | ||
![]() |
f1c77a06fb | ||
![]() |
bab66330ca | ||
![]() |
f5a1a906d1 | ||
![]() |
fc4f87b33e | ||
![]() |
95ac201b4b | ||
![]() |
dc84a74c7e | ||
![]() |
203fe47aa1 | ||
![]() |
6994b05ab6 | ||
![]() |
ad854ab716 | ||
![]() |
05c18fce67 | ||
![]() |
256cd89bb1 | ||
![]() |
3c522a7aa2 | ||
![]() |
d195bcc4b8 | ||
![]() |
bfe20e75f8 | ||
![]() |
8c8a193a70 | ||
![]() |
e0611ab192 | ||
![]() |
2ceb4a9526 | ||
![]() |
eec72794fd | ||
![]() |
c382af56a5 | ||
![]() |
846ab59e68 | ||
![]() |
5ec8bfe141 | ||
![]() |
baa7e440ed |
48 changed files with 6945 additions and 4692 deletions
6
.autom4te.cfg
Normal file
6
.autom4te.cfg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Disable autom4te cache to ensure that any change to ddclient.in triggers a
|
||||||
|
# rebuild of the configure script (which gets the version of ddclient from
|
||||||
|
# ddclient.in). See <https://lists.gnu.org/r/automake/2019-10/msg00002.html>.
|
||||||
|
begin-language: "Autoconf-without-aclocal-m4"
|
||||||
|
args: --no-cache
|
||||||
|
end-language: "Autoconf-without-aclocal-m4"
|
94
.github/workflows/ci.yml
vendored
94
.github/workflows/ci.yml
vendored
|
@ -6,6 +6,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
test-debian-like:
|
test-debian-like:
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
image:
|
image:
|
||||||
- ubuntu:latest
|
- ubuntu:latest
|
||||||
|
@ -32,10 +33,11 @@ jobs:
|
||||||
libtest-tcp-perl \
|
libtest-tcp-perl \
|
||||||
libtest-warnings-perl \
|
libtest-warnings-perl \
|
||||||
liburi-perl \
|
liburi-perl \
|
||||||
|
libwww-perl \
|
||||||
net-tools \
|
net-tools \
|
||||||
make \
|
make \
|
||||||
;
|
;
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: autogen
|
- name: autogen
|
||||||
run: ./autogen
|
run: ./autogen
|
||||||
- name: configure
|
- name: configure
|
||||||
|
@ -46,41 +48,45 @@ jobs:
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
||||||
- name: distribution tarball is complete
|
- name: distribution tarball is complete
|
||||||
run: ./.github/workflows/scripts/dist-tarball-check
|
run: ./.github/workflows/scripts/dist-tarball-check
|
||||||
|
- if: ${{ matrix.image == 'debian:testing' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: distribution-tarball
|
||||||
|
path: ddclient-*.tar.gz
|
||||||
|
|
||||||
#test-centos8:
|
test-fedora-like:
|
||||||
# runs-on: ubuntu-latest
|
strategy:
|
||||||
# container: centos:8
|
fail-fast: false
|
||||||
# steps:
|
matrix:
|
||||||
# - uses: actions/checkout@v2
|
image:
|
||||||
# - name: install dependencies
|
- fedora:39
|
||||||
# run: |
|
- fedora:latest
|
||||||
# dnf --refresh --enablerepo=PowerTools install -y \
|
- fedora:rawhide
|
||||||
# automake \
|
- almalinux:8
|
||||||
# make \
|
- almalinux:latest
|
||||||
# perl-HTTP-Daemon \
|
|
||||||
# perl-IO-Socket-INET6 \
|
|
||||||
# perl-Test-Warnings \
|
|
||||||
# perl-core \
|
|
||||||
# ;
|
|
||||||
# - name: autogen
|
|
||||||
# run: ./autogen
|
|
||||||
# - name: configure
|
|
||||||
# run: ./configure
|
|
||||||
# - name: check
|
|
||||||
# run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
|
||||||
# - name: distcheck
|
|
||||||
# run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
|
||||||
|
|
||||||
test-fedora:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: fedora
|
container:
|
||||||
|
image: ${{ matrix.image }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: install dependencies
|
- name: enable repositories (AlmaLinux 8)
|
||||||
|
if: ${{ matrix.image == 'almalinux:8' }}
|
||||||
run: |
|
run: |
|
||||||
dnf --refresh install -y \
|
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
|
||||||
|
dnf config-manager --set-enabled powertools
|
||||||
|
- name: enable repositories (AlmaLinux latest)
|
||||||
|
if: ${{ matrix.image == 'almalinux:latest' }}
|
||||||
|
run: |
|
||||||
|
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
|
||||||
|
dnf config-manager --set-enabled crb
|
||||||
|
- name: install dependencies
|
||||||
|
# The --skip-broken argument works around missing packages. (They're
|
||||||
|
# only used for testing, so it's OK to not install them.)
|
||||||
|
run: |
|
||||||
|
dnf --refresh install --skip-broken -y \
|
||||||
automake \
|
automake \
|
||||||
findutils \
|
findutils \
|
||||||
|
iproute \
|
||||||
make \
|
make \
|
||||||
curl \
|
curl \
|
||||||
perl \
|
perl \
|
||||||
|
@ -91,6 +97,8 @@ jobs:
|
||||||
perl-Test-MockModule \
|
perl-Test-MockModule \
|
||||||
perl-Test-TCP \
|
perl-Test-TCP \
|
||||||
perl-Test-Warnings \
|
perl-Test-Warnings \
|
||||||
|
perl-core \
|
||||||
|
perl-libwww-perl \
|
||||||
net-tools \
|
net-tools \
|
||||||
;
|
;
|
||||||
- name: autogen
|
- name: autogen
|
||||||
|
@ -101,29 +109,3 @@ jobs:
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
||||||
- name: distcheck
|
- name: distcheck
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
||||||
|
|
||||||
test-redhat-ubi7:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# we use redhats univeral base image which is not available on docker hub
|
|
||||||
# https://catalog.redhat.com/software/containers/ubi7/ubi/5c3592dcd70cc534b3a37814
|
|
||||||
container: registry.access.redhat.com/ubi7/ubi
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: install dependencies
|
|
||||||
run: |
|
|
||||||
yum install -y \
|
|
||||||
automake \
|
|
||||||
make \
|
|
||||||
perl-HTTP-Daemon \
|
|
||||||
perl-IO-Socket-INET6 \
|
|
||||||
perl-core \
|
|
||||||
iproute \
|
|
||||||
;
|
|
||||||
- name: autogen
|
|
||||||
run: ./autogen
|
|
||||||
- name: configure
|
|
||||||
run: ./configure
|
|
||||||
- name: check
|
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
|
||||||
- name: distcheck
|
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
|
||||||
|
|
49
.github/workflows/pr.yml
vendored
Normal file
49
.github/workflows/pr.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
name: Pull Request
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- unlabeled
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linear-history:
|
||||||
|
if: ${{ !contains(github.event.pull_request.labels.*.name, 'pr-permit-nonlinear') }}
|
||||||
|
name: Linear History
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: No new merge commits
|
||||||
|
run: |
|
||||||
|
log() { printf %s\\n "$*" >&2; }
|
||||||
|
error() { log "ERROR: $@"; }
|
||||||
|
fatal() { error "$@"; exit 1; }
|
||||||
|
try() { log "Running command $@"; "$@" || fatal "'$@' failed"; }
|
||||||
|
out=$(try git rev-list -n 1 --merges '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}') || exit 1
|
||||||
|
[ -z "${out}" ] || {
|
||||||
|
error "pull request includes a merge commit and does not have the 'pr-permit-nonlinear' label"
|
||||||
|
git show "${out}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
no-autosquash:
|
||||||
|
if: ${{ !contains(github.event.pull_request.labels.*.name, 'pr-permit-autosquash') }}
|
||||||
|
name: No --autosquash commits
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: 'No commits with messages starting with "fixup!", "squash!", or "amend!"'
|
||||||
|
run: |
|
||||||
|
log() { printf %s\\n "$*" >&2; }
|
||||||
|
error() { log "ERROR: $@"; }
|
||||||
|
fatal() { error "$@"; exit 1; }
|
||||||
|
try() { log "Running command $@"; "$@" || fatal "'$@' failed"; }
|
||||||
|
out=$(try git log --oneline '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}') || exit 1
|
||||||
|
! grep -E '^[^ ]* (fixup|squash|amend)!' <<EOF || fatal "--autosquash commits not allowed without the 'pr-permit-autosquash' label"
|
||||||
|
${out}
|
||||||
|
EOF
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -7,8 +7,11 @@ release
|
||||||
/Makefile.in
|
/Makefile.in
|
||||||
/aclocal.m4
|
/aclocal.m4
|
||||||
/autom4te.cache/
|
/autom4te.cache/
|
||||||
|
/build-aux/config.guess
|
||||||
|
/build-aux/config.sub
|
||||||
/build-aux/install-sh
|
/build-aux/install-sh
|
||||||
/build-aux/missing
|
/build-aux/missing
|
||||||
|
/build-aux/tap-driver.sh
|
||||||
/config.log
|
/config.log
|
||||||
/config.status
|
/config.status
|
||||||
/configure
|
/configure
|
||||||
|
|
|
@ -80,7 +80,7 @@ perltidy -l=99 -conv -ci=4 -ola -ce -nbbc -kis -pt=2 -b ddclient
|
||||||
|
|
||||||
## Git Hygiene
|
## Git Hygiene
|
||||||
|
|
||||||
* Please keep your pull request commits rebased on top of master.
|
* Please keep your pull request commits rebased on top of `main`.
|
||||||
* Please use `git rebase -i` to make your commits easy to review:
|
* Please use `git rebase -i` to make your commits easy to review:
|
||||||
- Put unrelated changes in separate commits
|
- Put unrelated changes in separate commits
|
||||||
- Squash your fixup commits
|
- Squash your fixup commits
|
||||||
|
@ -190,11 +190,11 @@ better to revert the original change then redo it:
|
||||||
|
|
||||||
### Merging Pull Requests
|
### Merging Pull Requests
|
||||||
|
|
||||||
To facilitate reviews and code archaeology, `master` should have a
|
To facilitate reviews and code archaeology, `main` should have a
|
||||||
semi-linear commit history like this:
|
semi-linear commit history like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
* f4e6e90 sandro.jaeckel@gmail.com 2020-05-31 07:29:51 +0200 (master)
|
* f4e6e90 sandro.jaeckel@gmail.com 2020-05-31 07:29:51 +0200 (main)
|
||||||
|\ Merge pull request #142 from rhansen/config-line-format
|
|\ Merge pull request #142 from rhansen/config-line-format
|
||||||
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|
||||||
|/ Expand comment documenting config line format
|
|/ Expand comment documenting config line format
|
||||||
|
@ -231,7 +231,7 @@ has value:
|
||||||
change was made) and the merge timestamp (when it went live).
|
change was made) and the merge timestamp (when it went live).
|
||||||
|
|
||||||
To achieve a history like the above, the pull request must be rebased
|
To achieve a history like the above, the pull request must be rebased
|
||||||
onto `master` before merging. Unfortunately, GitHub does not have a
|
onto `main` before merging. Unfortunately, GitHub does not have a
|
||||||
one-click way to do this (the "Rebase and merge" option does a
|
one-click way to do this (the "Rebase and merge" option does a
|
||||||
fast-forward merge, which is not what we want). See
|
fast-forward merge, which is not what we want). See
|
||||||
[isaacs/github#1143](https://github.com/isaacs/github/issues/1143) and
|
[isaacs/github#1143](https://github.com/isaacs/github/issues/1143) and
|
||||||
|
@ -254,15 +254,15 @@ git remote set-url origin git@github.com:ddclient/ddclient.git
|
||||||
# Add a remote for the fork used in the PR
|
# Add a remote for the fork used in the PR
|
||||||
git remote add "${PR_USER:?}" git@github.com:"${PR_USER:?}"/ddclient
|
git remote add "${PR_USER:?}" git@github.com:"${PR_USER:?}"/ddclient
|
||||||
|
|
||||||
# Fetch the latest commits for the PR and ddclient master
|
# Fetch the latest commits for the PR and ddclient main
|
||||||
git remote update -p
|
git remote update -p
|
||||||
|
|
||||||
# Switch to the pull request branch
|
# Switch to the pull request branch
|
||||||
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
|
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
|
||||||
|
|
||||||
# Rebase the commits (optionally using -i to clean up history) onto
|
# Rebase the commits (optionally using -i to clean up history) onto
|
||||||
# the current ddclient master branch
|
# the current ddclient main branch
|
||||||
git rebase origin/master
|
git rebase origin/main
|
||||||
|
|
||||||
# Force update the contributor's fork. This will only work if the
|
# Force update the contributor's fork. This will only work if the
|
||||||
# contributor has checked the "Allow edits by maintainers" box in the
|
# contributor has checked the "Allow edits by maintainers" box in the
|
||||||
|
@ -276,19 +276,19 @@ git push -f
|
||||||
# "Allow edits by maintainers", or if you prefer to merge manually,
|
# "Allow edits by maintainers", or if you prefer to merge manually,
|
||||||
# continue with the next steps.
|
# continue with the next steps.
|
||||||
|
|
||||||
# Switch to the local master branch
|
# Switch to the local main branch
|
||||||
git checkout master
|
git checkout main
|
||||||
|
|
||||||
# Make sure the local master branch is up to date
|
# Make sure the local main branch is up to date
|
||||||
git merge --ff-only origin/master
|
git merge --ff-only origin/main
|
||||||
|
|
||||||
# Merge in the rebased pull request branch **WITHOUT DOING A
|
# Merge in the rebased pull request branch **WITHOUT DOING A
|
||||||
# FAST-FORWARD MERGE**
|
# FAST-FORWARD MERGE**
|
||||||
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
|
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
|
||||||
|
|
||||||
# Review the commits before pushing
|
# Review the commits before pushing
|
||||||
git log --graph --oneline --decorate origin/master..
|
git log --graph --oneline --decorate origin/main..
|
||||||
|
|
||||||
# Push to ddclient master
|
# Push to ddclient main
|
||||||
git push origin master
|
git push origin main
|
||||||
```
|
```
|
||||||
|
|
205
ChangeLog.md
205
ChangeLog.md
|
@ -1,7 +1,210 @@
|
||||||
# ChangeLog
|
# ChangeLog
|
||||||
|
|
||||||
This document describes notable changes. For details, see the [source code
|
This document describes notable changes. For details, see the [source code
|
||||||
repository history](https://github.com/ddclient/ddclient/commits/master).
|
repository history](https://github.com/ddclient/ddclient/commits/main).
|
||||||
|
|
||||||
|
## v4.0.1-alpha (unreleased work-in-progress)
|
||||||
|
|
||||||
|
## 2025-01-19 v4.0.0
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
* ddclient now looks for `ddclient.conf` in `${sysconfdir}/ddclient` by
|
||||||
|
default instead of `${sysconfdir}`.
|
||||||
|
[#789](https://github.com/ddclient/ddclient/pull/789)
|
||||||
|
|
||||||
|
To retain the previous behavior, pass `'--with-confdir=${sysconfdir}'` to
|
||||||
|
`configure`. For example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Before v4.0.0:
|
||||||
|
./configure --sysconfdir=/etc
|
||||||
|
# Equivalent with v4.0.0 and later (the single quotes are intentional):
|
||||||
|
./configure --sysconfdir=/etc --with-confdir='${sysconfdir}'
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Before v4.0.0:
|
||||||
|
./configure --sysconfdir=/etc/ddclient
|
||||||
|
# Equivalent with v4.0.0 and later:
|
||||||
|
./configure --sysconfdir=/etc
|
||||||
|
```
|
||||||
|
|
||||||
|
* The `--ssl` option is now enabled by default.
|
||||||
|
[#705](https://github.com/ddclient/ddclient/pull/705)
|
||||||
|
* Unencrypted (plain) HTTP is now used instead of encrypted (TLS) HTTP if the
|
||||||
|
URL uses `http://` instead of `https://`, even if the `--ssl` option is
|
||||||
|
enabled. [#608](https://github.com/ddclient/ddclient/pull/608)
|
||||||
|
* The string argument to `--cmdv4` or `--cmdv6` is now executed as-is by the
|
||||||
|
system's shell, matching the behavior of the deprecated `--cmd` option.
|
||||||
|
This makes it possible to pass command-line arguments, which reduces the
|
||||||
|
need for a custom wrapper script. Beware that the string is also subject to
|
||||||
|
the shell's command substitution, quote handling, variable expansion, field
|
||||||
|
splitting, etc., so you may need to add extra escaping to ensure that any
|
||||||
|
special characters are preserved literally.
|
||||||
|
[#766](https://github.com/ddclient/ddclient/pull/766)
|
||||||
|
* The default web service for `--webv4` and `--webv6` has changed from Google
|
||||||
|
Domains (which has shut down) to ipify.
|
||||||
|
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
|
||||||
|
* Invalid command-line options or values are now fatal errors (instead of
|
||||||
|
discarded with a warning).
|
||||||
|
[#733](https://github.com/ddclient/ddclient/pull/733)
|
||||||
|
* All log messages are now written to STDERR, not a mix of STDOUT and STDERR.
|
||||||
|
[#676](https://github.com/ddclient/ddclient/pull/676)
|
||||||
|
* For `--protocol=freedns` and `--protocol=nfsn`, the core module
|
||||||
|
`Digest::SHA` is now required. Previously, `Digest::SHA1` was used (if
|
||||||
|
available) as an alternative to `Digest::SHA`.
|
||||||
|
[#685](https://github.com/ddclient/ddclient/pull/685)
|
||||||
|
* The `he` built-in web IP discovery service (`--webv4=he`, `--webv6=he`, and
|
||||||
|
`--web=he`) was renamed to `he.net` for consistency with the new `he.net`
|
||||||
|
protocol. The old name is still accepted but is deprecated and will be
|
||||||
|
removed in a future version of ddclient.
|
||||||
|
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||||
|
* Deprecated built-in web IP discovery services are not listed in the output
|
||||||
|
of `--list-web-services`.
|
||||||
|
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||||
|
* `dyndns2`: Support for "wait" response lines has been removed. The Dyn
|
||||||
|
documentation does not mention such responses, and the code to handle them,
|
||||||
|
untouched since at least 2006, is believed to be obsolete.
|
||||||
|
[#709](https://github.com/ddclient/ddclient/pull/709)
|
||||||
|
* `dyndns2`: The obsolete `static` and `custom` options have been removed.
|
||||||
|
Setting the options may produce a warning.
|
||||||
|
[#709](https://github.com/ddclient/ddclient/pull/709)
|
||||||
|
* The diagnostic `--geturl` command-line argument was removed.
|
||||||
|
[#712](https://github.com/ddclient/ddclient/pull/712)
|
||||||
|
* `easydns`: The default value for `min-interval` was increased from 5m to 10m
|
||||||
|
to match easyDNS documentation.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* `woima`: The dyn.woima.fi service appears to be defunct so support was
|
||||||
|
removed. [#716](https://github.com/ddclient/ddclient/pull/716)
|
||||||
|
* `googledomains`: Support was removed because the service shut down.
|
||||||
|
[#716](https://github.com/ddclient/ddclient/pull/716)
|
||||||
|
* The `--retry` option was removed.
|
||||||
|
[#732](https://github.com/ddclient/ddclient/pull/732)
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
* New `--mail-from` option to control the "From:" header of email messages.
|
||||||
|
[#565](https://github.com/ddclient/ddclient/pull/565)
|
||||||
|
* Simultaneous/separate updating of IPv4 (A) records and IPv6 (AAAA) records
|
||||||
|
is now supported in the following services: `gandi`
|
||||||
|
([#558](https://github.com/ddclient/ddclient/pull/558)), `nsupdate`
|
||||||
|
([#604](https://github.com/ddclient/ddclient/pull/604)), `noip`
|
||||||
|
([#603](https://github.com/ddclient/ddclient/pull/603)), `mythicdyn`
|
||||||
|
([#616](https://github.com/ddclient/ddclient/pull/616)), `godaddy`
|
||||||
|
([#560](https://github.com/ddclient/ddclient/pull/560)).
|
||||||
|
* `porkbun`: Added support for subdomains.
|
||||||
|
[#624](https://github.com/ddclient/ddclient/pull/624)
|
||||||
|
* `gandi`: Added support for personal access tokens.
|
||||||
|
[#636](https://github.com/ddclient/ddclient/pull/636)
|
||||||
|
* Comments after the `\` line continuation character are now supported.
|
||||||
|
[3c522a7a](https://github.com/ddclient/ddclient/commit/3c522a7aa235f63ae0439e5674e7406e20c90956)
|
||||||
|
* Minor improvements to `--help` output.
|
||||||
|
[#659](https://github.com/ddclient/ddclient/pull/659),
|
||||||
|
[#665](https://github.com/ddclient/ddclient/pull/665)
|
||||||
|
* Improved formatting of ddclient's version number.
|
||||||
|
[#639](https://github.com/ddclient/ddclient/pull/639)
|
||||||
|
* Updated sample systemd service unit file to improve logging in the systemd
|
||||||
|
journal. [#669](https://github.com/ddclient/ddclient/pull/669)
|
||||||
|
* The second and subsequent lines in a multi-line log message now have a
|
||||||
|
different prefix to distinguish them from separate log messages.
|
||||||
|
[#676](https://github.com/ddclient/ddclient/pull/676)
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* Log messages now include context, making it easier to troubleshoot issues.
|
||||||
|
[#725](https://github.com/ddclient/ddclient/pull/725)
|
||||||
|
* `emailonly`: New `protocol` option that simply emails you when your IP
|
||||||
|
address changes. [#654](https://github.com/ddclient/ddclient/pull/654)
|
||||||
|
* `he.net`: Added support for updating Hurricane Electric records.
|
||||||
|
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||||
|
* `dyndns2`, `domeneshop`, `dnsmadeeasy`, `keysystems`: The `server` option
|
||||||
|
can now include `http://` or `https://` to control the use of TLS. If
|
||||||
|
omitted, the value of the `ssl` option is used to determine the scheme.
|
||||||
|
[#703](https://github.com/ddclient/ddclient/pull/703)
|
||||||
|
* `ddns.fm`: New `protocol` option for updating [DDNS.FM](https://ddns.fm/)
|
||||||
|
records. [#695](https://github.com/ddclient/ddclient/pull/695)
|
||||||
|
* `inwx`: New `protocol` option for updating [INWX](https://www.inwx.com/)
|
||||||
|
records. [#690](https://github.com/ddclient/ddclient/pull/690)
|
||||||
|
* `domeneshop`: Add IPv6 support.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `duckdns`: Multiple hosts with the same IP address are now updated together.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `directnic`: Added support for updatng Directnic records.
|
||||||
|
[#726](https://github.com/ddclient/ddclient/pull/726)
|
||||||
|
* `porkbun`: The update URL hostname is now configurable via the `server`
|
||||||
|
option. [#752](https://github.com/ddclient/ddclient/pull/752)
|
||||||
|
* `dnsexit2`: Multiple hosts are updated in a single API call when possible.
|
||||||
|
[#684](https://github.com/ddclient/ddclient/pull/684)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
* Fixed numerous bugs in cache file (recap) handling.
|
||||||
|
[#740](https://github.com/ddclient/ddclient/pull/740)
|
||||||
|
* Fixed numerous bugs in command-line option and configuration file
|
||||||
|
processing. [#733](https://github.com/ddclient/ddclient/pull/733)
|
||||||
|
* `noip`: Fixed failure to honor IP discovery settings in some circumstances.
|
||||||
|
[#591](https://github.com/ddclient/ddclient/pull/591)
|
||||||
|
* Fixed `--usev6` with providers that have not yet been updated to use the new
|
||||||
|
separate IPv4/IPv6 logic.
|
||||||
|
[ad854ab7](https://github.com/ddclient/ddclient/commit/ad854ab716922f5f25742421ebd4c27646b86619)
|
||||||
|
* HTTP redirects (301, 302) are now followed.
|
||||||
|
[#592](https://github.com/ddclient/ddclient/pull/592)
|
||||||
|
* `keysystems`: Fixed update URL.
|
||||||
|
[#629](https://github.com/ddclient/ddclient/pull/629)
|
||||||
|
* `dondominio`: Fixed response parsing.
|
||||||
|
[#646](https://github.com/ddclient/ddclient/pull/646)
|
||||||
|
* Fixed `--web-ssl-validate` and `--fw-ssl-validate` options, which were
|
||||||
|
ignored in some cases (defaulting to validate).
|
||||||
|
[#661](https://github.com/ddclient/ddclient/pull/661)
|
||||||
|
* Explicitly setting `--web-skip`, `--webv4-skip`, `--webv6-skip`,
|
||||||
|
`--fw-skip`, `--fwv4-skip`, and `--fwv6-skip` to the empty string now
|
||||||
|
disables any built-in default skip. Before, setting to the empty string had
|
||||||
|
no effect. [#662](https://github.com/ddclient/ddclient/pull/662)
|
||||||
|
* `--use=disabled` now works.
|
||||||
|
[#665](https://github.com/ddclient/ddclient/pull/665)
|
||||||
|
* `--retry` and `--daemon` are incompatible with each other; ddclient now
|
||||||
|
errors out if both are provided.
|
||||||
|
[#666](https://github.com/ddclient/ddclient/pull/666)
|
||||||
|
* `--usev4=cisco` and `--usev4=cisco-asa` now work.
|
||||||
|
[#664](https://github.com/ddclient/ddclient/pull/664)
|
||||||
|
* Fixed "Scalar value better written as" Perl warning.
|
||||||
|
[#667](https://github.com/ddclient/ddclient/pull/667)
|
||||||
|
* Fixed "Invalid Value for keyword 'wtime' = ''" warning.
|
||||||
|
[#734](https://github.com/ddclient/ddclient/pull/734)
|
||||||
|
* Fixed unnecessary repeated updates for some services.
|
||||||
|
[#670](https://github.com/ddclient/ddclient/pull/670)
|
||||||
|
[#732](https://github.com/ddclient/ddclient/pull/732)
|
||||||
|
* Fixed DNSExit provider when configured with a zone and non-identical
|
||||||
|
hostname. [#674](https://github.com/ddclient/ddclient/pull/674)
|
||||||
|
* `infomaniak`: Fixed frequent forced updates after 25 days (`max-interval`).
|
||||||
|
[#691](https://github.com/ddclient/ddclient/pull/691)
|
||||||
|
* `infomaniak`: Fixed incorrect parsing of server response.
|
||||||
|
[#692](https://github.com/ddclient/ddclient/pull/692)
|
||||||
|
* `infomaniak`: Fixed incorrect handling of `nochg` responses.
|
||||||
|
[#723](https://github.com/ddclient/ddclient/pull/723)
|
||||||
|
* `regfishde`: Fixed IPv6 support.
|
||||||
|
[#691](https://github.com/ddclient/ddclient/pull/691)
|
||||||
|
* `easydns`: IPv4 and IPv6 addresses are now updated separately to be
|
||||||
|
consistent with the easyDNS documentation.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* `easydns`: Fixed parsing of result code from server response.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* `easydns`: Fixed successful updates treated as failed updates.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* Any IP addresses in an HTTP response's headers or in an HTTP error
|
||||||
|
response's body are now ignored when obtaining the IP address from a
|
||||||
|
web-based IP discovery service (`--usev4=webv4`, `--usev6=webv6`) or from a
|
||||||
|
router/firewall device.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `yandex`: Errors are now retried.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `gandi`: Fixed handling of error responses.
|
||||||
|
[#721](https://github.com/ddclient/ddclient/pull/721)
|
||||||
|
* `dyndns2`: Fixed handling of responses for multi-host updates.
|
||||||
|
[#728](https://github.com/ddclient/ddclient/pull/728)
|
||||||
|
* `porkbun`: The default update URL was updated from `porkbun.com` to
|
||||||
|
`api.porkbun.com`. [#752](https://github.com/ddclient/ddclient/pull/752)
|
||||||
|
|
||||||
## 2023-11-23 v3.11.2
|
## 2023-11-23 v3.11.2
|
||||||
|
|
||||||
|
|
52
Makefile.am
52
Makefile.am
|
@ -1,4 +1,4 @@
|
||||||
ACLOCAL_AMFLAGS = -I m4
|
ACLOCAL_AMFLAGS = -I build-aux/m4
|
||||||
EXTRA_DIST = \
|
EXTRA_DIST = \
|
||||||
CONTRIBUTING.md \
|
CONTRIBUTING.md \
|
||||||
COPYING \
|
COPYING \
|
||||||
|
@ -16,19 +16,7 @@ EXTRA_DIST = \
|
||||||
sample-get-ip-from-fritzbox
|
sample-get-ip-from-fritzbox
|
||||||
CLEANFILES =
|
CLEANFILES =
|
||||||
|
|
||||||
# Command that replaces substitution variables with their values.
|
|
||||||
subst = sed \
|
|
||||||
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
|
|
||||||
-e '1 s|^\#\!.*perl$$|\#\!$(PERL)|g' \
|
|
||||||
-e 's|@localstatedir[@]|$(localstatedir)|g' \
|
|
||||||
-e 's|@runstatedir[@]|$(runstatedir)|g' \
|
|
||||||
-e 's|@sysconfdir[@]|$(sysconfdir)|g' \
|
|
||||||
-e 's|@CURL[@]|$(CURL)|g'
|
|
||||||
|
|
||||||
# Files that will be generated by passing their *.in file through
|
|
||||||
# $(subst).
|
|
||||||
subst_files = ddclient ddclient.conf
|
subst_files = ddclient ddclient.conf
|
||||||
|
|
||||||
EXTRA_DIST += $(subst_files:=.in)
|
EXTRA_DIST += $(subst_files:=.in)
|
||||||
CLEANFILES += $(subst_files)
|
CLEANFILES += $(subst_files)
|
||||||
|
|
||||||
|
@ -36,7 +24,14 @@ $(subst_files): Makefile
|
||||||
rm -f '$@' '$@'.tmp
|
rm -f '$@' '$@'.tmp
|
||||||
in='$@'.in; \
|
in='$@'.in; \
|
||||||
test -f "$${in}" || in='$(srcdir)/'$${in}; \
|
test -f "$${in}" || in='$(srcdir)/'$${in}; \
|
||||||
$(subst) "$${in}" >'$@'.tmp && \
|
sed \
|
||||||
|
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
|
||||||
|
-e '1 s|^#\!.*perl$$|#\!$(PERL)|g' \
|
||||||
|
-e 's|@localstatedir[@]|$(localstatedir)|g' \
|
||||||
|
-e 's|@confdir[@]|$(confdir)|g' \
|
||||||
|
-e 's|@runstatedir[@]|$(runstatedir)|g' \
|
||||||
|
-e 's|@CURL[@]|$(CURL)|g' \
|
||||||
|
"$${in}" >'$@'.tmp && \
|
||||||
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
||||||
mv '$@'.tmp '$@'
|
mv '$@'.tmp '$@'
|
||||||
|
|
||||||
|
@ -45,7 +40,7 @@ ddclient.conf: $(srcdir)/ddclient.conf.in
|
||||||
|
|
||||||
bin_SCRIPTS = ddclient
|
bin_SCRIPTS = ddclient
|
||||||
|
|
||||||
sysconf_DATA = ddclient.conf
|
conf_DATA = ddclient.conf
|
||||||
|
|
||||||
install-data-local:
|
install-data-local:
|
||||||
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
||||||
|
@ -62,17 +57,36 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
|
||||||
-I'$(abs_top_srcdir)'/t/lib \
|
-I'$(abs_top_srcdir)'/t/lib \
|
||||||
-MDevel::Autoflush
|
-MDevel::Autoflush
|
||||||
handwritten_tests = \
|
handwritten_tests = \
|
||||||
|
t/builtinfw_query.pl \
|
||||||
|
t/check_value.pl \
|
||||||
t/get_ip_from_if.pl \
|
t/get_ip_from_if.pl \
|
||||||
|
t/geturl_connectivity.pl \
|
||||||
|
t/geturl_response.pl \
|
||||||
|
t/group_hosts_by.pl \
|
||||||
|
t/header_ok.pl \
|
||||||
|
t/interval_expired.pl \
|
||||||
t/is-and-extract-ipv4.pl \
|
t/is-and-extract-ipv4.pl \
|
||||||
t/is-and-extract-ipv6.pl \
|
t/is-and-extract-ipv6.pl \
|
||||||
t/is-and-extract-ipv6-global.pl \
|
t/is-and-extract-ipv6-global.pl \
|
||||||
|
t/logmsg.pl \
|
||||||
t/parse_assignments.pl \
|
t/parse_assignments.pl \
|
||||||
t/write_cache.pl
|
t/protocol_directnic.pl \
|
||||||
|
t/protocol_dnsexit2.pl \
|
||||||
|
t/protocol_dyndns2.pl \
|
||||||
|
t/read_recap.pl \
|
||||||
|
t/skip.pl \
|
||||||
|
t/ssl-validate.pl \
|
||||||
|
t/update_nics.pl \
|
||||||
|
t/use_cmd.pl \
|
||||||
|
t/use_web.pl \
|
||||||
|
t/variable_defaults.pl \
|
||||||
|
t/write_recap.pl
|
||||||
generated_tests = \
|
generated_tests = \
|
||||||
t/geturl_connectivity.pl \
|
|
||||||
t/version.pl
|
t/version.pl
|
||||||
TESTS = $(handwritten_tests) $(generated_tests)
|
TESTS = $(handwritten_tests) $(generated_tests)
|
||||||
|
$(TESTS): ddclient
|
||||||
EXTRA_DIST += $(handwritten_tests) \
|
EXTRA_DIST += $(handwritten_tests) \
|
||||||
|
.autom4te.cfg \
|
||||||
t/lib/Devel/Autoflush.pm \
|
t/lib/Devel/Autoflush.pm \
|
||||||
t/lib/Test/Builder.pm \
|
t/lib/Test/Builder.pm \
|
||||||
t/lib/Test/Builder/Formatter.pm \
|
t/lib/Test/Builder/Formatter.pm \
|
||||||
|
@ -143,5 +157,9 @@ EXTRA_DIST += $(handwritten_tests) \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-ca-cert.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-ca-cert.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
|
||||||
|
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem \
|
||||||
t/lib/ddclient/t.pm \
|
t/lib/ddclient/t.pm \
|
||||||
|
t/lib/ddclient/t/HTTPD.pm \
|
||||||
|
t/lib/ddclient/t/Logger.pm \
|
||||||
|
t/lib/ddclient/t/ip.pm \
|
||||||
t/lib/ok.pm
|
t/lib/ok.pm
|
||||||
|
|
203
README.md
203
README.md
|
@ -3,7 +3,35 @@
|
||||||
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
|
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
|
||||||
on many dynamic DNS services. It uses `curl` for internet access.
|
on many dynamic DNS services. It uses `curl` for internet access.
|
||||||
|
|
||||||
This is a friendly fork/continuation of https://github.com/ddclient/ddclient
|
on docker compose
|
||||||
|
```docker-compose
|
||||||
|
services:
|
||||||
|
ddclient:
|
||||||
|
image: lscr.io/linuxserver/ddclient:latest
|
||||||
|
container_name: ddclient
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=Europe/Rome
|
||||||
|
volumes:
|
||||||
|
- /home/orangepi/dockerfiles/ddclient/config:/config
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
file ddclient.conf per servizio DDNS di dynu.com da mettere nel folder config
|
||||||
|
```file
|
||||||
|
daemon=60 # check every 300 seconds
|
||||||
|
syslog=yes # log update msgs to syslog
|
||||||
|
mail=root # mail all msgs to root#mail-failure=root # mail failed update msgs to root
|
||||||
|
pid=/var/run/ddclient/ddclient.pid # record PID in file.
|
||||||
|
use=web, web=checkip.dynu.com/, web-skip='IP Address'
|
||||||
|
protocol=dyndns2 # default protocol
|
||||||
|
server=api.dynu.com
|
||||||
|
# default login
|
||||||
|
login=FabioMich66 # your default user
|
||||||
|
password=Master66! # your default password
|
||||||
|
wildcard=yes
|
||||||
|
patachina.casacam.net
|
||||||
|
```
|
||||||
|
|
||||||
## Alternatives
|
## Alternatives
|
||||||
|
|
||||||
|
@ -15,41 +43,44 @@ your dynamic DNS provider(s): <https://github.com/troglobit/inadyn> or
|
||||||
|
|
||||||
Dynamic DNS services currently supported include:
|
Dynamic DNS services currently supported include:
|
||||||
|
|
||||||
* [1984.is](https://www.1984.is/product/freedns)
|
* [1984.is](https://www.1984.is/product/freedns)
|
||||||
* [ChangeIP](https://www.changeip.com)
|
* [ChangeIP](https://www.changeip.com)
|
||||||
* [CloudFlare](https://www.cloudflare.com)
|
* [CloudFlare](https://www.cloudflare.com)
|
||||||
* [ClouDNS](https://www.cloudns.net)
|
* [ClouDNS](https://www.cloudns.net)
|
||||||
* [DigitalOcean](https://www.digitalocean.com/)
|
* [DDNS.fm](https://www.ddns.fm/)
|
||||||
* [dinahosting](https://dinahosting.com)
|
* [DigitalOcean](https://www.digitalocean.com/)
|
||||||
* [DonDominio](https://www.dondominio.com)
|
* [dinahosting](https://dinahosting.com)
|
||||||
* [DNS Made Easy](https://dnsmadeeasy.com)
|
* [Directnic](https://directnic.com)
|
||||||
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
* [DonDominio](https://www.dondominio.com)
|
||||||
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
* [DNS Made Easy](https://dnsmadeeasy.com)
|
||||||
* [DslReports](https://www.dslreports.com)
|
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
||||||
* [Duck DNS](https://duckdns.org)
|
* [dnsHome.de](https://www.dnshome.de)
|
||||||
* [DynDNS.com](https://account.dyn.com)
|
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
||||||
* [EasyDNS](https://www.easydns.com )
|
* [DslReports](https://www.dslreports.com)
|
||||||
* [Enom](https://www.enom.com)
|
* [Duck DNS](https://duckdns.org)
|
||||||
* [Freedns](https://freedns.afraid.org)
|
* [DynDNS.com](https://account.dyn.com)
|
||||||
* [Freemyip](https://freemyip.com)
|
* [EasyDNS](https://www.easydns.com )
|
||||||
* [Gandi](https://gandi.net)
|
* [Enom](https://www.enom.com)
|
||||||
* [GoDaddy](https://www.godaddy.com)
|
* [Freedns](https://freedns.afraid.org)
|
||||||
* [Google](https://domains.google)
|
* [Freemyip](https://freemyip.com)
|
||||||
* [Infomaniak](https://faq.infomaniak.com/2376)
|
* [Gandi](https://gandi.net)
|
||||||
* [Loopia](https://www.loopia.se)
|
* [GoDaddy](https://www.godaddy.com)
|
||||||
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
* [Hurricane Electric](https://dns.he.net)
|
||||||
* [NameCheap](https://www.namecheap.com)
|
* [Infomaniak](https://faq.infomaniak.com/2376)
|
||||||
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
* [INWX](https://www.inwx.com/)
|
||||||
* [Njalla](https://njal.la/docs/ddns)
|
* [Loopia](https://www.loopia.se)
|
||||||
* [Noip](https://www.noip.com)
|
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
||||||
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
* [NameCheap](https://www.namecheap.com)
|
||||||
* [OVH](https://www.ovhcloud.com)
|
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
||||||
* [Porkbun](https://porkbun.com)
|
* [Njalla](https://njal.la/docs/ddns)
|
||||||
* [regfish.de](https://www.regfish.de/domains/dyndns)
|
* [Noip](https://www.noip.com)
|
||||||
* [Sitelutions](https://www.sitelutions.com)
|
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
||||||
* [woima.fi](https://woima.fi)
|
* [OVH](https://www.ovhcloud.com)
|
||||||
* [Yandex](https://dns.yandex.com)
|
* [Porkbun](https://porkbun.com)
|
||||||
* [Zoneedit](https://www.zoneedit.com)
|
* [regfish.de](https://www.regfish.de/domains/dyndns)
|
||||||
|
* [Sitelutions](https://www.sitelutions.com)
|
||||||
|
* [Yandex](https://dns.yandex.com)
|
||||||
|
* [Zoneedit](https://www.zoneedit.com)
|
||||||
|
|
||||||
`ddclient` supports finding your IP address from many cable and DSL
|
`ddclient` supports finding your IP address from many cable and DSL
|
||||||
broadband routers.
|
broadband routers.
|
||||||
|
@ -104,7 +135,7 @@ operating system. See the image to the right for a list of distributions with a
|
||||||
```shell
|
```shell
|
||||||
./configure \
|
./configure \
|
||||||
--prefix=/usr \
|
--prefix=/usr \
|
||||||
--sysconfdir=/etc/ddclient \
|
--sysconfdir=/etc \
|
||||||
--localstatedir=/var
|
--localstatedir=/var
|
||||||
make
|
make
|
||||||
make VERBOSE=1 check
|
make VERBOSE=1 check
|
||||||
|
@ -125,43 +156,97 @@ start the first time by hand
|
||||||
|
|
||||||
systemctl start ddclient.service
|
systemctl start ddclient.service
|
||||||
|
|
||||||
|
## Known issues
|
||||||
|
This is a list for quick referencing of known issues. For further details check out the linked issues and the changelog.
|
||||||
|
|
||||||
|
Note that any issues prior to version v3.9.1 will not be listed here.
|
||||||
|
If a fix is committed but not yet part of any tagged release, the notes here will reference the not-yet-released version number.
|
||||||
|
|
||||||
|
### v3.11.2 - v3.9.1: SSL parameter breaks HTTP-only IP acquisition
|
||||||
|
|
||||||
|
The `ssl` parameter forces all connections to use HTTPS. While technically
|
||||||
|
working as expected, this behavior keeps coming up as a pain point when using
|
||||||
|
HTTP-only IP querying sites such as http://checkip.dyndns.org. Starting with
|
||||||
|
v4.0.0, the behavior is changed to respect `http://` in a URL. A separate
|
||||||
|
parameter to disallow all HTTP connections or warn about them may be added
|
||||||
|
later.
|
||||||
|
|
||||||
|
**Fix**: v4.0.0 uses HTTP to connect to URLs starting with `http://`. See
|
||||||
|
[here](https://github.com/ddclient/ddclient/pull/608) for more info.
|
||||||
|
|
||||||
|
**Workaround**: Disable the SSL parameter
|
||||||
|
|
||||||
|
### v3.10.0: Chunked encoding not corretly supported in IO::Socket HTTP code
|
||||||
|
Using the IO::Socket HTTP code will break in various ways whenever the server responds using HTTP 1.1 chunked encoding. Refer to [this issue](https://github.com/ddclient/ddclient/issues/548) for more info.
|
||||||
|
|
||||||
|
**Fix**: v3.11.0 - IO::Socket has been deprecated there and curl has been made the standard.
|
||||||
|
|
||||||
|
**Workaround**: Use curl for transfers by either setting `-curl` in the command line or by adding `curl=yes` in the config
|
||||||
|
|
||||||
|
### v3.10.0: Spammed updates to some providers
|
||||||
|
This issue arises when using the `use` parameter in the config and using one of these providers:
|
||||||
|
- Cloudflare
|
||||||
|
- Hetzner
|
||||||
|
- Digitalocean
|
||||||
|
- Infomaniak
|
||||||
|
|
||||||
|
**Fix**: v3.11.2
|
||||||
|
|
||||||
|
**Workaround**: Use the `usev4`/`usev6` parameters instead of `use`.
|
||||||
|
|
||||||
|
|
||||||
## TROUBLESHOOTING
|
## TROUBLESHOOTING
|
||||||
|
|
||||||
1. enable debugging and verbose messages: ``$ ddclient -daemon=0 -debug -verbose -noquiet``
|
* Enable debugging and verbose messages: `ddclient --daemon=0 --debug --verbose`
|
||||||
|
|
||||||
2. Do you need to specify a proxy?
|
* Do you need to specify a proxy?
|
||||||
If so, just add a ``proxy=your.isp.proxy`` to the ddclient.conf file.
|
If so, just add a `proxy=your.isp.proxy` to the `ddclient.conf` file.
|
||||||
|
|
||||||
3. Define the IP address of your router with ``fw=xxx.xxx.xxx.xxx`` in
|
* Define the IP address of your router with `fwv4=xxx.xxx.xxx.xxx` in
|
||||||
``/etc/ddclient/ddclient.conf`` and then try ``$ ddclient -daemon=0 -query`` to see if the router status web page can be understood.
|
`/etc/ddclient/ddclient.conf` and then try `$ ddclient --daemon=0 --query`
|
||||||
|
to see if the router status web page can be understood.
|
||||||
|
|
||||||
4. Need support for another router/firewall?
|
* Need support for another router/firewall?
|
||||||
Define the router status page yourself with: ``fw=url-to-your-router``'s-status-page ``fw-skip=any-string-preceding-your-IP-address``
|
Define the router yourself with:
|
||||||
|
|
||||||
ddclient does something like this to provide builtin support for
|
```
|
||||||
common routers.
|
usev4=fwv4
|
||||||
|
fwv4=url-to-your-router-status-page
|
||||||
|
fwv4-skip="regular expression matching any string preceding your IP address, if necessary"
|
||||||
|
```
|
||||||
|
|
||||||
|
ddclient does something like this to provide builtin support for common
|
||||||
|
routers.
|
||||||
For example, the Linksys routers could have been added with:
|
For example, the Linksys routers could have been added with:
|
||||||
|
|
||||||
fw=192.168.1.1/Status.htm
|
```
|
||||||
fw-skip=WAN.*?IP Address
|
usev4=fwv4
|
||||||
|
fwv4=192.168.1.1/Status.htm
|
||||||
|
fwv4-skip=WAN.*?IP Address
|
||||||
|
```
|
||||||
|
|
||||||
OR
|
OR [create a new issue](https://github.com/ddclient/ddclient/issues/new)
|
||||||
Send me the output from:
|
containing the output from:
|
||||||
``$ ddclient -geturl {fw-ip-status-url} [-login login [-password password]]``
|
|
||||||
and I'll add it to the next release!
|
|
||||||
|
|
||||||
ie. for my fw/router I used: ``$ ddclient -geturl 192.168.1.254/status.htm``
|
```
|
||||||
|
curl --include --location http://url.of.your.firewall/ip-status-page
|
||||||
|
```
|
||||||
|
|
||||||
5. Some broadband routers require the use of a password when ddclient
|
so that we can add a new firewall definition to a future release of
|
||||||
accesses its status page to determine the router's WAN IP address.
|
ddclient.
|
||||||
|
|
||||||
|
* Some broadband routers require the use of a password when ddclient accesses
|
||||||
|
its status page to determine the router's WAN IP address.
|
||||||
If this is the case for your router, add
|
If this is the case for your router, add
|
||||||
|
|
||||||
|
```
|
||||||
fw-login=your-router-login
|
fw-login=your-router-login
|
||||||
fw-password=your-router-password
|
fw-password=your-router-password
|
||||||
|
```
|
||||||
|
|
||||||
to the beginning of your ddclient.conf file.
|
to the beginning of your ddclient.conf file.
|
||||||
Note that some routers use either 'root' or 'admin' as their login
|
Note that some routers use either 'root' or 'admin' as their login while
|
||||||
while some others accept anything.
|
some others accept anything.
|
||||||
|
|
||||||
## USING DDCLIENT WITH `ppp`
|
## USING DDCLIENT WITH `ppp`
|
||||||
|
|
||||||
|
@ -199,7 +284,7 @@ In my case, it is named dhcpcd-eth0.exe and contains the lines:
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
PATH=/usr/bin:/root/bin:${PATH}
|
PATH=/usr/bin:/root/bin:${PATH}
|
||||||
logger -t dhcpcd IP address changed to $1
|
logger -t dhcpcd IP address changed to $1
|
||||||
ddclient -proxy fasthttp.sympatico.ca -wildcard -ip $1 | logger -t ddclient
|
ddclient --proxy fasthttp.sympatico.ca --wildcard --ip $1 | logger -t ddclient
|
||||||
exit 0
|
exit 0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
26
autogen
26
autogen
|
@ -7,18 +7,16 @@ fatal() { error "$@"; exit 1; }
|
||||||
try() { "$@" || fatal "'$@' failed"; }
|
try() { "$@" || fatal "'$@' failed"; }
|
||||||
|
|
||||||
try cd "${0%/*}"
|
try cd "${0%/*}"
|
||||||
try mkdir -p m4 build-aux
|
# aclocal complains if a directory passed to AC_CONFIG_MACRO_DIR doesn't exist.
|
||||||
|
try mkdir -p build-aux/m4
|
||||||
|
# autoreconf's '--force' option doesn't affect any of the files installed by the '--install' option.
|
||||||
|
# Remove the files to truly force them to be updated.
|
||||||
|
try rm -f \
|
||||||
|
aclocal.m4 \
|
||||||
|
build-aux/config.guess \
|
||||||
|
build-aux/config.sub \
|
||||||
|
build-aux/install-sh \
|
||||||
|
build-aux/missing \
|
||||||
|
build-aux/tap-driver.sh \
|
||||||
|
;
|
||||||
try autoreconf -fviW all
|
try autoreconf -fviW all
|
||||||
|
|
||||||
# Ignore changes to build-aux/tap-driver, but only if we're in a clone
|
|
||||||
# of the ddclient Git repository. Once CentOS 6 and RHEL 6 reach
|
|
||||||
# end-of-life we can delete build-aux/tap-driver.sh and this block of
|
|
||||||
# code. (tap-driver.sh is checked in to this Git repository only
|
|
||||||
# because we want to support all currently maintained CentOS and RHEL
|
|
||||||
# releases, and CentoOS 6 and RHEL 6 ship with Automake 1.11 which
|
|
||||||
# does not come with tap-driver.sh.)
|
|
||||||
command -v git >/dev/null || exit 0
|
|
||||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
||||||
cdup=$(try git rev-parse --show-cdup) || exit 1
|
|
||||||
[ -z "${cdup}" ] || exit 0
|
|
||||||
try git update-index --assume-unchanged -- build-aux/tap-driver.sh
|
|
||||||
|
|
|
@ -1,651 +0,0 @@
|
||||||
#! /bin/sh
|
|
||||||
# Copyright (C) 2011-2020 Free Software Foundation, Inc.
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2, or (at your option)
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
# As a special exception to the GNU General Public License, if you
|
|
||||||
# distribute this file as part of a program that contains a
|
|
||||||
# configuration script generated by Autoconf, you may include it under
|
|
||||||
# the same distribution terms that you use for the rest of that program.
|
|
||||||
|
|
||||||
# This file is maintained in Automake, please report
|
|
||||||
# bugs to <bug-automake@gnu.org> or send patches to
|
|
||||||
# <automake-patches@gnu.org>.
|
|
||||||
|
|
||||||
scriptversion=2013-12-23.17; # UTC
|
|
||||||
|
|
||||||
# Make unconditional expansion of undefined variables an error. This
|
|
||||||
# helps a lot in preventing typo-related bugs.
|
|
||||||
set -u
|
|
||||||
|
|
||||||
me=tap-driver.sh
|
|
||||||
|
|
||||||
fatal ()
|
|
||||||
{
|
|
||||||
echo "$me: fatal: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
usage_error ()
|
|
||||||
{
|
|
||||||
echo "$me: $*" >&2
|
|
||||||
print_usage >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
print_usage ()
|
|
||||||
{
|
|
||||||
cat <<END
|
|
||||||
Usage:
|
|
||||||
tap-driver.sh --test-name=NAME --log-file=PATH --trs-file=PATH
|
|
||||||
[--expect-failure={yes|no}] [--color-tests={yes|no}]
|
|
||||||
[--enable-hard-errors={yes|no}] [--ignore-exit]
|
|
||||||
[--diagnostic-string=STRING] [--merge|--no-merge]
|
|
||||||
[--comments|--no-comments] [--] TEST-COMMAND
|
|
||||||
The '--test-name', '-log-file' and '--trs-file' options are mandatory.
|
|
||||||
END
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO: better error handling in option parsing (in particular, ensure
|
|
||||||
# TODO: $log_file, $trs_file and $test_name are defined).
|
|
||||||
test_name= # Used for reporting.
|
|
||||||
log_file= # Where to save the result and output of the test script.
|
|
||||||
trs_file= # Where to save the metadata of the test run.
|
|
||||||
expect_failure=0
|
|
||||||
color_tests=0
|
|
||||||
merge=0
|
|
||||||
ignore_exit=0
|
|
||||||
comments=0
|
|
||||||
diag_string='#'
|
|
||||||
while test $# -gt 0; do
|
|
||||||
case $1 in
|
|
||||||
--help) print_usage; exit $?;;
|
|
||||||
--version) echo "$me $scriptversion"; exit $?;;
|
|
||||||
--test-name) test_name=$2; shift;;
|
|
||||||
--log-file) log_file=$2; shift;;
|
|
||||||
--trs-file) trs_file=$2; shift;;
|
|
||||||
--color-tests) color_tests=$2; shift;;
|
|
||||||
--expect-failure) expect_failure=$2; shift;;
|
|
||||||
--enable-hard-errors) shift;; # No-op.
|
|
||||||
--merge) merge=1;;
|
|
||||||
--no-merge) merge=0;;
|
|
||||||
--ignore-exit) ignore_exit=1;;
|
|
||||||
--comments) comments=1;;
|
|
||||||
--no-comments) comments=0;;
|
|
||||||
--diagnostic-string) diag_string=$2; shift;;
|
|
||||||
--) shift; break;;
|
|
||||||
-*) usage_error "invalid option: '$1'";;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
test $# -gt 0 || usage_error "missing test command"
|
|
||||||
|
|
||||||
case $expect_failure in
|
|
||||||
yes) expect_failure=1;;
|
|
||||||
*) expect_failure=0;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if test $color_tests = yes; then
|
|
||||||
init_colors='
|
|
||||||
color_map["red"]="[0;31m" # Red.
|
|
||||||
color_map["grn"]="[0;32m" # Green.
|
|
||||||
color_map["lgn"]="[1;32m" # Light green.
|
|
||||||
color_map["blu"]="[1;34m" # Blue.
|
|
||||||
color_map["mgn"]="[0;35m" # Magenta.
|
|
||||||
color_map["std"]="[m" # No color.
|
|
||||||
color_for_result["ERROR"] = "mgn"
|
|
||||||
color_for_result["PASS"] = "grn"
|
|
||||||
color_for_result["XPASS"] = "red"
|
|
||||||
color_for_result["FAIL"] = "red"
|
|
||||||
color_for_result["XFAIL"] = "lgn"
|
|
||||||
color_for_result["SKIP"] = "blu"'
|
|
||||||
else
|
|
||||||
init_colors=''
|
|
||||||
fi
|
|
||||||
|
|
||||||
# :; is there to work around a bug in bash 3.2 (and earlier) which
|
|
||||||
# does not always set '$?' properly on redirection failure.
|
|
||||||
# See the Autoconf manual for more details.
|
|
||||||
:;{
|
|
||||||
(
|
|
||||||
# Ignore common signals (in this subshell only!), to avoid potential
|
|
||||||
# problems with Korn shells. Some Korn shells are known to propagate
|
|
||||||
# to themselves signals that have killed a child process they were
|
|
||||||
# waiting for; this is done at least for SIGINT (and usually only for
|
|
||||||
# it, in truth). Without the `trap' below, such a behaviour could
|
|
||||||
# cause a premature exit in the current subshell, e.g., in case the
|
|
||||||
# test command it runs gets terminated by a SIGINT. Thus, the awk
|
|
||||||
# script we are piping into would never seen the exit status it
|
|
||||||
# expects on its last input line (which is displayed below by the
|
|
||||||
# last `echo $?' statement), and would thus die reporting an internal
|
|
||||||
# error.
|
|
||||||
# For more information, see the Autoconf manual and the threads:
|
|
||||||
# <https://lists.gnu.org/archive/html/bug-autoconf/2011-09/msg00004.html>
|
|
||||||
# <http://mail.opensolaris.org/pipermail/ksh93-integration-discuss/2009-February/004121.html>
|
|
||||||
trap : 1 3 2 13 15
|
|
||||||
if test $merge -gt 0; then
|
|
||||||
exec 2>&1
|
|
||||||
else
|
|
||||||
exec 2>&3
|
|
||||||
fi
|
|
||||||
"$@"
|
|
||||||
echo $?
|
|
||||||
) | LC_ALL=C ${AM_TAP_AWK-awk} \
|
|
||||||
-v me="$me" \
|
|
||||||
-v test_script_name="$test_name" \
|
|
||||||
-v log_file="$log_file" \
|
|
||||||
-v trs_file="$trs_file" \
|
|
||||||
-v expect_failure="$expect_failure" \
|
|
||||||
-v merge="$merge" \
|
|
||||||
-v ignore_exit="$ignore_exit" \
|
|
||||||
-v comments="$comments" \
|
|
||||||
-v diag_string="$diag_string" \
|
|
||||||
'
|
|
||||||
# TODO: the usages of "cat >&3" below could be optimized when using
|
|
||||||
# GNU awk, and/on on systems that supports /dev/fd/.
|
|
||||||
|
|
||||||
# Implementation note: in what follows, `result_obj` will be an
|
|
||||||
# associative array that (partly) simulates a TAP result object
|
|
||||||
# from the `TAP::Parser` perl module.
|
|
||||||
|
|
||||||
## ----------- ##
|
|
||||||
## FUNCTIONS ##
|
|
||||||
## ----------- ##
|
|
||||||
|
|
||||||
function fatal(msg)
|
|
||||||
{
|
|
||||||
print me ": " msg | "cat >&2"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function abort(where)
|
|
||||||
{
|
|
||||||
fatal("internal error " where)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert a boolean to a "yes"/"no" string.
|
|
||||||
function yn(bool)
|
|
||||||
{
|
|
||||||
return bool ? "yes" : "no";
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_test_result(result)
|
|
||||||
{
|
|
||||||
if (!test_results_index)
|
|
||||||
test_results_index = 0
|
|
||||||
test_results_list[test_results_index] = result
|
|
||||||
test_results_index += 1
|
|
||||||
test_results_seen[result] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Whether the test script should be re-run by "make recheck".
|
|
||||||
function must_recheck()
|
|
||||||
{
|
|
||||||
for (k in test_results_seen)
|
|
||||||
if (k != "XFAIL" && k != "PASS" && k != "SKIP")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Whether the content of the log file associated to this test should
|
|
||||||
# be copied into the "global" test-suite.log.
|
|
||||||
function copy_in_global_log()
|
|
||||||
{
|
|
||||||
for (k in test_results_seen)
|
|
||||||
if (k != "PASS")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_global_test_result()
|
|
||||||
{
|
|
||||||
if ("ERROR" in test_results_seen)
|
|
||||||
return "ERROR"
|
|
||||||
if ("FAIL" in test_results_seen || "XPASS" in test_results_seen)
|
|
||||||
return "FAIL"
|
|
||||||
all_skipped = 1
|
|
||||||
for (k in test_results_seen)
|
|
||||||
if (k != "SKIP")
|
|
||||||
all_skipped = 0
|
|
||||||
if (all_skipped)
|
|
||||||
return "SKIP"
|
|
||||||
return "PASS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringify_result_obj(result_obj)
|
|
||||||
{
|
|
||||||
if (result_obj["is_unplanned"] || result_obj["number"] != testno)
|
|
||||||
return "ERROR"
|
|
||||||
|
|
||||||
if (plan_seen == LATE_PLAN)
|
|
||||||
return "ERROR"
|
|
||||||
|
|
||||||
if (result_obj["directive"] == "TODO")
|
|
||||||
return result_obj["is_ok"] ? "XPASS" : "XFAIL"
|
|
||||||
|
|
||||||
if (result_obj["directive"] == "SKIP")
|
|
||||||
return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL;
|
|
||||||
|
|
||||||
if (length(result_obj["directive"]))
|
|
||||||
abort("in function stringify_result_obj()")
|
|
||||||
|
|
||||||
return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL
|
|
||||||
}
|
|
||||||
|
|
||||||
function decorate_result(result)
|
|
||||||
{
|
|
||||||
color_name = color_for_result[result]
|
|
||||||
if (color_name)
|
|
||||||
return color_map[color_name] "" result "" color_map["std"]
|
|
||||||
# If we are not using colorized output, or if we do not know how
|
|
||||||
# to colorize the given result, we should return it unchanged.
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function report(result, details)
|
|
||||||
{
|
|
||||||
if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/)
|
|
||||||
{
|
|
||||||
msg = ": " test_script_name
|
|
||||||
add_test_result(result)
|
|
||||||
}
|
|
||||||
else if (result == "#")
|
|
||||||
{
|
|
||||||
msg = " " test_script_name ":"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
abort("in function report()")
|
|
||||||
}
|
|
||||||
if (length(details))
|
|
||||||
msg = msg " " details
|
|
||||||
# Output on console might be colorized.
|
|
||||||
print decorate_result(result) msg
|
|
||||||
# Log the result in the log file too, to help debugging (this is
|
|
||||||
# especially true when said result is a TAP error or "Bail out!").
|
|
||||||
print result msg | "cat >&3";
|
|
||||||
}
|
|
||||||
|
|
||||||
function testsuite_error(error_message)
|
|
||||||
{
|
|
||||||
report("ERROR", "- " error_message)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_tap_result()
|
|
||||||
{
|
|
||||||
details = result_obj["number"];
|
|
||||||
if (length(result_obj["description"]))
|
|
||||||
details = details " " result_obj["description"]
|
|
||||||
|
|
||||||
if (plan_seen == LATE_PLAN)
|
|
||||||
{
|
|
||||||
details = details " # AFTER LATE PLAN";
|
|
||||||
}
|
|
||||||
else if (result_obj["is_unplanned"])
|
|
||||||
{
|
|
||||||
details = details " # UNPLANNED";
|
|
||||||
}
|
|
||||||
else if (result_obj["number"] != testno)
|
|
||||||
{
|
|
||||||
details = sprintf("%s # OUT-OF-ORDER (expecting %d)",
|
|
||||||
details, testno);
|
|
||||||
}
|
|
||||||
else if (result_obj["directive"])
|
|
||||||
{
|
|
||||||
details = details " # " result_obj["directive"];
|
|
||||||
if (length(result_obj["explanation"]))
|
|
||||||
details = details " " result_obj["explanation"]
|
|
||||||
}
|
|
||||||
|
|
||||||
report(stringify_result_obj(result_obj), details)
|
|
||||||
}
|
|
||||||
|
|
||||||
# `skip_reason` should be empty whenever planned > 0.
|
|
||||||
function handle_tap_plan(planned, skip_reason)
|
|
||||||
{
|
|
||||||
planned += 0 # Avoid getting confused if, say, `planned` is "00"
|
|
||||||
if (length(skip_reason) && planned > 0)
|
|
||||||
abort("in function handle_tap_plan()")
|
|
||||||
if (plan_seen)
|
|
||||||
{
|
|
||||||
# Error, only one plan per stream is acceptable.
|
|
||||||
testsuite_error("multiple test plans")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
planned_tests = planned
|
|
||||||
# The TAP plan can come before or after *all* the TAP results; we speak
|
|
||||||
# respectively of an "early" or a "late" plan. If we see the plan line
|
|
||||||
# after at least one TAP result has been seen, assume we have a late
|
|
||||||
# plan; in this case, any further test result seen after the plan will
|
|
||||||
# be flagged as an error.
|
|
||||||
plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN)
|
|
||||||
# If testno > 0, we have an error ("too many tests run") that will be
|
|
||||||
# automatically dealt with later, so do not worry about it here. If
|
|
||||||
# $plan_seen is true, we have an error due to a repeated plan, and that
|
|
||||||
# has already been dealt with above. Otherwise, we have a valid "plan
|
|
||||||
# with SKIP" specification, and should report it as a particular kind
|
|
||||||
# of SKIP result.
|
|
||||||
if (planned == 0 && testno == 0)
|
|
||||||
{
|
|
||||||
if (length(skip_reason))
|
|
||||||
skip_reason = "- " skip_reason;
|
|
||||||
report("SKIP", skip_reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extract_tap_comment(line)
|
|
||||||
{
|
|
||||||
if (index(line, diag_string) == 1)
|
|
||||||
{
|
|
||||||
# Strip leading `diag_string` from `line`.
|
|
||||||
line = substr(line, length(diag_string) + 1)
|
|
||||||
# And strip any leading and trailing whitespace left.
|
|
||||||
sub("^[ \t]*", "", line)
|
|
||||||
sub("[ \t]*$", "", line)
|
|
||||||
# Return what is left (if any).
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
# When this function is called, we know that line is a TAP result line,
|
|
||||||
# so that it matches the (perl) RE "^(not )?ok\b".
|
|
||||||
function setup_result_obj(line)
|
|
||||||
{
|
|
||||||
# Get the result, and remove it from the line.
|
|
||||||
result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0)
|
|
||||||
sub("^(not )?ok[ \t]*", "", line)
|
|
||||||
|
|
||||||
# If the result has an explicit number, get it and strip it; otherwise,
|
|
||||||
# automatically assing the next progresive number to it.
|
|
||||||
if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/)
|
|
||||||
{
|
|
||||||
match(line, "^[0-9]+")
|
|
||||||
# The final `+ 0` is to normalize numbers with leading zeros.
|
|
||||||
result_obj["number"] = substr(line, 1, RLENGTH) + 0
|
|
||||||
line = substr(line, RLENGTH + 1)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result_obj["number"] = testno
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan_seen == LATE_PLAN)
|
|
||||||
# No further test results are acceptable after a "late" TAP plan
|
|
||||||
# has been seen.
|
|
||||||
result_obj["is_unplanned"] = 1
|
|
||||||
else if (plan_seen && testno > planned_tests)
|
|
||||||
result_obj["is_unplanned"] = 1
|
|
||||||
else
|
|
||||||
result_obj["is_unplanned"] = 0
|
|
||||||
|
|
||||||
# Strip trailing and leading whitespace.
|
|
||||||
sub("^[ \t]*", "", line)
|
|
||||||
sub("[ \t]*$", "", line)
|
|
||||||
|
|
||||||
# This will have to be corrected if we have a "TODO"/"SKIP" directive.
|
|
||||||
result_obj["description"] = line
|
|
||||||
result_obj["directive"] = ""
|
|
||||||
result_obj["explanation"] = ""
|
|
||||||
|
|
||||||
if (index(line, "#") == 0)
|
|
||||||
return # No possible directive, nothing more to do.
|
|
||||||
|
|
||||||
# Directives are case-insensitive.
|
|
||||||
rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*"
|
|
||||||
|
|
||||||
# See whether we have the directive, and if yes, where.
|
|
||||||
pos = match(line, rx "$")
|
|
||||||
if (!pos)
|
|
||||||
pos = match(line, rx "[^a-zA-Z0-9_]")
|
|
||||||
|
|
||||||
# If there was no TAP directive, we have nothing more to do.
|
|
||||||
if (!pos)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Let`s now see if the TAP directive has been escaped. For example:
|
|
||||||
# escaped: ok \# SKIP
|
|
||||||
# not escaped: ok \\# SKIP
|
|
||||||
# escaped: ok \\\\\# SKIP
|
|
||||||
# not escaped: ok \ # SKIP
|
|
||||||
if (substr(line, pos, 1) == "#")
|
|
||||||
{
|
|
||||||
bslash_count = 0
|
|
||||||
for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--)
|
|
||||||
bslash_count += 1
|
|
||||||
if (bslash_count % 2)
|
|
||||||
return # Directive was escaped.
|
|
||||||
}
|
|
||||||
|
|
||||||
# Strip the directive and its explanation (if any) from the test
|
|
||||||
# description.
|
|
||||||
result_obj["description"] = substr(line, 1, pos - 1)
|
|
||||||
# Now remove the test description from the line, that has been dealt
|
|
||||||
# with already.
|
|
||||||
line = substr(line, pos)
|
|
||||||
# Strip the directive, and save its value (normalized to upper case).
|
|
||||||
sub("^[ \t]*#[ \t]*", "", line)
|
|
||||||
result_obj["directive"] = toupper(substr(line, 1, 4))
|
|
||||||
line = substr(line, 5)
|
|
||||||
# Now get the explanation for the directive (if any), with leading
|
|
||||||
# and trailing whitespace removed.
|
|
||||||
sub("^[ \t]*", "", line)
|
|
||||||
sub("[ \t]*$", "", line)
|
|
||||||
result_obj["explanation"] = line
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_test_exit_message(status)
|
|
||||||
{
|
|
||||||
if (status == 0)
|
|
||||||
return ""
|
|
||||||
if (status !~ /^[1-9][0-9]*$/)
|
|
||||||
abort("getting exit status")
|
|
||||||
if (status < 127)
|
|
||||||
exit_details = ""
|
|
||||||
else if (status == 127)
|
|
||||||
exit_details = " (command not found?)"
|
|
||||||
else if (status >= 128 && status <= 255)
|
|
||||||
exit_details = sprintf(" (terminated by signal %d?)", status - 128)
|
|
||||||
else if (status > 256 && status <= 384)
|
|
||||||
# We used to report an "abnormal termination" here, but some Korn
|
|
||||||
# shells, when a child process die due to signal number n, can leave
|
|
||||||
# in $? an exit status of 256+n instead of the more standard 128+n.
|
|
||||||
# Apparently, both behaviours are allowed by POSIX (2008), so be
|
|
||||||
# prepared to handle them both. See also Austing Group report ID
|
|
||||||
# 0000051 <http://www.austingroupbugs.net/view.php?id=51>
|
|
||||||
exit_details = sprintf(" (terminated by signal %d?)", status - 256)
|
|
||||||
else
|
|
||||||
# Never seen in practice.
|
|
||||||
exit_details = " (abnormal termination)"
|
|
||||||
return sprintf("exited with status %d%s", status, exit_details)
|
|
||||||
}
|
|
||||||
|
|
||||||
function write_test_results()
|
|
||||||
{
|
|
||||||
print ":global-test-result: " get_global_test_result() > trs_file
|
|
||||||
print ":recheck: " yn(must_recheck()) > trs_file
|
|
||||||
print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file
|
|
||||||
for (i = 0; i < test_results_index; i += 1)
|
|
||||||
print ":test-result: " test_results_list[i] > trs_file
|
|
||||||
close(trs_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
BEGIN {
|
|
||||||
|
|
||||||
## ------- ##
|
|
||||||
## SETUP ##
|
|
||||||
## ------- ##
|
|
||||||
|
|
||||||
'"$init_colors"'
|
|
||||||
|
|
||||||
# Properly initialized once the TAP plan is seen.
|
|
||||||
planned_tests = 0
|
|
||||||
|
|
||||||
COOKED_PASS = expect_failure ? "XPASS": "PASS";
|
|
||||||
COOKED_FAIL = expect_failure ? "XFAIL": "FAIL";
|
|
||||||
|
|
||||||
# Enumeration-like constants to remember which kind of plan (if any)
|
|
||||||
# has been seen. It is important that NO_PLAN evaluates "false" as
|
|
||||||
# a boolean.
|
|
||||||
NO_PLAN = 0
|
|
||||||
EARLY_PLAN = 1
|
|
||||||
LATE_PLAN = 2
|
|
||||||
|
|
||||||
testno = 0 # Number of test results seen so far.
|
|
||||||
bailed_out = 0 # Whether a "Bail out!" directive has been seen.
|
|
||||||
|
|
||||||
# Whether the TAP plan has been seen or not, and if yes, which kind
|
|
||||||
# it is ("early" is seen before any test result, "late" otherwise).
|
|
||||||
plan_seen = NO_PLAN
|
|
||||||
|
|
||||||
## --------- ##
|
|
||||||
## PARSING ##
|
|
||||||
## --------- ##
|
|
||||||
|
|
||||||
is_first_read = 1
|
|
||||||
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
# Involutions required so that we are able to read the exit status
|
|
||||||
# from the last input line.
|
|
||||||
st = getline
|
|
||||||
if (st < 0) # I/O error.
|
|
||||||
fatal("I/O error while reading from input stream")
|
|
||||||
else if (st == 0) # End-of-input
|
|
||||||
{
|
|
||||||
if (is_first_read)
|
|
||||||
abort("in input loop: only one input line")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (is_first_read)
|
|
||||||
{
|
|
||||||
is_first_read = 0
|
|
||||||
nextline = $0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
curline = nextline
|
|
||||||
nextline = $0
|
|
||||||
$0 = curline
|
|
||||||
}
|
|
||||||
# Copy any input line verbatim into the log file.
|
|
||||||
print | "cat >&3"
|
|
||||||
# Parsing of TAP input should stop after a "Bail out!" directive.
|
|
||||||
if (bailed_out)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TAP test result.
|
|
||||||
if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/)
|
|
||||||
{
|
|
||||||
testno += 1
|
|
||||||
setup_result_obj($0)
|
|
||||||
handle_tap_result()
|
|
||||||
}
|
|
||||||
# TAP plan (normal or "SKIP" without explanation).
|
|
||||||
else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/)
|
|
||||||
{
|
|
||||||
# The next two lines will put the number of planned tests in $0.
|
|
||||||
sub("^1\\.\\.", "")
|
|
||||||
sub("[^0-9]*$", "")
|
|
||||||
handle_tap_plan($0, "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
# TAP "SKIP" plan, with an explanation.
|
|
||||||
else if ($0 ~ /^1\.\.0+[ \t]*#/)
|
|
||||||
{
|
|
||||||
# The next lines will put the skip explanation in $0, stripping
|
|
||||||
# any leading and trailing whitespace. This is a little more
|
|
||||||
# tricky in truth, since we want to also strip a potential leading
|
|
||||||
# "SKIP" string from the message.
|
|
||||||
sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "")
|
|
||||||
sub("[ \t]*$", "");
|
|
||||||
handle_tap_plan(0, $0)
|
|
||||||
}
|
|
||||||
# "Bail out!" magic.
|
|
||||||
# Older versions of prove and TAP::Harness (e.g., 3.17) did not
|
|
||||||
# recognize a "Bail out!" directive when preceded by leading
|
|
||||||
# whitespace, but more modern versions (e.g., 3.23) do. So we
|
|
||||||
# emulate the latter, "more modern" behaviour.
|
|
||||||
else if ($0 ~ /^[ \t]*Bail out!/)
|
|
||||||
{
|
|
||||||
bailed_out = 1
|
|
||||||
# Get the bailout message (if any), with leading and trailing
|
|
||||||
# whitespace stripped. The message remains stored in `$0`.
|
|
||||||
sub("^[ \t]*Bail out![ \t]*", "");
|
|
||||||
sub("[ \t]*$", "");
|
|
||||||
# Format the error message for the
|
|
||||||
bailout_message = "Bail out!"
|
|
||||||
if (length($0))
|
|
||||||
bailout_message = bailout_message " " $0
|
|
||||||
testsuite_error(bailout_message)
|
|
||||||
}
|
|
||||||
# Maybe we have too look for dianogtic comments too.
|
|
||||||
else if (comments != 0)
|
|
||||||
{
|
|
||||||
comment = extract_tap_comment($0);
|
|
||||||
if (length(comment))
|
|
||||||
report("#", comment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
## -------- ##
|
|
||||||
## FINISH ##
|
|
||||||
## -------- ##
|
|
||||||
|
|
||||||
# A "Bail out!" directive should cause us to ignore any following TAP
|
|
||||||
# error, as well as a non-zero exit status from the TAP producer.
|
|
||||||
if (!bailed_out)
|
|
||||||
{
|
|
||||||
if (!plan_seen)
|
|
||||||
{
|
|
||||||
testsuite_error("missing test plan")
|
|
||||||
}
|
|
||||||
else if (planned_tests != testno)
|
|
||||||
{
|
|
||||||
bad_amount = testno > planned_tests ? "many" : "few"
|
|
||||||
testsuite_error(sprintf("too %s tests run (expected %d, got %d)",
|
|
||||||
bad_amount, planned_tests, testno))
|
|
||||||
}
|
|
||||||
if (!ignore_exit)
|
|
||||||
{
|
|
||||||
# Fetch exit status from the last line.
|
|
||||||
exit_message = get_test_exit_message(nextline)
|
|
||||||
if (exit_message)
|
|
||||||
testsuite_error(exit_message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write_test_results()
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
} # End of "BEGIN" block.
|
|
||||||
'
|
|
||||||
|
|
||||||
# TODO: document that we consume the file descriptor 3 :-(
|
|
||||||
} 3>"$log_file"
|
|
||||||
|
|
||||||
test $? -eq 0 || fatal "I/O or internal error"
|
|
||||||
|
|
||||||
# Local Variables:
|
|
||||||
# mode: shell-script
|
|
||||||
# sh-indentation: 2
|
|
||||||
# eval: (add-hook 'before-save-hook 'time-stamp)
|
|
||||||
# time-stamp-start: "scriptversion="
|
|
||||||
# time-stamp-format: "%:y-%02m-%02d.%02H"
|
|
||||||
# time-stamp-time-zone: "UTC0"
|
|
||||||
# time-stamp-end: "; # UTC"
|
|
||||||
# End:
|
|
49
configure.ac
49
configure.ac
|
@ -1,8 +1,17 @@
|
||||||
AC_PREREQ([2.63])
|
AC_PREREQ([2.63])
|
||||||
AC_INIT([ddclient], [3.11.2])
|
# Get the version from ddclient.in so that the same version string
|
||||||
|
# doesn't have to be maintained in two places. The m4_dquote macro is
|
||||||
|
# used instead of quote characters to ensure that the command is only
|
||||||
|
# run once. The command outputs quote characters to prevent
|
||||||
|
# incidental expansion (the m4_esyscmd macro does not quote the
|
||||||
|
# command output itself, so the command output is subject to
|
||||||
|
# expansion).
|
||||||
|
AC_INIT([ddclient], m4_dquote(m4_esyscmd([printf '[%s]' "$(./ddclient.in --version=short)"])))
|
||||||
|
# Needed because of the above invocation of ddclient.in.
|
||||||
|
AC_SUBST([CONFIGURE_DEPENDENCIES], ['$(top_srcdir)/ddclient.in'])
|
||||||
AC_CONFIG_SRCDIR([ddclient.in])
|
AC_CONFIG_SRCDIR([ddclient.in])
|
||||||
AC_CONFIG_AUX_DIR([build-aux])
|
AC_CONFIG_AUX_DIR([build-aux])
|
||||||
AC_CONFIG_MACRO_DIR([m4])
|
AC_CONFIG_MACRO_DIR([build-aux/m4])
|
||||||
AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||||
# If the automake dependency is bumped to v1.12 or newer, remove
|
# If the automake dependency is bumped to v1.12 or newer, remove
|
||||||
# build-aux/tap-driver.sh from the repository. Automake 1.12+ comes
|
# build-aux/tap-driver.sh from the repository. Automake 1.12+ comes
|
||||||
|
@ -14,6 +23,18 @@ AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||||
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
|
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
|
||||||
AM_SILENT_RULES
|
AM_SILENT_RULES
|
||||||
|
|
||||||
|
m4_define([CONFDIR_DEFAULT], [${sysconfdir}/AC_PACKAGE_NAME])
|
||||||
|
AC_ARG_WITH(
|
||||||
|
[confdir],
|
||||||
|
[AS_HELP_STRING(
|
||||||
|
[--with-confdir=DIR],
|
||||||
|
m4_expand([[look for ddclient.conf in DIR @<:@default: ]CONFDIR_DEFAULT[@:>@]]))],
|
||||||
|
[],
|
||||||
|
# The single quotes are intentional; see:
|
||||||
|
# https://www.gnu.org/software/automake/manual/html_node/Uniform.html
|
||||||
|
[with_confdir='CONFDIR_DEFAULT'])
|
||||||
|
AC_SUBST([confdir], [${with_confdir}])
|
||||||
|
|
||||||
AC_PROG_MKDIR_P
|
AC_PROG_MKDIR_P
|
||||||
|
|
||||||
# The Fedora Docker image doesn't come with the 'findutils' package.
|
# The Fedora Docker image doesn't come with the 'findutils' package.
|
||||||
|
@ -27,7 +48,18 @@ AC_PROG_MKDIR_P
|
||||||
AC_PATH_PROG([FIND], [find])
|
AC_PATH_PROG([FIND], [find])
|
||||||
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
|
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
|
||||||
|
|
||||||
AC_PATH_PROG([CURL], [curl])
|
AC_ARG_WITH([curl],
|
||||||
|
[AS_HELP_STRING([[--with-curl[=CURL]]], [use CURL as absolute path to curl executable])],
|
||||||
|
[],
|
||||||
|
[with_curl=yes])
|
||||||
|
AS_CASE([${with_curl}],
|
||||||
|
[[yes]], [AC_PATH_PROG([CURL], [curl])],
|
||||||
|
[[no]], [CURL=],
|
||||||
|
[
|
||||||
|
AC_MSG_CHECKING([for curl])
|
||||||
|
CURL=${with_curl}
|
||||||
|
AC_MSG_RESULT([${CURL}])
|
||||||
|
]);
|
||||||
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
|
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
|
||||||
|
|
||||||
AX_WITH_PROG([PERL], perl)
|
AX_WITH_PROG([PERL], perl)
|
||||||
|
@ -40,6 +72,7 @@ AC_SUBST([PERL])
|
||||||
# package doesn't depend on all of them, so their availability can't
|
# package doesn't depend on all of them, so their availability can't
|
||||||
# be assumed.
|
# be assumed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
|
Data::Dumper
|
||||||
File::Basename
|
File::Basename
|
||||||
File::Path
|
File::Path
|
||||||
File::Temp
|
File::Temp
|
||||||
|
@ -54,9 +87,12 @@ m4_foreach_w([_m], [
|
||||||
# then some tests will fail. Only prints a warning if not installed.
|
# then some tests will fail. Only prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
B
|
B
|
||||||
Data::Dumper
|
Exporter
|
||||||
File::Spec::Functions
|
File::Spec::Functions
|
||||||
File::Temp
|
File::Temp
|
||||||
|
List::Util
|
||||||
|
Scalar::Util
|
||||||
|
re
|
||||||
], [AX_PROG_PERL_MODULES([_m], [],
|
], [AX_PROG_PERL_MODULES([_m], [],
|
||||||
[AC_MSG_WARN([some tests will fail due to missing module _m])])])
|
[AC_MSG_WARN([some tests will fail due to missing module _m])])])
|
||||||
|
|
||||||
|
@ -65,24 +101,23 @@ m4_foreach_w([_m], [
|
||||||
# prints a warning if not installed.
|
# prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
Carp
|
Carp
|
||||||
Exporter
|
|
||||||
HTTP::Daemon=6.12
|
HTTP::Daemon=6.12
|
||||||
HTTP::Daemon::SSL
|
HTTP::Daemon::SSL
|
||||||
HTTP::Message::PSGI
|
HTTP::Message::PSGI
|
||||||
HTTP::Request
|
HTTP::Request
|
||||||
HTTP::Response
|
HTTP::Response
|
||||||
Scalar::Util
|
JSON::PP
|
||||||
Test::MockModule
|
Test::MockModule
|
||||||
Test::TCP
|
Test::TCP
|
||||||
Test::Warnings
|
Test::Warnings
|
||||||
Time::HiRes
|
Time::HiRes
|
||||||
URI
|
URI
|
||||||
|
parent
|
||||||
], [AX_PROG_PERL_MODULES([_m], [],
|
], [AX_PROG_PERL_MODULES([_m], [],
|
||||||
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
||||||
|
|
||||||
AC_CONFIG_FILES([
|
AC_CONFIG_FILES([
|
||||||
Makefile
|
Makefile
|
||||||
t/geturl_connectivity.pl
|
|
||||||
t/version.pl
|
t/version.pl
|
||||||
])
|
])
|
||||||
AC_OUTPUT
|
AC_OUTPUT
|
||||||
|
|
183
ddclient.conf.in
183
ddclient.conf.in
|
@ -16,15 +16,21 @@
|
||||||
## are mentioned here.
|
## are mentioned here.
|
||||||
##
|
##
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
## Use encryption (TLS) when the scheme (either "http://" or "https://") is
|
||||||
|
## missing from a URL. Defaults to "yes".
|
||||||
|
#ssl=yes
|
||||||
|
|
||||||
daemon=300 # check every 300 seconds
|
daemon=300 # check every 300 seconds
|
||||||
syslog=yes # log update msgs to syslog
|
syslog=yes # log update msgs to syslog
|
||||||
mail=root # mail all msgs to root
|
mail=root # mail all msgs to root
|
||||||
mail-failure=root # mail failed update msgs to root
|
mail-failure=root # mail failed update msgs to root
|
||||||
|
# mail-from=root # set the email "From:" header to "root". If
|
||||||
|
# unset (the default) or empty, the from address
|
||||||
|
# depends on your system's default behavior.
|
||||||
pid=@runstatedir@/ddclient.pid # record PID in file.
|
pid=@runstatedir@/ddclient.pid # record PID in file.
|
||||||
ssl=yes # use ssl-support. Works with
|
# postscript=script # run script after updating. The new IP is
|
||||||
# ssl-library
|
# added as argument.
|
||||||
# postscript=script # run script after updating. The
|
|
||||||
# new IP is added as argument.
|
|
||||||
#
|
#
|
||||||
#use=watchguard-soho, fw=192.168.111.1:80 # via Watchguard's SOHO FW
|
#use=watchguard-soho, fw=192.168.111.1:80 # via Watchguard's SOHO FW
|
||||||
#use=netopia-r910, fw=192.168.111.1:80 # via Netopia R910 FW
|
#use=netopia-r910, fw=192.168.111.1:80 # via Netopia R910 FW
|
||||||
|
@ -47,6 +53,10 @@ ssl=yes # use ssl-support. Works with
|
||||||
## To obtain an IP address from FW status page (using fw-login, fw-password)
|
## To obtain an IP address from FW status page (using fw-login, fw-password)
|
||||||
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
|
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
|
||||||
#
|
#
|
||||||
|
## To obtain an IP address via UPnP from router
|
||||||
|
## Requires miniupnpc to be installed on the system.
|
||||||
|
#use=cmd, cmd=external-ip
|
||||||
|
#
|
||||||
## To obtain an IP address from Web status page (using the proxy if defined)
|
## To obtain an IP address from Web status page (using the proxy if defined)
|
||||||
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
||||||
## Using use=web is enough to get it working.
|
## Using use=web is enough to get it working.
|
||||||
|
@ -78,26 +88,6 @@ ssl=yes # use ssl-support. Works with
|
||||||
# protocol=dyndns2 \
|
# protocol=dyndns2 \
|
||||||
# your-dynamic-host.dyndns.org
|
# your-dynamic-host.dyndns.org
|
||||||
|
|
||||||
##
|
|
||||||
## dyndns.org static addresses
|
|
||||||
##
|
|
||||||
## (supports variables: wildcard,mx,backupmx)
|
|
||||||
##
|
|
||||||
# static=yes, \
|
|
||||||
# server=members.dyndns.org, \
|
|
||||||
# protocol=dyndns2 \
|
|
||||||
# your-static-host.dyndns.org
|
|
||||||
|
|
||||||
##
|
|
||||||
## dyndns.org custom addresses
|
|
||||||
##
|
|
||||||
## (supports variables: wildcard,mx,backupmx)
|
|
||||||
##
|
|
||||||
# custom=yes, \
|
|
||||||
# server=members.dyndns.org, \
|
|
||||||
# protocol=dyndns2 \
|
|
||||||
# your-domain.top-level,your-other-domain.top-level
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## ZoneEdit (zoneedit.com)
|
## ZoneEdit (zoneedit.com)
|
||||||
##
|
##
|
||||||
|
@ -147,10 +137,10 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
||||||
##
|
##
|
||||||
# protocol = nfsn, \
|
# protocol=nfsn, \
|
||||||
|
# zone=example.com, \
|
||||||
# login=member-login, \
|
# login=member-login, \
|
||||||
# password=api-key, \
|
# password=api-key \
|
||||||
# zone=example.com \
|
|
||||||
# example.com,subdomain.example.com
|
# example.com,subdomain.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -171,7 +161,7 @@ ssl=yes # use ssl-support. Works with
|
||||||
# ssl=yes, \
|
# ssl=yes, \
|
||||||
# server=dynupdate.no-ip.com, \
|
# server=dynupdate.no-ip.com, \
|
||||||
# login=your-noip-login, \
|
# login=your-noip-login, \
|
||||||
# password=your-noip-password, \
|
# password=your-noip-password \
|
||||||
# your-host.domain.com, your-2nd-host.domain.com
|
# your-host.domain.com, your-2nd-host.domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -186,30 +176,40 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## CloudFlare (www.cloudflare.com)
|
## CloudFlare (www.cloudflare.com)
|
||||||
##
|
##
|
||||||
#protocol=cloudflare, \
|
# protocol=cloudflare, \
|
||||||
#zone=domain.tld, \
|
# zone=domain.tld, \
|
||||||
#ttl=1, \
|
# ttl=1, \
|
||||||
#login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (without double quotes).
|
# login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (without double quotes).
|
||||||
#password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
|
# password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
|
||||||
#domain.tld,my.domain.tld
|
# domain.tld,my.domain.tld
|
||||||
|
|
||||||
##
|
##
|
||||||
## Gandi (gandi.net)
|
## Gandi (gandi.net)
|
||||||
##
|
##
|
||||||
## Single host update
|
## Single host update
|
||||||
# protocol=gandi, \
|
# protocol=gandi
|
||||||
# zone=example.com, \
|
# zone=example.com
|
||||||
# password=my-gandi-api-key, \
|
# password=my-gandi-access-token
|
||||||
# ttl=3h \
|
# use-personal-access-token=yes
|
||||||
|
# ttl=10800 # optional
|
||||||
# myhost.example.com
|
# myhost.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## Google Domains (www.google.com/domains)
|
## GoDaddy (godaddy.com)
|
||||||
##
|
##
|
||||||
# protocol=googledomains,
|
# protocol=godaddy, \
|
||||||
# login=my-auto-generated-username,
|
# password=my-godaddy-api-key, \
|
||||||
# password=my-auto-generated-password
|
# password=my-godaddy-secret, \
|
||||||
# my.domain.tld, otherhost.domain.tld
|
# ttl=600 \
|
||||||
|
# zone=example.com, \
|
||||||
|
# myhost.example.com,nexthost.example.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## Hurricane Electric (dns.he.net)
|
||||||
|
##
|
||||||
|
# protocol=he.net, \
|
||||||
|
# password=my-genereated-password \
|
||||||
|
# myhost.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## Duckdns (http://www.duckdns.org/)
|
## Duckdns (http://www.duckdns.org/)
|
||||||
|
@ -227,6 +227,14 @@ ssl=yes # use ssl-support. Works with
|
||||||
# password=my-token
|
# password=my-token
|
||||||
# myhost
|
# myhost
|
||||||
|
|
||||||
|
##
|
||||||
|
## DDNS.FM (https://ddns.fm/)
|
||||||
|
##
|
||||||
|
#
|
||||||
|
# protocol=ddns.fm,
|
||||||
|
# password=my-token
|
||||||
|
# myhost.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## MyOnlinePortal (http://myonlineportal.net)
|
## MyOnlinePortal (http://myonlineportal.net)
|
||||||
##
|
##
|
||||||
|
@ -243,23 +251,23 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## nsupdate.info IPV4(https://www.nsupdate.info)
|
## nsupdate.info IPV4(https://www.nsupdate.info)
|
||||||
##
|
##
|
||||||
#use=web, web=http://ipv4.nsupdate.info/myip
|
# use=web, web=http://ipv4.nsupdate.info/myip
|
||||||
#protocol=dyndns2
|
# protocol=dyndns2
|
||||||
#server=ipv4.nsupdate.info
|
# server=ipv4.nsupdate.info
|
||||||
#login=domain.nsupdate.info
|
# login=domain.nsupdate.info
|
||||||
#password='123'
|
# password='123'
|
||||||
#domain.nsupdate.info
|
# domain.nsupdate.info
|
||||||
|
|
||||||
##
|
##
|
||||||
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
||||||
## ddclient releases <= 3.8.1 do not support IPv6
|
## ddclient releases <= 3.8.1 do not support IPv6
|
||||||
##
|
##
|
||||||
#usev6=if, if=eth0
|
# usev6=if, if=eth0
|
||||||
#protocol=dyndns2
|
# protocol=dyndns2
|
||||||
#server=ipv6.nsupdate.info
|
# server=ipv6.nsupdate.info
|
||||||
#login=domain.nsupdate.info
|
# login=domain.nsupdate.info
|
||||||
#password='123'
|
# password='123'
|
||||||
#domain.nsupdate.info
|
# domain.nsupdate.info
|
||||||
|
|
||||||
##
|
##
|
||||||
## Yandex.Mail for Domain (domain.yandex.com)
|
## Yandex.Mail for Domain (domain.yandex.com)
|
||||||
|
@ -291,8 +299,9 @@ ssl=yes # use ssl-support. Works with
|
||||||
# protocol=porkbun
|
# protocol=porkbun
|
||||||
# apikey=APIKey
|
# apikey=APIKey
|
||||||
# secretapikey=SecretAPIKey
|
# secretapikey=SecretAPIKey
|
||||||
|
# root-domain=example.com
|
||||||
# host.example.com,host2.sub.example.com
|
# host.example.com,host2.sub.example.com
|
||||||
# on-root-domain=yes example.com,sub.example.com
|
# example.com,sub.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## ClouDNS (https://www.cloudns.net)
|
## ClouDNS (https://www.cloudns.net)
|
||||||
|
@ -312,17 +321,17 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## dnsexit (www.dnsexit.com)
|
## dnsexit (www.dnsexit.com)
|
||||||
##
|
##
|
||||||
#protocol=dnsexit, \
|
# protocol=dnsexit, \
|
||||||
#login=myusername, \
|
# login=myusername, \
|
||||||
#password=mypassword, \
|
# password=mypassword, \
|
||||||
#subdomain-1.domain.com,subdomain-2.domain.com
|
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## dnsexit2 (API method www.dnsexit.com)
|
## dnsexit2 (API method www.dnsexit.com)
|
||||||
##
|
##
|
||||||
#protocol=dnsexit2
|
# protocol=dnsexit2
|
||||||
#password=MyAPIKey
|
# password=MyAPIKey
|
||||||
#subdomain-1.domain.com,subdomain-2.domain.com
|
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## domeneshop (www.domeneshop.no)
|
## domeneshop (www.domeneshop.no)
|
||||||
|
@ -358,10 +367,18 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## DigitalOcean (www.digitalocean.com)
|
## DigitalOcean (www.digitalocean.com)
|
||||||
##
|
##
|
||||||
#protocol=digitalocean, \
|
# protocol=digitalocean, \
|
||||||
#zone=example.com, \
|
# zone=example.com, \
|
||||||
#password=api-token \
|
# password=api-token \
|
||||||
#example.com,sub.example.com
|
# example.com,sub.example.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## Directnic (directnic.com)
|
||||||
|
##
|
||||||
|
# protocol=directnic,
|
||||||
|
# urlv4=https://directnic.com/dns/gateway/ipv4_token/
|
||||||
|
# urlv6=https://directnic.com/dns/gateway/ipv6_token/
|
||||||
|
# my-domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## Infomaniak (www.infomaniak.com)
|
## Infomaniak (www.infomaniak.com)
|
||||||
|
@ -370,3 +387,35 @@ ssl=yes # use ssl-support. Works with
|
||||||
# login=ddns_username,
|
# login=ddns_username,
|
||||||
# password=ddns_password
|
# password=ddns_password
|
||||||
# example.com
|
# example.com
|
||||||
|
#
|
||||||
|
# N.B. the infomaniak protocol is obsolete. Please use dyndns2 instead:
|
||||||
|
#
|
||||||
|
# protocol=dyndns2,
|
||||||
|
# use=web, web=infomaniak.com/ip.php/
|
||||||
|
# login=ddns_username,
|
||||||
|
# password=ddns_password
|
||||||
|
# redirect=2
|
||||||
|
# example.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## Email Only
|
||||||
|
##
|
||||||
|
# protocol=emailonly
|
||||||
|
# host.example.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## dnsHome.de
|
||||||
|
##
|
||||||
|
# protocol=dyndns2 \
|
||||||
|
# server=www.dnshome.de \
|
||||||
|
# login=subdomain.domain.tld \
|
||||||
|
# password=your_password \
|
||||||
|
# subdomain.domain.tld
|
||||||
|
|
||||||
|
##
|
||||||
|
## INWX
|
||||||
|
##
|
||||||
|
# protocol=inwx \
|
||||||
|
# login=my-inwx-DynDNS-account-username \
|
||||||
|
# password=my-inwx-DynDNS-account-password \
|
||||||
|
# myhost.example.org
|
||||||
|
|
6602
ddclient.in
6602
ddclient.in
File diff suppressed because it is too large
Load diff
|
@ -10,7 +10,3 @@
|
||||||
## force an update twice a month (only if you are not using daemon-mode)
|
## force an update twice a month (only if you are not using daemon-mode)
|
||||||
##
|
##
|
||||||
## 30 23 1,15 * * root /usr/bin/ddclient -daemon=0 -syslog -quiet -force
|
## 30 23 1,15 * * root /usr/bin/ddclient -daemon=0 -syslog -quiet -force
|
||||||
######################################################################
|
|
||||||
## retry failed updates every hour (only if you are not using daemon-mode)
|
|
||||||
##
|
|
||||||
## 0 * * * * root /usr/bin/ddclient -daemon=0 -syslog -quiet retry
|
|
||||||
|
|
|
@ -4,9 +4,10 @@ Wants=network-online.target
|
||||||
After=network-online.target nss-lookup.target
|
After=network-online.target nss-lookup.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=forking
|
Type=exec
|
||||||
PIDFile=/run/ddclient.pid
|
Environment=daemon_interval=5m
|
||||||
ExecStart=/usr/bin/ddclient
|
ExecStart=/usr/bin/ddclient --daemon ${daemon_interval} --foreground
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
169
t/builtinfw_query.pl
Normal file
169
t/builtinfw_query.pl
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
|
||||||
|
sub setbuiltinfw {
|
||||||
|
my ($fw) = @_;
|
||||||
|
no warnings 'once';
|
||||||
|
$ddclient::builtinfw{$fw->{name}} = $fw;
|
||||||
|
%ddclient::ip_strategies = ddclient::builtinfw_strategy($fw->{name});
|
||||||
|
%ddclient::ipv4_strategies = ddclient::builtinfwv4_strategy($fw->{name});
|
||||||
|
%ddclient::ipv6_strategies = ddclient::builtinfwv6_strategy($fw->{name});
|
||||||
|
}
|
||||||
|
|
||||||
|
my @gotcalls;
|
||||||
|
|
||||||
|
my $skip_test_fw = 't/builtinfw_query.pl skip test';
|
||||||
|
setbuiltinfw({
|
||||||
|
name => $skip_test_fw,
|
||||||
|
query => sub { return '192.0.2.1 skip1 192.0.2.2 skip2 192.0.2.3'; },
|
||||||
|
queryv4 => sub { return '192.0.2.4 skip1 192.0.2.5 skip3 192.0.2.6'; },
|
||||||
|
queryv6 => sub { return '2001:db8::1 skip1 2001:db8::2 skip4 2001:db8::3'; },
|
||||||
|
});
|
||||||
|
|
||||||
|
my @skip_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'query',
|
||||||
|
getip => \&ddclient::get_ip,
|
||||||
|
useopt => 'use',
|
||||||
|
cfgxtra => {},
|
||||||
|
want => '192.0.2.2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'queryv4',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
cfgxtra => {'fwv4-skip' => 'skip3'},
|
||||||
|
want => '192.0.2.6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'queryv4 with fw-skip fallback',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
cfgxtra => {},
|
||||||
|
want => '192.0.2.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'queryv6',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
cfgxtra => {'fwv6-skip' => 'skip4'},
|
||||||
|
want => '2001:db8::3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# Support for --usev6=<builtin> wasn't added until after --fwv6-skip was added, so fallback
|
||||||
|
# to the deprecated --fw-skip option was never needed.
|
||||||
|
desc => 'queryv6 ignores fw-skip',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
cfgxtra => {},
|
||||||
|
want => '2001:db8::1',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@skip_test_cases) {
|
||||||
|
my $h = "t/builtinfw_query.pl $tc->{desc}";
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
$tc->{useopt} => $skip_test_fw,
|
||||||
|
'fw-skip' => 'skip1',
|
||||||
|
%{$tc->{cfgxtra}},
|
||||||
|
};
|
||||||
|
my $got = $tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
|
||||||
|
is($got, $tc->{want}, $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
my $default_inputs_fw = 't/builtinfw_query.pl default inputs';
|
||||||
|
setbuiltinfw({
|
||||||
|
name => $default_inputs_fw,
|
||||||
|
query => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.1'; },
|
||||||
|
queryv4 => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.2'; },
|
||||||
|
queryv6 => sub { my %p = @_; push(@gotcalls, \%p); return '2001:db8::1'; },
|
||||||
|
});
|
||||||
|
my @default_inputs_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'use with default inputs',
|
||||||
|
getip => \&ddclient::get_ip,
|
||||||
|
useopt => 'use',
|
||||||
|
want => {use => $default_inputs_fw, fw => 'server', 'fw-skip' => 'skip',
|
||||||
|
'fw-login' => 'login', 'fw-password' => 'password', 'fw-ssl-validate' => 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4 with default inputs',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
want => {usev4 => $default_inputs_fw, fwv4 => 'serverv4', fw => 'server',
|
||||||
|
'fwv4-skip' => 'skipv4', 'fw-skip' => 'skip', 'fw-login' => 'login',
|
||||||
|
'fw-password' => 'password', 'fw-ssl-validate' => 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6 with default inputs',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
want => {usev6 => $default_inputs_fw, fwv6 => 'serverv6', 'fwv6-skip' => 'skipv6'},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for my $tc (@default_inputs_test_cases) {
|
||||||
|
my $h = "t/builtinfw_query.pl $tc->{desc}";
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
$tc->{useopt} => $default_inputs_fw,
|
||||||
|
'fw' => 'server',
|
||||||
|
'fwv4' => 'serverv4',
|
||||||
|
'fwv6' => 'serverv6',
|
||||||
|
'fw-login' => 'login',
|
||||||
|
'fw-password' => 'password',
|
||||||
|
'fw-ssl-validate' => 1,
|
||||||
|
'fw-skip' => 'skip',
|
||||||
|
'fwv4-skip' => 'skipv4',
|
||||||
|
'fwv6-skip' => 'skipv6',
|
||||||
|
};
|
||||||
|
@gotcalls = ();
|
||||||
|
$tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
|
||||||
|
is_deeply(\@gotcalls, [$tc->{want}], $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
my $custom_inputs_fw = 't/builtinfw_query.pl custom inputs';
|
||||||
|
setbuiltinfw({
|
||||||
|
name => $custom_inputs_fw,
|
||||||
|
query => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.1'; },
|
||||||
|
inputs => ['if'],
|
||||||
|
queryv4 => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.2'; },
|
||||||
|
inputsv4 => ['ifv4'],
|
||||||
|
queryv6 => sub { my %p = @_; push(@gotcalls, \%p); return '2001:db8::1'; },
|
||||||
|
inputsv6 => ['ifv6'],
|
||||||
|
});
|
||||||
|
|
||||||
|
my @custom_inputs_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'use with custom inputs',
|
||||||
|
getip => \&ddclient::get_ip,
|
||||||
|
useopt => 'use',
|
||||||
|
want => {use => $custom_inputs_fw, if => 'eth0'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4 with custom inputs',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
want => {usev4 => $custom_inputs_fw, ifv4 => 'eth4'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6 with custom inputs',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
want => {usev6 => $custom_inputs_fw, ifv6 => 'eth6'},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@custom_inputs_test_cases) {
|
||||||
|
my $h = "t/builtinfw_query.pl $tc->{desc}";
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
$tc->{useopt} => $custom_inputs_fw,
|
||||||
|
'if' => 'eth0',
|
||||||
|
'ifv4' => 'eth4',
|
||||||
|
'ifv6' => 'eth6',
|
||||||
|
};
|
||||||
|
@gotcalls = ();
|
||||||
|
$tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
|
||||||
|
is_deeply(\@gotcalls, [$tc->{want}], $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
53
t/check_value.pl
Normal file
53
t/check_value.pl
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use Test::More;
|
||||||
|
use strict;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
type => ddclient::T_FQDN(),
|
||||||
|
input => 'example.com',
|
||||||
|
want => 'example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_FQDN(),
|
||||||
|
input => 'example',
|
||||||
|
want_invalid => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'https://www.example.com',
|
||||||
|
want => 'https://www.example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'https://directnic.com/dns/gateway/ad133/',
|
||||||
|
want => 'https://directnic.com/dns/gateway/ad133/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'HTTPS://MixedCase.com/',
|
||||||
|
want => 'HTTPS://MixedCase.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'ftp://bad.protocol/',
|
||||||
|
want_invalid => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'bad-url',
|
||||||
|
want_invalid => 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
my $got;
|
||||||
|
my $got_invalid = !(eval {
|
||||||
|
$got = ddclient::check_value($tc->{input},
|
||||||
|
ddclient::setv($tc->{type}, 0, 0, undef, undef));
|
||||||
|
1;
|
||||||
|
});
|
||||||
|
is($got_invalid, !!$tc->{want_invalid}, "$tc->{type}: $tc->{input}: validity");
|
||||||
|
is($got, $tc->{want}, "$tc->{type}: $tc->{input}: normalization") if !$tc->{want_invalid};
|
||||||
|
}
|
||||||
|
done_testing();
|
|
@ -1,12 +1,7 @@
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
use ddclient::t;
|
use ddclient::t;
|
||||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
|
||||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
|
||||||
|
|
||||||
# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid
|
|
||||||
# accidentally interfering with the Test Anything Protocol messages written by Test::More.)
|
|
||||||
#STDOUT->autoflush(1);
|
|
||||||
#$ddclient::globals{'debug'} = 1;
|
|
||||||
|
|
||||||
subtest "get_default_interface tests" => sub {
|
subtest "get_default_interface tests" => sub {
|
||||||
for my $sample (@ddclient::t::routing_samples) {
|
for my $sample (@ddclient::t::routing_samples) {
|
||||||
|
@ -39,23 +34,30 @@ subtest "get_ip_from_interface tests" => sub {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
subtest "Get default interface and IP for test system" => sub {
|
subtest "Get default interface and IP for test system (IPv4)" => sub {
|
||||||
my $interface = ddclient::get_default_interface(4);
|
my $interface = ddclient::get_default_interface(4);
|
||||||
if ($interface) {
|
plan(skip_all => 'no IPv4 interface') if !$interface;
|
||||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||||
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
||||||
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
||||||
is($ip1, $ip2, "Check IPv4 from default interface");
|
is($ip1, $ip2, "Check IPv4 from default interface");
|
||||||
|
SKIP: {
|
||||||
|
skip('default interface does not have an appropriate IPv4 addresses') if !$ip1;
|
||||||
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
|
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
|
||||||
}
|
}
|
||||||
$interface = ddclient::get_default_interface(6);
|
};
|
||||||
if ($interface) {
|
|
||||||
|
subtest "Get default interface and IP for test system (IPv6)" => sub {
|
||||||
|
my $interface = ddclient::get_default_interface(6);
|
||||||
|
plan(skip_all => 'no IPv6 interface') if !$interface;
|
||||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||||
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
||||||
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
||||||
is($ip1, $ip2, "Check IPv6 from default interface");
|
is($ip1, $ip2, "Check IPv6 from default interface");
|
||||||
|
SKIP: {
|
||||||
|
skip('default interface does not have an appropriate IPv6 addresses') if !$ip1;
|
||||||
ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
|
ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
57
t/geturl_connectivity.pl
Normal file
57
t/geturl_connectivity.pl
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
$ddclient::globals{'ssl_ca_file'} = $ca_file;
|
||||||
|
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
for my $ssl (0, 1) {
|
||||||
|
my $httpd = httpd($ipv, $ssl) or next;
|
||||||
|
$httpd->run(sub {
|
||||||
|
return [200, ['Content-Type' => 'application/octet-stream'], [$_[0]->as_string()]];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{ipv6_opt => 0, server_ipv => '4', client_ipv => ''},
|
||||||
|
{ipv6_opt => 0, server_ipv => '4', client_ipv => '4'},
|
||||||
|
# IPv* client to a non-SSL IPv6 server is not expected to work unless opt('ipv6') is true
|
||||||
|
{ipv6_opt => 0, server_ipv => '6', client_ipv => '6'},
|
||||||
|
|
||||||
|
# Fetch without ssl
|
||||||
|
{ server_ipv => '4', client_ipv => '' },
|
||||||
|
{ server_ipv => '4', client_ipv => '4' },
|
||||||
|
{ server_ipv => '6', client_ipv => '' },
|
||||||
|
{ server_ipv => '6', client_ipv => '6' },
|
||||||
|
|
||||||
|
# Fetch with ssl
|
||||||
|
{ ssl => 1, server_ipv => '4', client_ipv => '' },
|
||||||
|
{ ssl => 1, server_ipv => '4', client_ipv => '4' },
|
||||||
|
{ ssl => 1, server_ipv => '6', client_ipv => '' },
|
||||||
|
{ ssl => 1, server_ipv => '6', client_ipv => '6' },
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
$tc->{ipv6_opt} //= 0;
|
||||||
|
$tc->{ssl} //= 0;
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1)
|
||||||
|
if $tc->{server_ipv} eq '6' && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1)
|
||||||
|
if $tc->{server_ipv} eq '6' && !$httpd_ipv6_supported;
|
||||||
|
skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$httpd_ssl_supported;
|
||||||
|
my $uri = httpd($tc->{server_ipv}, $tc->{ssl})->endpoint();
|
||||||
|
my $name = sprintf("IPv%s client to %s%s",
|
||||||
|
$tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '');
|
||||||
|
$ddclient::globals{'ipv6'} = $tc->{ipv6_opt};
|
||||||
|
my $got = ddclient::geturl(url => $uri, ipversion => $tc->{client_ipv});
|
||||||
|
isnt($got // '', '', $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
|
@ -1,93 +0,0 @@
|
||||||
use Test::More;
|
|
||||||
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@);
|
|
||||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
|
||||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
|
||||||
my $has_http_daemon_ssl = eval { require HTTP::Daemon::SSL; };
|
|
||||||
my $ipv6_supported = eval {
|
|
||||||
require IO::Socket::IP;
|
|
||||||
my $ipv6_socket = IO::Socket::IP->new(
|
|
||||||
Domain => 'PF_INET6',
|
|
||||||
LocalHost => '::1',
|
|
||||||
Listen => 1,
|
|
||||||
);
|
|
||||||
defined($ipv6_socket);
|
|
||||||
};
|
|
||||||
|
|
||||||
my $http_daemon_supports_ipv6 = eval {
|
|
||||||
require HTTP::Daemon;
|
|
||||||
HTTP::Daemon->VERSION(6.12);
|
|
||||||
};
|
|
||||||
|
|
||||||
# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid
|
|
||||||
# accidentally interfering with the Test Anything Protocol messages written by Test::More.)
|
|
||||||
#STDOUT->autoflush(1);
|
|
||||||
#$ddclient::globals{'verbose'} = 1;
|
|
||||||
|
|
||||||
my $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
|
|
||||||
$ddclient::globals{'ssl_ca_file'} = "$certdir/dummy-ca-cert.pem";
|
|
||||||
|
|
||||||
sub run_httpd {
|
|
||||||
my ($ipv6, $ssl) = @_;
|
|
||||||
return undef if $ssl && !$has_http_daemon_ssl;
|
|
||||||
return undef if $ipv6 && (!$ipv6_supported || !$http_daemon_supports_ipv6);
|
|
||||||
my $httpd = ddclient::Test::Fake::HTTPD->new(
|
|
||||||
host => $ipv6 ? '::1' : '127.0.0.1',
|
|
||||||
scheme => $ssl ? 'https' : 'http',
|
|
||||||
daemon_args => {
|
|
||||||
SSL_cert_file => "$certdir/dummy-server-cert.pem",
|
|
||||||
SSL_key_file => "$certdir/dummy-server-key.pem",
|
|
||||||
V6Only => 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
$httpd->run(sub {
|
|
||||||
# Echo back the full request.
|
|
||||||
return [200, ['Content-Type' => 'application/octet-stream'], [$_[0]->as_string()]];
|
|
||||||
});
|
|
||||||
diag(sprintf("started IPv%s%s server running at %s",
|
|
||||||
$ipv6 ? '6' : '4', $ssl ? ' SSL' : '', $httpd->endpoint()));
|
|
||||||
return $httpd;
|
|
||||||
}
|
|
||||||
|
|
||||||
my %httpd = (
|
|
||||||
'4' => {'http' => run_httpd(0, 0), 'https' => run_httpd(0, 1)},
|
|
||||||
'6' => {'http' => run_httpd(1, 0), 'https' => run_httpd(1, 1)},
|
|
||||||
);
|
|
||||||
|
|
||||||
my @test_cases = (
|
|
||||||
{ipv6_opt => 0, server_ipv => '4', client_ipv => ''},
|
|
||||||
{ipv6_opt => 0, server_ipv => '4', client_ipv => '4'},
|
|
||||||
# IPv* client to a non-SSL IPv6 server is not expected to work unless opt('ipv6') is true
|
|
||||||
{ipv6_opt => 0, server_ipv => '6', client_ipv => '6'},
|
|
||||||
|
|
||||||
# Fetch without ssl
|
|
||||||
{ server_ipv => '4', client_ipv => '' },
|
|
||||||
{ server_ipv => '4', client_ipv => '4' },
|
|
||||||
{ server_ipv => '6', client_ipv => '' },
|
|
||||||
{ server_ipv => '6', client_ipv => '6' },
|
|
||||||
|
|
||||||
# Fetch with ssl
|
|
||||||
{ ssl => 1, server_ipv => '4', client_ipv => '' },
|
|
||||||
{ ssl => 1, server_ipv => '4', client_ipv => '4' },
|
|
||||||
{ ssl => 1, server_ipv => '6', client_ipv => '' },
|
|
||||||
{ ssl => 1, server_ipv => '6', client_ipv => '6' },
|
|
||||||
);
|
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
|
||||||
$tc->{ipv6_opt} //= 0;
|
|
||||||
$tc->{ssl} //= 0;
|
|
||||||
SKIP: {
|
|
||||||
skip("IPv6 not supported on this system", 1)
|
|
||||||
if $tc->{server_ipv} eq '6' && !$ipv6_supported;
|
|
||||||
skip("HTTP::Daemon too old for IPv6 support", 1)
|
|
||||||
if $tc->{server_ipv} eq '6' && !$http_daemon_supports_ipv6;
|
|
||||||
skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$has_http_daemon_ssl;
|
|
||||||
my $uri = $httpd{$tc->{server_ipv}}{$tc->{ssl} ? 'https' : 'http'}->endpoint();
|
|
||||||
my $name = sprintf("IPv%s client to %s%s",
|
|
||||||
$tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '');
|
|
||||||
$ddclient::globals{'ipv6'} = $tc->{ipv6_opt};
|
|
||||||
my $got = ddclient::geturl(url => $uri, ipversion => $tc->{client_ipv});
|
|
||||||
isnt($got // '', '', $name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
done_testing();
|
|
27
t/geturl_response.pl
Normal file
27
t/geturl_response.pl
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
|
# Fake curl. Use the printf utility, which can process escapes. This allows Perl to drive the fake
|
||||||
|
# curl with plain ASCII and get arbitrary bytes back, avoiding problems caused by any encoding that
|
||||||
|
# might be done by Perl (e.g., "use open ':encoding(UTF-8)';").
|
||||||
|
my @fakecurl = ('sh', '-c', 'printf %b "$1"', '--');
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'binary body',
|
||||||
|
# Body is UTF-8 encoded ✨ (U+2728 Sparkles) followed by a 0xff byte (invalid UTF-8).
|
||||||
|
printf => join('\r\n', ('HTTP/1.1 200 OK', '', '\0342\0234\0250\0377')),
|
||||||
|
# The raw bytes should come through as equally valued codepoints. They must not be decoded.
|
||||||
|
want => "HTTP/1.1 200 OK\n\n\xe2\x9c\xa8\xff",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
@ddclient::curl = (@fakecurl, $tc->{printf});
|
||||||
|
$ddclient::curl if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
my $got = ddclient::geturl(url => 'http://ignored');
|
||||||
|
is($got, $tc->{want}, $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
113
t/group_hosts_by.pl
Normal file
113
t/group_hosts_by.pl
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
eval { require Data::Dumper; } or skip($@, 1);
|
||||||
|
Data::Dumper->import();
|
||||||
|
|
||||||
|
my $h1 = 'h1';
|
||||||
|
my $h2 = 'h2';
|
||||||
|
my $h3 = 'h3';
|
||||||
|
|
||||||
|
$ddclient::config{$h1} = {
|
||||||
|
common => 'common',
|
||||||
|
h1h2 => 'h1 and h2',
|
||||||
|
unique => 'h1',
|
||||||
|
falsy => 0,
|
||||||
|
maybeunset => 'unique',
|
||||||
|
};
|
||||||
|
$ddclient::config{$h2} = {
|
||||||
|
common => 'common',
|
||||||
|
h1h2 => 'h1 and h2',
|
||||||
|
unique => 'h2',
|
||||||
|
falsy => '',
|
||||||
|
maybeunset => undef, # should not be grouped with unset
|
||||||
|
};
|
||||||
|
$ddclient::config{$h3} = {
|
||||||
|
common => 'common',
|
||||||
|
h1h2 => 'unique',
|
||||||
|
unique => 'h3',
|
||||||
|
falsy => undef,
|
||||||
|
# maybeunset is intentionally not set
|
||||||
|
};
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'empty attribute set yields single group with all hosts',
|
||||||
|
groupby => [qw()],
|
||||||
|
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'common attribute yields single group with all hosts',
|
||||||
|
groupby => [qw(common)],
|
||||||
|
want => [{cfg => {common => 'common'}, hosts => [$h1, $h2, $h3]}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'subset share a value',
|
||||||
|
groupby => [qw(h1h2)],
|
||||||
|
want => [
|
||||||
|
{cfg => {h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
|
||||||
|
{cfg => {h1h2 => 'unique'}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'all unique',
|
||||||
|
groupby => [qw(unique)],
|
||||||
|
want => [
|
||||||
|
{cfg => {unique => 'h1'}, hosts => [$h1]},
|
||||||
|
{cfg => {unique => 'h2'}, hosts => [$h2]},
|
||||||
|
{cfg => {unique => 'h3'}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'combination',
|
||||||
|
groupby => [qw(common h1h2)],
|
||||||
|
want => [
|
||||||
|
{cfg => {common => 'common', h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
|
||||||
|
{cfg => {common => 'common', h1h2 => 'unique'}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'falsy values',
|
||||||
|
groupby => [qw(falsy)],
|
||||||
|
want => [
|
||||||
|
{cfg => {falsy => 0}, hosts => [$h1]},
|
||||||
|
{cfg => {falsy => ''}, hosts => [$h2]},
|
||||||
|
# undef intentionally becomes unset because undef always means "fall back to global or
|
||||||
|
# default".
|
||||||
|
{cfg => {}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'set, unset, undef',
|
||||||
|
groupby => [qw(maybeunset)],
|
||||||
|
want => [
|
||||||
|
{cfg => {maybeunset => 'unique'}, hosts => [$h1]},
|
||||||
|
# undef intentionally becomes unset because undef always means "fall back to global or
|
||||||
|
# default".
|
||||||
|
{cfg => {}, hosts => [$h2, $h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'missing attribute',
|
||||||
|
groupby => [qw(thisdoesnotexist)],
|
||||||
|
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
my @got = ddclient::group_hosts_by([$h1, $h2, $h3], @{$tc->{groupby}});
|
||||||
|
# @got is used as a set of sets. Sort everything to make comparison easier.
|
||||||
|
$_->{hosts} = [sort(@{$_->{hosts}})] for @got;
|
||||||
|
@got = sort({
|
||||||
|
for (my $i = 0; $i < @{$a->{hosts}} && $i < @{$b->{hosts}}; ++$i) {
|
||||||
|
my $x = $a->{hosts}[$i] cmp $b->{hosts}[$i];
|
||||||
|
return $x if $x != 0;
|
||||||
|
}
|
||||||
|
return @{$a->{hosts}} <=> @{$b->{hosts}};
|
||||||
|
} @got);
|
||||||
|
is_deeply(\@got, $tc->{want}, $tc->{desc})
|
||||||
|
or diag(Data::Dumper->new([\@got, $tc->{want}],
|
||||||
|
[qw(got want)])->Sortkeys(1)->Useqq(1)->Dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
74
t/header_ok.pl
Normal file
74
t/header_ok.pl
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
my $have_mock = eval { require Test::MockModule; };
|
||||||
|
|
||||||
|
my $failmsg;
|
||||||
|
my $module;
|
||||||
|
if ($have_mock) {
|
||||||
|
$module = Test::MockModule->new('ddclient');
|
||||||
|
# Note: 'mock' is used instead of 'redefine' because 'redefine' is not available in the versions
|
||||||
|
# of Test::MockModule distributed with old Debian and Ubuntu releases.
|
||||||
|
$module->mock('failed', sub { $failmsg //= ''; $failmsg .= sprintf(shift, @_) . "\n"; });
|
||||||
|
}
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'malformed not OK',
|
||||||
|
input => 'malformed',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/unexpected/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'HTTP/1.1 200 OK',
|
||||||
|
input => 'HTTP/1.1 200 OK',
|
||||||
|
want => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'HTTP/2 200 OK',
|
||||||
|
input => 'HTTP/2 200 OK',
|
||||||
|
want => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'HTTP/3 200 OK',
|
||||||
|
input => 'HTTP/3 200 OK',
|
||||||
|
want => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => '401 not OK, fallback message',
|
||||||
|
input => 'HTTP/1.1 401 ',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/authentication failed/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => '403 not OK, fallback message',
|
||||||
|
input => 'HTTP/1.1 403 ',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/not authorized/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'other 4xx not OK',
|
||||||
|
input => 'HTTP/1.1 456 bad',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/bad/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'only first line is logged on error',
|
||||||
|
input => "HTTP/1.1 404 not found\n\nbody",
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/(?!body)/,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
subtest $tc->{desc} => sub {
|
||||||
|
$failmsg = '';
|
||||||
|
is(ddclient::header_ok($tc->{input}), $tc->{want}, 'return value matches');
|
||||||
|
SKIP: {
|
||||||
|
skip('Test::MockModule not available') if !$have_mock;
|
||||||
|
like($failmsg, $tc->{wantmsg} // qr/^$/, 'fail message matches');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
51
t/interval_expired.pl
Normal file
51
t/interval_expired.pl
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
|
my $h = 't/interval_expired.pl';
|
||||||
|
|
||||||
|
my $default_now = 1000000000;
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
now => 'inf',
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cache => '-inf',
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cache => undef, # Falsy cache value.
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
now => 0,
|
||||||
|
cache => 0, # Different kind of falsy cache value.
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
$tc->{now} //= $default_now;
|
||||||
|
# For convenience, $tc->{cache} is an offset from $tc->{now}, not an absolute time..
|
||||||
|
my $cachetime = $tc->{now} + $tc->{cache} if defined($tc->{cache});
|
||||||
|
$ddclient::config{$h} = {'interval' => $tc->{interval}};
|
||||||
|
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
$ddclient::cache{$h} = {'cached-time' => $cachetime} if defined($cachetime);
|
||||||
|
%ddclient::cache if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
$ddclient::now = $tc->{now};
|
||||||
|
$ddclient::now if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
my $desc = "now=$tc->{now}, cache=${\($cachetime // 'undef')}, interval=$tc->{interval}";
|
||||||
|
is(ddclient::interval_expired($h, 'cached-time', 'interval'), $tc->{want}, $desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
|
@ -1,8 +1,11 @@
|
||||||
# Copied from https://metacpan.org/pod/release/MASAKI/Test-Fake-HTTPD-0.08/lib/Test/Fake/HTTPD.pm
|
# Copied from https://metacpan.org/release/MASAKI/Test-Fake-HTTPD-0.09/source/lib/Test/Fake/HTTPD.pm
|
||||||
# and modified as follows:
|
# and modified as follows:
|
||||||
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/4 to add IPv6 support.
|
# * Added this comment block.
|
||||||
|
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/6 to fix server exit if TLS
|
||||||
|
# negotiation fails.
|
||||||
# * Changed package name to ddclient::Test::Fake::HTTPD.
|
# * Changed package name to ddclient::Test::Fake::HTTPD.
|
||||||
#
|
#
|
||||||
|
# Copyright: 2011-2020 NAKAGAWA Masaki <masaki@cpan.org>
|
||||||
# License: This library is free software; you can redistribute it and/or modify it under the same
|
# License: This library is free software; you can redistribute it and/or modify it under the same
|
||||||
# terms as Perl itself.
|
# terms as Perl itself.
|
||||||
|
|
||||||
|
@ -20,7 +23,7 @@ use Scalar::Util qw(blessed weaken);
|
||||||
use Carp qw(croak);
|
use Carp qw(croak);
|
||||||
use Exporter qw(import);
|
use Exporter qw(import);
|
||||||
|
|
||||||
our $VERSION = '0.08';
|
our $VERSION = '0.09';
|
||||||
$VERSION = eval $VERSION;
|
$VERSION = eval $VERSION;
|
||||||
|
|
||||||
our @EXPORT = qw(
|
our @EXPORT = qw(
|
||||||
|
@ -101,9 +104,10 @@ sub run {
|
||||||
$self->port || '<default>',
|
$self->port || '<default>',
|
||||||
$@ eq '' ? '' : ": $@")) unless $d;
|
$@ eq '' ? '' : ": $@")) unless $d;
|
||||||
|
|
||||||
$d->accept; # wait for port check from parent process
|
while (1) {
|
||||||
|
# accept can return undef if TLS handshake fails (e.g., port test or client rejects
|
||||||
while (my $c = $d->accept) {
|
# cert).
|
||||||
|
my $c = $d->accept or next;
|
||||||
while (my $req = $c->get_request) {
|
while (my $req = $c->get_request) {
|
||||||
my $res = $self->_to_http_res($app->($req));
|
my $res = $self->_to_http_res($app->($req));
|
||||||
$c->send_response($res);
|
$c->send_response($res);
|
||||||
|
@ -143,7 +147,7 @@ sub endpoint {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
my $uri = URI->new($self->scheme . ':');
|
my $uri = URI->new($self->scheme . ':');
|
||||||
my $host = $self->host;
|
my $host = $self->host;
|
||||||
$host = 'localhost' if !defined($host) || $host eq '0.0.0.0' || $host eq '::';
|
$host = 'localhost' if !defined($host) || $host eq '' || $host eq '0.0.0.0' || $host eq '::';
|
||||||
$uri->host($host);
|
$uri->host($host);
|
||||||
$uri->port($self->port);
|
$uri->port($self->port);
|
||||||
return $uri;
|
return $uri;
|
||||||
|
|
80
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem
Normal file
80
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
6c:bf:34:52:19:4d:c9:29:2b:a6:8b:41:59:aa:c6:c5:1f:a2:bb:10
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer: CN=Root Certification Authority
|
||||||
|
Validity
|
||||||
|
Not Before: Jan 8 08:24:32 2025 GMT
|
||||||
|
Not After : Jan 9 08:24:32 2125 GMT
|
||||||
|
Subject: CN=Root Certification Authority
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (2048 bit)
|
||||||
|
Modulus:
|
||||||
|
00:c3:3d:19:6b:72:0a:9e:87:c0:28:a1:ff:d0:08:
|
||||||
|
21:55:52:71:92:f2:98:36:75:fc:95:b4:0c:5e:c9:
|
||||||
|
98:b3:3c:a1:ee:cf:91:6f:07:bf:82:c9:d5:51:c0:
|
||||||
|
eb:f8:46:17:41:52:1d:c6:89:ec:63:dd:5c:30:87:
|
||||||
|
a7:b5:0d:dd:ae:bf:46:fd:de:1a:be:1d:69:83:0d:
|
||||||
|
fb:d9:5a:33:0b:8d:5f:63:76:fc:a8:b1:54:37:1e:
|
||||||
|
0b:12:44:93:90:39:1c:48:ee:f0:f2:12:fe:dc:fb:
|
||||||
|
58:a5:76:3b:e8:e8:94:44:1e:9d:03:22:5f:21:6a:
|
||||||
|
17:66:d1:4a:bf:12:d7:3c:15:76:11:76:09:ab:bf:
|
||||||
|
21:ef:0c:a5:a9:e0:08:99:63:19:26:e4:d8:5d:c2:
|
||||||
|
40:8b:98:e6:5d:df:b3:8c:63:e2:01:7c:5e:fb:55:
|
||||||
|
39:a8:67:78:80:d2:6b:61:b2:e2:2e:93:c0:9d:91:
|
||||||
|
0e:a1:79:4f:fc:38:94:ff:6f:65:18:8f:3e:0b:8c:
|
||||||
|
1f:cd:48:d7:46:5a:a2:76:d6:e0:bd:3c:aa:3d:44:
|
||||||
|
9e:50:e6:fd:e1:12:1a:ee:a1:9a:69:48:60:63:da:
|
||||||
|
41:ae:a7:3d:36:1b:95:fb:b7:f1:0d:60:cd:2f:e3:
|
||||||
|
b1:1f:b1:db:b4:98:a6:62:87:de:54:80:d1:45:43:
|
||||||
|
5b:25
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
E1:7C:D3:C3:9E:C7:F5:2C:DA:7C:D7:85:78:91:BA:26:88:61:F9:D4
|
||||||
|
X509v3 Authority Key Identifier:
|
||||||
|
E1:7C:D3:C3:9E:C7:F5:2C:DA:7C:D7:85:78:91:BA:26:88:61:F9:D4
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Certificate Sign, CRL Sign
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Signature Value:
|
||||||
|
9d:dc:49:c6:14:13:19:38:d9:14:b5:70:f0:3b:01:8e:d7:32:
|
||||||
|
a7:69:f0:21:68:ec:ad:8c:ee:53:7d:16:64:7d:3e:c2:d2:ac:
|
||||||
|
5a:54:17:55:84:43:1e:46:1d:42:01:fb:89:e0:db:ec:e8:f0:
|
||||||
|
3c:22:82:54:1d:38:12:21:45:3c:37:44:3b:2e:c9:4d:ed:8d:
|
||||||
|
6e:46:f5:a5:cc:ba:39:61:ab:df:cf:1f:d2:c9:40:e2:db:3f:
|
||||||
|
05:ea:83:14:93:5f:0e:3d:33:be:98:04:80:87:25:3a:6c:ff:
|
||||||
|
8e:87:6a:32:ed:1e:ec:54:90:9b:2a:6e:12:05:6a:9d:15:48:
|
||||||
|
3c:ea:c6:9e:ab:71:58:1e:34:95:3f:9b:9e:e3:e5:4b:fb:9e:
|
||||||
|
32:f2:d6:59:bf:8d:09:d6:e4:9e:9e:47:b9:d6:78:5f:f3:0c:
|
||||||
|
98:ab:56:f0:18:5d:63:8e:83:ee:c1:f2:84:da:0e:64:af:1c:
|
||||||
|
18:ff:b3:f9:15:0b:02:50:77:d1:0b:6e:ba:61:bc:9e:c3:37:
|
||||||
|
63:91:26:e8:ce:77:9a:47:8f:ef:38:8f:9c:7f:f1:ab:7b:65:
|
||||||
|
a5:96:b6:92:2e:c7:d3:c3:7a:54:0d:d6:76:f5:d6:88:13:3b:
|
||||||
|
17:e2:02:4e:3b:4d:10:95:0a:bb:47:e9:48:25:76:1d:7b:19:
|
||||||
|
5c:6f:b8:a1
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDQTCCAimgAwIBAgIUbL80UhlNySkrpotBWarGxR+iuxAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwJzElMCMGA1UEAwwcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAgFw0y
|
||||||
|
NTAxMDgwODI0MzJaGA8yMTI1MDEwOTA4MjQzMlowJzElMCMGA1UEAwwcUm9vdCBD
|
||||||
|
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBAMM9GWtyCp6HwCih/9AIIVVScZLymDZ1/JW0DF7JmLM8oe7PkW8Hv4LJ
|
||||||
|
1VHA6/hGF0FSHcaJ7GPdXDCHp7UN3a6/Rv3eGr4daYMN+9laMwuNX2N2/KixVDce
|
||||||
|
CxJEk5A5HEju8PIS/tz7WKV2O+jolEQenQMiXyFqF2bRSr8S1zwVdhF2Cau/Ie8M
|
||||||
|
pangCJljGSbk2F3CQIuY5l3fs4xj4gF8XvtVOahneIDSa2Gy4i6TwJ2RDqF5T/w4
|
||||||
|
lP9vZRiPPguMH81I10ZaonbW4L08qj1EnlDm/eESGu6hmmlIYGPaQa6nPTYblfu3
|
||||||
|
8Q1gzS/jsR+x27SYpmKH3lSA0UVDWyUCAwEAAaNjMGEwHQYDVR0OBBYEFOF808Oe
|
||||||
|
x/Us2nzXhXiRuiaIYfnUMB8GA1UdIwQYMBaAFOF808Oex/Us2nzXhXiRuiaIYfnU
|
||||||
|
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUA
|
||||||
|
A4IBAQCd3EnGFBMZONkUtXDwOwGO1zKnafAhaOytjO5TfRZkfT7C0qxaVBdVhEMe
|
||||||
|
Rh1CAfuJ4Nvs6PA8IoJUHTgSIUU8N0Q7LslN7Y1uRvWlzLo5Yavfzx/SyUDi2z8F
|
||||||
|
6oMUk18OPTO+mASAhyU6bP+Oh2oy7R7sVJCbKm4SBWqdFUg86saeq3FYHjSVP5ue
|
||||||
|
4+VL+54y8tZZv40J1uSenke51nhf8wyYq1bwGF1jjoPuwfKE2g5krxwY/7P5FQsC
|
||||||
|
UHfRC266YbyewzdjkSbozneaR4/vOI+cf/Gre2WllraSLsfTw3pUDdZ29daIEzsX
|
||||||
|
4gJOO00QlQq7R+lIJXYdexlcb7ih
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -560,3 +560,5 @@ EOF
|
||||||
want_ipv6_if => "en0",
|
want_ipv6_if => "en0",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
1;
|
||||||
|
|
161
t/lib/ddclient/t/HTTPD.pm
Normal file
161
t/lib/ddclient/t/HTTPD.pm
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package ddclient::t::HTTPD;
|
||||||
|
|
||||||
|
use v5.10.1;
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use parent qw(ddclient::Test::Fake::HTTPD);
|
||||||
|
|
||||||
|
use Exporter qw(import);
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { require 'ddclient'; }
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
our @EXPORT = qw(
|
||||||
|
httpd
|
||||||
|
httpd_ok httpd_required $httpd_supported $httpd_support_error
|
||||||
|
httpd_ipv6_ok httpd_ipv6_required $httpd_ipv6_supported $httpd_ipv6_support_error
|
||||||
|
httpd_ssl_ok httpd_ssl_required $httpd_ssl_supported $httpd_ssl_support_error
|
||||||
|
$ca_file $certdir $other_ca_file
|
||||||
|
$textplain
|
||||||
|
);
|
||||||
|
|
||||||
|
our $httpd_supported;
|
||||||
|
our $httpd_support_error;
|
||||||
|
BEGIN {
|
||||||
|
$httpd_supported = eval {
|
||||||
|
require parent; parent->import(qw(ddclient::Test::Fake::HTTPD));
|
||||||
|
require JSON::PP; JSON::PP->import();
|
||||||
|
1;
|
||||||
|
} or $httpd_support_error = $@;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_ok {
|
||||||
|
ok($httpd_supported, "HTTPD is supported") or diag($httpd_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_required {
|
||||||
|
plan(skip_all => $httpd_support_error) if !$httpd_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $httpd_ssl_supported = $httpd_supported;
|
||||||
|
our $httpd_ssl_support_error = $httpd_support_error;
|
||||||
|
$httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; }
|
||||||
|
or $httpd_ssl_support_error = $@
|
||||||
|
if $httpd_ssl_supported;
|
||||||
|
|
||||||
|
sub httpd_ssl_ok {
|
||||||
|
ok($httpd_ssl_supported, "SSL is supported") or diag($httpd_ssl_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_ssl_required {
|
||||||
|
plan(skip_all => $httpd_ssl_support_error) if !$httpd_ssl_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $httpd_ipv6_supported = $httpd_supported;
|
||||||
|
our $httpd_ipv6_support_error = $httpd_support_error;
|
||||||
|
$httpd_ipv6_supported = $ipv6_supported
|
||||||
|
or $httpd_ipv6_support_error = $ipv6_support_error
|
||||||
|
if $httpd_ipv6_supported;
|
||||||
|
$httpd_ipv6_supported = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); }
|
||||||
|
or $httpd_ipv6_support_error = $@
|
||||||
|
if $httpd_ipv6_supported;
|
||||||
|
|
||||||
|
sub httpd_ipv6_ok {
|
||||||
|
ok($httpd_ipv6_supported, "test HTTP server supports IPv6") or diag($httpd_ipv6_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_ipv6_required {
|
||||||
|
plan(skip_all => $httpd_ipv6_support_error) if !$httpd_ipv6_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $textplain = ['content-type' => 'text/plain; charset=utf-8'];
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my $class = shift;
|
||||||
|
my $self = $class->SUPER::new(@_);
|
||||||
|
$self->{_requests} = []; # Log of received requests.
|
||||||
|
$self->{_responses} = []; # Script of responses to play back.
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub run {
|
||||||
|
my ($self, $app) = @_;
|
||||||
|
$self->SUPER::run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
push(@{$self->{_requests}}, $req);
|
||||||
|
my $res = $app->($req) if defined($app);
|
||||||
|
return $res if defined($res);
|
||||||
|
if ($req->uri()->path() eq '/control') {
|
||||||
|
pop(@{$self->{_requests}});
|
||||||
|
if ($req->method() eq 'PUT') {
|
||||||
|
return [400, $textplain, ['content must be json']]
|
||||||
|
if $req->headers()->content_type() ne 'application/json';
|
||||||
|
eval { @{$self->{_responses}} = @{decode_json($req->content())}; 1; }
|
||||||
|
or return [400, $textplain, ['content is not valid json']];
|
||||||
|
@{$self->{_requests}} = ();
|
||||||
|
return [200, $textplain, ["successfully reset request log and response script"]];
|
||||||
|
} elsif ($req->method() eq 'GET') {
|
||||||
|
my @reqs = map($_->as_string(), @{$self->{_requests}});
|
||||||
|
return [200, ['content-type' => 'application/json'], [encode_json(\@reqs)]];
|
||||||
|
} else {
|
||||||
|
return [405, $textplain, ['unsupported method: ' . $req->method()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shift(@{$self->{_responses}}) // [500, $textplain, ["no more scripted responses"]];
|
||||||
|
});
|
||||||
|
diag("started server running at " . $self->endpoint());
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub reset {
|
||||||
|
my $self = shift;
|
||||||
|
my $ep = $self->endpoint();
|
||||||
|
my $got = ddclient::geturl(url => "$ep/control");
|
||||||
|
diag("http response:\n$got");
|
||||||
|
ddclient::header_ok($got)
|
||||||
|
or BAIL_OUT("failed to get log of requests from test http server at $ep");
|
||||||
|
$got =~ s/^.*?\n\n//s;
|
||||||
|
my @got = map(HTTP::Request->parse($_), @{decode_json($got)});
|
||||||
|
ddclient::header_ok(ddclient::geturl(
|
||||||
|
url => "$ep/control",
|
||||||
|
method => 'PUT',
|
||||||
|
headers => ['content-type: application/json'],
|
||||||
|
data => encode_json(\@_),
|
||||||
|
)) or BAIL_OUT("failed to reset the test http server at $ep");
|
||||||
|
return @got;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
|
||||||
|
our $ca_file = "$certdir/dummy-ca-cert.pem";
|
||||||
|
our $other_ca_file = "$certdir/other-ca-cert.pem";
|
||||||
|
|
||||||
|
my %daemons;
|
||||||
|
|
||||||
|
sub httpd {
|
||||||
|
my ($ipv, $ssl) = @_;
|
||||||
|
$ipv //= '';
|
||||||
|
$ssl = !!$ssl;
|
||||||
|
return undef if !$httpd_supported;
|
||||||
|
return undef if $ipv eq '6' && !$httpd_ipv6_supported;
|
||||||
|
return undef if $ssl && !$httpd_ssl_supported;
|
||||||
|
if (!defined($daemons{$ipv}{$ssl})) {
|
||||||
|
my $host
|
||||||
|
= $ipv eq '4' ? '127.0.0.1'
|
||||||
|
: $ipv eq '6' ? '::1'
|
||||||
|
: $httpd_ipv6_supported ? '::1'
|
||||||
|
: '127.0.0.1';
|
||||||
|
$daemons{$ipv}{$ssl} = __PACKAGE__->new(
|
||||||
|
host => $host,
|
||||||
|
scheme => $ssl ? 'https' : 'http',
|
||||||
|
daemon_args => {
|
||||||
|
(V6Only => $ipv eq '6' ? 1 : 0) x ($host eq '::1'),
|
||||||
|
(SSL_cert_file => "$certdir/dummy-server-cert.pem",
|
||||||
|
SSL_key_file => "$certdir/dummy-server-key.pem") x $ssl,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $daemons{$ipv}{$ssl};
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
39
t/lib/ddclient/t/Logger.pm
Normal file
39
t/lib/ddclient/t/Logger.pm
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package ddclient::t::Logger;
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use parent qw(-norequire ddclient::Logger);
|
||||||
|
|
||||||
|
{
|
||||||
|
package ddclient::t::LoggerAbort;
|
||||||
|
use overload '""' => qw(stringify);
|
||||||
|
sub new {
|
||||||
|
my ($class, %args) = @_;
|
||||||
|
return bless(\%args, $class);
|
||||||
|
}
|
||||||
|
sub stringify {
|
||||||
|
return 'logged a FATAL message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my ($class, $parent, $labelre) = @_;
|
||||||
|
my $self = $class->SUPER::new(undef, $parent);
|
||||||
|
$self->{logs} = [];
|
||||||
|
$self->{_labelre} = $labelre;
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _log {
|
||||||
|
my ($self, $args) = @_;
|
||||||
|
my $lre = $self->{_labelre};
|
||||||
|
my $lbl = $args->{label};
|
||||||
|
push(@{$self->{logs}}, $args) if !defined($lre) || (defined($lbl) && $lbl =~ $lre);
|
||||||
|
return $self->SUPER::_log($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _abort {
|
||||||
|
my ($self) = @_;
|
||||||
|
push(@{$self->{logs}}, 'aborted');
|
||||||
|
die(ddclient::t::LoggerAbort->new());
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
30
t/lib/ddclient/t/ip.pm
Normal file
30
t/lib/ddclient/t/ip.pm
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package ddclient::t::ip;
|
||||||
|
|
||||||
|
use v5.10.1;
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use Exporter qw(import);
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
our @EXPORT = qw(ipv6_ok ipv6_required $ipv6_supported $ipv6_support_error);
|
||||||
|
|
||||||
|
our $ipv6_support_error;
|
||||||
|
our $ipv6_supported = eval {
|
||||||
|
require IO::Socket::IP;
|
||||||
|
my $ipv6_socket = IO::Socket::IP->new(
|
||||||
|
Domain => 'PF_INET6',
|
||||||
|
LocalHost => '::1',
|
||||||
|
Listen => 1,
|
||||||
|
);
|
||||||
|
defined($ipv6_socket);
|
||||||
|
} or $ipv6_support_error = $@;
|
||||||
|
|
||||||
|
sub ipv6_ok {
|
||||||
|
ok($ipv6_supported, "system supports IPv6") or diag($ipv6_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub ipv6_required {
|
||||||
|
plan(skip_all => $ipv6_support_error) if !$ipv6_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
167
t/logmsg.pl
Normal file
167
t/logmsg.pl
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'adds a newline',
|
||||||
|
args => ['xyz'],
|
||||||
|
want => "xyz\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'removes one trailing newline (before adding a newline)',
|
||||||
|
args => ["xyz \n\t\n\n"],
|
||||||
|
want => "xyz \n\t\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'accepts msg keyword parameter',
|
||||||
|
args => [msg => 'xyz'],
|
||||||
|
want => "xyz\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'msg keyword parameter trumps message parameter',
|
||||||
|
args => [msg => 'kw', 'pos'],
|
||||||
|
want => "kw\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'msg keyword parameter trumps message parameter',
|
||||||
|
args => [msg => 'kw', 'pos'],
|
||||||
|
want => "kw\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'email appends to email body',
|
||||||
|
args => [email => 1, 'foo'],
|
||||||
|
init_email => "preexisting message\n",
|
||||||
|
want_email => "preexisting message\nfoo\n",
|
||||||
|
want => "foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single-line label',
|
||||||
|
args => [label => 'LBL', 'foo'],
|
||||||
|
want => "LBL: > foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multi-line label',
|
||||||
|
args => [label => 'LBL', "foo\nbar"],
|
||||||
|
want => ("LBL: > foo\n" .
|
||||||
|
"LBL: bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single-line long label',
|
||||||
|
args => [label => 'VERY LONG LABEL', 'foo'],
|
||||||
|
want => "VERY LONG LABEL: > foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multi-line long label',
|
||||||
|
args => [label => 'VERY LONG LABEL', "foo\nbar"],
|
||||||
|
want => ("VERY LONG LABEL: > foo\n" .
|
||||||
|
"VERY LONG LABEL: bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single line, no label, single context',
|
||||||
|
args => ['foo'],
|
||||||
|
ctxs => ['only context'],
|
||||||
|
want => "[only context]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single line, no label, two contexts',
|
||||||
|
args => ['foo'],
|
||||||
|
ctxs => ['context one', 'context two'],
|
||||||
|
want => "[context one][context two]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single line, label, two contexts',
|
||||||
|
args => [label => 'LBL', 'foo'],
|
||||||
|
ctxs => ['context one', 'context two'],
|
||||||
|
want => "LBL: [context one][context two]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple lines, label, two contexts',
|
||||||
|
args => [label => 'LBL', "foo\nbar"],
|
||||||
|
ctxs => ['context one', 'context two'],
|
||||||
|
want => ("LBL: [context one][context two]> foo\n" .
|
||||||
|
"LBL: [context one][context two] bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'string ctx arg',
|
||||||
|
args => [label => 'LBL', ctx => 'three', "foo\nbar"],
|
||||||
|
ctxs => ['one', 'two'],
|
||||||
|
want => ("LBL: [one][two][three]> foo\n" .
|
||||||
|
"LBL: [one][two][three] bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'arrayref ctx arg',
|
||||||
|
args => [label => 'LBL', ctx => ['three', 'four'], "foo\nbar"],
|
||||||
|
ctxs => ['one', 'two'],
|
||||||
|
want => ("LBL: [one][two][three][four]> foo\n" .
|
||||||
|
"LBL: [one][two][three][four] bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'undef ctx',
|
||||||
|
args => [label => 'LBL', "foo"],
|
||||||
|
ctxs => ['one', undef],
|
||||||
|
want => "LBL: [one]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'arrayref ctx',
|
||||||
|
args => [label => 'LBL', "foo"],
|
||||||
|
ctxs => ['one', ['two', 'three']],
|
||||||
|
want => "LBL: [one][two][three]> foo\n",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
subtest $tc->{desc} => sub {
|
||||||
|
$tc->{wantemail} //= '';
|
||||||
|
my $output;
|
||||||
|
open(my $fh, '>', \$output);
|
||||||
|
local $ddclient::emailbody = $tc->{init_email} // '';
|
||||||
|
local $ddclient::_l = $ddclient::_l;
|
||||||
|
$ddclient::_l = ddclient::pushlogctx($_) for @{$tc->{ctxs} // []};
|
||||||
|
{
|
||||||
|
local *STDERR = $fh;
|
||||||
|
ddclient::logmsg(@{$tc->{args}});
|
||||||
|
}
|
||||||
|
close($fh);
|
||||||
|
is($output, $tc->{want}, 'output text matches');
|
||||||
|
is($ddclient::emailbody, $tc->{want_email} // '', 'email content matches');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my @logfmt_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'single argument is printed directly, not via sprintf',
|
||||||
|
args => ['%%'],
|
||||||
|
want => "DEBUG: > %%\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple arguments are formatted via sprintf',
|
||||||
|
args => ['%s', 'foo'],
|
||||||
|
want => "DEBUG: > foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single argument with context',
|
||||||
|
args => [ctx => 'context', '%%'],
|
||||||
|
want => "DEBUG: [context]> %%\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple arguments with context',
|
||||||
|
args => [ctx => 'context', '%s', 'foo'],
|
||||||
|
want => "DEBUG: [context]> foo\n",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@logfmt_test_cases) {
|
||||||
|
my $got;
|
||||||
|
open(my $fh, '>', \$got);
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
%ddclient::globals if 0;
|
||||||
|
{
|
||||||
|
local *STDERR = $fh;
|
||||||
|
ddclient::debug(@{$tc->{args}});
|
||||||
|
}
|
||||||
|
close($fh);
|
||||||
|
is($got, $tc->{want}, $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
|
@ -44,8 +44,20 @@ my @test_cases = (
|
||||||
tc('unquoted escaped backslash', "a=\\\\", { a => "\\" }, ""),
|
tc('unquoted escaped backslash', "a=\\\\", { a => "\\" }, ""),
|
||||||
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
|
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
|
||||||
tc('dquoted escaped dquote', "a=\"\\\"\"", { a => '"' }, ""),
|
tc('dquoted escaped dquote', "a=\"\\\"\"", { a => '"' }, ""),
|
||||||
|
tc('env: empty', "a_env=", {}, ""),
|
||||||
|
tc('env: unset', "a_env=UNSET", {}, ""),
|
||||||
|
tc('env: set', "a_env=TEST", { a => 'val' }, ""),
|
||||||
|
tc('env: single quoted', "a_env='TEST'", { a => 'val' }, ""),
|
||||||
|
tc('newline: quoted value', "a='1\n2'", { a => "1\n2" }, ""),
|
||||||
|
tc('newline: escaped value', "a=1\\\n2", { a => "1\n2" }, ""),
|
||||||
|
tc('newline: between vars', "a=1 \n b=2", { a => '1' }, "\n b=2"),
|
||||||
|
tc('newline: terminating', "a=1 \n", { a => '1' }, "\n"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
delete($ENV{''});
|
||||||
|
delete($ENV{UNSET});
|
||||||
|
$ENV{TEST} = 'val';
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
for my $tc (@test_cases) {
|
||||||
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
|
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
|
||||||
subtest $tc->{name} => sub {
|
subtest $tc->{name} => sub {
|
||||||
|
|
169
t/protocol_directnic.pl
Normal file
169
t/protocol_directnic.pl
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::Logger;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
ddclient::load_json_support('directnic');
|
||||||
|
|
||||||
|
httpd()->run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Test server received request:\n" . $req->as_string());
|
||||||
|
my $headers = ['content-type' => 'text/plain; charset=utf-8'];
|
||||||
|
if ($req->uri->as_string =~ m/\/dns\/gateway\/(abc|def)\/\?data=([^&]*)/) {
|
||||||
|
return [200, ['Content-Type' => 'application/json'], [encode_json({
|
||||||
|
result => 'success',
|
||||||
|
message => "Your record was updated to $2",
|
||||||
|
})]];
|
||||||
|
} elsif ($req->uri->as_string =~ m/\/dns\/gateway\/bad_token\/\?data=([^&]*)/) {
|
||||||
|
return [200, ['Content-Type' => 'application/json'], [encode_json({
|
||||||
|
result => 'error',
|
||||||
|
message => "There was an error updating your record.",
|
||||||
|
})]];
|
||||||
|
} elsif ($req->uri->as_string =~ m/\/bad\/path\/\?data=([^&]*)/) {
|
||||||
|
return [200, ['Content-Type' => 'application/json'], ['unexpected response body']];
|
||||||
|
}
|
||||||
|
return [400, $headers, ['unexpected request: ' . $req->uri()]]
|
||||||
|
});
|
||||||
|
|
||||||
|
my $hostname = httpd()->endpoint();
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'IPv4, good',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/dns/gateway/abc/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, failed',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/dns/gateway/bad_token/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'failed'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/There was an error updating your record/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, bad',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/bad/path/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'bad'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/response is not a JSON object:\nunexpected response body/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, unexpected response',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/unexpected/path/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/400 Bad Request/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, no urlv4',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/missing urlv4 option/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv6, good',
|
||||||
|
cfg => {h1 => {urlv6 => "$hostname/dns/gateway/abc/", wantipv6 => '2001:db8::1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4 and IPv6, good',
|
||||||
|
cfg => {h1 => {
|
||||||
|
urlv4 => "$hostname/dns/gateway/abc/",
|
||||||
|
urlv6 => "$hostname/dns/gateway/def/",
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1',
|
||||||
|
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4 and IPv6, mixed success',
|
||||||
|
cfg => {h1 => {
|
||||||
|
urlv4 => "$hostname/dns/gateway/bad_token/",
|
||||||
|
urlv6 => "$hostname/dns/gateway/def/",
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
}},
|
||||||
|
wantips => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'failed',
|
||||||
|
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/There was an error updating your record/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Starting test: $tc->{desc}");
|
||||||
|
diag('==============================================================================');
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
|
||||||
|
local %ddclient::config = %{$tc->{cfg}};
|
||||||
|
local %ddclient::recap;
|
||||||
|
{
|
||||||
|
local $ddclient::_l = $l;
|
||||||
|
ddclient::nic_directnic_update(undef, sort(keys(%{$tc->{cfg}})));
|
||||||
|
}
|
||||||
|
is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
$tc->{wantlogs} //= [];
|
||||||
|
subtest("$tc->{desc}: logs" => sub {
|
||||||
|
my @got = @{$l->{logs}};
|
||||||
|
my @want = @{$tc->{wantlogs}};
|
||||||
|
for my $i (0..$#want) {
|
||||||
|
last if $i >= @got;
|
||||||
|
my $got = $got[$i];
|
||||||
|
my $want = $want[$i];
|
||||||
|
subtest("log $i" => sub {
|
||||||
|
is($got->{label}, $want->{label}, "label matches");
|
||||||
|
is_deeply($got->{ctx}, $want->{ctx}, "context matches");
|
||||||
|
like($got->{msg}, $want->{msg}, "message matches");
|
||||||
|
}) or diag(ddclient::repr(Values => [$got, $want], Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my @unexpected = @got[@want..$#got];
|
||||||
|
ok(@unexpected == 0, "no unexpected logs")
|
||||||
|
or diag(ddclient::repr(\@unexpected, Names => ['*unexpected']));
|
||||||
|
my @missing = @want[@got..$#want];
|
||||||
|
ok(@missing == 0, "no missing logs")
|
||||||
|
or diag(ddclient::repr(\@missing, Names => ['*missing']));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
242
t/protocol_dnsexit2.pl
Normal file
242
t/protocol_dnsexit2.pl
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::Logger;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
|
||||||
|
ddclient::load_json_support('dnsexit2');
|
||||||
|
|
||||||
|
httpd()->run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
return undef if $req->uri()->path() eq '/control';
|
||||||
|
return [200, ['Content-Type' => 'application/json'], [encode_json({
|
||||||
|
code => 0,
|
||||||
|
message => 'Success'
|
||||||
|
})]];
|
||||||
|
});
|
||||||
|
|
||||||
|
sub cmp_update {
|
||||||
|
my ($a, $b) = @_;
|
||||||
|
return $a->{name} cmp $b->{name} || $a->{type} cmp $b->{type};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub sort_updates {
|
||||||
|
my ($req) = @_;
|
||||||
|
return {
|
||||||
|
%$req,
|
||||||
|
update => [sort({ cmp_update($a, $b); } @{$req->{update}})],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub sort_reqs {
|
||||||
|
my @reqs = map(sort_updates($_), @_);
|
||||||
|
my @sorted = sort({
|
||||||
|
my $ret = $a->{domain} cmp $b->{domain};
|
||||||
|
$ret = @{$a->{update}} <=> @{$b->{update}} if !$ret;
|
||||||
|
my $i = 0;
|
||||||
|
while (!$ret && $i < @{$a->{update}} && $i < @{$b->{update}}) {
|
||||||
|
$ret = cmp_update($a->{update}[$i], $b->{update}[$i]);
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
} @reqs);
|
||||||
|
return @sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'both IPv4 and IPv6 are updated together',
|
||||||
|
cfg => {
|
||||||
|
'host.my.example.com' => {
|
||||||
|
ttl => 5,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
zone => 'my.example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'my.example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => 'host',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content => '2001:db8::1',
|
||||||
|
name => 'host',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'AAAA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'zone defaults to host',
|
||||||
|
cfg => {
|
||||||
|
'host.my.example.com' => {
|
||||||
|
ttl => 10,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'host.my.example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => '',
|
||||||
|
ttl => 10,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'two hosts, different zones',
|
||||||
|
cfg => {
|
||||||
|
'host1.example.com' => {
|
||||||
|
ttl => 5,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
# 'zone' intentionally not set, so it will default to 'host1.example.com'.
|
||||||
|
},
|
||||||
|
'host2.example.com' => {
|
||||||
|
ttl => 10,
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [
|
||||||
|
{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'host1.example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => '',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '2001:db8::1',
|
||||||
|
name => 'host2',
|
||||||
|
ttl => 10,
|
||||||
|
type => 'AAAA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'two hosts, same zone',
|
||||||
|
cfg => {
|
||||||
|
'host1.example.com' => {
|
||||||
|
ttl => 5,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
'host2.example.com' => {
|
||||||
|
ttl => 10,
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [
|
||||||
|
{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => 'host1',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content => '2001:db8::1',
|
||||||
|
name => 'host2',
|
||||||
|
ttl => 10,
|
||||||
|
type => 'AAAA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'host outside of zone',
|
||||||
|
cfg => {
|
||||||
|
'host.example' => {
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want_fatal => qr{hostname does not end with the zone: example.com},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
subtest($tc->{desc} => sub {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
local %ddclient::config = ();
|
||||||
|
my @hosts = keys(%{$tc->{cfg}});
|
||||||
|
for my $h (@hosts) {
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
password => 'key',
|
||||||
|
path => '/update',
|
||||||
|
server => httpd()->endpoint(),
|
||||||
|
%{$tc->{cfg}{$h}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^FATAL$/);
|
||||||
|
my $err = do {
|
||||||
|
local $ddclient::_l = $l;
|
||||||
|
local $@;
|
||||||
|
(eval { ddclient::nic_dnsexit2_update(undef, @hosts); 1; })
|
||||||
|
? undef : ($@ // 'unknown error');
|
||||||
|
};
|
||||||
|
my @requests = httpd()->reset();
|
||||||
|
my @got;
|
||||||
|
for (my $i = 0; $i < @requests; $i++) {
|
||||||
|
subtest("request $i" => sub {
|
||||||
|
my $req = $requests[$i];
|
||||||
|
is($req->method(), 'POST', 'method is POST');
|
||||||
|
is($req->uri()->as_string(), '/update', 'path is /update');
|
||||||
|
is($req->header('content-type'), 'application/json', 'Content-Type is JSON');
|
||||||
|
is($req->header('accept'), 'application/json', 'Accept is JSON');
|
||||||
|
my $got = decode_json($req->content());
|
||||||
|
is(ref($got), 'HASH', 'request content is a JSON object');
|
||||||
|
is(ref($got->{update}), 'ARRAY', 'JSON object has array "update" property');
|
||||||
|
push(@got, $got);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@got = sort_reqs(@got);
|
||||||
|
my @want = sort_reqs(@{$tc->{want} // []});
|
||||||
|
is_deeply(\@got, \@want, 'request objects match');
|
||||||
|
subtest('expected (or lack of) error' => sub {
|
||||||
|
if (is(defined($err), defined($tc->{want_fatal}), 'error existence') && defined($err)) {
|
||||||
|
my @got = @{$l->{logs}};
|
||||||
|
if (is(scalar(@got), 2, 'logged two events')) {
|
||||||
|
is($got[0]->{label}, 'FATAL', 'first logged event is a FATAL message');
|
||||||
|
like($got[0]->{msg}, $tc->{want_fatal}, 'first logged event message matches');
|
||||||
|
is($got[1], 'aborted', 'second logged event is an "aborted" event');
|
||||||
|
isa_ok($err, qw(ddclient::t::LoggerAbort));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
279
t/protocol_dyndns2.pl
Normal file
279
t/protocol_dyndns2.pl
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
use MIME::Base64;
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::Logger;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
httpd()->run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Test server received request:\n" . $req->as_string());
|
||||||
|
return undef if $req->uri()->path() eq '/control';
|
||||||
|
my $wantauthn = 'Basic ' . encode_base64('username:password', '');
|
||||||
|
return [401, [@$textplain, 'www-authenticate' => 'Basic realm="realm", charset="UTF-8"'],
|
||||||
|
['authentication required']] if ($req->header('authorization') // '') ne $wantauthn;
|
||||||
|
return [400, $textplain, ['invalid method: ' . $req->method()]] if $req->method() ne 'GET';
|
||||||
|
return undef;
|
||||||
|
});
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, good',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, nochg',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['nochg'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'WARNING', ctx => ['h1'], msg => qr/nochg/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, bad',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['nohost'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'nohost'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/nohost/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, unexpected',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['WAT'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'WAT'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/unexpected.*WAT/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, multiple hosts, multiple good',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good 192.0.2.1',
|
||||||
|
'good',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, multiple hosts, mixed success',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h3 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good',
|
||||||
|
'nochg',
|
||||||
|
'dnserr',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h3 => {'status-ipv4' => 'dnserr'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'WARNING', ctx => ['h2'], msg => qr/nochg/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
{label => 'FAILED', ctx => ['h3'], msg => qr/dnserr/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv6, single host, good',
|
||||||
|
cfg => {h1 => {wantipv6 => '2001:db8::1'}},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1&myip=2001:db8::1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4 and IPv6, single host, good',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1,2001:db8::1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1',
|
||||||
|
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'excess status line',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good',
|
||||||
|
'good',
|
||||||
|
'WAT',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
{label => 'WARNING', ctx => ['h1,h2'], msg => qr/unexpected.*\nWAT$/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple hosts, single failure',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => ['abuse'],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'abuse'},
|
||||||
|
h2 => {'status-ipv4' => 'abuse'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/abuse/},
|
||||||
|
{label => 'FAILED', ctx => ['h2'], msg => qr/abuse/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple hosts, single success',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'WARNING', ctx => ['h1,h2'], msg => qr//},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple hosts, fewer results',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h3 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good',
|
||||||
|
'nochg',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h3 => {'status-ipv4' => 'unknown'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'WARNING', ctx => ['h2'], msg => qr/nochg/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
{label => 'FAILED', ctx => ['h3'], msg => qr/assuming failure/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Starting test: $tc->{desc}");
|
||||||
|
diag('==============================================================================');
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
|
||||||
|
local %ddclient::config;
|
||||||
|
local %ddclient::recap;
|
||||||
|
$ddclient::config{$_} = {
|
||||||
|
login => 'username',
|
||||||
|
password => 'password',
|
||||||
|
server => httpd()->endpoint(),
|
||||||
|
script => '/nic/update',
|
||||||
|
%{$tc->{cfg}{$_}},
|
||||||
|
} for keys(%{$tc->{cfg}});
|
||||||
|
httpd()->reset([200, $textplain, [map("$_\n", @{$tc->{resp}})]]);
|
||||||
|
{
|
||||||
|
local $ddclient::_l = $l;
|
||||||
|
ddclient::nic_dyndns2_update(undef, sort(keys(%{$tc->{cfg}})));
|
||||||
|
}
|
||||||
|
my @requests = httpd()->reset();
|
||||||
|
is(scalar(@requests), 1, "$tc->{desc}: single update request");
|
||||||
|
my $req = shift(@requests);
|
||||||
|
is($req->uri()->path(), '/nic/update', "$tc->{desc}: request path");
|
||||||
|
is($req->uri()->query(), $tc->{wantquery}, "$tc->{desc}: request query");
|
||||||
|
is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
$tc->{wantlogs} //= [];
|
||||||
|
subtest("$tc->{desc}: logs" => sub {
|
||||||
|
my @got = @{$l->{logs}};
|
||||||
|
my @want = @{$tc->{wantlogs}};
|
||||||
|
for my $i (0..$#want) {
|
||||||
|
last if $i >= @got;
|
||||||
|
my $got = $got[$i];
|
||||||
|
my $want = $want[$i];
|
||||||
|
subtest("log $i" => sub {
|
||||||
|
is($got->{label}, $want->{label}, "label matches");
|
||||||
|
is_deeply($got->{ctx}, $want->{ctx}, "context matches");
|
||||||
|
like($got->{msg}, $want->{msg}, "message matches");
|
||||||
|
}) or diag(ddclient::repr(Values => [$got, $want], Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my @unexpected = @got[@want..$#got];
|
||||||
|
ok(@unexpected == 0, "no unexpected logs")
|
||||||
|
or diag(ddclient::repr(\@unexpected, Names => ['*unexpected']));
|
||||||
|
my @missing = @want[@got..$#want];
|
||||||
|
ok(@missing == 0, "no missing logs")
|
||||||
|
or diag(ddclient::repr(\@missing, Names => ['*missing']));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
107
t/read_recap.pl
Normal file
107
t/read_recap.pl
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
use File::Temp;
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
local %ddclient::protocols = (
|
||||||
|
protocol_a => ddclient::Protocol->new(
|
||||||
|
recapvars => {
|
||||||
|
host => ddclient::T_STRING(),
|
||||||
|
var_a => ddclient::T_BOOL(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
protocol_b => ddclient::Protocol->new(
|
||||||
|
recapvars => {
|
||||||
|
host => ddclient::T_STRING(),
|
||||||
|
var_b => ddclient::T_NUMBER(),
|
||||||
|
},
|
||||||
|
cfgvars => {
|
||||||
|
var_b_non_recap => {type => ddclient::T_ANY()},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
local %ddclient::cfgvars = (merged => {map({ %{$ddclient::protocols{$_}{cfgvars} // {}}; }
|
||||||
|
sort(keys(%ddclient::protocols)))});
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => "ok value",
|
||||||
|
cachefile_lines => ["var_a=yes host_a"],
|
||||||
|
want => {host_a => {host => 'host_a', var_a => 1}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "unknown host",
|
||||||
|
cachefile_lines => ["var_a=yes host_c"],
|
||||||
|
want => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "unknown var",
|
||||||
|
cachefile_lines => ["var_b=123 host_a"],
|
||||||
|
want => {host_a => {host => 'host_a'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "invalid value",
|
||||||
|
cachefile_lines => ["var_a=wat host_a"],
|
||||||
|
want => {host_a => {host => 'host_a'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "multiple entries",
|
||||||
|
cachefile_lines => [
|
||||||
|
"var_a=yes host_a",
|
||||||
|
"var_b=123 host_b",
|
||||||
|
],
|
||||||
|
want => {
|
||||||
|
host_a => {host => 'host_a', var_a => 1},
|
||||||
|
host_b => {host => 'host_b', var_b => 123},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "non-recap vars are not loaded to %recap",
|
||||||
|
cachefile_lines => ["var_b_non_recap=foo host_b"],
|
||||||
|
want => {host_b => {host => 'host_b'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "non-recap vars are scrubbed from %recap",
|
||||||
|
cachefile_lines => ["var_b_non_recap=foo host_b"],
|
||||||
|
recap => {host_b => {host => 'host_b', var_b_non_recap => 'foo'}},
|
||||||
|
want => {host_b => {host => 'host_b'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "unknown hosts are scrubbed from %recap",
|
||||||
|
cachefile_lines => ["host_a", "host_c"],
|
||||||
|
recap => {host_a => {host => 'host_a'}, host_c => {host => 'host_c'}},
|
||||||
|
want => {host_a => {host => 'host_a'}},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
my $cachef = File::Temp->new();
|
||||||
|
print($cachef join('', map("$_\n", "## $ddclient::program-$ddclient::version",
|
||||||
|
@{$tc->{cachefile_lines}})));
|
||||||
|
$cachef->close();
|
||||||
|
local $ddclient::globals{cache} = "$cachef";
|
||||||
|
local %ddclient::recap = %{$tc->{recap} // {}};
|
||||||
|
my %want_config = (
|
||||||
|
host_a => {protocol => 'protocol_a'},
|
||||||
|
host_b => {protocol => 'protocol_b'},
|
||||||
|
);
|
||||||
|
# Deep clone %want_config so we can check for changes.
|
||||||
|
local %ddclient::config;
|
||||||
|
$ddclient::config{$_} = {%{$want_config{$_}}} for keys(%want_config);
|
||||||
|
|
||||||
|
ddclient::read_recap($cachef->filename());
|
||||||
|
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_TODO};
|
||||||
|
is_deeply(\%ddclient::recap, $tc->{want}, "$tc->{desc}: %recap")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{want}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
is_deeply(\%ddclient::config, \%want_config, "$tc->{desc}: %config")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::config, \%want_config],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
150
t/skip.pl
Normal file
150
t/skip.pl
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
httpd('4')->run(
|
||||||
|
sub { return [200, ['Content-Type' => 'text/plain'], ['127.0.0.1 skip 127.0.0.2']]; });
|
||||||
|
httpd('6')->run(
|
||||||
|
sub { return [200, ['Content-Type' => 'text/plain'], ['::1 skip ::2']]; })
|
||||||
|
if httpd('6');
|
||||||
|
|
||||||
|
my $builtinwebv4 = 't/skip.pl webv4';
|
||||||
|
my $builtinwebv6 = 't/skip.pl webv6';
|
||||||
|
my $builtinfw = 't/skip.pl fw';
|
||||||
|
|
||||||
|
$ddclient::builtinweb{$builtinwebv4} = {'url' => httpd('4')->endpoint(), 'skip' => 'skip'};
|
||||||
|
$ddclient::builtinweb{$builtinwebv6} = {'url' => httpd('6')->endpoint(), 'skip' => 'skip'}
|
||||||
|
if httpd('6');
|
||||||
|
$ddclient::builtinfw{$builtinfw} = {name => 'test', skip => 'skip'};
|
||||||
|
%ddclient::builtinfw if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
%ddclient::ip_strategies = (%ddclient::ip_strategies, ddclient::builtinfw_strategy($builtinfw));
|
||||||
|
%ddclient::ipv4_strategies =
|
||||||
|
(%ddclient::ipv4_strategies, ddclient::builtinfwv4_strategy($builtinfw));
|
||||||
|
%ddclient::ipv6_strategies =
|
||||||
|
(%ddclient::ipv6_strategies, ddclient::builtinfwv6_strategy($builtinfw));
|
||||||
|
|
||||||
|
sub run_test_case {
|
||||||
|
my %tc = @_;
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
my $h = 't/skip.pl';
|
||||||
|
$ddclient::config{$h} = $tc{cfg};
|
||||||
|
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
is(ddclient::get_ip(ddclient::strategy_inputs('use', $h)), $tc{want}, $tc{desc})
|
||||||
|
if ($tc{cfg}{use});
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc{want}, $tc{desc})
|
||||||
|
if ($tc{cfg}{usev4});
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc{want}, $tc{desc})
|
||||||
|
if ($tc{cfg}{usev6});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subtest "use=web web='$builtinwebv4'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "web-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'use' => 'web',
|
||||||
|
'web' => $builtinwebv4,
|
||||||
|
'web-skip' => '',
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'web-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'use' => 'web',
|
||||||
|
'web' => $builtinwebv4,
|
||||||
|
'web-skip' => undef,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "usev4=webv4 webv4='$builtinwebv4'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "webv4-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'usev4' => 'webv4',
|
||||||
|
'webv4' => $builtinwebv4,
|
||||||
|
'webv4-skip' => '',
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'webv4-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'usev4' => 'webv4',
|
||||||
|
'webv4' => $builtinwebv4,
|
||||||
|
'webv4-skip' => undef,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "usev6=webv6 webv6='$builtinwebv6'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "webv6-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
'webv6' => $builtinwebv6,
|
||||||
|
'webv6-skip' => '',
|
||||||
|
},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => '::1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'webv6-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
'webv6' => $builtinwebv6,
|
||||||
|
'webv6-skip' => undef,
|
||||||
|
},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => '::2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "use='$builtinfw'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "fw-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'fw' => httpd('4')->endpoint(),
|
||||||
|
'fw-skip' => '',
|
||||||
|
'use' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'fw-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'fw' => httpd('4')->endpoint(),
|
||||||
|
'fw-skip' => undef,
|
||||||
|
'use' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "usev4='$builtinfw'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "fwv4-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'fwv4' => httpd('4')->endpoint(),
|
||||||
|
'fwv4-skip' => '',
|
||||||
|
'usev4' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'fwv4-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'fwv4' => httpd('4')->endpoint(),
|
||||||
|
'fwv4-skip' => undef,
|
||||||
|
'usev4' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
done_testing();
|
94
t/ssl-validate.pl
Normal file
94
t/ssl-validate.pl
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
httpd_ssl_required();
|
||||||
|
|
||||||
|
httpd('4', 1)->run(sub { return [200, $textplain, ['127.0.0.1']]; });
|
||||||
|
httpd('6', 1)->run(sub { return [200, $textplain, ['::1']]; }) if httpd('6', 1);
|
||||||
|
my $h = 't/ssl-validate.pl';
|
||||||
|
my %ep = (
|
||||||
|
'4' => httpd('4', 1)->endpoint(),
|
||||||
|
'6' => httpd('6', 1) ? httpd('6', 1)->endpoint() : undef,
|
||||||
|
);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'usev4=webv4 web-ssl-validate=no',
|
||||||
|
cfg => {'usev4' => 'webv4', 'web-ssl-validate' => 0, 'webv4' => $ep{'4'}},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=webv4 web-ssl-validate=yes',
|
||||||
|
cfg => {'usev4' => 'webv4', 'web-ssl-validate' => 1, 'webv4' => $ep{'4'}},
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6=webv6 web-ssl-validate=no',
|
||||||
|
cfg => {'usev6' => 'webv6', 'web-ssl-validate' => 0, 'webv6' => $ep{'6'}},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => '::1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6=webv6 web-ssl-validate=yes',
|
||||||
|
cfg => {'usev6' => 'webv6', 'web-ssl-validate' => 1, 'webv6' => $ep{'6'}},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=cisco-asa fw-ssl-validate=no',
|
||||||
|
cfg => {'usev4' => 'cisco-asa', 'fw-ssl-validate' => 0,
|
||||||
|
# cisco-asa adds https:// to the URL. :-/
|
||||||
|
'fwv4' => substr($ep{'4'}, length('https://'))},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=cisco-asa fw-ssl-validate=yes',
|
||||||
|
cfg => {'usev4' => 'cisco-asa', 'fw-ssl-validate' => 1,
|
||||||
|
# cisco-asa adds https:// to the URL. :-/
|
||||||
|
'fwv4' => substr($ep{'4'}, length('https://'))},
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=fwv4 fw-ssl-validate=no',
|
||||||
|
cfg => {'usev4' => 'fwv4', 'fw-ssl-validate' => 0, 'fwv4' => $ep{'4'}},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=fwv4 fw-ssl-validate=yes',
|
||||||
|
cfg => {'usev4' => 'fwv4', 'fw-ssl-validate' => 1, 'fwv4' => $ep{'4'}},
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
# $ddclient::globals{'ssl_ca_file'} is intentionally NOT set to $ca_file so that we can
|
||||||
|
# test what happens when certificate validation fails. However, if curl can't find any CA
|
||||||
|
# certificates (which may be the case in some minimal test environments, such as Docker
|
||||||
|
# images and Debian package builder chroots), it will immediately close the connection
|
||||||
|
# after it sends the TLS client hello and before it receives the server hello (in Debian
|
||||||
|
# sid as of 2025-01-08, anyway). This confuses IO::Socket::SSL (used by
|
||||||
|
# Test::Fake::HTTPD), causing it to hang in the middle of the TLS handshake waiting for
|
||||||
|
# input that will never arrive. To work around this, the CA certificate file is explicitly
|
||||||
|
# set to an unrelated certificate so that curl has something to read.
|
||||||
|
local $ddclient::globals{'ssl_ca_file'} = $other_ca_file;
|
||||||
|
local $ddclient::config{$h} = $tc->{cfg};
|
||||||
|
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if ($tc->{cfg}{usev4});
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if ($tc->{cfg}{usev6});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
395
t/update_nics.pl
Normal file
395
t/update_nics.pl
Normal file
|
@ -0,0 +1,395 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
use File::Temp;
|
||||||
|
BEGIN { eval { require HTTP::Request; 1; } or plan(skip_all => $@); }
|
||||||
|
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
|
use List::Util qw(max);
|
||||||
|
use Scalar::Util qw(refaddr);
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
httpd('4')->run();
|
||||||
|
httpd('6')->run() if httpd('6');
|
||||||
|
local %ddclient::builtinweb = (
|
||||||
|
v4 => {url => "" . httpd('4')->endpoint()},
|
||||||
|
defined(httpd('6')) ? (v6 => {url => "" . httpd('6')->endpoint()}) : (),
|
||||||
|
);
|
||||||
|
|
||||||
|
# Sentinel value used by `mergecfg` that means "this hash entry should be deleted if it exists."
|
||||||
|
my $DOES_NOT_EXIST = [];
|
||||||
|
|
||||||
|
sub mergecfg {
|
||||||
|
my %ret;
|
||||||
|
for my $cfg (@_) {
|
||||||
|
next if !defined($cfg);
|
||||||
|
for my $h (keys(%$cfg)) {
|
||||||
|
if (refaddr($cfg->{$h}) == refaddr($DOES_NOT_EXIST)) {
|
||||||
|
delete($ret{$h});
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
$ret{$h} = {%{$ret{$h} // {}}, %{$cfg->{$h}}};
|
||||||
|
for my $k (keys(%{$ret{$h}})) {
|
||||||
|
my $a = refaddr($ret{$h}{$k});
|
||||||
|
delete($ret{$h}{$k}) if defined($a) && $a == refaddr($DOES_NOT_EXIST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return \%ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
local $ddclient::now = 1000;
|
||||||
|
our @updates;
|
||||||
|
local %ddclient::protocols = (
|
||||||
|
# The `legacy` protocol reads the legacy `wantip` property and sets the legacy `ip` and `status`
|
||||||
|
# properties. (Modern protocol implementations read `wantipv4` and `wantipv6` and set `ipv4`,
|
||||||
|
# `ipv6`, `status-ipv4`, and `status-ipv6`.) It always succeeds.
|
||||||
|
legacy => ddclient::LegacyProtocol->new(
|
||||||
|
update => sub {
|
||||||
|
my $self = shift;
|
||||||
|
ddclient::debug('in update');
|
||||||
|
push(@updates, [@_]);
|
||||||
|
for my $h (@_) {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($h);
|
||||||
|
ddclient::debug('updating host');
|
||||||
|
$ddclient::recap{$h}{status} = 'good';
|
||||||
|
$ddclient::recap{$h}{ip} = delete($ddclient::config{$h}{wantip});
|
||||||
|
$ddclient::recap{$h}{mtime} = $ddclient::now;
|
||||||
|
}
|
||||||
|
ddclient::debug('returning from update');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, fresh, $desc",
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
{
|
||||||
|
desc => 'legacy, fresh, use=web (IPv6)',
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
'use' => 'web',
|
||||||
|
'web' => 'v6',
|
||||||
|
}},
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'legacy, fresh, usev6=webv6',
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
}},
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'legacy, fresh, usev4=webv4 usev6=webv6',
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
'usev4' => 'webv4',
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, no change, not yet time, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, min-interval elapsed but no change, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, needs update, not yet time, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'warned-min-interval' => $ddclient::now,
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, min-interval elapsed, needs update, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, previous failed update, not yet time to retry, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-error-interval'),
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - max(ddclient::opt('min-error-interval'),
|
||||||
|
ddclient::opt('min-interval')) - 1,
|
||||||
|
'status-ipv4' => 'failed',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'warned-min-error-interval' => $ddclient::now,
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, previous failed update, time to retry, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-error-interval') - 1,
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-error-interval') - 2,
|
||||||
|
'status-ipv4' => 'failed',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", sort(keys(%cfg))));
|
||||||
|
{
|
||||||
|
desc => "deduplicates identical IP discovery, $desc",
|
||||||
|
cfg => {
|
||||||
|
hosta => {protocol => 'legacy', %cfg},
|
||||||
|
hostb => {protocol => 'legacy', %cfg},
|
||||||
|
},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['hosta', 'hostb']],
|
||||||
|
want_recap_changes => {
|
||||||
|
hosta => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
},
|
||||||
|
hostb => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
{
|
||||||
|
desc => "deduplicates identical IP discovery, usev6=webv6",
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {
|
||||||
|
hosta => {protocol => 'legacy', usev6 => 'webv6'},
|
||||||
|
hostb => {protocol => 'legacy', usev6 => 'webv6'},
|
||||||
|
},
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['hosta', 'hostb']],
|
||||||
|
want_recap_changes => {
|
||||||
|
hosta => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
},
|
||||||
|
hostb => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
subtest($tc->{desc} => sub {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
$tc->{"want_reqs_webv$ipv"} //= 0;
|
||||||
|
my $want = $tc->{"want_reqs_webv$ipv"};
|
||||||
|
next if !defined(httpd($ipv)) && $want == 0;
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
|
||||||
|
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
|
||||||
|
httpd($ipv)->reset(([200, $textplain, [$ip]]) x $want);
|
||||||
|
}
|
||||||
|
$tc->{recap}{$_}{host} //= $_ for keys(%{$tc->{recap} // {}});
|
||||||
|
# Deep copy `%{$tc->{recap}}` so that updates to `%ddclient::recap` don't mutate it.
|
||||||
|
local %ddclient::recap = %{mergecfg($tc->{recap})};
|
||||||
|
my $cachef = File::Temp->new();
|
||||||
|
# $cachef is an object that stringifies to a filename.
|
||||||
|
local $ddclient::globals{cache} = "$cachef";
|
||||||
|
$tc->{cfg} = {map({
|
||||||
|
($_ => {
|
||||||
|
host => $_,
|
||||||
|
web => 'v4',
|
||||||
|
webv4 => 'v4',
|
||||||
|
webv6 => 'v6',
|
||||||
|
%{$tc->{cfg}{$_}},
|
||||||
|
});
|
||||||
|
} keys(%{$tc->{cfg} // {}}))};
|
||||||
|
# Deep copy `%{$tc->{cfg}}` so that updates to `%ddclient::config` don't mutate it.
|
||||||
|
local %ddclient::config = %{mergecfg($tc->{cfg})};
|
||||||
|
local @updates;
|
||||||
|
|
||||||
|
ddclient::update_nics();
|
||||||
|
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
next if !defined(httpd($ipv));
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
|
||||||
|
my @gotreqs = httpd($ipv)->reset();
|
||||||
|
my $got = @gotreqs;
|
||||||
|
my $want = $tc->{"want_reqs_webv$ipv"};
|
||||||
|
is($got, $want, "number of requests to webv$ipv service");
|
||||||
|
}
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_updates_TODO};
|
||||||
|
is_deeply(\@updates, $tc->{want_updates} // [], 'got expected updates')
|
||||||
|
or diag(ddclient::repr(Values => [\@updates, $tc->{want_updates}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my %want_recap = %{mergecfg($tc->{recap}, $tc->{want_recap_changes})};
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_recap_changes_TODO};
|
||||||
|
is_deeply(\%ddclient::recap, \%want_recap, 'recap matches')
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, \%want_recap],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my %want_cfg = %{mergecfg($tc->{cfg}, $tc->{want_cfg_changes})};
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_cfg_changes_TODO};
|
||||||
|
is_deeply(\%ddclient::config, \%want_cfg, 'config matches')
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::config, \%want_cfg],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
41
t/use_cmd.pl
Normal file
41
t/use_cmd.pl
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
|
||||||
|
my @test_cases;
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
|
||||||
|
for my $use ('use', "usev$ipv") {
|
||||||
|
my @cmds = ();
|
||||||
|
push(@cmds, 'cmd') if $use eq 'use' || $ipv eq '6';
|
||||||
|
push(@cmds, "cmdv$ipv") if $use ne 'use';
|
||||||
|
for my $cmd (@cmds) {
|
||||||
|
my $cmdarg = "echo '$ip'";
|
||||||
|
push(
|
||||||
|
@test_cases,
|
||||||
|
{
|
||||||
|
desc => "$use=$cmd $cmd=\"$cmdarg\"",
|
||||||
|
cfg => {$use => $cmd, $cmd => $cmdarg},
|
||||||
|
want => $ip,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
my $h = 'test-host';
|
||||||
|
local $ddclient::config{$h} = $tc->{cfg};
|
||||||
|
is(ddclient::get_ip(ddclient::strategy_inputs('use', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{use};
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev4};
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev6};
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
87
t/use_web.pl
Normal file
87
t/use_web.pl
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
my $builtinweb = 't/use_web.pl builtinweb';
|
||||||
|
my $h = 't/use_web.pl hostname';
|
||||||
|
|
||||||
|
my $headers = [
|
||||||
|
@$textplain,
|
||||||
|
'this-ipv4-should-be-ignored' => 'skip skip2 192.0.2.255',
|
||||||
|
'this-ipv6-should-be-ignored' => 'skip skip2 2001:db8::ff',
|
||||||
|
];
|
||||||
|
httpd('4')->run(sub { return [200, $headers, ['192.0.2.1 skip 192.0.2.2 skip2 192.0.2.3']]; });
|
||||||
|
httpd('6')->run(sub { return [200, $headers, ['2001:db8::1 skip 2001:db8::2 skip2 2001:db8::3']]; })
|
||||||
|
if httpd('6');
|
||||||
|
my %ep = (
|
||||||
|
'4' => httpd('4')->endpoint(),
|
||||||
|
'6' => httpd('6') ? httpd('6')->endpoint() : undef,
|
||||||
|
);
|
||||||
|
|
||||||
|
my @test_cases;
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
my $ipv4 = $ipv eq '4';
|
||||||
|
for my $sfx ('', "v$ipv") {
|
||||||
|
push(
|
||||||
|
@test_cases,
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<url> IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $ep{$ipv}},
|
||||||
|
want => $ipv4 ? '192.0.2.1' : '2001:db8::1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<url> web$sfx-skip=skip IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $ep{$ipv}, "web$sfx-skip" => 'skip'},
|
||||||
|
# Note that "skip" should skip past the first "skip" and not past "skip2".
|
||||||
|
want => $ipv4 ? '192.0.2.2' : '2001:db8::2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<builtinweb> IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb},
|
||||||
|
biw => {url => $ep{$ipv}},
|
||||||
|
want => $ipv4 ? '192.0.2.1' : '2001:db8::1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<builtinweb w/skip> IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb},
|
||||||
|
biw => {url => $ep{$ipv}, skip => 'skip'},
|
||||||
|
# Note that "skip" should skip past the first "skip" and not past "skip2".
|
||||||
|
want => $ipv4 ? '192.0.2.2' : '2001:db8::2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<builtinweb w/skip> web$sfx-skip=skip2 IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb, "web$sfx-skip" => 'skip2'},
|
||||||
|
biw => {url => $ep{$ipv}, skip => 'skip'},
|
||||||
|
want => $ipv4 ? '192.0.2.3' : '2001:db8::3',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
local $ddclient::builtinweb{$builtinweb} = $tc->{biw};
|
||||||
|
$ddclient::builtinweb if 0;
|
||||||
|
local $ddclient::config{$h} = $tc->{cfg};
|
||||||
|
$ddclient::config if 0;
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
is(ddclient::get_ip(ddclient::strategy_inputs('use', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{use};
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev4};
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev6};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
100
t/variable_defaults.pl
Normal file
100
t/variable_defaults.pl
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use re qw(is_regexp);
|
||||||
|
|
||||||
|
my %variable_collections = (
|
||||||
|
map({ ($_ => $ddclient::cfgvars{$_}) } grep($_ ne 'merged', keys(%ddclient::cfgvars))),
|
||||||
|
map({ ("protocol=$_" => $ddclient::protocols{$_}{cfgvars}); } keys(%ddclient::protocols)),
|
||||||
|
);
|
||||||
|
my %seen;
|
||||||
|
my @test_cases = (
|
||||||
|
map({
|
||||||
|
my $vcn = $_;
|
||||||
|
my $vc = $variable_collections{$_};
|
||||||
|
map({
|
||||||
|
my $def = $vc->{$_};
|
||||||
|
my $seen = exists($seen{$def});
|
||||||
|
$seen{$def} = undef;
|
||||||
|
({desc => "$vcn $_", def => $vc->{$_}}) x !$seen;
|
||||||
|
} sort(keys(%$vc)));
|
||||||
|
} sort(keys(%variable_collections))),
|
||||||
|
);
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
if ($tc->{def}{required}) {
|
||||||
|
is($tc->{def}{default}, undef, "'$tc->{desc}' (required) has no default");
|
||||||
|
} else {
|
||||||
|
# Preserve all existing variables in $cfgvars{merged} so that variables with dynamic
|
||||||
|
# defaults can reference them.
|
||||||
|
local %ddclient::cfgvars = (merged => {
|
||||||
|
%{$ddclient::cfgvars{merged}},
|
||||||
|
'var for test' => $tc->{def},
|
||||||
|
});
|
||||||
|
# Variables with dynamic defaults will need their own unit tests, but we can still check the
|
||||||
|
# clean-slate hostless default.
|
||||||
|
local %ddclient::config;
|
||||||
|
local %ddclient::opt;
|
||||||
|
local %ddclient::globals;
|
||||||
|
my $norm;
|
||||||
|
my $default = ddclient::default('var for test');
|
||||||
|
diag("'$tc->{desc}' default: " . ($default // '<undefined>'));
|
||||||
|
is($default, $tc->{def}{default}, "'$tc->{desc}' default() return value matches default")
|
||||||
|
if ref($tc->{def}{default}) ne 'CODE';
|
||||||
|
my $valid = eval { $norm = ddclient::check_value($default, $tc->{def}); 1; } or diag($@);
|
||||||
|
ok($valid, "'$tc->{desc}' (optional) has a valid default");
|
||||||
|
is($norm, $default, "'$tc->{desc}' default normalizes to itself") if $valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my @use_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'clean slate hostless default',
|
||||||
|
want => 'ip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usage string',
|
||||||
|
host => '<usage>',
|
||||||
|
want => qr/disabled.*ip|ip.*disabled/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4 disables use by default',
|
||||||
|
host => 'host',
|
||||||
|
cfg => {usev4 => 'webv4'},
|
||||||
|
want => 'disabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6 disables use by default',
|
||||||
|
host => 'host',
|
||||||
|
cfg => {usev4 => 'webv4'},
|
||||||
|
want => 'disabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'explicitly setting use re-enables it',
|
||||||
|
host => 'host',
|
||||||
|
cfg => {use => 'web', usev4 => 'webv4'},
|
||||||
|
want => 'web',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for my $tc (@use_test_cases) {
|
||||||
|
my $desc = "'use' dynamic default: $tc->{desc}";
|
||||||
|
local %ddclient::protocols = (protocol => ddclient::Protocol->new());
|
||||||
|
local %ddclient::cfgvars = (merged => {
|
||||||
|
'protocol' => $ddclient::cfgvars{'merged'}{'protocol'},
|
||||||
|
'use' => $ddclient::cfgvars{'protocol-common-defaults'}{'use'},
|
||||||
|
'usev4' => $ddclient::cfgvars{'merged'}{'usev4'},
|
||||||
|
'usev6' => $ddclient::cfgvars{'merged'}{'usev6'},
|
||||||
|
});
|
||||||
|
local %ddclient::config = (host => {protocol => 'protocol', %{$tc->{cfg} // {}}});
|
||||||
|
local %ddclient::opt;
|
||||||
|
local %ddclient::globals;
|
||||||
|
|
||||||
|
my $got = ddclient::opt('use', $tc->{host});
|
||||||
|
|
||||||
|
if (is_regexp($tc->{want})) {
|
||||||
|
like($got, $tc->{want}, $desc);
|
||||||
|
} else {
|
||||||
|
is($got, $tc->{want}, $desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
|
@ -4,6 +4,59 @@ use version;
|
||||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
is(ddclient->VERSION(), version->parse('v@PACKAGE_VERSION@'), "version matches Autoconf config");
|
ok(ddclient::parse_version($ddclient::VERSION),
|
||||||
|
"module's Perl version string is in opinionated form");
|
||||||
|
|
||||||
|
my $n = qr/0|[1-9]\d{0,2}/;
|
||||||
|
like($ddclient::version, qr/^$n\.$n\.$n(?:-alpha|-beta\.$n|-rc\.$n|\+r\.$n)?$/,
|
||||||
|
"human-readable version is in opinionated form");
|
||||||
|
|
||||||
|
my @tcs = (
|
||||||
|
['v1.0_0', '1-alpha'],
|
||||||
|
['v1.0.0_0', '1.0-alpha'],
|
||||||
|
['v1.2.3.0_0', '1.2.3-alpha'],
|
||||||
|
['v1.2.3.4.0_0', '1.2.3.4-alpha'],
|
||||||
|
['v1.0_1', '1-beta.1'],
|
||||||
|
['v1.0.0_1', '1.0-beta.1'],
|
||||||
|
['v1.2.3.0_1', '1.2.3-beta.1'],
|
||||||
|
['v1.2.3.4.0_1', '1.2.3.4-beta.1'],
|
||||||
|
['v1.2.3.0_899', '1.2.3-beta.899'],
|
||||||
|
['v1.0_901', '1-rc.1'],
|
||||||
|
['v1.0.0_901', '1.0-rc.1'],
|
||||||
|
['v1.2.3.0_901', '1.2.3-rc.1'],
|
||||||
|
['v1.2.3.4.0_901', '1.2.3.4-rc.1'],
|
||||||
|
['v1.2.3.0_998', '1.2.3-rc.98'],
|
||||||
|
['v1.999', '1'],
|
||||||
|
['v1.0.999', '1.0'],
|
||||||
|
['v1.2.3.999', '1.2.3'],
|
||||||
|
['v1.2.3.4.999', '1.2.3.4'],
|
||||||
|
['v1.999.1', '1+r.1'],
|
||||||
|
['v1.0.999.1', '1.0+r.1'],
|
||||||
|
['v1.2.3.999.1', '1.2.3+r.1'],
|
||||||
|
['v1.2.3.4.999.1', '1.2.3.4+r.1'],
|
||||||
|
['v1.2.3.999.999', '1.2.3+r.999'],
|
||||||
|
[$ddclient::VERSION, $ddclient::version],
|
||||||
|
);
|
||||||
|
|
||||||
|
subtest 'humanize_version' => sub {
|
||||||
|
for my $tc (@tcs) {
|
||||||
|
my ($pv, $want) = @$tc;
|
||||||
|
is(ddclient::humanize_version($pv), $want, "$pv -> $want");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest 'human-readable version can be translated back to Perl version' => sub {
|
||||||
|
for my $tc (@tcs) {
|
||||||
|
my ($want, $hv) = @$tc;
|
||||||
|
my $pv = "v$hv";
|
||||||
|
$pv =~ s/^(?!.*-)(.*?)(?:\+r\.(\d+))?$/"$1.999" . (defined($2) ? ".$2" : "")/e;
|
||||||
|
$pv =~ s/-alpha$/.0_0/;
|
||||||
|
$pv =~ s/-beta\.(\d+)$/.0_$1/;
|
||||||
|
$pv =~ s/-rc\.(\d+)$/'.0_' . (900 + $1)/e;
|
||||||
|
is($pv, $want, "$hv -> $want");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
is($ddclient::version, '@PACKAGE_VERSION@', "version matches version in Autoconf");
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|
|
@ -35,7 +35,7 @@ my @test_cases = (
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
for my $tc (@test_cases) {
|
||||||
$warning = undef;
|
$warning = undef;
|
||||||
ddclient::write_cache($tc->{f});
|
ddclient::write_recap($tc->{f});
|
||||||
subtest $tc->{name} => sub {
|
subtest $tc->{name} => sub {
|
||||||
if (defined($tc->{warning_regex})) {
|
if (defined($tc->{warning_regex})) {
|
||||||
like($warning, $tc->{warning_regex}, "expected warning message");
|
like($warning, $tc->{warning_regex}, "expected warning message");
|
Loading…
Reference in a new issue