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:
|
||||
test-debian-like:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image:
|
||||
- ubuntu:latest
|
||||
|
@ -32,10 +33,11 @@ jobs:
|
|||
libtest-tcp-perl \
|
||||
libtest-warnings-perl \
|
||||
liburi-perl \
|
||||
libwww-perl \
|
||||
net-tools \
|
||||
make \
|
||||
;
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: autogen
|
||||
run: ./autogen
|
||||
- name: configure
|
||||
|
@ -46,41 +48,45 @@ jobs:
|
|||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
||||
- name: distribution tarball is complete
|
||||
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:
|
||||
# runs-on: ubuntu-latest
|
||||
# container: centos:8
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: install dependencies
|
||||
# run: |
|
||||
# dnf --refresh --enablerepo=PowerTools install -y \
|
||||
# automake \
|
||||
# make \
|
||||
# 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:
|
||||
test-fedora-like:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image:
|
||||
- fedora:39
|
||||
- fedora:latest
|
||||
- fedora:rawhide
|
||||
- almalinux:8
|
||||
- almalinux:latest
|
||||
runs-on: ubuntu-latest
|
||||
container: fedora
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install dependencies
|
||||
- uses: actions/checkout@v4
|
||||
- name: enable repositories (AlmaLinux 8)
|
||||
if: ${{ matrix.image == 'almalinux:8' }}
|
||||
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 \
|
||||
findutils \
|
||||
iproute \
|
||||
make \
|
||||
curl \
|
||||
perl \
|
||||
|
@ -91,6 +97,8 @@ jobs:
|
|||
perl-Test-MockModule \
|
||||
perl-Test-TCP \
|
||||
perl-Test-Warnings \
|
||||
perl-core \
|
||||
perl-libwww-perl \
|
||||
net-tools \
|
||||
;
|
||||
- name: autogen
|
||||
|
@ -101,29 +109,3 @@ jobs:
|
|||
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
||||
- name: 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
|
||||
/aclocal.m4
|
||||
/autom4te.cache/
|
||||
/build-aux/config.guess
|
||||
/build-aux/config.sub
|
||||
/build-aux/install-sh
|
||||
/build-aux/missing
|
||||
/build-aux/tap-driver.sh
|
||||
/config.log
|
||||
/config.status
|
||||
/configure
|
||||
|
|
|
@ -80,7 +80,7 @@ perltidy -l=99 -conv -ci=4 -ola -ce -nbbc -kis -pt=2 -b ddclient
|
|||
|
||||
## 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:
|
||||
- Put unrelated changes in separate commits
|
||||
- Squash your fixup commits
|
||||
|
@ -190,11 +190,11 @@ better to revert the original change then redo it:
|
|||
|
||||
### 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:
|
||||
|
||||
```
|
||||
* 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
|
||||
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|
||||
|/ Expand comment documenting config line format
|
||||
|
@ -231,7 +231,7 @@ has value:
|
|||
change was made) and the merge timestamp (when it went live).
|
||||
|
||||
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
|
||||
fast-forward merge, which is not what we want). See
|
||||
[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
|
||||
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
|
||||
|
||||
# Switch to the pull request branch
|
||||
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
|
||||
|
||||
# Rebase the commits (optionally using -i to clean up history) onto
|
||||
# the current ddclient master branch
|
||||
git rebase origin/master
|
||||
# the current ddclient main branch
|
||||
git rebase origin/main
|
||||
|
||||
# Force update the contributor's fork. This will only work if 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,
|
||||
# continue with the next steps.
|
||||
|
||||
# Switch to the local master branch
|
||||
git checkout master
|
||||
# Switch to the local main branch
|
||||
git checkout main
|
||||
|
||||
# Make sure the local master branch is up to date
|
||||
git merge --ff-only origin/master
|
||||
# Make sure the local main branch is up to date
|
||||
git merge --ff-only origin/main
|
||||
|
||||
# Merge in the rebased pull request branch **WITHOUT DOING A
|
||||
# FAST-FORWARD MERGE**
|
||||
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
|
||||
|
||||
# Review the commits before pushing
|
||||
git log --graph --oneline --decorate origin/master..
|
||||
git log --graph --oneline --decorate origin/main..
|
||||
|
||||
# Push to ddclient master
|
||||
git push origin master
|
||||
# Push to ddclient main
|
||||
git push origin main
|
||||
```
|
||||
|
|
205
ChangeLog.md
205
ChangeLog.md
|
@ -1,7 +1,210 @@
|
|||
# ChangeLog
|
||||
|
||||
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
|
||||
|
||||
|
|
52
Makefile.am
52
Makefile.am
|
@ -1,4 +1,4 @@
|
|||
ACLOCAL_AMFLAGS = -I m4
|
||||
ACLOCAL_AMFLAGS = -I build-aux/m4
|
||||
EXTRA_DIST = \
|
||||
CONTRIBUTING.md \
|
||||
COPYING \
|
||||
|
@ -16,19 +16,7 @@ EXTRA_DIST = \
|
|||
sample-get-ip-from-fritzbox
|
||||
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
|
||||
|
||||
EXTRA_DIST += $(subst_files:=.in)
|
||||
CLEANFILES += $(subst_files)
|
||||
|
||||
|
@ -36,7 +24,14 @@ $(subst_files): Makefile
|
|||
rm -f '$@' '$@'.tmp
|
||||
in='$@'.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; }
|
||||
mv '$@'.tmp '$@'
|
||||
|
||||
|
@ -45,7 +40,7 @@ ddclient.conf: $(srcdir)/ddclient.conf.in
|
|||
|
||||
bin_SCRIPTS = ddclient
|
||||
|
||||
sysconf_DATA = ddclient.conf
|
||||
conf_DATA = ddclient.conf
|
||||
|
||||
install-data-local:
|
||||
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
||||
|
@ -62,17 +57,36 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
|
|||
-I'$(abs_top_srcdir)'/t/lib \
|
||||
-MDevel::Autoflush
|
||||
handwritten_tests = \
|
||||
t/builtinfw_query.pl \
|
||||
t/check_value.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-ipv6.pl \
|
||||
t/is-and-extract-ipv6-global.pl \
|
||||
t/logmsg.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 = \
|
||||
t/geturl_connectivity.pl \
|
||||
t/version.pl
|
||||
TESTS = $(handwritten_tests) $(generated_tests)
|
||||
$(TESTS): ddclient
|
||||
EXTRA_DIST += $(handwritten_tests) \
|
||||
.autom4te.cfg \
|
||||
t/lib/Devel/Autoflush.pm \
|
||||
t/lib/Test/Builder.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-server-cert.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/HTTPD.pm \
|
||||
t/lib/ddclient/t/Logger.pm \
|
||||
t/lib/ddclient/t/ip.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
|
||||
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
|
||||
|
||||
|
@ -15,41 +43,44 @@ your dynamic DNS provider(s): <https://github.com/troglobit/inadyn> or
|
|||
|
||||
Dynamic DNS services currently supported include:
|
||||
|
||||
* [1984.is](https://www.1984.is/product/freedns)
|
||||
* [ChangeIP](https://www.changeip.com)
|
||||
* [CloudFlare](https://www.cloudflare.com)
|
||||
* [ClouDNS](https://www.cloudns.net)
|
||||
* [DigitalOcean](https://www.digitalocean.com/)
|
||||
* [dinahosting](https://dinahosting.com)
|
||||
* [DonDominio](https://www.dondominio.com)
|
||||
* [DNS Made Easy](https://dnsmadeeasy.com)
|
||||
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
||||
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
||||
* [DslReports](https://www.dslreports.com)
|
||||
* [Duck DNS](https://duckdns.org)
|
||||
* [DynDNS.com](https://account.dyn.com)
|
||||
* [EasyDNS](https://www.easydns.com )
|
||||
* [Enom](https://www.enom.com)
|
||||
* [Freedns](https://freedns.afraid.org)
|
||||
* [Freemyip](https://freemyip.com)
|
||||
* [Gandi](https://gandi.net)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
* [Google](https://domains.google)
|
||||
* [Infomaniak](https://faq.infomaniak.com/2376)
|
||||
* [Loopia](https://www.loopia.se)
|
||||
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
||||
* [NameCheap](https://www.namecheap.com)
|
||||
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
||||
* [Njalla](https://njal.la/docs/ddns)
|
||||
* [Noip](https://www.noip.com)
|
||||
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
||||
* [OVH](https://www.ovhcloud.com)
|
||||
* [Porkbun](https://porkbun.com)
|
||||
* [regfish.de](https://www.regfish.de/domains/dyndns)
|
||||
* [Sitelutions](https://www.sitelutions.com)
|
||||
* [woima.fi](https://woima.fi)
|
||||
* [Yandex](https://dns.yandex.com)
|
||||
* [Zoneedit](https://www.zoneedit.com)
|
||||
* [1984.is](https://www.1984.is/product/freedns)
|
||||
* [ChangeIP](https://www.changeip.com)
|
||||
* [CloudFlare](https://www.cloudflare.com)
|
||||
* [ClouDNS](https://www.cloudns.net)
|
||||
* [DDNS.fm](https://www.ddns.fm/)
|
||||
* [DigitalOcean](https://www.digitalocean.com/)
|
||||
* [dinahosting](https://dinahosting.com)
|
||||
* [Directnic](https://directnic.com)
|
||||
* [DonDominio](https://www.dondominio.com)
|
||||
* [DNS Made Easy](https://dnsmadeeasy.com)
|
||||
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
||||
* [dnsHome.de](https://www.dnshome.de)
|
||||
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
||||
* [DslReports](https://www.dslreports.com)
|
||||
* [Duck DNS](https://duckdns.org)
|
||||
* [DynDNS.com](https://account.dyn.com)
|
||||
* [EasyDNS](https://www.easydns.com )
|
||||
* [Enom](https://www.enom.com)
|
||||
* [Freedns](https://freedns.afraid.org)
|
||||
* [Freemyip](https://freemyip.com)
|
||||
* [Gandi](https://gandi.net)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
* [Hurricane Electric](https://dns.he.net)
|
||||
* [Infomaniak](https://faq.infomaniak.com/2376)
|
||||
* [INWX](https://www.inwx.com/)
|
||||
* [Loopia](https://www.loopia.se)
|
||||
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
||||
* [NameCheap](https://www.namecheap.com)
|
||||
* [NearlyFreeSpeech.net](https://www.nearlyfreespeech.net/services/dns)
|
||||
* [Njalla](https://njal.la/docs/ddns)
|
||||
* [Noip](https://www.noip.com)
|
||||
* nsupdate - see nsupdate(1) and ddns-confgen(8)
|
||||
* [OVH](https://www.ovhcloud.com)
|
||||
* [Porkbun](https://porkbun.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
|
||||
broadband routers.
|
||||
|
@ -104,7 +135,7 @@ operating system. See the image to the right for a list of distributions with a
|
|||
```shell
|
||||
./configure \
|
||||
--prefix=/usr \
|
||||
--sysconfdir=/etc/ddclient \
|
||||
--sysconfdir=/etc \
|
||||
--localstatedir=/var
|
||||
make
|
||||
make VERBOSE=1 check
|
||||
|
@ -125,43 +156,97 @@ start the first time by hand
|
|||
|
||||
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
|
||||
|
||||
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?
|
||||
If so, just add a ``proxy=your.isp.proxy`` to the ddclient.conf file.
|
||||
* Do you need to specify a proxy?
|
||||
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
|
||||
``/etc/ddclient/ddclient.conf`` and then try ``$ ddclient -daemon=0 -query`` to see if the router status web page can be understood.
|
||||
* 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.
|
||||
|
||||
4. 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``
|
||||
* Need support for another router/firewall?
|
||||
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:
|
||||
|
||||
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
|
||||
Send me the output from:
|
||||
``$ ddclient -geturl {fw-ip-status-url} [-login login [-password password]]``
|
||||
and I'll add it to the next release!
|
||||
OR [create a new issue](https://github.com/ddclient/ddclient/issues/new)
|
||||
containing the output from:
|
||||
|
||||
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
|
||||
accesses its status page to determine the router's WAN IP address.
|
||||
so that we can add a new firewall definition to a future release of
|
||||
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
|
||||
|
||||
```
|
||||
fw-login=your-router-login
|
||||
fw-password=your-router-password
|
||||
```
|
||||
|
||||
to the beginning of your ddclient.conf file.
|
||||
Note that some routers use either 'root' or 'admin' as their login
|
||||
while some others accept anything.
|
||||
to the beginning of your ddclient.conf file.
|
||||
Note that some routers use either 'root' or 'admin' as their login while
|
||||
some others accept anything.
|
||||
|
||||
## USING DDCLIENT WITH `ppp`
|
||||
|
||||
|
@ -199,7 +284,7 @@ In my case, it is named dhcpcd-eth0.exe and contains the lines:
|
|||
#!/bin/sh
|
||||
PATH=/usr/bin:/root/bin:${PATH}
|
||||
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
|
||||
```
|
||||
|
||||
|
|
26
autogen
26
autogen
|
@ -7,18 +7,16 @@ fatal() { error "$@"; exit 1; }
|
|||
try() { "$@" || fatal "'$@' failed"; }
|
||||
|
||||
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
|
||||
|
||||
# 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_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_AUX_DIR([build-aux])
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
AC_CONFIG_MACRO_DIR([build-aux/m4])
|
||||
AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||
# If the automake dependency is bumped to v1.12 or newer, remove
|
||||
# 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_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
|
||||
|
||||
# The Fedora Docker image doesn't come with the 'findutils' package.
|
||||
|
@ -27,7 +48,18 @@ AC_PROG_MKDIR_P
|
|||
AC_PATH_PROG([FIND], [find])
|
||||
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])])
|
||||
|
||||
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
|
||||
# be assumed.
|
||||
m4_foreach_w([_m], [
|
||||
Data::Dumper
|
||||
File::Basename
|
||||
File::Path
|
||||
File::Temp
|
||||
|
@ -54,9 +87,12 @@ m4_foreach_w([_m], [
|
|||
# then some tests will fail. Only prints a warning if not installed.
|
||||
m4_foreach_w([_m], [
|
||||
B
|
||||
Data::Dumper
|
||||
Exporter
|
||||
File::Spec::Functions
|
||||
File::Temp
|
||||
List::Util
|
||||
Scalar::Util
|
||||
re
|
||||
], [AX_PROG_PERL_MODULES([_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.
|
||||
m4_foreach_w([_m], [
|
||||
Carp
|
||||
Exporter
|
||||
HTTP::Daemon=6.12
|
||||
HTTP::Daemon::SSL
|
||||
HTTP::Message::PSGI
|
||||
HTTP::Request
|
||||
HTTP::Response
|
||||
Scalar::Util
|
||||
JSON::PP
|
||||
Test::MockModule
|
||||
Test::TCP
|
||||
Test::Warnings
|
||||
Time::HiRes
|
||||
URI
|
||||
parent
|
||||
], [AX_PROG_PERL_MODULES([_m], [],
|
||||
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
||||
|
||||
AC_CONFIG_FILES([
|
||||
Makefile
|
||||
t/geturl_connectivity.pl
|
||||
t/version.pl
|
||||
])
|
||||
AC_OUTPUT
|
||||
|
|
183
ddclient.conf.in
183
ddclient.conf.in
|
@ -16,15 +16,21 @@
|
|||
## 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
|
||||
syslog=yes # log update msgs to syslog
|
||||
mail=root # mail all 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.
|
||||
ssl=yes # use ssl-support. Works with
|
||||
# ssl-library
|
||||
# postscript=script # run script after updating. The
|
||||
# new IP is 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=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)
|
||||
#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)
|
||||
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
||||
## Using use=web is enough to get it working.
|
||||
|
@ -78,26 +88,6 @@ ssl=yes # use ssl-support. Works with
|
|||
# protocol=dyndns2 \
|
||||
# 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)
|
||||
##
|
||||
|
@ -147,10 +137,10 @@ ssl=yes # use ssl-support. Works with
|
|||
##
|
||||
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
||||
##
|
||||
# protocol = nfsn, \
|
||||
# protocol=nfsn, \
|
||||
# zone=example.com, \
|
||||
# login=member-login, \
|
||||
# password=api-key, \
|
||||
# zone=example.com \
|
||||
# password=api-key \
|
||||
# example.com,subdomain.example.com
|
||||
|
||||
##
|
||||
|
@ -171,7 +161,7 @@ ssl=yes # use ssl-support. Works with
|
|||
# ssl=yes, \
|
||||
# server=dynupdate.no-ip.com, \
|
||||
# login=your-noip-login, \
|
||||
# password=your-noip-password, \
|
||||
# password=your-noip-password \
|
||||
# your-host.domain.com, your-2nd-host.domain.com
|
||||
|
||||
##
|
||||
|
@ -186,30 +176,40 @@ ssl=yes # use ssl-support. Works with
|
|||
##
|
||||
## CloudFlare (www.cloudflare.com)
|
||||
##
|
||||
#protocol=cloudflare, \
|
||||
#zone=domain.tld, \
|
||||
#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).
|
||||
#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
|
||||
# protocol=cloudflare, \
|
||||
# zone=domain.tld, \
|
||||
# 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).
|
||||
# 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
|
||||
|
||||
##
|
||||
## Gandi (gandi.net)
|
||||
##
|
||||
## Single host update
|
||||
# protocol=gandi, \
|
||||
# zone=example.com, \
|
||||
# password=my-gandi-api-key, \
|
||||
# ttl=3h \
|
||||
# protocol=gandi
|
||||
# zone=example.com
|
||||
# password=my-gandi-access-token
|
||||
# use-personal-access-token=yes
|
||||
# ttl=10800 # optional
|
||||
# myhost.example.com
|
||||
|
||||
##
|
||||
## Google Domains (www.google.com/domains)
|
||||
## GoDaddy (godaddy.com)
|
||||
##
|
||||
# protocol=googledomains,
|
||||
# login=my-auto-generated-username,
|
||||
# password=my-auto-generated-password
|
||||
# my.domain.tld, otherhost.domain.tld
|
||||
# protocol=godaddy, \
|
||||
# password=my-godaddy-api-key, \
|
||||
# password=my-godaddy-secret, \
|
||||
# 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/)
|
||||
|
@ -227,6 +227,14 @@ ssl=yes # use ssl-support. Works with
|
|||
# password=my-token
|
||||
# myhost
|
||||
|
||||
##
|
||||
## DDNS.FM (https://ddns.fm/)
|
||||
##
|
||||
#
|
||||
# protocol=ddns.fm,
|
||||
# password=my-token
|
||||
# myhost.example.com
|
||||
|
||||
##
|
||||
## MyOnlinePortal (http://myonlineportal.net)
|
||||
##
|
||||
|
@ -243,23 +251,23 @@ ssl=yes # use ssl-support. Works with
|
|||
##
|
||||
## nsupdate.info IPV4(https://www.nsupdate.info)
|
||||
##
|
||||
#use=web, web=http://ipv4.nsupdate.info/myip
|
||||
#protocol=dyndns2
|
||||
#server=ipv4.nsupdate.info
|
||||
#login=domain.nsupdate.info
|
||||
#password='123'
|
||||
#domain.nsupdate.info
|
||||
# use=web, web=http://ipv4.nsupdate.info/myip
|
||||
# protocol=dyndns2
|
||||
# server=ipv4.nsupdate.info
|
||||
# login=domain.nsupdate.info
|
||||
# password='123'
|
||||
# domain.nsupdate.info
|
||||
|
||||
##
|
||||
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
||||
## ddclient releases <= 3.8.1 do not support IPv6
|
||||
##
|
||||
#usev6=if, if=eth0
|
||||
#protocol=dyndns2
|
||||
#server=ipv6.nsupdate.info
|
||||
#login=domain.nsupdate.info
|
||||
#password='123'
|
||||
#domain.nsupdate.info
|
||||
# usev6=if, if=eth0
|
||||
# protocol=dyndns2
|
||||
# server=ipv6.nsupdate.info
|
||||
# login=domain.nsupdate.info
|
||||
# password='123'
|
||||
# domain.nsupdate.info
|
||||
|
||||
##
|
||||
## Yandex.Mail for Domain (domain.yandex.com)
|
||||
|
@ -291,8 +299,9 @@ ssl=yes # use ssl-support. Works with
|
|||
# protocol=porkbun
|
||||
# apikey=APIKey
|
||||
# secretapikey=SecretAPIKey
|
||||
# root-domain=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)
|
||||
|
@ -312,17 +321,17 @@ ssl=yes # use ssl-support. Works with
|
|||
##
|
||||
## dnsexit (www.dnsexit.com)
|
||||
##
|
||||
#protocol=dnsexit, \
|
||||
#login=myusername, \
|
||||
#password=mypassword, \
|
||||
#subdomain-1.domain.com,subdomain-2.domain.com
|
||||
# protocol=dnsexit, \
|
||||
# login=myusername, \
|
||||
# password=mypassword, \
|
||||
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||
|
||||
##
|
||||
## dnsexit2 (API method www.dnsexit.com)
|
||||
##
|
||||
#protocol=dnsexit2
|
||||
#password=MyAPIKey
|
||||
#subdomain-1.domain.com,subdomain-2.domain.com
|
||||
# protocol=dnsexit2
|
||||
# password=MyAPIKey
|
||||
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||
|
||||
##
|
||||
## domeneshop (www.domeneshop.no)
|
||||
|
@ -358,10 +367,18 @@ ssl=yes # use ssl-support. Works with
|
|||
##
|
||||
## DigitalOcean (www.digitalocean.com)
|
||||
##
|
||||
#protocol=digitalocean, \
|
||||
#zone=example.com, \
|
||||
#password=api-token \
|
||||
#example.com,sub.example.com
|
||||
# protocol=digitalocean, \
|
||||
# zone=example.com, \
|
||||
# password=api-token \
|
||||
# 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)
|
||||
|
@ -370,3 +387,35 @@ ssl=yes # use ssl-support. Works with
|
|||
# login=ddns_username,
|
||||
# password=ddns_password
|
||||
# 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)
|
||||
##
|
||||
## 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
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
PIDFile=/run/ddclient.pid
|
||||
ExecStart=/usr/bin/ddclient
|
||||
Type=exec
|
||||
Environment=daemon_interval=5m
|
||||
ExecStart=/usr/bin/ddclient --daemon ${daemon_interval} --foreground
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
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;
|
||||
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||
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 {
|
||||
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);
|
||||
if ($interface) {
|
||||
plan(skip_all => 'no IPv4 interface') if !$interface;
|
||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
||||
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
||||
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)");
|
||||
}
|
||||
$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, "lo0", "Check for loopback 'lo0'");
|
||||
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
||||
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
||||
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)");
|
||||
}
|
||||
};
|
||||
|
|
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:
|
||||
# * 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.
|
||||
#
|
||||
# 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
|
||||
# terms as Perl itself.
|
||||
|
||||
|
@ -20,7 +23,7 @@ use Scalar::Util qw(blessed weaken);
|
|||
use Carp qw(croak);
|
||||
use Exporter qw(import);
|
||||
|
||||
our $VERSION = '0.08';
|
||||
our $VERSION = '0.09';
|
||||
$VERSION = eval $VERSION;
|
||||
|
||||
our @EXPORT = qw(
|
||||
|
@ -101,9 +104,10 @@ sub run {
|
|||
$self->port || '<default>',
|
||||
$@ eq '' ? '' : ": $@")) unless $d;
|
||||
|
||||
$d->accept; # wait for port check from parent process
|
||||
|
||||
while (my $c = $d->accept) {
|
||||
while (1) {
|
||||
# accept can return undef if TLS handshake fails (e.g., port test or client rejects
|
||||
# cert).
|
||||
my $c = $d->accept or next;
|
||||
while (my $req = $c->get_request) {
|
||||
my $res = $self->_to_http_res($app->($req));
|
||||
$c->send_response($res);
|
||||
|
@ -143,7 +147,7 @@ sub endpoint {
|
|||
my $self = shift;
|
||||
my $uri = URI->new($self->scheme . ':');
|
||||
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->port($self->port);
|
||||
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",
|
||||
},
|
||||
);
|
||||
|
||||
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('squoted escaped squote', "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) {
|
||||
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
|
||||
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); }
|
||||
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();
|
||||
|
|
|
@ -35,7 +35,7 @@ my @test_cases = (
|
|||
|
||||
for my $tc (@test_cases) {
|
||||
$warning = undef;
|
||||
ddclient::write_cache($tc->{f});
|
||||
ddclient::write_recap($tc->{f});
|
||||
subtest $tc->{name} => sub {
|
||||
if (defined($tc->{warning_regex})) {
|
||||
like($warning, $tc->{warning_regex}, "expected warning message");
|
Loading…
Reference in a new issue