Compare commits
791 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 | ||
![]() |
4a1b06630b | ||
![]() |
58c5e4dc3a | ||
![]() |
30f68e4098 | ||
![]() |
5d022b0520 | ||
![]() |
9145dc1bfd | ||
![]() |
3a224b66a4 | ||
![]() |
afa1275253 | ||
![]() |
23bfa31ea5 | ||
![]() |
66af014aad | ||
![]() |
456fe79c2d | ||
![]() |
ea8216fae4 | ||
![]() |
e2ce23ed33 | ||
![]() |
2d7610b7de | ||
![]() |
9913e0ec29 | ||
![]() |
8ed2963b79 | ||
![]() |
17828eedf5 | ||
![]() |
39bdce9bb6 | ||
![]() |
d4f9816a6a | ||
![]() |
33a5c1b07d | ||
![]() |
15f9472ac9 | ||
![]() |
ef496d108f | ||
![]() |
1715bd3a17 | ||
![]() |
d4db5cbc90 | ||
![]() |
7869acb266 | ||
![]() |
17fd6ec083 | ||
![]() |
6c1f9632fa | ||
![]() |
541e7f350c | ||
![]() |
5af1550e62 | ||
![]() |
e37cf19350 | ||
![]() |
379b1832de | ||
![]() |
3678befe76 | ||
![]() |
b8bed7112f | ||
![]() |
fbc64243f3 | ||
![]() |
a9c1e545fb | ||
![]() |
211404b5d1 | ||
![]() |
52b5eea6f4 | ||
![]() |
2af841acdb | ||
![]() |
ae905b3baf | ||
![]() |
ec4d83bc3f | ||
![]() |
a4eab34ab4 | ||
![]() |
2536a5e37a | ||
![]() |
3136871720 | ||
![]() |
d3d7bda8ca | ||
![]() |
218e1a9974 | ||
![]() |
bbed067864 | ||
![]() |
82af20f8d4 | ||
![]() |
1c91a5aa84 | ||
![]() |
841ffcbdaa | ||
![]() |
aedf6f866b | ||
![]() |
038ac6643c | ||
![]() |
92604ab78d | ||
![]() |
6312030c57 | ||
![]() |
a1e68a5b21 | ||
![]() |
e92c54f6f9 | ||
![]() |
419716fd22 | ||
![]() |
c436a34ede | ||
![]() |
2ef926c577 | ||
![]() |
b49bd9eda9 | ||
![]() |
407f0c6350 | ||
![]() |
4458cceb1b | ||
![]() |
07fbbeb5bb | ||
![]() |
b225d37528 | ||
![]() |
36de4e0b88 | ||
![]() |
611b41e7a6 | ||
![]() |
d3a353990b | ||
![]() |
8f7ddce2ec | ||
![]() |
c5a956e386 | ||
![]() |
65a1bcc7d9 | ||
![]() |
9fccfde9e5 | ||
![]() |
76e441c542 | ||
![]() |
8bbafa5988 | ||
![]() |
999dab7f53 | ||
![]() |
5596ee83d1 | ||
![]() |
dbc40557d2 | ||
![]() |
d35d62f3e7 | ||
![]() |
11a5bd5e7e | ||
![]() |
cca4291360 | ||
![]() |
fa6c95f511 | ||
![]() |
e910204b3d | ||
![]() |
8d3a383587 | ||
![]() |
5fcf4b37f2 | ||
![]() |
63a78a7f46 | ||
![]() |
086093839d | ||
![]() |
ff48a0ccb9 | ||
![]() |
3149171aa7 | ||
![]() |
fcd3ef8b5a | ||
![]() |
10041d225c | ||
![]() |
c4ff6c12bd | ||
![]() |
c747f2737a | ||
![]() |
1a6e4431ab | ||
![]() |
89e30d0aa8 | ||
![]() |
f01110aedb | ||
![]() |
3dffe5372a | ||
![]() |
e8cc636474 | ||
![]() |
d13469fde5 | ||
![]() |
36744e5b20 | ||
![]() |
ce5dfe3501 |
59 changed files with 7617 additions and 5227 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"
|
122
.github/workflows/ci.yml
vendored
122
.github/workflows/ci.yml
vendored
|
@ -6,10 +6,11 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
test-debian-like:
|
test-debian-like:
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
image:
|
image:
|
||||||
- ubuntu:latest
|
- ubuntu:latest
|
||||||
- ubuntu:16.04
|
- ubuntu:20.04
|
||||||
- debian:testing
|
- debian:testing
|
||||||
- debian:stable
|
- debian:stable
|
||||||
- debian:oldstable
|
- debian:oldstable
|
||||||
|
@ -24,19 +25,19 @@ jobs:
|
||||||
automake \
|
automake \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
git \
|
git \
|
||||||
|
curl \
|
||||||
libhttp-daemon-perl \
|
libhttp-daemon-perl \
|
||||||
libhttp-daemon-ssl-perl \
|
libhttp-daemon-ssl-perl \
|
||||||
libio-socket-inet6-perl \
|
|
||||||
libio-socket-ip-perl \
|
|
||||||
libplack-perl \
|
libplack-perl \
|
||||||
libtest-mockmodule-perl \
|
libtest-mockmodule-perl \
|
||||||
libtest-tcp-perl \
|
libtest-tcp-perl \
|
||||||
libtest-warnings-perl \
|
libtest-warnings-perl \
|
||||||
liburi-perl \
|
liburi-perl \
|
||||||
|
libwww-perl \
|
||||||
net-tools \
|
net-tools \
|
||||||
make \
|
make \
|
||||||
;
|
;
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: autogen
|
- name: autogen
|
||||||
run: ./autogen
|
run: ./autogen
|
||||||
- name: configure
|
- name: configure
|
||||||
|
@ -47,64 +48,47 @@ jobs:
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
||||||
- name: distribution tarball is complete
|
- name: distribution tarball is complete
|
||||||
run: ./.github/workflows/scripts/dist-tarball-check
|
run: ./.github/workflows/scripts/dist-tarball-check
|
||||||
|
- if: ${{ matrix.image == 'debian:testing' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: distribution-tarball
|
||||||
|
path: ddclient-*.tar.gz
|
||||||
|
|
||||||
#test-centos6:
|
test-fedora-like:
|
||||||
# runs-on: ubuntu-latest
|
strategy:
|
||||||
# container: centos:6
|
fail-fast: false
|
||||||
# steps:
|
matrix:
|
||||||
# - uses: actions/checkout@v1
|
image:
|
||||||
# - name: install dependencies
|
- fedora:39
|
||||||
# run: |
|
- fedora:latest
|
||||||
# yum install -y \
|
- fedora:rawhide
|
||||||
# automake \
|
- almalinux:8
|
||||||
# perl-IO-Socket-INET6 \
|
- almalinux:latest
|
||||||
# perl-core \
|
|
||||||
# perl-libwww-perl \
|
|
||||||
# ;
|
|
||||||
# - 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-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:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: fedora
|
container:
|
||||||
|
image: ${{ matrix.image }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: install dependencies
|
- name: enable repositories (AlmaLinux 8)
|
||||||
|
if: ${{ matrix.image == 'almalinux:8' }}
|
||||||
run: |
|
run: |
|
||||||
dnf --refresh install -y \
|
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
|
||||||
|
dnf config-manager --set-enabled powertools
|
||||||
|
- name: enable repositories (AlmaLinux latest)
|
||||||
|
if: ${{ matrix.image == 'almalinux:latest' }}
|
||||||
|
run: |
|
||||||
|
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
|
||||||
|
dnf config-manager --set-enabled crb
|
||||||
|
- name: install dependencies
|
||||||
|
# The --skip-broken argument works around missing packages. (They're
|
||||||
|
# only used for testing, so it's OK to not install them.)
|
||||||
|
run: |
|
||||||
|
dnf --refresh install --skip-broken -y \
|
||||||
automake \
|
automake \
|
||||||
findutils \
|
findutils \
|
||||||
|
iproute \
|
||||||
make \
|
make \
|
||||||
|
curl \
|
||||||
perl \
|
perl \
|
||||||
perl-HTTP-Daemon \
|
perl-HTTP-Daemon \
|
||||||
perl-HTTP-Daemon-SSL \
|
perl-HTTP-Daemon-SSL \
|
||||||
|
@ -113,6 +97,8 @@ jobs:
|
||||||
perl-Test-MockModule \
|
perl-Test-MockModule \
|
||||||
perl-Test-TCP \
|
perl-Test-TCP \
|
||||||
perl-Test-Warnings \
|
perl-Test-Warnings \
|
||||||
|
perl-core \
|
||||||
|
perl-libwww-perl \
|
||||||
net-tools \
|
net-tools \
|
||||||
;
|
;
|
||||||
- name: autogen
|
- name: autogen
|
||||||
|
@ -123,29 +109,3 @@ jobs:
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
||||||
- name: distcheck
|
- name: distcheck
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
||||||
|
|
||||||
test-redhat-ubi7:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# we use redhats univeral base image which is not available on docker hub
|
|
||||||
# https://catalog.redhat.com/software/containers/ubi7/ubi/5c3592dcd70cc534b3a37814
|
|
||||||
container: registry.access.redhat.com/ubi7/ubi
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: install dependencies
|
|
||||||
run: |
|
|
||||||
yum install -y \
|
|
||||||
automake \
|
|
||||||
make \
|
|
||||||
perl-HTTP-Daemon \
|
|
||||||
perl-IO-Socket-INET6 \
|
|
||||||
perl-core \
|
|
||||||
iproute \
|
|
||||||
;
|
|
||||||
- name: autogen
|
|
||||||
run: ./autogen
|
|
||||||
- name: configure
|
|
||||||
run: ./configure
|
|
||||||
- name: check
|
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always check
|
|
||||||
- name: distcheck
|
|
||||||
run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
|
|
||||||
|
|
49
.github/workflows/pr.yml
vendored
Normal file
49
.github/workflows/pr.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
name: Pull Request
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- unlabeled
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linear-history:
|
||||||
|
if: ${{ !contains(github.event.pull_request.labels.*.name, 'pr-permit-nonlinear') }}
|
||||||
|
name: Linear History
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: No new merge commits
|
||||||
|
run: |
|
||||||
|
log() { printf %s\\n "$*" >&2; }
|
||||||
|
error() { log "ERROR: $@"; }
|
||||||
|
fatal() { error "$@"; exit 1; }
|
||||||
|
try() { log "Running command $@"; "$@" || fatal "'$@' failed"; }
|
||||||
|
out=$(try git rev-list -n 1 --merges '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}') || exit 1
|
||||||
|
[ -z "${out}" ] || {
|
||||||
|
error "pull request includes a merge commit and does not have the 'pr-permit-nonlinear' label"
|
||||||
|
git show "${out}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
no-autosquash:
|
||||||
|
if: ${{ !contains(github.event.pull_request.labels.*.name, 'pr-permit-autosquash') }}
|
||||||
|
name: No --autosquash commits
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: 'No commits with messages starting with "fixup!", "squash!", or "amend!"'
|
||||||
|
run: |
|
||||||
|
log() { printf %s\\n "$*" >&2; }
|
||||||
|
error() { log "ERROR: $@"; }
|
||||||
|
fatal() { error "$@"; exit 1; }
|
||||||
|
try() { log "Running command $@"; "$@" || fatal "'$@' failed"; }
|
||||||
|
out=$(try git log --oneline '${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}') || exit 1
|
||||||
|
! grep -E '^[^ ]* (fixup|squash|amend)!' <<EOF || fatal "--autosquash commits not allowed without the 'pr-permit-autosquash' label"
|
||||||
|
${out}
|
||||||
|
EOF
|
1
.github/workflows/scripts/dist-tarball-check
vendored
1
.github/workflows/scripts/dist-tarball-check
vendored
|
@ -34,6 +34,7 @@ try git archive --format=tar --prefix=git-repo/ HEAD \
|
||||||
.github \
|
.github \
|
||||||
.gitignore \
|
.gitignore \
|
||||||
docs/ipv6-design-doc.md \
|
docs/ipv6-design-doc.md \
|
||||||
|
docs/ProviderGuidelines.md \
|
||||||
shell.nix \
|
shell.nix \
|
||||||
;
|
;
|
||||||
# TODO: Delete this next line once support for Automake 1.11 is dropped and
|
# TODO: Delete this next line once support for Automake 1.11 is dropped and
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -7,8 +7,11 @@ release
|
||||||
/Makefile.in
|
/Makefile.in
|
||||||
/aclocal.m4
|
/aclocal.m4
|
||||||
/autom4te.cache/
|
/autom4te.cache/
|
||||||
|
/build-aux/config.guess
|
||||||
|
/build-aux/config.sub
|
||||||
/build-aux/install-sh
|
/build-aux/install-sh
|
||||||
/build-aux/missing
|
/build-aux/missing
|
||||||
|
/build-aux/tap-driver.sh
|
||||||
/config.log
|
/config.log
|
||||||
/config.status
|
/config.status
|
||||||
/configure
|
/configure
|
||||||
|
|
|
@ -80,7 +80,7 @@ perltidy -l=99 -conv -ci=4 -ola -ce -nbbc -kis -pt=2 -b ddclient
|
||||||
|
|
||||||
## Git Hygiene
|
## Git Hygiene
|
||||||
|
|
||||||
* Please keep your pull request commits rebased on top of master.
|
* Please keep your pull request commits rebased on top of `main`.
|
||||||
* Please use `git rebase -i` to make your commits easy to review:
|
* Please use `git rebase -i` to make your commits easy to review:
|
||||||
- Put unrelated changes in separate commits
|
- Put unrelated changes in separate commits
|
||||||
- Squash your fixup commits
|
- Squash your fixup commits
|
||||||
|
@ -190,11 +190,11 @@ better to revert the original change then redo it:
|
||||||
|
|
||||||
### Merging Pull Requests
|
### Merging Pull Requests
|
||||||
|
|
||||||
To facilitate reviews and code archaeology, `master` should have a
|
To facilitate reviews and code archaeology, `main` should have a
|
||||||
semi-linear commit history like this:
|
semi-linear commit history like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
* f4e6e90 sandro.jaeckel@gmail.com 2020-05-31 07:29:51 +0200 (master)
|
* f4e6e90 sandro.jaeckel@gmail.com 2020-05-31 07:29:51 +0200 (main)
|
||||||
|\ Merge pull request #142 from rhansen/config-line-format
|
|\ Merge pull request #142 from rhansen/config-line-format
|
||||||
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|
| * 30180ed rhansen@rhansen.org 2020-05-30 13:09:38 -0400
|
||||||
|/ Expand comment documenting config line format
|
|/ Expand comment documenting config line format
|
||||||
|
@ -231,7 +231,7 @@ has value:
|
||||||
change was made) and the merge timestamp (when it went live).
|
change was made) and the merge timestamp (when it went live).
|
||||||
|
|
||||||
To achieve a history like the above, the pull request must be rebased
|
To achieve a history like the above, the pull request must be rebased
|
||||||
onto `master` before merging. Unfortunately, GitHub does not have a
|
onto `main` before merging. Unfortunately, GitHub does not have a
|
||||||
one-click way to do this (the "Rebase and merge" option does a
|
one-click way to do this (the "Rebase and merge" option does a
|
||||||
fast-forward merge, which is not what we want). See
|
fast-forward merge, which is not what we want). See
|
||||||
[isaacs/github#1143](https://github.com/isaacs/github/issues/1143) and
|
[isaacs/github#1143](https://github.com/isaacs/github/issues/1143) and
|
||||||
|
@ -254,15 +254,15 @@ git remote set-url origin git@github.com:ddclient/ddclient.git
|
||||||
# Add a remote for the fork used in the PR
|
# Add a remote for the fork used in the PR
|
||||||
git remote add "${PR_USER:?}" git@github.com:"${PR_USER:?}"/ddclient
|
git remote add "${PR_USER:?}" git@github.com:"${PR_USER:?}"/ddclient
|
||||||
|
|
||||||
# Fetch the latest commits for the PR and ddclient master
|
# Fetch the latest commits for the PR and ddclient main
|
||||||
git remote update -p
|
git remote update -p
|
||||||
|
|
||||||
# Switch to the pull request branch
|
# Switch to the pull request branch
|
||||||
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
|
git checkout -b "${PR_USER:?}-${PR_BRANCH:?}" "${PR_USER:?}/${PR_BRANCH:?}"
|
||||||
|
|
||||||
# Rebase the commits (optionally using -i to clean up history) onto
|
# Rebase the commits (optionally using -i to clean up history) onto
|
||||||
# the current ddclient master branch
|
# the current ddclient main branch
|
||||||
git rebase origin/master
|
git rebase origin/main
|
||||||
|
|
||||||
# Force update the contributor's fork. This will only work if the
|
# Force update the contributor's fork. This will only work if the
|
||||||
# contributor has checked the "Allow edits by maintainers" box in the
|
# contributor has checked the "Allow edits by maintainers" box in the
|
||||||
|
@ -276,19 +276,19 @@ git push -f
|
||||||
# "Allow edits by maintainers", or if you prefer to merge manually,
|
# "Allow edits by maintainers", or if you prefer to merge manually,
|
||||||
# continue with the next steps.
|
# continue with the next steps.
|
||||||
|
|
||||||
# Switch to the local master branch
|
# Switch to the local main branch
|
||||||
git checkout master
|
git checkout main
|
||||||
|
|
||||||
# Make sure the local master branch is up to date
|
# Make sure the local main branch is up to date
|
||||||
git merge --ff-only origin/master
|
git merge --ff-only origin/main
|
||||||
|
|
||||||
# Merge in the rebased pull request branch **WITHOUT DOING A
|
# Merge in the rebased pull request branch **WITHOUT DOING A
|
||||||
# FAST-FORWARD MERGE**
|
# FAST-FORWARD MERGE**
|
||||||
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
|
git merge --no-ff "${PR_USER:?}-${PR_BRANCH:?}"
|
||||||
|
|
||||||
# Review the commits before pushing
|
# Review the commits before pushing
|
||||||
git log --graph --oneline --decorate origin/master..
|
git log --graph --oneline --decorate origin/main..
|
||||||
|
|
||||||
# Push to ddclient master
|
# Push to ddclient main
|
||||||
git push origin master
|
git push origin main
|
||||||
```
|
```
|
||||||
|
|
265
ChangeLog.md
265
ChangeLog.md
|
@ -1,17 +1,278 @@
|
||||||
# ChangeLog
|
# ChangeLog
|
||||||
|
|
||||||
This document describes notable changes. For details, see the [source code
|
This document describes notable changes. For details, see the [source code
|
||||||
repository history](https://github.com/ddclient/ddclient/commits/master).
|
repository history](https://github.com/ddclient/ddclient/commits/main).
|
||||||
|
|
||||||
|
## v4.0.1-alpha (unreleased work-in-progress)
|
||||||
|
|
||||||
|
## 2025-01-19 v4.0.0
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
* ddclient now looks for `ddclient.conf` in `${sysconfdir}/ddclient` by
|
||||||
|
default instead of `${sysconfdir}`.
|
||||||
|
[#789](https://github.com/ddclient/ddclient/pull/789)
|
||||||
|
|
||||||
|
To retain the previous behavior, pass `'--with-confdir=${sysconfdir}'` to
|
||||||
|
`configure`. For example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Before v4.0.0:
|
||||||
|
./configure --sysconfdir=/etc
|
||||||
|
# Equivalent with v4.0.0 and later (the single quotes are intentional):
|
||||||
|
./configure --sysconfdir=/etc --with-confdir='${sysconfdir}'
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Before v4.0.0:
|
||||||
|
./configure --sysconfdir=/etc/ddclient
|
||||||
|
# Equivalent with v4.0.0 and later:
|
||||||
|
./configure --sysconfdir=/etc
|
||||||
|
```
|
||||||
|
|
||||||
|
* The `--ssl` option is now enabled by default.
|
||||||
|
[#705](https://github.com/ddclient/ddclient/pull/705)
|
||||||
|
* Unencrypted (plain) HTTP is now used instead of encrypted (TLS) HTTP if the
|
||||||
|
URL uses `http://` instead of `https://`, even if the `--ssl` option is
|
||||||
|
enabled. [#608](https://github.com/ddclient/ddclient/pull/608)
|
||||||
|
* The string argument to `--cmdv4` or `--cmdv6` is now executed as-is by the
|
||||||
|
system's shell, matching the behavior of the deprecated `--cmd` option.
|
||||||
|
This makes it possible to pass command-line arguments, which reduces the
|
||||||
|
need for a custom wrapper script. Beware that the string is also subject to
|
||||||
|
the shell's command substitution, quote handling, variable expansion, field
|
||||||
|
splitting, etc., so you may need to add extra escaping to ensure that any
|
||||||
|
special characters are preserved literally.
|
||||||
|
[#766](https://github.com/ddclient/ddclient/pull/766)
|
||||||
|
* The default web service for `--webv4` and `--webv6` has changed from Google
|
||||||
|
Domains (which has shut down) to ipify.
|
||||||
|
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
|
||||||
|
* Invalid command-line options or values are now fatal errors (instead of
|
||||||
|
discarded with a warning).
|
||||||
|
[#733](https://github.com/ddclient/ddclient/pull/733)
|
||||||
|
* All log messages are now written to STDERR, not a mix of STDOUT and STDERR.
|
||||||
|
[#676](https://github.com/ddclient/ddclient/pull/676)
|
||||||
|
* For `--protocol=freedns` and `--protocol=nfsn`, the core module
|
||||||
|
`Digest::SHA` is now required. Previously, `Digest::SHA1` was used (if
|
||||||
|
available) as an alternative to `Digest::SHA`.
|
||||||
|
[#685](https://github.com/ddclient/ddclient/pull/685)
|
||||||
|
* The `he` built-in web IP discovery service (`--webv4=he`, `--webv6=he`, and
|
||||||
|
`--web=he`) was renamed to `he.net` for consistency with the new `he.net`
|
||||||
|
protocol. The old name is still accepted but is deprecated and will be
|
||||||
|
removed in a future version of ddclient.
|
||||||
|
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||||
|
* Deprecated built-in web IP discovery services are not listed in the output
|
||||||
|
of `--list-web-services`.
|
||||||
|
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||||
|
* `dyndns2`: Support for "wait" response lines has been removed. The Dyn
|
||||||
|
documentation does not mention such responses, and the code to handle them,
|
||||||
|
untouched since at least 2006, is believed to be obsolete.
|
||||||
|
[#709](https://github.com/ddclient/ddclient/pull/709)
|
||||||
|
* `dyndns2`: The obsolete `static` and `custom` options have been removed.
|
||||||
|
Setting the options may produce a warning.
|
||||||
|
[#709](https://github.com/ddclient/ddclient/pull/709)
|
||||||
|
* The diagnostic `--geturl` command-line argument was removed.
|
||||||
|
[#712](https://github.com/ddclient/ddclient/pull/712)
|
||||||
|
* `easydns`: The default value for `min-interval` was increased from 5m to 10m
|
||||||
|
to match easyDNS documentation.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* `woima`: The dyn.woima.fi service appears to be defunct so support was
|
||||||
|
removed. [#716](https://github.com/ddclient/ddclient/pull/716)
|
||||||
|
* `googledomains`: Support was removed because the service shut down.
|
||||||
|
[#716](https://github.com/ddclient/ddclient/pull/716)
|
||||||
|
* The `--retry` option was removed.
|
||||||
|
[#732](https://github.com/ddclient/ddclient/pull/732)
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
* New `--mail-from` option to control the "From:" header of email messages.
|
||||||
|
[#565](https://github.com/ddclient/ddclient/pull/565)
|
||||||
|
* Simultaneous/separate updating of IPv4 (A) records and IPv6 (AAAA) records
|
||||||
|
is now supported in the following services: `gandi`
|
||||||
|
([#558](https://github.com/ddclient/ddclient/pull/558)), `nsupdate`
|
||||||
|
([#604](https://github.com/ddclient/ddclient/pull/604)), `noip`
|
||||||
|
([#603](https://github.com/ddclient/ddclient/pull/603)), `mythicdyn`
|
||||||
|
([#616](https://github.com/ddclient/ddclient/pull/616)), `godaddy`
|
||||||
|
([#560](https://github.com/ddclient/ddclient/pull/560)).
|
||||||
|
* `porkbun`: Added support for subdomains.
|
||||||
|
[#624](https://github.com/ddclient/ddclient/pull/624)
|
||||||
|
* `gandi`: Added support for personal access tokens.
|
||||||
|
[#636](https://github.com/ddclient/ddclient/pull/636)
|
||||||
|
* Comments after the `\` line continuation character are now supported.
|
||||||
|
[3c522a7a](https://github.com/ddclient/ddclient/commit/3c522a7aa235f63ae0439e5674e7406e20c90956)
|
||||||
|
* Minor improvements to `--help` output.
|
||||||
|
[#659](https://github.com/ddclient/ddclient/pull/659),
|
||||||
|
[#665](https://github.com/ddclient/ddclient/pull/665)
|
||||||
|
* Improved formatting of ddclient's version number.
|
||||||
|
[#639](https://github.com/ddclient/ddclient/pull/639)
|
||||||
|
* Updated sample systemd service unit file to improve logging in the systemd
|
||||||
|
journal. [#669](https://github.com/ddclient/ddclient/pull/669)
|
||||||
|
* The second and subsequent lines in a multi-line log message now have a
|
||||||
|
different prefix to distinguish them from separate log messages.
|
||||||
|
[#676](https://github.com/ddclient/ddclient/pull/676)
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* Log messages now include context, making it easier to troubleshoot issues.
|
||||||
|
[#725](https://github.com/ddclient/ddclient/pull/725)
|
||||||
|
* `emailonly`: New `protocol` option that simply emails you when your IP
|
||||||
|
address changes. [#654](https://github.com/ddclient/ddclient/pull/654)
|
||||||
|
* `he.net`: Added support for updating Hurricane Electric records.
|
||||||
|
[#682](https://github.com/ddclient/ddclient/pull/682)
|
||||||
|
* `dyndns2`, `domeneshop`, `dnsmadeeasy`, `keysystems`: The `server` option
|
||||||
|
can now include `http://` or `https://` to control the use of TLS. If
|
||||||
|
omitted, the value of the `ssl` option is used to determine the scheme.
|
||||||
|
[#703](https://github.com/ddclient/ddclient/pull/703)
|
||||||
|
* `ddns.fm`: New `protocol` option for updating [DDNS.FM](https://ddns.fm/)
|
||||||
|
records. [#695](https://github.com/ddclient/ddclient/pull/695)
|
||||||
|
* `inwx`: New `protocol` option for updating [INWX](https://www.inwx.com/)
|
||||||
|
records. [#690](https://github.com/ddclient/ddclient/pull/690)
|
||||||
|
* `domeneshop`: Add IPv6 support.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `duckdns`: Multiple hosts with the same IP address are now updated together.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `directnic`: Added support for updatng Directnic records.
|
||||||
|
[#726](https://github.com/ddclient/ddclient/pull/726)
|
||||||
|
* `porkbun`: The update URL hostname is now configurable via the `server`
|
||||||
|
option. [#752](https://github.com/ddclient/ddclient/pull/752)
|
||||||
|
* `dnsexit2`: Multiple hosts are updated in a single API call when possible.
|
||||||
|
[#684](https://github.com/ddclient/ddclient/pull/684)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
* Fixed numerous bugs in cache file (recap) handling.
|
||||||
|
[#740](https://github.com/ddclient/ddclient/pull/740)
|
||||||
|
* Fixed numerous bugs in command-line option and configuration file
|
||||||
|
processing. [#733](https://github.com/ddclient/ddclient/pull/733)
|
||||||
|
* `noip`: Fixed failure to honor IP discovery settings in some circumstances.
|
||||||
|
[#591](https://github.com/ddclient/ddclient/pull/591)
|
||||||
|
* Fixed `--usev6` with providers that have not yet been updated to use the new
|
||||||
|
separate IPv4/IPv6 logic.
|
||||||
|
[ad854ab7](https://github.com/ddclient/ddclient/commit/ad854ab716922f5f25742421ebd4c27646b86619)
|
||||||
|
* HTTP redirects (301, 302) are now followed.
|
||||||
|
[#592](https://github.com/ddclient/ddclient/pull/592)
|
||||||
|
* `keysystems`: Fixed update URL.
|
||||||
|
[#629](https://github.com/ddclient/ddclient/pull/629)
|
||||||
|
* `dondominio`: Fixed response parsing.
|
||||||
|
[#646](https://github.com/ddclient/ddclient/pull/646)
|
||||||
|
* Fixed `--web-ssl-validate` and `--fw-ssl-validate` options, which were
|
||||||
|
ignored in some cases (defaulting to validate).
|
||||||
|
[#661](https://github.com/ddclient/ddclient/pull/661)
|
||||||
|
* Explicitly setting `--web-skip`, `--webv4-skip`, `--webv6-skip`,
|
||||||
|
`--fw-skip`, `--fwv4-skip`, and `--fwv6-skip` to the empty string now
|
||||||
|
disables any built-in default skip. Before, setting to the empty string had
|
||||||
|
no effect. [#662](https://github.com/ddclient/ddclient/pull/662)
|
||||||
|
* `--use=disabled` now works.
|
||||||
|
[#665](https://github.com/ddclient/ddclient/pull/665)
|
||||||
|
* `--retry` and `--daemon` are incompatible with each other; ddclient now
|
||||||
|
errors out if both are provided.
|
||||||
|
[#666](https://github.com/ddclient/ddclient/pull/666)
|
||||||
|
* `--usev4=cisco` and `--usev4=cisco-asa` now work.
|
||||||
|
[#664](https://github.com/ddclient/ddclient/pull/664)
|
||||||
|
* Fixed "Scalar value better written as" Perl warning.
|
||||||
|
[#667](https://github.com/ddclient/ddclient/pull/667)
|
||||||
|
* Fixed "Invalid Value for keyword 'wtime' = ''" warning.
|
||||||
|
[#734](https://github.com/ddclient/ddclient/pull/734)
|
||||||
|
* Fixed unnecessary repeated updates for some services.
|
||||||
|
[#670](https://github.com/ddclient/ddclient/pull/670)
|
||||||
|
[#732](https://github.com/ddclient/ddclient/pull/732)
|
||||||
|
* Fixed DNSExit provider when configured with a zone and non-identical
|
||||||
|
hostname. [#674](https://github.com/ddclient/ddclient/pull/674)
|
||||||
|
* `infomaniak`: Fixed frequent forced updates after 25 days (`max-interval`).
|
||||||
|
[#691](https://github.com/ddclient/ddclient/pull/691)
|
||||||
|
* `infomaniak`: Fixed incorrect parsing of server response.
|
||||||
|
[#692](https://github.com/ddclient/ddclient/pull/692)
|
||||||
|
* `infomaniak`: Fixed incorrect handling of `nochg` responses.
|
||||||
|
[#723](https://github.com/ddclient/ddclient/pull/723)
|
||||||
|
* `regfishde`: Fixed IPv6 support.
|
||||||
|
[#691](https://github.com/ddclient/ddclient/pull/691)
|
||||||
|
* `easydns`: IPv4 and IPv6 addresses are now updated separately to be
|
||||||
|
consistent with the easyDNS documentation.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* `easydns`: Fixed parsing of result code from server response.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* `easydns`: Fixed successful updates treated as failed updates.
|
||||||
|
[#713](https://github.com/ddclient/ddclient/pull/713)
|
||||||
|
* Any IP addresses in an HTTP response's headers or in an HTTP error
|
||||||
|
response's body are now ignored when obtaining the IP address from a
|
||||||
|
web-based IP discovery service (`--usev4=webv4`, `--usev6=webv6`) or from a
|
||||||
|
router/firewall device.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `yandex`: Errors are now retried.
|
||||||
|
[#719](https://github.com/ddclient/ddclient/pull/719)
|
||||||
|
* `gandi`: Fixed handling of error responses.
|
||||||
|
[#721](https://github.com/ddclient/ddclient/pull/721)
|
||||||
|
* `dyndns2`: Fixed handling of responses for multi-host updates.
|
||||||
|
[#728](https://github.com/ddclient/ddclient/pull/728)
|
||||||
|
* `porkbun`: The default update URL was updated from `porkbun.com` to
|
||||||
|
`api.porkbun.com`. [#752](https://github.com/ddclient/ddclient/pull/752)
|
||||||
|
|
||||||
|
## 2023-11-23 v3.11.2
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
* Fixed simultaneous IPv4 and IPv6 updates for provider duckdns
|
||||||
|
* Fixed caching issues for new providers when using the old 'use' config parameter
|
||||||
|
|
||||||
|
## 2023-10-25 v3.11.1
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
* Fixed simultaneous IPv4 and IPv6 updates for provider porkbun
|
||||||
|
* Removed @PACKAGE_VERSION@ placeholder in ddclient.in for now
|
||||||
|
to allow downstream to adopt the proper build process first.
|
||||||
|
See [here](https://github.com/ddclient/ddclient/issues/579) for the discussion.
|
||||||
|
|
||||||
|
## 2023-10-21 v3.11.0
|
||||||
|
This version is the same as v3.11.0_1 (except for the updated version number in the code).
|
||||||
|
Refer to [v3.11 release plan discussions](https://github.com/ddclient/ddclient/issues/552) for the reasons.
|
||||||
|
|
||||||
|
## 2023-10-15 v3.11.0_1
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
* ddclient now requires curl. The Perl modules IO::Socket::IP and IO::Socket::SSL are no longer used.
|
||||||
|
* ddclient no longer ships any example files for init systems that use `/etc/init.d`.
|
||||||
|
This was done because those files where effectively unmaintained, untested by the developers and only updated by downstream distros.
|
||||||
|
If you where relying on those files, please copy them into your packaging.
|
||||||
|
* The defunct `dnsexit` protocol is removed (replaced by `dnsexit2`).
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
* Introduced `usev4` and `usev6` for separate IPv4/IPv6 configuration. These will replace the legacy `use` eventually.
|
||||||
|
* Added support for moving secrets out of the configuration through environment variables
|
||||||
|
* Extended postscript mechanism
|
||||||
|
* sample-get-ip-from-fritzbox: Added environment variable to override hostname
|
||||||
|
* Warn about hosts where no IP could be determined - and skip the (bogus) update.
|
||||||
|
|
||||||
|
### Provider updates:
|
||||||
|
* Added regfish
|
||||||
|
* Added domeneshop.no
|
||||||
|
* Added Mythic Beasts
|
||||||
|
* Added Porkbun
|
||||||
|
* Added Enom
|
||||||
|
* Added DigitalOcean
|
||||||
|
* Added Infomaniak
|
||||||
|
* Added DNSExit API v2
|
||||||
|
* Removed old DNSExit API
|
||||||
|
* Extended EasyDNS to support IPv6
|
||||||
|
* Extended duckdns to support IPv6
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
* Fixed various issues with caching
|
||||||
|
* Fixed issues with Hetzner zones
|
||||||
|
* The OVH provider now ignores extra data returned
|
||||||
|
* Merge multiple configs for the same hostname instead of use the last
|
||||||
|
|
||||||
## 2022-10-20 v3.10.0
|
## 2022-10-20 v3.10.0
|
||||||
|
|
||||||
### New features
|
### New features
|
||||||
|
|
||||||
* Added support for domaindiscount24.com
|
* Added support for domaindiscount24.com
|
||||||
* Added support for njal.la
|
* Added support for njal.la
|
||||||
|
|
||||||
## 2022-05-15 v3.10.0_2
|
## 2022-05-15 v3.10.0_2
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
* Fix version number being unable to parse
|
* Fix version number being unable to parse
|
||||||
|
|
||||||
## 2022-05-15 v3.10.0_1
|
## 2022-05-15 v3.10.0_1
|
||||||
|
|
60
Makefile.am
60
Makefile.am
|
@ -1,4 +1,4 @@
|
||||||
ACLOCAL_AMFLAGS = -I m4
|
ACLOCAL_AMFLAGS = -I build-aux/m4
|
||||||
EXTRA_DIST = \
|
EXTRA_DIST = \
|
||||||
CONTRIBUTING.md \
|
CONTRIBUTING.md \
|
||||||
COPYING \
|
COPYING \
|
||||||
|
@ -6,36 +6,17 @@ EXTRA_DIST = \
|
||||||
ChangeLog.md \
|
ChangeLog.md \
|
||||||
README.cisco \
|
README.cisco \
|
||||||
README.md \
|
README.md \
|
||||||
README.ssl \
|
|
||||||
autogen \
|
autogen \
|
||||||
sample-ddclient-wrapper.sh \
|
sample-ddclient-wrapper.sh \
|
||||||
sample-etc_cron.d_ddclient \
|
sample-etc_cron.d_ddclient \
|
||||||
sample-etc_dhclient-exit-hooks \
|
sample-etc_dhclient-exit-hooks \
|
||||||
sample-etc_dhcpc_dhcpcd-eth0.exe \
|
sample-etc_dhcpc_dhcpcd-eth0.exe \
|
||||||
sample-etc_ppp_ip-up.local \
|
sample-etc_ppp_ip-up.local \
|
||||||
sample-etc_rc.d_ddclient.freebsd \
|
|
||||||
sample-etc_rc.d_init.d_ddclient \
|
|
||||||
sample-etc_rc.d_init.d_ddclient.alpine \
|
|
||||||
sample-etc_rc.d_init.d_ddclient.lsb \
|
|
||||||
sample-etc_rc.d_init.d_ddclient.redhat \
|
|
||||||
sample-etc_rc.d_init.d_ddclient.ubuntu \
|
|
||||||
sample-etc_systemd.service \
|
sample-etc_systemd.service \
|
||||||
sample-get-ip-from-fritzbox
|
sample-get-ip-from-fritzbox
|
||||||
CLEANFILES =
|
CLEANFILES =
|
||||||
|
|
||||||
# Command that replaces substitution variables with their values.
|
|
||||||
subst = sed \
|
|
||||||
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
|
|
||||||
-e '1 s|^\#\!.*perl$$|\#\!$(PERL)|g' \
|
|
||||||
-e 's|@localstatedir[@]|$(localstatedir)|g' \
|
|
||||||
-e 's|@runstatedir[@]|$(runstatedir)|g' \
|
|
||||||
-e 's|@sysconfdir[@]|$(sysconfdir)|g' \
|
|
||||||
-e 's|@CURL[@]|$(CURL)|g'
|
|
||||||
|
|
||||||
# Files that will be generated by passing their *.in file through
|
|
||||||
# $(subst).
|
|
||||||
subst_files = ddclient ddclient.conf
|
subst_files = ddclient ddclient.conf
|
||||||
|
|
||||||
EXTRA_DIST += $(subst_files:=.in)
|
EXTRA_DIST += $(subst_files:=.in)
|
||||||
CLEANFILES += $(subst_files)
|
CLEANFILES += $(subst_files)
|
||||||
|
|
||||||
|
@ -43,7 +24,14 @@ $(subst_files): Makefile
|
||||||
rm -f '$@' '$@'.tmp
|
rm -f '$@' '$@'.tmp
|
||||||
in='$@'.in; \
|
in='$@'.in; \
|
||||||
test -f "$${in}" || in='$(srcdir)/'$${in}; \
|
test -f "$${in}" || in='$(srcdir)/'$${in}; \
|
||||||
$(subst) "$${in}" >'$@'.tmp && \
|
sed \
|
||||||
|
-e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
|
||||||
|
-e '1 s|^#\!.*perl$$|#\!$(PERL)|g' \
|
||||||
|
-e 's|@localstatedir[@]|$(localstatedir)|g' \
|
||||||
|
-e 's|@confdir[@]|$(confdir)|g' \
|
||||||
|
-e 's|@runstatedir[@]|$(runstatedir)|g' \
|
||||||
|
-e 's|@CURL[@]|$(CURL)|g' \
|
||||||
|
"$${in}" >'$@'.tmp && \
|
||||||
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
{ ! test -x "$${in}" || chmod +x '$@'.tmp; }
|
||||||
mv '$@'.tmp '$@'
|
mv '$@'.tmp '$@'
|
||||||
|
|
||||||
|
@ -52,7 +40,7 @@ ddclient.conf: $(srcdir)/ddclient.conf.in
|
||||||
|
|
||||||
bin_SCRIPTS = ddclient
|
bin_SCRIPTS = ddclient
|
||||||
|
|
||||||
sysconf_DATA = ddclient.conf
|
conf_DATA = ddclient.conf
|
||||||
|
|
||||||
install-data-local:
|
install-data-local:
|
||||||
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
$(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
|
||||||
|
@ -69,18 +57,36 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
|
||||||
-I'$(abs_top_srcdir)'/t/lib \
|
-I'$(abs_top_srcdir)'/t/lib \
|
||||||
-MDevel::Autoflush
|
-MDevel::Autoflush
|
||||||
handwritten_tests = \
|
handwritten_tests = \
|
||||||
|
t/builtinfw_query.pl \
|
||||||
|
t/check_value.pl \
|
||||||
t/get_ip_from_if.pl \
|
t/get_ip_from_if.pl \
|
||||||
t/geturl_ssl.pl \
|
t/geturl_connectivity.pl \
|
||||||
|
t/geturl_response.pl \
|
||||||
|
t/group_hosts_by.pl \
|
||||||
|
t/header_ok.pl \
|
||||||
|
t/interval_expired.pl \
|
||||||
t/is-and-extract-ipv4.pl \
|
t/is-and-extract-ipv4.pl \
|
||||||
t/is-and-extract-ipv6.pl \
|
t/is-and-extract-ipv6.pl \
|
||||||
t/is-and-extract-ipv6-global.pl \
|
t/is-and-extract-ipv6-global.pl \
|
||||||
|
t/logmsg.pl \
|
||||||
t/parse_assignments.pl \
|
t/parse_assignments.pl \
|
||||||
t/write_cache.pl
|
t/protocol_directnic.pl \
|
||||||
|
t/protocol_dnsexit2.pl \
|
||||||
|
t/protocol_dyndns2.pl \
|
||||||
|
t/read_recap.pl \
|
||||||
|
t/skip.pl \
|
||||||
|
t/ssl-validate.pl \
|
||||||
|
t/update_nics.pl \
|
||||||
|
t/use_cmd.pl \
|
||||||
|
t/use_web.pl \
|
||||||
|
t/variable_defaults.pl \
|
||||||
|
t/write_recap.pl
|
||||||
generated_tests = \
|
generated_tests = \
|
||||||
t/geturl_connectivity.pl \
|
|
||||||
t/version.pl
|
t/version.pl
|
||||||
TESTS = $(handwritten_tests) $(generated_tests)
|
TESTS = $(handwritten_tests) $(generated_tests)
|
||||||
|
$(TESTS): ddclient
|
||||||
EXTRA_DIST += $(handwritten_tests) \
|
EXTRA_DIST += $(handwritten_tests) \
|
||||||
|
.autom4te.cfg \
|
||||||
t/lib/Devel/Autoflush.pm \
|
t/lib/Devel/Autoflush.pm \
|
||||||
t/lib/Test/Builder.pm \
|
t/lib/Test/Builder.pm \
|
||||||
t/lib/Test/Builder/Formatter.pm \
|
t/lib/Test/Builder/Formatter.pm \
|
||||||
|
@ -151,5 +157,9 @@ EXTRA_DIST += $(handwritten_tests) \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-ca-cert.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-ca-cert.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
|
||||||
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
|
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
|
||||||
|
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem \
|
||||||
t/lib/ddclient/t.pm \
|
t/lib/ddclient/t.pm \
|
||||||
|
t/lib/ddclient/t/HTTPD.pm \
|
||||||
|
t/lib/ddclient/t/Logger.pm \
|
||||||
|
t/lib/ddclient/t/ip.pm \
|
||||||
t/lib/ok.pm
|
t/lib/ok.pm
|
||||||
|
|
283
README.md
283
README.md
|
@ -1,55 +1,101 @@
|
||||||
# DDCLIENT v3.10.0
|
# DDCLIENT
|
||||||
|
|
||||||
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
|
`ddclient` is a Perl client used to update dynamic DNS entries for accounts
|
||||||
on many dynamic DNS services.
|
on many dynamic DNS services. It uses `curl` for internet access.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
You might also want to consider using one of the following, if they support
|
||||||
|
your dynamic DNS provider(s): <https://github.com/troglobit/inadyn> or
|
||||||
|
<https://github.com/lopsided98/dnsupdate>.
|
||||||
|
|
||||||
## Supported services
|
## Supported services
|
||||||
|
|
||||||
Dynamic DNS services currently supported include:
|
Dynamic DNS services currently supported include:
|
||||||
|
|
||||||
DynDNS.com - See http://www.dyndns.com for details on obtaining a free account.
|
* [1984.is](https://www.1984.is/product/freedns)
|
||||||
Zoneedit - See http://www.zoneedit.com for details.
|
* [ChangeIP](https://www.changeip.com)
|
||||||
EasyDNS - See http://www.easydns.com for details.
|
* [CloudFlare](https://www.cloudflare.com)
|
||||||
NameCheap - See http://www.namecheap.com for details
|
* [ClouDNS](https://www.cloudns.net)
|
||||||
DslReports - See http://www.dslreports.com for details
|
* [DDNS.fm](https://www.ddns.fm/)
|
||||||
Sitelutions - See http://www.sitelutions.com for details
|
* [DigitalOcean](https://www.digitalocean.com/)
|
||||||
Loopia - See http://www.loopia.se for details
|
* [dinahosting](https://dinahosting.com)
|
||||||
Noip - See http://www.noip.com/ for details
|
* [Directnic](https://directnic.com)
|
||||||
Freedns - See http://freedns.afraid.org/ for details
|
* [DonDominio](https://www.dondominio.com)
|
||||||
ChangeIP - See http://www.changeip.com/ for details
|
* [DNS Made Easy](https://dnsmadeeasy.com)
|
||||||
nsupdate - See nsupdate(1) and ddns-confgen(8) for details
|
* [DNSExit](https://dnsexit.com/dns/dns-api)
|
||||||
CloudFlare - See https://www.cloudflare.com/ for details
|
* [dnsHome.de](https://www.dnshome.de)
|
||||||
GoDaddy - See https://www.godaddy.com/ for details
|
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
|
||||||
Google - See http://www.google.com/domains for details
|
* [DslReports](https://www.dslreports.com)
|
||||||
Duckdns - See https://duckdns.org/ for details
|
* [Duck DNS](https://duckdns.org)
|
||||||
Freemyip - See https://freemyip.com for details
|
* [DynDNS.com](https://account.dyn.com)
|
||||||
woima.fi - See https://woima.fi/ for details
|
* [EasyDNS](https://www.easydns.com )
|
||||||
Yandex - See https://domain.yandex.com/ for details
|
* [Enom](https://www.enom.com)
|
||||||
DNS Made Easy - See https://dnsmadeeasy.com/ for details
|
* [Freedns](https://freedns.afraid.org)
|
||||||
DonDominio - See https://www.dondominio.com for details
|
* [Freemyip](https://freemyip.com)
|
||||||
NearlyFreeSpeech.net - See https://www.nearlyfreespeech.net/services/dns for details
|
* [Gandi](https://gandi.net)
|
||||||
OVH - See https://www.ovh.com for details
|
* [GoDaddy](https://www.godaddy.com)
|
||||||
ClouDNS - See https://www.cloudns.net
|
* [Hurricane Electric](https://dns.he.net)
|
||||||
dinahosting - See https://dinahosting.com
|
* [Infomaniak](https://faq.infomaniak.com/2376)
|
||||||
Gandi - See https://gandi.net
|
* [INWX](https://www.inwx.com/)
|
||||||
dnsexit - See https://dnsexit.com/ for details
|
* [Loopia](https://www.loopia.se)
|
||||||
1984.is - See https://www.1984.is/product/freedns/ for details
|
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
|
||||||
Njal.la - See https://njal.la/docs/ddns/
|
* [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` now supports many cable and DSL broadband routers.
|
`ddclient` supports finding your IP address from many cable and DSL
|
||||||
|
broadband routers.
|
||||||
|
|
||||||
Comments, suggestions and requests: use the issues on https://github.com/ddclient/ddclient/issues/new
|
Comments, suggestions and requests: please file an issue at
|
||||||
|
https://github.com/ddclient/ddclient/issues/new
|
||||||
|
|
||||||
The code was originally written by Paul Burry and is now hosted and maintained
|
The code was originally written by Paul Burry and is now hosted and
|
||||||
through github.com. Please check out http://ddclient.net
|
maintained through github.com. Please check out https://ddclient.net
|
||||||
|
|
||||||
## REQUIREMENTS
|
## REQUIREMENTS
|
||||||
|
|
||||||
* An account from a supported dynamic DNS service provider
|
* An account from a supported dynamic DNS service provider
|
||||||
* Perl v5.10.1 or later
|
* Perl v5.10.1 or later
|
||||||
* `IO::Socket::SSL` perl library for ssl-support
|
|
||||||
* `JSON::PP` perl library for JSON support
|
* `JSON::PP` perl library for JSON support
|
||||||
* `IO::Socket::INET6` perl library for ipv6-support
|
|
||||||
* Linux, macOS, or any other Unix-ish system
|
* Linux, macOS, or any other Unix-ish system
|
||||||
* An implementation of `make` (such as [GNU
|
* An implementation of `make` (such as [GNU
|
||||||
Make](https://www.gnu.org/software/make/))
|
Make](https://www.gnu.org/software/make/))
|
||||||
|
@ -69,8 +115,7 @@ See https://github.com/ddclient/ddclient/releases
|
||||||
<img src="https://repology.org/badge/vertical-allrepos/ddclient.svg" alt="Packaging status" align="right">
|
<img src="https://repology.org/badge/vertical-allrepos/ddclient.svg" alt="Packaging status" align="right">
|
||||||
</a>
|
</a>
|
||||||
The easiest way to install ddclient is to install a package offered by your
|
The easiest way to install ddclient is to install a package offered by your
|
||||||
operating system. See the image to the right for a list of distributions with a
|
operating system. See the image to the right for a list of distributions with a ddclient package.
|
||||||
ddclient package.
|
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
|
@ -78,8 +123,8 @@ ddclient package.
|
||||||
the directory:
|
the directory:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tar xvfa ddclient-3.10.0.tar.gz
|
tar xvfa ddclient-3.XX.X.tar.gz
|
||||||
cd ddclient-3.10.0
|
cd ddclient-3.XX.X
|
||||||
```
|
```
|
||||||
|
|
||||||
(If you are installing from a clone of the Git repository, you
|
(If you are installing from a clone of the Git repository, you
|
||||||
|
@ -90,7 +135,7 @@ ddclient package.
|
||||||
```shell
|
```shell
|
||||||
./configure \
|
./configure \
|
||||||
--prefix=/usr \
|
--prefix=/usr \
|
||||||
--sysconfdir=/etc/ddclient \
|
--sysconfdir=/etc \
|
||||||
--localstatedir=/var
|
--localstatedir=/var
|
||||||
make
|
make
|
||||||
make VERBOSE=1 check
|
make VERBOSE=1 check
|
||||||
|
@ -111,119 +156,97 @@ start the first time by hand
|
||||||
|
|
||||||
systemctl start ddclient.service
|
systemctl start ddclient.service
|
||||||
|
|
||||||
#### Redhat style rc files and daemon-mode
|
## Known issues
|
||||||
|
This is a list for quick referencing of known issues. For further details check out the linked issues and the changelog.
|
||||||
|
|
||||||
cp sample-etc_rc.d_init.d_ddclient /etc/rc.d/init.d/ddclient
|
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.
|
||||||
|
|
||||||
enable automatic startup when booting. also check your distribution
|
### v3.11.2 - v3.9.1: SSL parameter breaks HTTP-only IP acquisition
|
||||||
|
|
||||||
/sbin/chkconfig --add ddclient
|
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.
|
||||||
|
|
||||||
start the first time by hand
|
**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.
|
||||||
|
|
||||||
/etc/rc.d/init.d/ddclient start
|
**Workaround**: Disable the SSL parameter
|
||||||
|
|
||||||
#### Alpine style rc files and daemon-mode
|
### 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.
|
||||||
|
|
||||||
cp sample-etc_rc.d_init.d_ddclient.alpine /etc/init.d/ddclient
|
**Fix**: v3.11.0 - IO::Socket has been deprecated there and curl has been made the standard.
|
||||||
|
|
||||||
enable automatic startup when booting
|
**Workaround**: Use curl for transfers by either setting `-curl` in the command line or by adding `curl=yes` in the config
|
||||||
|
|
||||||
rc-update add ddclient
|
### 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
|
||||||
|
|
||||||
make sure you have perl installed
|
**Fix**: v3.11.2
|
||||||
|
|
||||||
apk add perl
|
**Workaround**: Use the `usev4`/`usev6` parameters instead of `use`.
|
||||||
|
|
||||||
start the first time by hand
|
|
||||||
|
|
||||||
rc-service ddclient start
|
|
||||||
|
|
||||||
#### Ubuntu style rc files and daemon-mode
|
|
||||||
|
|
||||||
cp sample-etc_rc.d_init.d_ddclient.ubuntu /etc/init.d/ddclient
|
|
||||||
|
|
||||||
enable automatic startup when booting
|
|
||||||
|
|
||||||
update-rc.d ddclient defaults
|
|
||||||
|
|
||||||
make sure you have perl and the required modules installed
|
|
||||||
|
|
||||||
apt-get install perl libdata-validate-ip-perl libio-socket-ssl-perl
|
|
||||||
|
|
||||||
if you plan to use cloudflare or feedns you need the perl json module
|
|
||||||
|
|
||||||
apt-get install libjson-pp-perl
|
|
||||||
|
|
||||||
for IPv6 you also need to instal the perl io-socket-inet6 module
|
|
||||||
|
|
||||||
apt install libio-socket-inet6-perl
|
|
||||||
|
|
||||||
start the first time by hand
|
|
||||||
|
|
||||||
service ddclient start
|
|
||||||
|
|
||||||
#### FreeBSD style rc files and daemon mode
|
|
||||||
|
|
||||||
mkdir -p /usr/local/etc/rc.d
|
|
||||||
cp sample-etc_rc.d_ddclient.freebsd /usr/local/etc/rc.d/ddclient
|
|
||||||
|
|
||||||
enable automatic startup when booting
|
|
||||||
|
|
||||||
sysrc ddclient_enable=YES
|
|
||||||
|
|
||||||
make sure you have perl and the required modules installed
|
|
||||||
|
|
||||||
pkg install perl5 p5-Data-Validate-IP p5-IO-Socket-SSL
|
|
||||||
|
|
||||||
if you plan to use cloudflare or feedns you need the perl json module
|
|
||||||
|
|
||||||
pkg install p5-JSON-PP
|
|
||||||
|
|
||||||
start the service manually for the first time
|
|
||||||
|
|
||||||
service ddclient start
|
|
||||||
|
|
||||||
|
|
||||||
If you are not using daemon-mode, configure cron and dhcp or ppp as described below.
|
|
||||||
|
|
||||||
## TROUBLESHOOTING
|
## TROUBLESHOOTING
|
||||||
|
|
||||||
1. enable debugging and verbose messages: ``$ ddclient -daemon=0 -debug -verbose -noquiet``
|
* Enable debugging and verbose messages: `ddclient --daemon=0 --debug --verbose`
|
||||||
|
|
||||||
2. Do you need to specify a proxy?
|
* Do you need to specify a proxy?
|
||||||
If so, just add a ``proxy=your.isp.proxy`` to the ddclient.conf file.
|
If so, just add a `proxy=your.isp.proxy` to the `ddclient.conf` file.
|
||||||
|
|
||||||
3. Define the IP address of your router with ``fw=xxx.xxx.xxx.xxx`` in
|
* Define the IP address of your router with `fwv4=xxx.xxx.xxx.xxx` in
|
||||||
``/etc/ddclient/ddclient.conf`` and then try ``$ ddclient -daemon=0 -query`` to see if the router status web page can be understood.
|
`/etc/ddclient/ddclient.conf` and then try `$ ddclient --daemon=0 --query`
|
||||||
|
to see if the router status web page can be understood.
|
||||||
|
|
||||||
4. Need support for another router/firewall?
|
* Need support for another router/firewall?
|
||||||
Define the router status page yourself with: ``fw=url-to-your-router``'s-status-page ``fw-skip=any-string-preceding-your-IP-address``
|
Define the router yourself with:
|
||||||
|
|
||||||
ddclient does something like this to provide builtin support for
|
```
|
||||||
common routers.
|
usev4=fwv4
|
||||||
For example, the Linksys routers could have been added with:
|
fwv4=url-to-your-router-status-page
|
||||||
|
fwv4-skip="regular expression matching any string preceding your IP address, if necessary"
|
||||||
|
```
|
||||||
|
|
||||||
fw=192.168.1.1/Status.htm
|
ddclient does something like this to provide builtin support for common
|
||||||
fw-skip=WAN.*?IP Address
|
routers.
|
||||||
|
For example, the Linksys routers could have been added with:
|
||||||
|
|
||||||
OR
|
```
|
||||||
Send me the output from:
|
usev4=fwv4
|
||||||
``$ ddclient -geturl {fw-ip-status-url} [-login login [-password password]]``
|
fwv4=192.168.1.1/Status.htm
|
||||||
and I'll add it to the next release!
|
fwv4-skip=WAN.*?IP Address
|
||||||
|
```
|
||||||
|
|
||||||
ie. for my fw/router I used: ``$ ddclient -geturl 192.168.1.254/status.htm``
|
OR [create a new issue](https://github.com/ddclient/ddclient/issues/new)
|
||||||
|
containing the output from:
|
||||||
|
|
||||||
5. Some broadband routers require the use of a password when ddclient
|
```
|
||||||
accesses its status page to determine the router's WAN IP address.
|
curl --include --location http://url.of.your.firewall/ip-status-page
|
||||||
If this is the case for your router, add
|
```
|
||||||
|
|
||||||
|
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-login=your-router-login
|
||||||
fw-password=your-router-password
|
fw-password=your-router-password
|
||||||
|
```
|
||||||
|
|
||||||
to the beginning of your ddclient.conf file.
|
to the beginning of your ddclient.conf file.
|
||||||
Note that some routers use either 'root' or 'admin' as their login
|
Note that some routers use either 'root' or 'admin' as their login while
|
||||||
while some others accept anything.
|
some others accept anything.
|
||||||
|
|
||||||
## USING DDCLIENT WITH `ppp`
|
## USING DDCLIENT WITH `ppp`
|
||||||
|
|
||||||
|
@ -246,7 +269,7 @@ not become stale.
|
||||||
cp sample-etc_cron.d_ddclient /etc/cron.d/ddclient
|
cp sample-etc_cron.d_ddclient /etc/cron.d/ddclient
|
||||||
vi /etc/cron.d/ddclient
|
vi /etc/cron.d/ddclient
|
||||||
|
|
||||||
## USING DDCLIENT WITH `dhcpcd-1.3.17`
|
## USING DDCLIENT WITH `dhcpcd`
|
||||||
|
|
||||||
If you are using dhcpcd-1.3.17 or thereabouts, you can easily update
|
If you are using dhcpcd-1.3.17 or thereabouts, you can easily update
|
||||||
your DynDNS entry automatically every time your lease is obtained
|
your DynDNS entry automatically every time your lease is obtained
|
||||||
|
@ -261,7 +284,7 @@ In my case, it is named dhcpcd-eth0.exe and contains the lines:
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
PATH=/usr/bin:/root/bin:${PATH}
|
PATH=/usr/bin:/root/bin:${PATH}
|
||||||
logger -t dhcpcd IP address changed to $1
|
logger -t dhcpcd IP address changed to $1
|
||||||
ddclient -proxy fasthttp.sympatico.ca -wildcard -ip $1 | logger -t ddclient
|
ddclient --proxy fasthttp.sympatico.ca --wildcard --ip $1 | logger -t ddclient
|
||||||
exit 0
|
exit 0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
13
README.ssl
13
README.ssl
|
@ -1,13 +0,0 @@
|
||||||
Since 3.7.0, ddclient support ssl-updates
|
|
||||||
To use ssl, put "ssl=yes" in your configuration and make sure
|
|
||||||
you have IO::Socket::SSL.
|
|
||||||
|
|
||||||
On debian, you need libio-socket-ssl-perl to have IO::Socket::SSL
|
|
||||||
|
|
||||||
On alpine, you need perl-io-socket-ssl to have IO::Socket::SSL
|
|
||||||
|
|
||||||
ssl support is tested on folowing dynamic dns providers:
|
|
||||||
- dyndns.com
|
|
||||||
- freemyip.com
|
|
||||||
- DNS Made Easy
|
|
||||||
- dondominio.com
|
|
26
autogen
26
autogen
|
@ -7,18 +7,16 @@ fatal() { error "$@"; exit 1; }
|
||||||
try() { "$@" || fatal "'$@' failed"; }
|
try() { "$@" || fatal "'$@' failed"; }
|
||||||
|
|
||||||
try cd "${0%/*}"
|
try cd "${0%/*}"
|
||||||
try mkdir -p m4 build-aux
|
# aclocal complains if a directory passed to AC_CONFIG_MACRO_DIR doesn't exist.
|
||||||
|
try mkdir -p build-aux/m4
|
||||||
|
# autoreconf's '--force' option doesn't affect any of the files installed by the '--install' option.
|
||||||
|
# Remove the files to truly force them to be updated.
|
||||||
|
try rm -f \
|
||||||
|
aclocal.m4 \
|
||||||
|
build-aux/config.guess \
|
||||||
|
build-aux/config.sub \
|
||||||
|
build-aux/install-sh \
|
||||||
|
build-aux/missing \
|
||||||
|
build-aux/tap-driver.sh \
|
||||||
|
;
|
||||||
try autoreconf -fviW all
|
try autoreconf -fviW all
|
||||||
|
|
||||||
# Ignore changes to build-aux/tap-driver, but only if we're in a clone
|
|
||||||
# of the ddclient Git repository. Once CentOS 6 and RHEL 6 reach
|
|
||||||
# end-of-life we can delete build-aux/tap-driver.sh and this block of
|
|
||||||
# code. (tap-driver.sh is checked in to this Git repository only
|
|
||||||
# because we want to support all currently maintained CentOS and RHEL
|
|
||||||
# releases, and CentoOS 6 and RHEL 6 ship with Automake 1.11 which
|
|
||||||
# does not come with tap-driver.sh.)
|
|
||||||
command -v git >/dev/null || exit 0
|
|
||||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
||||||
cdup=$(try git rev-parse --show-cdup) || exit 1
|
|
||||||
[ -z "${cdup}" ] || exit 0
|
|
||||||
try git update-index --assume-unchanged -- build-aux/tap-driver.sh
|
|
||||||
|
|
|
@ -1,651 +0,0 @@
|
||||||
#! /bin/sh
|
|
||||||
# Copyright (C) 2011-2020 Free Software Foundation, Inc.
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2, or (at your option)
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
# As a special exception to the GNU General Public License, if you
|
|
||||||
# distribute this file as part of a program that contains a
|
|
||||||
# configuration script generated by Autoconf, you may include it under
|
|
||||||
# the same distribution terms that you use for the rest of that program.
|
|
||||||
|
|
||||||
# This file is maintained in Automake, please report
|
|
||||||
# bugs to <bug-automake@gnu.org> or send patches to
|
|
||||||
# <automake-patches@gnu.org>.
|
|
||||||
|
|
||||||
scriptversion=2013-12-23.17; # UTC
|
|
||||||
|
|
||||||
# Make unconditional expansion of undefined variables an error. This
|
|
||||||
# helps a lot in preventing typo-related bugs.
|
|
||||||
set -u
|
|
||||||
|
|
||||||
me=tap-driver.sh
|
|
||||||
|
|
||||||
fatal ()
|
|
||||||
{
|
|
||||||
echo "$me: fatal: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
usage_error ()
|
|
||||||
{
|
|
||||||
echo "$me: $*" >&2
|
|
||||||
print_usage >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
print_usage ()
|
|
||||||
{
|
|
||||||
cat <<END
|
|
||||||
Usage:
|
|
||||||
tap-driver.sh --test-name=NAME --log-file=PATH --trs-file=PATH
|
|
||||||
[--expect-failure={yes|no}] [--color-tests={yes|no}]
|
|
||||||
[--enable-hard-errors={yes|no}] [--ignore-exit]
|
|
||||||
[--diagnostic-string=STRING] [--merge|--no-merge]
|
|
||||||
[--comments|--no-comments] [--] TEST-COMMAND
|
|
||||||
The '--test-name', '-log-file' and '--trs-file' options are mandatory.
|
|
||||||
END
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO: better error handling in option parsing (in particular, ensure
|
|
||||||
# TODO: $log_file, $trs_file and $test_name are defined).
|
|
||||||
test_name= # Used for reporting.
|
|
||||||
log_file= # Where to save the result and output of the test script.
|
|
||||||
trs_file= # Where to save the metadata of the test run.
|
|
||||||
expect_failure=0
|
|
||||||
color_tests=0
|
|
||||||
merge=0
|
|
||||||
ignore_exit=0
|
|
||||||
comments=0
|
|
||||||
diag_string='#'
|
|
||||||
while test $# -gt 0; do
|
|
||||||
case $1 in
|
|
||||||
--help) print_usage; exit $?;;
|
|
||||||
--version) echo "$me $scriptversion"; exit $?;;
|
|
||||||
--test-name) test_name=$2; shift;;
|
|
||||||
--log-file) log_file=$2; shift;;
|
|
||||||
--trs-file) trs_file=$2; shift;;
|
|
||||||
--color-tests) color_tests=$2; shift;;
|
|
||||||
--expect-failure) expect_failure=$2; shift;;
|
|
||||||
--enable-hard-errors) shift;; # No-op.
|
|
||||||
--merge) merge=1;;
|
|
||||||
--no-merge) merge=0;;
|
|
||||||
--ignore-exit) ignore_exit=1;;
|
|
||||||
--comments) comments=1;;
|
|
||||||
--no-comments) comments=0;;
|
|
||||||
--diagnostic-string) diag_string=$2; shift;;
|
|
||||||
--) shift; break;;
|
|
||||||
-*) usage_error "invalid option: '$1'";;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
test $# -gt 0 || usage_error "missing test command"
|
|
||||||
|
|
||||||
case $expect_failure in
|
|
||||||
yes) expect_failure=1;;
|
|
||||||
*) expect_failure=0;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if test $color_tests = yes; then
|
|
||||||
init_colors='
|
|
||||||
color_map["red"]="[0;31m" # Red.
|
|
||||||
color_map["grn"]="[0;32m" # Green.
|
|
||||||
color_map["lgn"]="[1;32m" # Light green.
|
|
||||||
color_map["blu"]="[1;34m" # Blue.
|
|
||||||
color_map["mgn"]="[0;35m" # Magenta.
|
|
||||||
color_map["std"]="[m" # No color.
|
|
||||||
color_for_result["ERROR"] = "mgn"
|
|
||||||
color_for_result["PASS"] = "grn"
|
|
||||||
color_for_result["XPASS"] = "red"
|
|
||||||
color_for_result["FAIL"] = "red"
|
|
||||||
color_for_result["XFAIL"] = "lgn"
|
|
||||||
color_for_result["SKIP"] = "blu"'
|
|
||||||
else
|
|
||||||
init_colors=''
|
|
||||||
fi
|
|
||||||
|
|
||||||
# :; is there to work around a bug in bash 3.2 (and earlier) which
|
|
||||||
# does not always set '$?' properly on redirection failure.
|
|
||||||
# See the Autoconf manual for more details.
|
|
||||||
:;{
|
|
||||||
(
|
|
||||||
# Ignore common signals (in this subshell only!), to avoid potential
|
|
||||||
# problems with Korn shells. Some Korn shells are known to propagate
|
|
||||||
# to themselves signals that have killed a child process they were
|
|
||||||
# waiting for; this is done at least for SIGINT (and usually only for
|
|
||||||
# it, in truth). Without the `trap' below, such a behaviour could
|
|
||||||
# cause a premature exit in the current subshell, e.g., in case the
|
|
||||||
# test command it runs gets terminated by a SIGINT. Thus, the awk
|
|
||||||
# script we are piping into would never seen the exit status it
|
|
||||||
# expects on its last input line (which is displayed below by the
|
|
||||||
# last `echo $?' statement), and would thus die reporting an internal
|
|
||||||
# error.
|
|
||||||
# For more information, see the Autoconf manual and the threads:
|
|
||||||
# <https://lists.gnu.org/archive/html/bug-autoconf/2011-09/msg00004.html>
|
|
||||||
# <http://mail.opensolaris.org/pipermail/ksh93-integration-discuss/2009-February/004121.html>
|
|
||||||
trap : 1 3 2 13 15
|
|
||||||
if test $merge -gt 0; then
|
|
||||||
exec 2>&1
|
|
||||||
else
|
|
||||||
exec 2>&3
|
|
||||||
fi
|
|
||||||
"$@"
|
|
||||||
echo $?
|
|
||||||
) | LC_ALL=C ${AM_TAP_AWK-awk} \
|
|
||||||
-v me="$me" \
|
|
||||||
-v test_script_name="$test_name" \
|
|
||||||
-v log_file="$log_file" \
|
|
||||||
-v trs_file="$trs_file" \
|
|
||||||
-v expect_failure="$expect_failure" \
|
|
||||||
-v merge="$merge" \
|
|
||||||
-v ignore_exit="$ignore_exit" \
|
|
||||||
-v comments="$comments" \
|
|
||||||
-v diag_string="$diag_string" \
|
|
||||||
'
|
|
||||||
# TODO: the usages of "cat >&3" below could be optimized when using
|
|
||||||
# GNU awk, and/on on systems that supports /dev/fd/.
|
|
||||||
|
|
||||||
# Implementation note: in what follows, `result_obj` will be an
|
|
||||||
# associative array that (partly) simulates a TAP result object
|
|
||||||
# from the `TAP::Parser` perl module.
|
|
||||||
|
|
||||||
## ----------- ##
|
|
||||||
## FUNCTIONS ##
|
|
||||||
## ----------- ##
|
|
||||||
|
|
||||||
function fatal(msg)
|
|
||||||
{
|
|
||||||
print me ": " msg | "cat >&2"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function abort(where)
|
|
||||||
{
|
|
||||||
fatal("internal error " where)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert a boolean to a "yes"/"no" string.
|
|
||||||
function yn(bool)
|
|
||||||
{
|
|
||||||
return bool ? "yes" : "no";
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_test_result(result)
|
|
||||||
{
|
|
||||||
if (!test_results_index)
|
|
||||||
test_results_index = 0
|
|
||||||
test_results_list[test_results_index] = result
|
|
||||||
test_results_index += 1
|
|
||||||
test_results_seen[result] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Whether the test script should be re-run by "make recheck".
|
|
||||||
function must_recheck()
|
|
||||||
{
|
|
||||||
for (k in test_results_seen)
|
|
||||||
if (k != "XFAIL" && k != "PASS" && k != "SKIP")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Whether the content of the log file associated to this test should
|
|
||||||
# be copied into the "global" test-suite.log.
|
|
||||||
function copy_in_global_log()
|
|
||||||
{
|
|
||||||
for (k in test_results_seen)
|
|
||||||
if (k != "PASS")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_global_test_result()
|
|
||||||
{
|
|
||||||
if ("ERROR" in test_results_seen)
|
|
||||||
return "ERROR"
|
|
||||||
if ("FAIL" in test_results_seen || "XPASS" in test_results_seen)
|
|
||||||
return "FAIL"
|
|
||||||
all_skipped = 1
|
|
||||||
for (k in test_results_seen)
|
|
||||||
if (k != "SKIP")
|
|
||||||
all_skipped = 0
|
|
||||||
if (all_skipped)
|
|
||||||
return "SKIP"
|
|
||||||
return "PASS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringify_result_obj(result_obj)
|
|
||||||
{
|
|
||||||
if (result_obj["is_unplanned"] || result_obj["number"] != testno)
|
|
||||||
return "ERROR"
|
|
||||||
|
|
||||||
if (plan_seen == LATE_PLAN)
|
|
||||||
return "ERROR"
|
|
||||||
|
|
||||||
if (result_obj["directive"] == "TODO")
|
|
||||||
return result_obj["is_ok"] ? "XPASS" : "XFAIL"
|
|
||||||
|
|
||||||
if (result_obj["directive"] == "SKIP")
|
|
||||||
return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL;
|
|
||||||
|
|
||||||
if (length(result_obj["directive"]))
|
|
||||||
abort("in function stringify_result_obj()")
|
|
||||||
|
|
||||||
return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL
|
|
||||||
}
|
|
||||||
|
|
||||||
function decorate_result(result)
|
|
||||||
{
|
|
||||||
color_name = color_for_result[result]
|
|
||||||
if (color_name)
|
|
||||||
return color_map[color_name] "" result "" color_map["std"]
|
|
||||||
# If we are not using colorized output, or if we do not know how
|
|
||||||
# to colorize the given result, we should return it unchanged.
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function report(result, details)
|
|
||||||
{
|
|
||||||
if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/)
|
|
||||||
{
|
|
||||||
msg = ": " test_script_name
|
|
||||||
add_test_result(result)
|
|
||||||
}
|
|
||||||
else if (result == "#")
|
|
||||||
{
|
|
||||||
msg = " " test_script_name ":"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
abort("in function report()")
|
|
||||||
}
|
|
||||||
if (length(details))
|
|
||||||
msg = msg " " details
|
|
||||||
# Output on console might be colorized.
|
|
||||||
print decorate_result(result) msg
|
|
||||||
# Log the result in the log file too, to help debugging (this is
|
|
||||||
# especially true when said result is a TAP error or "Bail out!").
|
|
||||||
print result msg | "cat >&3";
|
|
||||||
}
|
|
||||||
|
|
||||||
function testsuite_error(error_message)
|
|
||||||
{
|
|
||||||
report("ERROR", "- " error_message)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_tap_result()
|
|
||||||
{
|
|
||||||
details = result_obj["number"];
|
|
||||||
if (length(result_obj["description"]))
|
|
||||||
details = details " " result_obj["description"]
|
|
||||||
|
|
||||||
if (plan_seen == LATE_PLAN)
|
|
||||||
{
|
|
||||||
details = details " # AFTER LATE PLAN";
|
|
||||||
}
|
|
||||||
else if (result_obj["is_unplanned"])
|
|
||||||
{
|
|
||||||
details = details " # UNPLANNED";
|
|
||||||
}
|
|
||||||
else if (result_obj["number"] != testno)
|
|
||||||
{
|
|
||||||
details = sprintf("%s # OUT-OF-ORDER (expecting %d)",
|
|
||||||
details, testno);
|
|
||||||
}
|
|
||||||
else if (result_obj["directive"])
|
|
||||||
{
|
|
||||||
details = details " # " result_obj["directive"];
|
|
||||||
if (length(result_obj["explanation"]))
|
|
||||||
details = details " " result_obj["explanation"]
|
|
||||||
}
|
|
||||||
|
|
||||||
report(stringify_result_obj(result_obj), details)
|
|
||||||
}
|
|
||||||
|
|
||||||
# `skip_reason` should be empty whenever planned > 0.
|
|
||||||
function handle_tap_plan(planned, skip_reason)
|
|
||||||
{
|
|
||||||
planned += 0 # Avoid getting confused if, say, `planned` is "00"
|
|
||||||
if (length(skip_reason) && planned > 0)
|
|
||||||
abort("in function handle_tap_plan()")
|
|
||||||
if (plan_seen)
|
|
||||||
{
|
|
||||||
# Error, only one plan per stream is acceptable.
|
|
||||||
testsuite_error("multiple test plans")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
planned_tests = planned
|
|
||||||
# The TAP plan can come before or after *all* the TAP results; we speak
|
|
||||||
# respectively of an "early" or a "late" plan. If we see the plan line
|
|
||||||
# after at least one TAP result has been seen, assume we have a late
|
|
||||||
# plan; in this case, any further test result seen after the plan will
|
|
||||||
# be flagged as an error.
|
|
||||||
plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN)
|
|
||||||
# If testno > 0, we have an error ("too many tests run") that will be
|
|
||||||
# automatically dealt with later, so do not worry about it here. If
|
|
||||||
# $plan_seen is true, we have an error due to a repeated plan, and that
|
|
||||||
# has already been dealt with above. Otherwise, we have a valid "plan
|
|
||||||
# with SKIP" specification, and should report it as a particular kind
|
|
||||||
# of SKIP result.
|
|
||||||
if (planned == 0 && testno == 0)
|
|
||||||
{
|
|
||||||
if (length(skip_reason))
|
|
||||||
skip_reason = "- " skip_reason;
|
|
||||||
report("SKIP", skip_reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extract_tap_comment(line)
|
|
||||||
{
|
|
||||||
if (index(line, diag_string) == 1)
|
|
||||||
{
|
|
||||||
# Strip leading `diag_string` from `line`.
|
|
||||||
line = substr(line, length(diag_string) + 1)
|
|
||||||
# And strip any leading and trailing whitespace left.
|
|
||||||
sub("^[ \t]*", "", line)
|
|
||||||
sub("[ \t]*$", "", line)
|
|
||||||
# Return what is left (if any).
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
# When this function is called, we know that line is a TAP result line,
|
|
||||||
# so that it matches the (perl) RE "^(not )?ok\b".
|
|
||||||
function setup_result_obj(line)
|
|
||||||
{
|
|
||||||
# Get the result, and remove it from the line.
|
|
||||||
result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0)
|
|
||||||
sub("^(not )?ok[ \t]*", "", line)
|
|
||||||
|
|
||||||
# If the result has an explicit number, get it and strip it; otherwise,
|
|
||||||
# automatically assing the next progresive number to it.
|
|
||||||
if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/)
|
|
||||||
{
|
|
||||||
match(line, "^[0-9]+")
|
|
||||||
# The final `+ 0` is to normalize numbers with leading zeros.
|
|
||||||
result_obj["number"] = substr(line, 1, RLENGTH) + 0
|
|
||||||
line = substr(line, RLENGTH + 1)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result_obj["number"] = testno
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan_seen == LATE_PLAN)
|
|
||||||
# No further test results are acceptable after a "late" TAP plan
|
|
||||||
# has been seen.
|
|
||||||
result_obj["is_unplanned"] = 1
|
|
||||||
else if (plan_seen && testno > planned_tests)
|
|
||||||
result_obj["is_unplanned"] = 1
|
|
||||||
else
|
|
||||||
result_obj["is_unplanned"] = 0
|
|
||||||
|
|
||||||
# Strip trailing and leading whitespace.
|
|
||||||
sub("^[ \t]*", "", line)
|
|
||||||
sub("[ \t]*$", "", line)
|
|
||||||
|
|
||||||
# This will have to be corrected if we have a "TODO"/"SKIP" directive.
|
|
||||||
result_obj["description"] = line
|
|
||||||
result_obj["directive"] = ""
|
|
||||||
result_obj["explanation"] = ""
|
|
||||||
|
|
||||||
if (index(line, "#") == 0)
|
|
||||||
return # No possible directive, nothing more to do.
|
|
||||||
|
|
||||||
# Directives are case-insensitive.
|
|
||||||
rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*"
|
|
||||||
|
|
||||||
# See whether we have the directive, and if yes, where.
|
|
||||||
pos = match(line, rx "$")
|
|
||||||
if (!pos)
|
|
||||||
pos = match(line, rx "[^a-zA-Z0-9_]")
|
|
||||||
|
|
||||||
# If there was no TAP directive, we have nothing more to do.
|
|
||||||
if (!pos)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Let`s now see if the TAP directive has been escaped. For example:
|
|
||||||
# escaped: ok \# SKIP
|
|
||||||
# not escaped: ok \\# SKIP
|
|
||||||
# escaped: ok \\\\\# SKIP
|
|
||||||
# not escaped: ok \ # SKIP
|
|
||||||
if (substr(line, pos, 1) == "#")
|
|
||||||
{
|
|
||||||
bslash_count = 0
|
|
||||||
for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--)
|
|
||||||
bslash_count += 1
|
|
||||||
if (bslash_count % 2)
|
|
||||||
return # Directive was escaped.
|
|
||||||
}
|
|
||||||
|
|
||||||
# Strip the directive and its explanation (if any) from the test
|
|
||||||
# description.
|
|
||||||
result_obj["description"] = substr(line, 1, pos - 1)
|
|
||||||
# Now remove the test description from the line, that has been dealt
|
|
||||||
# with already.
|
|
||||||
line = substr(line, pos)
|
|
||||||
# Strip the directive, and save its value (normalized to upper case).
|
|
||||||
sub("^[ \t]*#[ \t]*", "", line)
|
|
||||||
result_obj["directive"] = toupper(substr(line, 1, 4))
|
|
||||||
line = substr(line, 5)
|
|
||||||
# Now get the explanation for the directive (if any), with leading
|
|
||||||
# and trailing whitespace removed.
|
|
||||||
sub("^[ \t]*", "", line)
|
|
||||||
sub("[ \t]*$", "", line)
|
|
||||||
result_obj["explanation"] = line
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_test_exit_message(status)
|
|
||||||
{
|
|
||||||
if (status == 0)
|
|
||||||
return ""
|
|
||||||
if (status !~ /^[1-9][0-9]*$/)
|
|
||||||
abort("getting exit status")
|
|
||||||
if (status < 127)
|
|
||||||
exit_details = ""
|
|
||||||
else if (status == 127)
|
|
||||||
exit_details = " (command not found?)"
|
|
||||||
else if (status >= 128 && status <= 255)
|
|
||||||
exit_details = sprintf(" (terminated by signal %d?)", status - 128)
|
|
||||||
else if (status > 256 && status <= 384)
|
|
||||||
# We used to report an "abnormal termination" here, but some Korn
|
|
||||||
# shells, when a child process die due to signal number n, can leave
|
|
||||||
# in $? an exit status of 256+n instead of the more standard 128+n.
|
|
||||||
# Apparently, both behaviours are allowed by POSIX (2008), so be
|
|
||||||
# prepared to handle them both. See also Austing Group report ID
|
|
||||||
# 0000051 <http://www.austingroupbugs.net/view.php?id=51>
|
|
||||||
exit_details = sprintf(" (terminated by signal %d?)", status - 256)
|
|
||||||
else
|
|
||||||
# Never seen in practice.
|
|
||||||
exit_details = " (abnormal termination)"
|
|
||||||
return sprintf("exited with status %d%s", status, exit_details)
|
|
||||||
}
|
|
||||||
|
|
||||||
function write_test_results()
|
|
||||||
{
|
|
||||||
print ":global-test-result: " get_global_test_result() > trs_file
|
|
||||||
print ":recheck: " yn(must_recheck()) > trs_file
|
|
||||||
print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file
|
|
||||||
for (i = 0; i < test_results_index; i += 1)
|
|
||||||
print ":test-result: " test_results_list[i] > trs_file
|
|
||||||
close(trs_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
BEGIN {
|
|
||||||
|
|
||||||
## ------- ##
|
|
||||||
## SETUP ##
|
|
||||||
## ------- ##
|
|
||||||
|
|
||||||
'"$init_colors"'
|
|
||||||
|
|
||||||
# Properly initialized once the TAP plan is seen.
|
|
||||||
planned_tests = 0
|
|
||||||
|
|
||||||
COOKED_PASS = expect_failure ? "XPASS": "PASS";
|
|
||||||
COOKED_FAIL = expect_failure ? "XFAIL": "FAIL";
|
|
||||||
|
|
||||||
# Enumeration-like constants to remember which kind of plan (if any)
|
|
||||||
# has been seen. It is important that NO_PLAN evaluates "false" as
|
|
||||||
# a boolean.
|
|
||||||
NO_PLAN = 0
|
|
||||||
EARLY_PLAN = 1
|
|
||||||
LATE_PLAN = 2
|
|
||||||
|
|
||||||
testno = 0 # Number of test results seen so far.
|
|
||||||
bailed_out = 0 # Whether a "Bail out!" directive has been seen.
|
|
||||||
|
|
||||||
# Whether the TAP plan has been seen or not, and if yes, which kind
|
|
||||||
# it is ("early" is seen before any test result, "late" otherwise).
|
|
||||||
plan_seen = NO_PLAN
|
|
||||||
|
|
||||||
## --------- ##
|
|
||||||
## PARSING ##
|
|
||||||
## --------- ##
|
|
||||||
|
|
||||||
is_first_read = 1
|
|
||||||
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
# Involutions required so that we are able to read the exit status
|
|
||||||
# from the last input line.
|
|
||||||
st = getline
|
|
||||||
if (st < 0) # I/O error.
|
|
||||||
fatal("I/O error while reading from input stream")
|
|
||||||
else if (st == 0) # End-of-input
|
|
||||||
{
|
|
||||||
if (is_first_read)
|
|
||||||
abort("in input loop: only one input line")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (is_first_read)
|
|
||||||
{
|
|
||||||
is_first_read = 0
|
|
||||||
nextline = $0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
curline = nextline
|
|
||||||
nextline = $0
|
|
||||||
$0 = curline
|
|
||||||
}
|
|
||||||
# Copy any input line verbatim into the log file.
|
|
||||||
print | "cat >&3"
|
|
||||||
# Parsing of TAP input should stop after a "Bail out!" directive.
|
|
||||||
if (bailed_out)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TAP test result.
|
|
||||||
if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/)
|
|
||||||
{
|
|
||||||
testno += 1
|
|
||||||
setup_result_obj($0)
|
|
||||||
handle_tap_result()
|
|
||||||
}
|
|
||||||
# TAP plan (normal or "SKIP" without explanation).
|
|
||||||
else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/)
|
|
||||||
{
|
|
||||||
# The next two lines will put the number of planned tests in $0.
|
|
||||||
sub("^1\\.\\.", "")
|
|
||||||
sub("[^0-9]*$", "")
|
|
||||||
handle_tap_plan($0, "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
# TAP "SKIP" plan, with an explanation.
|
|
||||||
else if ($0 ~ /^1\.\.0+[ \t]*#/)
|
|
||||||
{
|
|
||||||
# The next lines will put the skip explanation in $0, stripping
|
|
||||||
# any leading and trailing whitespace. This is a little more
|
|
||||||
# tricky in truth, since we want to also strip a potential leading
|
|
||||||
# "SKIP" string from the message.
|
|
||||||
sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "")
|
|
||||||
sub("[ \t]*$", "");
|
|
||||||
handle_tap_plan(0, $0)
|
|
||||||
}
|
|
||||||
# "Bail out!" magic.
|
|
||||||
# Older versions of prove and TAP::Harness (e.g., 3.17) did not
|
|
||||||
# recognize a "Bail out!" directive when preceded by leading
|
|
||||||
# whitespace, but more modern versions (e.g., 3.23) do. So we
|
|
||||||
# emulate the latter, "more modern" behaviour.
|
|
||||||
else if ($0 ~ /^[ \t]*Bail out!/)
|
|
||||||
{
|
|
||||||
bailed_out = 1
|
|
||||||
# Get the bailout message (if any), with leading and trailing
|
|
||||||
# whitespace stripped. The message remains stored in `$0`.
|
|
||||||
sub("^[ \t]*Bail out![ \t]*", "");
|
|
||||||
sub("[ \t]*$", "");
|
|
||||||
# Format the error message for the
|
|
||||||
bailout_message = "Bail out!"
|
|
||||||
if (length($0))
|
|
||||||
bailout_message = bailout_message " " $0
|
|
||||||
testsuite_error(bailout_message)
|
|
||||||
}
|
|
||||||
# Maybe we have too look for dianogtic comments too.
|
|
||||||
else if (comments != 0)
|
|
||||||
{
|
|
||||||
comment = extract_tap_comment($0);
|
|
||||||
if (length(comment))
|
|
||||||
report("#", comment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
## -------- ##
|
|
||||||
## FINISH ##
|
|
||||||
## -------- ##
|
|
||||||
|
|
||||||
# A "Bail out!" directive should cause us to ignore any following TAP
|
|
||||||
# error, as well as a non-zero exit status from the TAP producer.
|
|
||||||
if (!bailed_out)
|
|
||||||
{
|
|
||||||
if (!plan_seen)
|
|
||||||
{
|
|
||||||
testsuite_error("missing test plan")
|
|
||||||
}
|
|
||||||
else if (planned_tests != testno)
|
|
||||||
{
|
|
||||||
bad_amount = testno > planned_tests ? "many" : "few"
|
|
||||||
testsuite_error(sprintf("too %s tests run (expected %d, got %d)",
|
|
||||||
bad_amount, planned_tests, testno))
|
|
||||||
}
|
|
||||||
if (!ignore_exit)
|
|
||||||
{
|
|
||||||
# Fetch exit status from the last line.
|
|
||||||
exit_message = get_test_exit_message(nextline)
|
|
||||||
if (exit_message)
|
|
||||||
testsuite_error(exit_message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write_test_results()
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
} # End of "BEGIN" block.
|
|
||||||
'
|
|
||||||
|
|
||||||
# TODO: document that we consume the file descriptor 3 :-(
|
|
||||||
} 3>"$log_file"
|
|
||||||
|
|
||||||
test $? -eq 0 || fatal "I/O or internal error"
|
|
||||||
|
|
||||||
# Local Variables:
|
|
||||||
# mode: shell-script
|
|
||||||
# sh-indentation: 2
|
|
||||||
# eval: (add-hook 'before-save-hook 'time-stamp)
|
|
||||||
# time-stamp-start: "scriptversion="
|
|
||||||
# time-stamp-format: "%:y-%02m-%02d.%02H"
|
|
||||||
# time-stamp-time-zone: "UTC0"
|
|
||||||
# time-stamp-end: "; # UTC"
|
|
||||||
# End:
|
|
54
configure.ac
54
configure.ac
|
@ -1,8 +1,17 @@
|
||||||
AC_PREREQ([2.63])
|
AC_PREREQ([2.63])
|
||||||
AC_INIT([ddclient], [3.10.0_2])
|
# Get the version from ddclient.in so that the same version string
|
||||||
|
# doesn't have to be maintained in two places. The m4_dquote macro is
|
||||||
|
# used instead of quote characters to ensure that the command is only
|
||||||
|
# run once. The command outputs quote characters to prevent
|
||||||
|
# incidental expansion (the m4_esyscmd macro does not quote the
|
||||||
|
# command output itself, so the command output is subject to
|
||||||
|
# expansion).
|
||||||
|
AC_INIT([ddclient], m4_dquote(m4_esyscmd([printf '[%s]' "$(./ddclient.in --version=short)"])))
|
||||||
|
# Needed because of the above invocation of ddclient.in.
|
||||||
|
AC_SUBST([CONFIGURE_DEPENDENCIES], ['$(top_srcdir)/ddclient.in'])
|
||||||
AC_CONFIG_SRCDIR([ddclient.in])
|
AC_CONFIG_SRCDIR([ddclient.in])
|
||||||
AC_CONFIG_AUX_DIR([build-aux])
|
AC_CONFIG_AUX_DIR([build-aux])
|
||||||
AC_CONFIG_MACRO_DIR([m4])
|
AC_CONFIG_MACRO_DIR([build-aux/m4])
|
||||||
AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||||
# If the automake dependency is bumped to v1.12 or newer, remove
|
# If the automake dependency is bumped to v1.12 or newer, remove
|
||||||
# build-aux/tap-driver.sh from the repository. Automake 1.12+ comes
|
# build-aux/tap-driver.sh from the repository. Automake 1.12+ comes
|
||||||
|
@ -14,6 +23,18 @@ AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||||
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
|
AM_INIT_AUTOMAKE([1.11 -Wall -Werror foreign subdir-objects parallel-tests])
|
||||||
AM_SILENT_RULES
|
AM_SILENT_RULES
|
||||||
|
|
||||||
|
m4_define([CONFDIR_DEFAULT], [${sysconfdir}/AC_PACKAGE_NAME])
|
||||||
|
AC_ARG_WITH(
|
||||||
|
[confdir],
|
||||||
|
[AS_HELP_STRING(
|
||||||
|
[--with-confdir=DIR],
|
||||||
|
m4_expand([[look for ddclient.conf in DIR @<:@default: ]CONFDIR_DEFAULT[@:>@]]))],
|
||||||
|
[],
|
||||||
|
# The single quotes are intentional; see:
|
||||||
|
# https://www.gnu.org/software/automake/manual/html_node/Uniform.html
|
||||||
|
[with_confdir='CONFDIR_DEFAULT'])
|
||||||
|
AC_SUBST([confdir], [${with_confdir}])
|
||||||
|
|
||||||
AC_PROG_MKDIR_P
|
AC_PROG_MKDIR_P
|
||||||
|
|
||||||
# The Fedora Docker image doesn't come with the 'findutils' package.
|
# The Fedora Docker image doesn't come with the 'findutils' package.
|
||||||
|
@ -27,7 +48,19 @@ AC_PROG_MKDIR_P
|
||||||
AC_PATH_PROG([FIND], [find])
|
AC_PATH_PROG([FIND], [find])
|
||||||
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
|
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
|
||||||
|
|
||||||
AC_PATH_PROG([CURL], [curl])
|
AC_ARG_WITH([curl],
|
||||||
|
[AS_HELP_STRING([[--with-curl[=CURL]]], [use CURL as absolute path to curl executable])],
|
||||||
|
[],
|
||||||
|
[with_curl=yes])
|
||||||
|
AS_CASE([${with_curl}],
|
||||||
|
[[yes]], [AC_PATH_PROG([CURL], [curl])],
|
||||||
|
[[no]], [CURL=],
|
||||||
|
[
|
||||||
|
AC_MSG_CHECKING([for curl])
|
||||||
|
CURL=${with_curl}
|
||||||
|
AC_MSG_RESULT([${CURL}])
|
||||||
|
]);
|
||||||
|
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
|
||||||
|
|
||||||
AX_WITH_PROG([PERL], perl)
|
AX_WITH_PROG([PERL], perl)
|
||||||
AX_PROG_PERL_VERSION([5.10.1], [],
|
AX_PROG_PERL_VERSION([5.10.1], [],
|
||||||
|
@ -39,11 +72,11 @@ AC_SUBST([PERL])
|
||||||
# package doesn't depend on all of them, so their availability can't
|
# package doesn't depend on all of them, so their availability can't
|
||||||
# be assumed.
|
# be assumed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
|
Data::Dumper
|
||||||
File::Basename
|
File::Basename
|
||||||
File::Path
|
File::Path
|
||||||
File::Temp
|
File::Temp
|
||||||
Getopt::Long
|
Getopt::Long
|
||||||
IO::Socket::INET
|
|
||||||
Socket
|
Socket
|
||||||
Sys::Hostname
|
Sys::Hostname
|
||||||
version=0.77
|
version=0.77
|
||||||
|
@ -54,9 +87,12 @@ m4_foreach_w([_m], [
|
||||||
# then some tests will fail. Only prints a warning if not installed.
|
# then some tests will fail. Only prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
B
|
B
|
||||||
Data::Dumper
|
Exporter
|
||||||
File::Spec::Functions
|
File::Spec::Functions
|
||||||
File::Temp
|
File::Temp
|
||||||
|
List::Util
|
||||||
|
Scalar::Util
|
||||||
|
re
|
||||||
], [AX_PROG_PERL_MODULES([_m], [],
|
], [AX_PROG_PERL_MODULES([_m], [],
|
||||||
[AC_MSG_WARN([some tests will fail due to missing module _m])])])
|
[AC_MSG_WARN([some tests will fail due to missing module _m])])])
|
||||||
|
|
||||||
|
@ -65,27 +101,23 @@ m4_foreach_w([_m], [
|
||||||
# prints a warning if not installed.
|
# prints a warning if not installed.
|
||||||
m4_foreach_w([_m], [
|
m4_foreach_w([_m], [
|
||||||
Carp
|
Carp
|
||||||
Exporter
|
|
||||||
HTTP::Daemon=6.12
|
HTTP::Daemon=6.12
|
||||||
HTTP::Daemon::SSL
|
HTTP::Daemon::SSL
|
||||||
HTTP::Message::PSGI
|
HTTP::Message::PSGI
|
||||||
HTTP::Request
|
HTTP::Request
|
||||||
HTTP::Response
|
HTTP::Response
|
||||||
IO::Socket::INET6
|
JSON::PP
|
||||||
IO::Socket::IP
|
|
||||||
IO::Socket::SSL
|
|
||||||
Scalar::Util
|
|
||||||
Test::MockModule
|
Test::MockModule
|
||||||
Test::TCP
|
Test::TCP
|
||||||
Test::Warnings
|
Test::Warnings
|
||||||
Time::HiRes
|
Time::HiRes
|
||||||
URI
|
URI
|
||||||
|
parent
|
||||||
], [AX_PROG_PERL_MODULES([_m], [],
|
], [AX_PROG_PERL_MODULES([_m], [],
|
||||||
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
[AC_MSG_WARN([some tests may be skipped due to missing module _m])])])
|
||||||
|
|
||||||
AC_CONFIG_FILES([
|
AC_CONFIG_FILES([
|
||||||
Makefile
|
Makefile
|
||||||
t/geturl_connectivity.pl
|
|
||||||
t/version.pl
|
t/version.pl
|
||||||
])
|
])
|
||||||
AC_OUTPUT
|
AC_OUTPUT
|
||||||
|
|
318
ddclient.conf.in
318
ddclient.conf.in
|
@ -1,12 +1,12 @@
|
||||||
######################################################################
|
######################################################################
|
||||||
##
|
##
|
||||||
## Define default global variables with lines like:
|
## Define default global variables with lines like:
|
||||||
## var=value [, var=value]*
|
## var=value [, var=value]*
|
||||||
## These values will be used for each following host unless overridden
|
## These values will be used for each following host unless overridden
|
||||||
## with a local variable definition.
|
## with a local variable definition.
|
||||||
##
|
##
|
||||||
## Define local variables for one or more hosts with:
|
## Define local variables for one or more hosts with:
|
||||||
## var=value [, var=value]* host.and.domain[,host2.and.domain...]
|
## var=value [, var=value]* host.and.domain[,host2.and.domain...]
|
||||||
##
|
##
|
||||||
## Lines can be continued on the following line by ending the line
|
## Lines can be continued on the following line by ending the line
|
||||||
## with a \
|
## with a \
|
||||||
|
@ -16,37 +16,47 @@
|
||||||
## are mentioned here.
|
## are mentioned here.
|
||||||
##
|
##
|
||||||
######################################################################
|
######################################################################
|
||||||
daemon=300 # check every 300 seconds
|
|
||||||
syslog=yes # log update msgs to syslog
|
## Use encryption (TLS) when the scheme (either "http://" or "https://") is
|
||||||
mail=root # mail all msgs to root
|
## missing from a URL. Defaults to "yes".
|
||||||
mail-failure=root # mail failed update msgs to root
|
#ssl=yes
|
||||||
pid=@runstatedir@/ddclient.pid # record PID in file.
|
|
||||||
ssl=yes # use ssl-support. Works with
|
daemon=300 # check every 300 seconds
|
||||||
# ssl-library
|
syslog=yes # log update msgs to syslog
|
||||||
# postscript=script # run script after updating. The
|
mail=root # mail all msgs to root
|
||||||
# new IP is added as argument.
|
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.
|
||||||
|
# postscript=script # run script after updating. The new IP is
|
||||||
|
# added as argument.
|
||||||
#
|
#
|
||||||
#use=watchguard-soho, fw=192.168.111.1:80 # via Watchguard's SOHO FW
|
#use=watchguard-soho, fw=192.168.111.1:80 # via Watchguard's SOHO FW
|
||||||
#use=netopia-r910, fw=192.168.111.1:80 # via Netopia R910 FW
|
#use=netopia-r910, fw=192.168.111.1:80 # via Netopia R910 FW
|
||||||
#use=smc-barricade, fw=192.168.123.254:80 # via SMC's Barricade FW
|
#use=smc-barricade, fw=192.168.123.254:80 # via SMC's Barricade FW
|
||||||
#use=netgear-rt3xx, fw=192.168.0.1:80 # via Netgear's internet FW
|
#use=netgear-rt3xx, fw=192.168.0.1:80 # via Netgear's internet FW
|
||||||
#use=linksys, fw=192.168.1.1:80 # via Linksys's internet FW
|
#use=linksys, fw=192.168.1.1:80 # via Linksys's internet FW
|
||||||
#use=maxgate-ugate3x00, fw=192.168.0.1:80 # via MaxGate's UGATE-3x00 FW
|
#use=maxgate-ugate3x00, fw=192.168.0.1:80 # via MaxGate's UGATE-3x00 FW
|
||||||
#use=elsa-lancom-dsl10, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
#use=elsa-lancom-dsl10, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||||
#use=elsa-lancom-dsl10-ch01, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
#use=elsa-lancom-dsl10-ch01, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||||
#use=elsa-lancom-dsl10-ch02, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
#use=elsa-lancom-dsl10-ch02, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
|
||||||
#use=alcatel-stp, fw=10.0.0.138:80 # via Alcatel Speed Touch Pro
|
#use=alcatel-stp, fw=10.0.0.138:80 # via Alcatel Speed Touch Pro
|
||||||
#use=xsense-aero, fw=192.168.1.1:80 # via Xsense Aero Router
|
#use=xsense-aero, fw=192.168.1.1:80 # via Xsense Aero Router
|
||||||
#use=allnet-1298, fw=192.168.1.1:80 # via AllNet 1298 DSL Router
|
#use=allnet-1298, fw=192.168.1.1:80 # via AllNet 1298 DSL Router
|
||||||
#use=3com-oc-remote812, fw=192.168.0.254:80 # via 3com OfficeConnect Remote 812
|
#use=3com-oc-remote812, fw=192.168.0.254:80 # via 3com OfficeConnect Remote 812
|
||||||
#use=e-tech, fw=192.168.1.1:80 # via E-tech Router
|
#use=e-tech, fw=192.168.1.1:80 # via E-tech Router
|
||||||
#use=cayman-3220h, fw=192.168.0.1:1080 # via Cayman 3220-H DSL Router
|
#use=cayman-3220h, fw=192.168.0.1:1080 # via Cayman 3220-H DSL Router
|
||||||
#
|
#
|
||||||
#fw-login=admin, fw-password=XXXXXX # FW login and password
|
#fw-login=admin, fw-password=XXXXXX # FW login and password
|
||||||
#
|
#
|
||||||
## To obtain an IP address from FW status page (using fw-login, fw-password)
|
## To obtain an IP address from FW status page (using fw-login, fw-password)
|
||||||
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
|
#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
|
||||||
#
|
#
|
||||||
|
## To obtain an IP address via UPnP from router
|
||||||
|
## Requires miniupnpc to be installed on the system.
|
||||||
|
#use=cmd, cmd=external-ip
|
||||||
|
#
|
||||||
## To obtain an IP address from Web status page (using the proxy if defined)
|
## To obtain an IP address from Web status page (using the proxy if defined)
|
||||||
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
## by default, checkip.dyndns.org is used if you use the dyndns protocol.
|
||||||
## Using use=web is enough to get it working.
|
## Using use=web is enough to get it working.
|
||||||
|
@ -54,75 +64,55 @@ ssl=yes # use ssl-support. Works with
|
||||||
## get banned from their service.
|
## get banned from their service.
|
||||||
#use=web, web=checkip.dyndns.org/, web-skip='IP Address' # found after IP Address
|
#use=web, web=checkip.dyndns.org/, web-skip='IP Address' # found after IP Address
|
||||||
#
|
#
|
||||||
#use=ip, ip=127.0.0.1 # via static IP's
|
#use=ip, ip=127.0.0.1 # via static IP's
|
||||||
#use=if, if=eth0 # via interfaces
|
#use=if, if=eth0 # via interfaces
|
||||||
#use=web # via web
|
#use=web # via web
|
||||||
#
|
#
|
||||||
#protocol=dyndns2 # default protocol
|
#protocol=dyndns2 # default protocol
|
||||||
#proxy=fasthttp.sympatico.ca:80 # default proxy
|
#proxy=fasthttp.sympatico.ca:80 # default proxy
|
||||||
#server=members.dyndns.org # default server
|
#server=members.dyndns.org # default server
|
||||||
#server=members.dyndns.org:8245 # default server (bypassing proxies)
|
#server=members.dyndns.org:8245 # default server (bypassing proxies)
|
||||||
|
|
||||||
#login=your-login # default login
|
#login=your-login # default login
|
||||||
#password=test # default password
|
#password=test # default password
|
||||||
#mx=mx.for.your.host # default MX
|
#mx=mx.for.your.host # default MX
|
||||||
#backupmx=yes|no # host is primary MX?
|
#backupmx=yes|no # host is primary MX?
|
||||||
#wildcard=yes|no # add wildcard CNAME?
|
#wildcard=yes|no # add wildcard CNAME?
|
||||||
|
|
||||||
##
|
##
|
||||||
## dyndns.org dynamic addresses
|
## dyndns.org dynamic addresses
|
||||||
##
|
##
|
||||||
## (supports variables: wildcard,mx,backupmx)
|
## (supports variables: wildcard,mx,backupmx)
|
||||||
##
|
##
|
||||||
# server=members.dyndns.org, \
|
# server=members.dyndns.org, \
|
||||||
# protocol=dyndns2 \
|
# protocol=dyndns2 \
|
||||||
# your-dynamic-host.dyndns.org
|
# your-dynamic-host.dyndns.org
|
||||||
|
|
||||||
##
|
|
||||||
## dyndns.org static addresses
|
|
||||||
##
|
|
||||||
## (supports variables: wildcard,mx,backupmx)
|
|
||||||
##
|
|
||||||
# static=yes, \
|
|
||||||
# server=members.dyndns.org, \
|
|
||||||
# protocol=dyndns2 \
|
|
||||||
# your-static-host.dyndns.org
|
|
||||||
|
|
||||||
##
|
|
||||||
## dyndns.org custom addresses
|
|
||||||
##
|
|
||||||
## (supports variables: wildcard,mx,backupmx)
|
|
||||||
##
|
|
||||||
# custom=yes, \
|
|
||||||
# server=members.dyndns.org, \
|
|
||||||
# protocol=dyndns2 \
|
|
||||||
# your-domain.top-level,your-other-domain.top-level
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## ZoneEdit (zoneedit.com)
|
## ZoneEdit (zoneedit.com)
|
||||||
##
|
##
|
||||||
# server=dynamic.zoneedit.com, \
|
# server=dynamic.zoneedit.com, \
|
||||||
# protocol=zoneedit1, \
|
# protocol=zoneedit1, \
|
||||||
# login=your-zoneedit-login, \
|
# login=your-zoneedit-login, \
|
||||||
# password=your-zoneedit-password \
|
# password=your-zoneedit-password \
|
||||||
# your.any.domain,your-2nd.any.dom
|
# your.any.domain,your-2nd.any.dom
|
||||||
|
|
||||||
##
|
##
|
||||||
## EasyDNS (easydns.com)
|
## EasyDNS (easydns.com)
|
||||||
##
|
##
|
||||||
# server=members.easydns.com, \
|
# server=members.easydns.com, \
|
||||||
# protocol=easydns, \
|
# protocol=easydns, \
|
||||||
# login=your-easydns-login, \
|
# login=your-easydns-login, \
|
||||||
# password=your-easydns-password \
|
# password=your-easydns-password \
|
||||||
# your.any.domain,your-2nd.any.domain
|
# your.any.domain,your-2nd.any.domain
|
||||||
|
|
||||||
##
|
##
|
||||||
## dslreports.com dynamic-host monitoring
|
## dslreports.com dynamic-host monitoring
|
||||||
##
|
##
|
||||||
# server=members.dslreports.com \
|
# server=members.dslreports.com \
|
||||||
# protocol=dslreports1, \
|
# protocol=dslreports1, \
|
||||||
# login=dslreports-login, \
|
# login=dslreports-login, \
|
||||||
# password=dslreports-password \
|
# password=dslreports-password \
|
||||||
# dslreports-unique-id
|
# dslreports-unique-id
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -130,16 +120,16 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
# use=web, web=members.orgdns.org/nic/ip
|
# use=web, web=members.orgdns.org/nic/ip
|
||||||
# protocol=dyndns2
|
# protocol=dyndns2
|
||||||
# server=www.orgdns.org \
|
# server=www.orgdns.org \
|
||||||
# login=yourLoginName \
|
# login=yourLoginName \
|
||||||
# password=yourPassword \
|
# password=yourPassword \
|
||||||
# yourSubdomain.orgdns.org
|
# yourSubdomain.orgdns.org
|
||||||
|
|
||||||
##
|
##
|
||||||
## NameCheap (namecheap.com)
|
## NameCheap (namecheap.com)
|
||||||
##
|
##
|
||||||
# protocol=namecheap, \
|
# protocol=namecheap, \
|
||||||
# server=dynamicdns.park-your-domain.com, \
|
# server=dynamicdns.park-your-domain.com, \
|
||||||
# login=example.com, \
|
# login=example.com, \
|
||||||
# password=example.com-password \
|
# password=example.com-password \
|
||||||
# subdomain.example.com
|
# subdomain.example.com
|
||||||
|
@ -147,10 +137,10 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
||||||
##
|
##
|
||||||
# protocol = nfsn, \
|
# protocol=nfsn, \
|
||||||
|
# zone=example.com, \
|
||||||
# login=member-login, \
|
# login=member-login, \
|
||||||
# password=api-key, \
|
# password=api-key \
|
||||||
# zone=example.com \
|
|
||||||
# example.com,subdomain.example.com
|
# example.com,subdomain.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -171,7 +161,7 @@ ssl=yes # use ssl-support. Works with
|
||||||
# ssl=yes, \
|
# ssl=yes, \
|
||||||
# server=dynupdate.no-ip.com, \
|
# server=dynupdate.no-ip.com, \
|
||||||
# login=your-noip-login, \
|
# login=your-noip-login, \
|
||||||
# password=your-noip-password, \
|
# password=your-noip-password \
|
||||||
# your-host.domain.com, your-2nd-host.domain.com
|
# your-host.domain.com, your-2nd-host.domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -186,30 +176,40 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## CloudFlare (www.cloudflare.com)
|
## CloudFlare (www.cloudflare.com)
|
||||||
##
|
##
|
||||||
#protocol=cloudflare, \
|
# protocol=cloudflare, \
|
||||||
#zone=domain.tld, \
|
# zone=domain.tld, \
|
||||||
#ttl=1, \
|
# ttl=1, \
|
||||||
#login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (wihtout double quotes).
|
# login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (without double quotes).
|
||||||
#password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
|
# password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
|
||||||
#domain.tld,my.domain.tld
|
# domain.tld,my.domain.tld
|
||||||
|
|
||||||
##
|
##
|
||||||
## Gandi (gandi.net)
|
## Gandi (gandi.net)
|
||||||
##
|
##
|
||||||
## Single host update
|
## Single host update
|
||||||
# protocol=gandi, \
|
# protocol=gandi
|
||||||
# zone=example.com, \
|
# zone=example.com
|
||||||
# password=my-gandi-api-key, \
|
# password=my-gandi-access-token
|
||||||
# ttl=3h \
|
# use-personal-access-token=yes
|
||||||
|
# ttl=10800 # optional
|
||||||
# myhost.example.com
|
# myhost.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## Google Domains (www.google.com/domains)
|
## GoDaddy (godaddy.com)
|
||||||
##
|
##
|
||||||
# protocol=googledomains,
|
# protocol=godaddy, \
|
||||||
# login=my-auto-generated-username,
|
# password=my-godaddy-api-key, \
|
||||||
# password=my-auto-generated-password
|
# password=my-godaddy-secret, \
|
||||||
# my.domain.tld, otherhost.domain.tld
|
# ttl=600 \
|
||||||
|
# zone=example.com, \
|
||||||
|
# myhost.example.com,nexthost.example.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## Hurricane Electric (dns.he.net)
|
||||||
|
##
|
||||||
|
# protocol=he.net, \
|
||||||
|
# password=my-genereated-password \
|
||||||
|
# myhost.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## Duckdns (http://www.duckdns.org/)
|
## Duckdns (http://www.duckdns.org/)
|
||||||
|
@ -227,6 +227,14 @@ ssl=yes # use ssl-support. Works with
|
||||||
# password=my-token
|
# password=my-token
|
||||||
# myhost
|
# myhost
|
||||||
|
|
||||||
|
##
|
||||||
|
## DDNS.FM (https://ddns.fm/)
|
||||||
|
##
|
||||||
|
#
|
||||||
|
# protocol=ddns.fm,
|
||||||
|
# password=my-token
|
||||||
|
# myhost.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## MyOnlinePortal (http://myonlineportal.net)
|
## MyOnlinePortal (http://myonlineportal.net)
|
||||||
##
|
##
|
||||||
|
@ -243,23 +251,23 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## nsupdate.info IPV4(https://www.nsupdate.info)
|
## nsupdate.info IPV4(https://www.nsupdate.info)
|
||||||
##
|
##
|
||||||
#use=web, web=http://ipv4.nsupdate.info/myip
|
# use=web, web=http://ipv4.nsupdate.info/myip
|
||||||
#protocol=dyndns2
|
# protocol=dyndns2
|
||||||
#server=ipv4.nsupdate.info
|
# server=ipv4.nsupdate.info
|
||||||
#login=domain.nsupdate.info
|
# login=domain.nsupdate.info
|
||||||
#password='123'
|
# password='123'
|
||||||
#domain.nsupdate.info
|
# domain.nsupdate.info
|
||||||
|
|
||||||
##
|
##
|
||||||
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
## nsupdate.info IPV6 (https://www.nsupdate.info)
|
||||||
## ddclient releases <= 3.8.1 do not support IPv6
|
## ddclient releases <= 3.8.1 do not support IPv6
|
||||||
##
|
##
|
||||||
#usev6=if, if=eth0
|
# usev6=if, if=eth0
|
||||||
#protocol=dyndns2
|
# protocol=dyndns2
|
||||||
#server=ipv6.nsupdate.info
|
# server=ipv6.nsupdate.info
|
||||||
#login=domain.nsupdate.info
|
# login=domain.nsupdate.info
|
||||||
#password='123'
|
# password='123'
|
||||||
#domain.nsupdate.info
|
# domain.nsupdate.info
|
||||||
|
|
||||||
##
|
##
|
||||||
## Yandex.Mail for Domain (domain.yandex.com)
|
## Yandex.Mail for Domain (domain.yandex.com)
|
||||||
|
@ -285,6 +293,16 @@ ssl=yes # use ssl-support. Works with
|
||||||
# password=your_password
|
# password=your_password
|
||||||
# test.example.com
|
# test.example.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## Porkbun (https://porkbun.com/)
|
||||||
|
##
|
||||||
|
# protocol=porkbun
|
||||||
|
# apikey=APIKey
|
||||||
|
# secretapikey=SecretAPIKey
|
||||||
|
# root-domain=example.com
|
||||||
|
# host.example.com,host2.sub.example.com
|
||||||
|
# example.com,sub.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## ClouDNS (https://www.cloudns.net)
|
## ClouDNS (https://www.cloudns.net)
|
||||||
##
|
##
|
||||||
|
@ -303,10 +321,25 @@ ssl=yes # use ssl-support. Works with
|
||||||
##
|
##
|
||||||
## dnsexit (www.dnsexit.com)
|
## dnsexit (www.dnsexit.com)
|
||||||
##
|
##
|
||||||
#protocol=dnsexit, \
|
# protocol=dnsexit, \
|
||||||
#login=myusername, \
|
# login=myusername, \
|
||||||
#password=mypassword, \
|
# password=mypassword, \
|
||||||
#subdomain-1.domain.com,subdomain-2.domain.com
|
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## dnsexit2 (API method www.dnsexit.com)
|
||||||
|
##
|
||||||
|
# protocol=dnsexit2
|
||||||
|
# password=MyAPIKey
|
||||||
|
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## domeneshop (www.domeneshop.no)
|
||||||
|
##
|
||||||
|
# protocol=domeneshop
|
||||||
|
# login=<token>
|
||||||
|
# password=<secret>
|
||||||
|
# subdomain-1.domain.com,subdomain-2.domain.com
|
||||||
|
|
||||||
##
|
##
|
||||||
## Njal.la (http://njal.la/)
|
## Njal.la (http://njal.la/)
|
||||||
|
@ -315,3 +348,74 @@ ssl=yes # use ssl-support. Works with
|
||||||
# password=mypassword
|
# password=mypassword
|
||||||
# quietreply=no|yes
|
# quietreply=no|yes
|
||||||
# my-domain.com
|
# my-domain.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## regfish.de (www.regfish.de/)
|
||||||
|
##
|
||||||
|
# protocol=regfishde,
|
||||||
|
# password=mypassword
|
||||||
|
# my-domain.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## Enom (www.enom.com)
|
||||||
|
##
|
||||||
|
# protocol=enom,
|
||||||
|
# login=domain.name,
|
||||||
|
# password=domain-password
|
||||||
|
# my-domain.com
|
||||||
|
|
||||||
|
##
|
||||||
|
## DigitalOcean (www.digitalocean.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)
|
||||||
|
##
|
||||||
|
# protocol=infomaniak,
|
||||||
|
# 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
|
||||||
|
|
7378
ddclient.in
7378
ddclient.in
File diff suppressed because it is too large
Load diff
39
docs/ProviderGuidelines.md
Normal file
39
docs/ProviderGuidelines.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Provider implementations
|
||||||
|
|
||||||
|
Author: [@LenardHess](https://github.com/LenardHess/)\
|
||||||
|
Date: 2023-11-23
|
||||||
|
|
||||||
|
This document is meant to detail the mechanisms that provider implementation shall use. It differentiates between new and legacy provider implementations. The former are adhering to the IPv6 support updates being done to ddclient, the legacy ones are from before that update.
|
||||||
|
|
||||||
|
## New provider Implementation
|
||||||
|
1. Grab the IP(s) from $config{$host}{'wantipv4'} and/or $config{$host}{'wantipv6'}
|
||||||
|
2. Optional: Query the provider for the current IP record(s). If they are already good, skip updating IP record(s)
|
||||||
|
3. Update the IP record(s).
|
||||||
|
4. If successful (or if the records were already good):
|
||||||
|
- Set 'status-ipv4' and/or 'status-ipv6' to 'good'
|
||||||
|
- Set 'ipv4' and/or 'ipv6' to the IP that has been set
|
||||||
|
- Set 'mtime' to the current time
|
||||||
|
5. If not successful:
|
||||||
|
- Set 'status-ipv4' and/or 'status-ipv6' to an error message
|
||||||
|
- Set 'atime' to the current time
|
||||||
|
|
||||||
|
The new provider implementation should not set 'status' nor 'ip'. They're part of the legacy infrastructure and ddclient will take care of setting them correctly.
|
||||||
|
|
||||||
|
## Legacy provider implementations
|
||||||
|
1. Grab the IP from $config{$host}{'wantip'}
|
||||||
|
2. Optional: Query the provider for the current IP record. If it is already good, skip updating IP record
|
||||||
|
3. Update the IP record.
|
||||||
|
4. If successful (or if the record was already good):
|
||||||
|
- Set 'status' to 'good'
|
||||||
|
- Set 'ip' to the IP that has been set
|
||||||
|
- Set 'mtime' to the current time
|
||||||
|
5. If not successful:
|
||||||
|
- Set 'status' to an error message
|
||||||
|
- Set 'atime' to the current time
|
||||||
|
|
||||||
|
# ToDo
|
||||||
|
- Decide/Inquire whether services prefer querying the IP first. Then decide whether to make it mandatory.
|
||||||
|
- Write guidelines on checking existing records (i.e. check TTL as well?).
|
||||||
|
- Start a list of providers and their implementation state
|
||||||
|
- Add more details to this document
|
||||||
|
- Whether 'wantip*' ought to be deleted when read or not.
|
|
@ -10,7 +10,3 @@
|
||||||
## force an update twice a month (only if you are not using daemon-mode)
|
## force an update twice a month (only if you are not using daemon-mode)
|
||||||
##
|
##
|
||||||
## 30 23 1,15 * * root /usr/bin/ddclient -daemon=0 -syslog -quiet -force
|
## 30 23 1,15 * * root /usr/bin/ddclient -daemon=0 -syslog -quiet -force
|
||||||
######################################################################
|
|
||||||
## retry failed updates every hour (only if you are not using daemon-mode)
|
|
||||||
##
|
|
||||||
## 0 * * * * root /usr/bin/ddclient -daemon=0 -syslog -quiet retry
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# PROVIDE: ddclient
|
|
||||||
# REQUIRE: LOGIN
|
|
||||||
# KEYWORD: shutdown
|
|
||||||
#
|
|
||||||
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
|
||||||
# to enable this service:
|
|
||||||
#
|
|
||||||
# ddclient_enable (bool): Set to NO by default.
|
|
||||||
# Set it to YES to enable ddclient.
|
|
||||||
|
|
||||||
. /etc/rc.subr
|
|
||||||
|
|
||||||
name=ddclient
|
|
||||||
rcvar=ddclient_enable
|
|
||||||
ddclient_conf="/etc/ddclient/ddclient.conf"
|
|
||||||
|
|
||||||
command="/usr/local/sbin/${name}"
|
|
||||||
load_rc_config $name
|
|
||||||
|
|
||||||
delay=$(grep -v '^\s*#' "${ddclient_conf}" | grep -i -m 1 "daemon" | awk -F '=' '{print $2}')
|
|
||||||
|
|
||||||
if [ -z "${delay}" ]
|
|
||||||
then
|
|
||||||
ddclient_flags="-daemon 300"
|
|
||||||
else
|
|
||||||
ddclient_flags=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_rc_command "$1"
|
|
|
@ -1,100 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# ddclient This shell script takes care of starting and stopping
|
|
||||||
# ddclient.
|
|
||||||
#
|
|
||||||
# chkconfig: 2345 65 35
|
|
||||||
# description: ddclient provides support for updating dynamic DNS services.
|
|
||||||
|
|
||||||
CONF=/etc/ddclient/ddclient.conf
|
|
||||||
program=ddclient
|
|
||||||
|
|
||||||
[ -f $CONF ] || exit 0
|
|
||||||
|
|
||||||
system=unknown
|
|
||||||
if [ -f /etc/fedora-release ]; then
|
|
||||||
system=fedora
|
|
||||||
elif [ -f /etc/redhat-release ]; then
|
|
||||||
system=redhat
|
|
||||||
elif [ -f /etc/debian_version ]; then
|
|
||||||
system=debian
|
|
||||||
fi
|
|
||||||
|
|
||||||
PID=''
|
|
||||||
if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then
|
|
||||||
. /etc/init.d/functions
|
|
||||||
PID=`pidofproc $program`
|
|
||||||
else
|
|
||||||
PID=`ps -aef | grep "$program - sleep" | grep -v grep | awk '{print $2}'`
|
|
||||||
fi
|
|
||||||
|
|
||||||
PATH=/usr/bin:/usr/local/bin:${PATH}
|
|
||||||
export PATH
|
|
||||||
|
|
||||||
# See how we were called.
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
# See if daemon=value is specified in the config file.
|
|
||||||
# Assumptions:
|
|
||||||
# * there are no quoted "#" characters before "daemon="
|
|
||||||
# (if there is a "#" it starts a comment)
|
|
||||||
# * "daemon=" does not appear in a password or value
|
|
||||||
# * if the interval value is 0, it is not quoted
|
|
||||||
INTERVAL=$(sed -e '
|
|
||||||
s/^\([^#]*[,[:space:]]\)\{0,1\}daemon=\([^,[:space:]]*\).*$/\2/
|
|
||||||
t quit
|
|
||||||
d
|
|
||||||
:quit
|
|
||||||
q
|
|
||||||
' "$CONF")
|
|
||||||
if [ -z "$DELAY" ] || [ "$DELAY" = "0" ]; then
|
|
||||||
DELAY="-daemon 300"
|
|
||||||
else
|
|
||||||
# use the interval specified in the config file
|
|
||||||
DELAY=''
|
|
||||||
fi
|
|
||||||
echo -n "Starting ddclient: "
|
|
||||||
if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then
|
|
||||||
daemon $program $DELAY
|
|
||||||
else
|
|
||||||
ddclient $DELAY
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
# Stop daemon.
|
|
||||||
echo -n "Shutting down ddclient: "
|
|
||||||
if [ -n "$PID" ]; then
|
|
||||||
if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then
|
|
||||||
killproc $program
|
|
||||||
else
|
|
||||||
kill $PID
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "ddclient is not running"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
$0 stop
|
|
||||||
$0 start
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then
|
|
||||||
status $program
|
|
||||||
else
|
|
||||||
if test "$PID"; then
|
|
||||||
for p in $PID; do
|
|
||||||
echo "$program (pid $p) is running"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "$program is stopped"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: ddclient {start|stop|restart|status}"
|
|
||||||
exit 1
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,38 +0,0 @@
|
||||||
#!/sbin/openrc-run
|
|
||||||
description="ddclient Daemon for Alpine"
|
|
||||||
command="/usr/bin/ddclient"
|
|
||||||
config_file="/etc/ddclient/ddclient.conf"
|
|
||||||
command_args=""
|
|
||||||
pidfile=$(grep -v '^\s*#' "${config_file}" | grep -i -m 1 pid= | awk -F '=' '{print $2}')
|
|
||||||
delay=$(grep -v '^\s*#' "${config_file}" | grep -i -m 1 "daemon" | awk -F '=' '{print $2}')
|
|
||||||
|
|
||||||
if [ -z "${delay}" ]
|
|
||||||
then
|
|
||||||
command_args="-daemon 300"
|
|
||||||
else
|
|
||||||
command_args=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
use logger
|
|
||||||
need net
|
|
||||||
after firewall
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
ebegin "Starting ddclient"
|
|
||||||
start-stop-daemon --start \
|
|
||||||
--exec "${command}" \
|
|
||||||
--pidfile "${pidfile}" \
|
|
||||||
-- \
|
|
||||||
${command_args}
|
|
||||||
eend $?
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
ebegin "Stopping ddclient"
|
|
||||||
start-stop-daemon --stop --exec "${command}" \
|
|
||||||
--pidfile "${pidfile}"
|
|
||||||
eend $?
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# ddclient This shell script takes care of starting and stopping
|
|
||||||
# ddclient.
|
|
||||||
#
|
|
||||||
# chkconfig: 2345 65 35
|
|
||||||
# description: ddclient provides support for updating dynamic DNS services.
|
|
||||||
#
|
|
||||||
# Above is for RedHat and now the LSB part
|
|
||||||
### BEGIN INIT INFO
|
|
||||||
# Provides: ddclient
|
|
||||||
# Required-Start: $syslog $remote_fs
|
|
||||||
# Should-Start: $time ypbind sendmail
|
|
||||||
# Required-Stop: $syslog $remote_fs
|
|
||||||
# Should-Stop: $time ypbind sendmail
|
|
||||||
# Default-Start: 3 5
|
|
||||||
# Default-Stop: 0 1 2 6
|
|
||||||
# Short-Description: ddclient provides support for updating dynamic DNS services
|
|
||||||
# Description: ddclient is a Perl client used to update dynamic DNS
|
|
||||||
# entries for accounts on many dynamic DNS services and
|
|
||||||
# can be used on many types of firewalls
|
|
||||||
### END INIT INFO
|
|
||||||
#
|
|
||||||
###
|
|
||||||
|
|
||||||
[ -f /etc/ddclient/ddclient.conf ] || exit 0
|
|
||||||
|
|
||||||
DDCLIENT_BIN=/usr/bin/ddclient
|
|
||||||
|
|
||||||
#
|
|
||||||
# LSB Standard (SuSE,RedHat,...)
|
|
||||||
#
|
|
||||||
if [ -f /lib/lsb/init-functions ] ; then
|
|
||||||
. /lib/lsb/init-functions
|
|
||||||
fi
|
|
||||||
|
|
||||||
# See how we were called.
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
echo -n "Starting ddclient "
|
|
||||||
start_daemon $DDCLIENT_BIN -daemon 300
|
|
||||||
rc_status -v
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
echo -n "Shutting down ddclient "
|
|
||||||
killproc -TERM `basename $DDCLIENT_BIN`
|
|
||||||
rc_status -v
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
$0 stop
|
|
||||||
$0 start
|
|
||||||
rc_status
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
echo -n "Checking for service ddclient "
|
|
||||||
checkproc `basename $DDCLIENT_BIN`w
|
|
||||||
rc_status -v
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: ddclient {start|stop|restart|status}"
|
|
||||||
exit 1
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# ddclient This shell script takes care of starting and stopping
|
|
||||||
# ddclient.
|
|
||||||
#
|
|
||||||
# chkconfig: 2345 65 35
|
|
||||||
# description: ddclient provides support for updating dynamic DNS services.
|
|
||||||
|
|
||||||
[ -f /etc/ddclient/ddclient.conf ] || exit 0
|
|
||||||
|
|
||||||
. /etc/rc.d/init.d/functions
|
|
||||||
|
|
||||||
# See how we were called.
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
# Start daemon.
|
|
||||||
echo -n "Starting ddclient: "
|
|
||||||
touch /var/lock/subsys/ddclient
|
|
||||||
daemon ddclient -daemon 300
|
|
||||||
echo
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
# Stop daemon.
|
|
||||||
echo -n "Shutting down ddclient: "
|
|
||||||
killproc ddclient
|
|
||||||
echo
|
|
||||||
rm -f /var/lock/subsys/ddclient
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
$0 stop
|
|
||||||
$0 start
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
status ddclient
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: ddclient {start|stop|restart|status}"
|
|
||||||
exit 1
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,57 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
### BEGIN INIT INFO
|
|
||||||
# Provides: ddclient
|
|
||||||
# Required-Start: $remote_fs $syslog $network
|
|
||||||
# Required-Stop: $remote_fs $syslog $network
|
|
||||||
# Default-Start: 2 3 4 5
|
|
||||||
# Default-Stop: 0 1 6
|
|
||||||
# Short-Description: Start ddclient daemon at boot time
|
|
||||||
# Description: Start ddclient that provides support for updating dynamic DNS services. Originally submitted by paolo martinelli, updated by joe passavanti
|
|
||||||
### END INIT INFO
|
|
||||||
|
|
||||||
DDCLIENT=/usr/bin/ddclient
|
|
||||||
CONF=/etc/ddclient/ddclient.conf
|
|
||||||
PIDFILE=/var/run/ddclient.pid
|
|
||||||
|
|
||||||
test -x $DDCLIENT || exit 0
|
|
||||||
test -f $CONF || exit 0
|
|
||||||
|
|
||||||
. /lib/lsb/init-functions
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
if [ ! -f $PIDFILE ]; then
|
|
||||||
log_begin_msg "Starting ddclient..."
|
|
||||||
DELAY=`grep -v '^\s*#' $CONF | grep -i -m 1 "daemon" | awk -F '=' '{print $2}'`
|
|
||||||
if [ -z "$DELAY" ] ; then
|
|
||||||
DELAY="-daemon 300"
|
|
||||||
else
|
|
||||||
DELAY=''
|
|
||||||
fi
|
|
||||||
start-stop-daemon -S -q -p $PIDFILE -x $DDCLIENT -- $DELAY
|
|
||||||
log_end_msg $?
|
|
||||||
else
|
|
||||||
log_warning_msg "Service ddclient already running..."
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
if [ -f $PIDFILE ] ; then
|
|
||||||
log_begin_msg "Stopping ddclient..."
|
|
||||||
start-stop-daemon -K -q -p $PIDFILE
|
|
||||||
log_end_msg $?
|
|
||||||
rm -f $PIDFILE
|
|
||||||
else
|
|
||||||
log_warning_msg "No ddclient running..."
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
restart|reload|force-reload)
|
|
||||||
$0 stop
|
|
||||||
$0 start
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_success_msg "Usage: $0 {start|stop|restart|reload|force-reload}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,11 +1,13 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Dynamic DNS Update Client
|
Description=Dynamic DNS Update Client
|
||||||
After=network.target network-online.target
|
Wants=network-online.target
|
||||||
|
After=network-online.target nss-lookup.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=forking
|
Type=exec
|
||||||
PIDFile=/run/ddclient.pid
|
Environment=daemon_interval=5m
|
||||||
ExecStart=/usr/bin/ddclient
|
ExecStart=/usr/bin/ddclient --daemon ${daemon_interval} --foreground
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
|
@ -9,12 +9,13 @@
|
||||||
#
|
#
|
||||||
# All credits for this one liner go to the author of this blog:
|
# All credits for this one liner go to the author of this blog:
|
||||||
# http://scytale.name/blog/2010/01/fritzbox-wan-ip
|
# http://scytale.name/blog/2010/01/fritzbox-wan-ip
|
||||||
# As the author explains its not required to tamper with the provided IP for the FritzBox
|
|
||||||
# as it always binds to that address for UPnP.
|
|
||||||
# Disclaimer: It might be necessary to make the script executable
|
# Disclaimer: It might be necessary to make the script executable
|
||||||
|
|
||||||
|
# Set default hostname to connect to the FritzBox
|
||||||
|
: ${FRITZ_BOX_HOSTNAME:=fritz.box}
|
||||||
|
|
||||||
curl -s -H 'Content-Type: text/xml; charset="utf-8"' \
|
curl -s -H 'Content-Type: text/xml; charset="utf-8"' \
|
||||||
-H 'SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress' \
|
-H 'SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress' \
|
||||||
-d '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:GetExternalIPAddress xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1" /></s:Body></s:Envelope>' \
|
-d '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:GetExternalIPAddress xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1" /></s:Body></s:Envelope>' \
|
||||||
'http://fritz.box:49000/igdupnp/control/WANIPConn1' | \
|
"http://$FRITZ_BOX_HOSTNAME:49000/igdupnp/control/WANIPConn1" | \
|
||||||
grep -Eo '\<[[:digit:]]{1,3}(\.[[:digit:]]{1,3}){3}\>'
|
grep -Eo '\<[[:digit:]]{1,3}(\.[[:digit:]]{1,3}){3}\>'
|
||||||
|
|
169
t/builtinfw_query.pl
Normal file
169
t/builtinfw_query.pl
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
|
||||||
|
sub setbuiltinfw {
|
||||||
|
my ($fw) = @_;
|
||||||
|
no warnings 'once';
|
||||||
|
$ddclient::builtinfw{$fw->{name}} = $fw;
|
||||||
|
%ddclient::ip_strategies = ddclient::builtinfw_strategy($fw->{name});
|
||||||
|
%ddclient::ipv4_strategies = ddclient::builtinfwv4_strategy($fw->{name});
|
||||||
|
%ddclient::ipv6_strategies = ddclient::builtinfwv6_strategy($fw->{name});
|
||||||
|
}
|
||||||
|
|
||||||
|
my @gotcalls;
|
||||||
|
|
||||||
|
my $skip_test_fw = 't/builtinfw_query.pl skip test';
|
||||||
|
setbuiltinfw({
|
||||||
|
name => $skip_test_fw,
|
||||||
|
query => sub { return '192.0.2.1 skip1 192.0.2.2 skip2 192.0.2.3'; },
|
||||||
|
queryv4 => sub { return '192.0.2.4 skip1 192.0.2.5 skip3 192.0.2.6'; },
|
||||||
|
queryv6 => sub { return '2001:db8::1 skip1 2001:db8::2 skip4 2001:db8::3'; },
|
||||||
|
});
|
||||||
|
|
||||||
|
my @skip_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'query',
|
||||||
|
getip => \&ddclient::get_ip,
|
||||||
|
useopt => 'use',
|
||||||
|
cfgxtra => {},
|
||||||
|
want => '192.0.2.2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'queryv4',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
cfgxtra => {'fwv4-skip' => 'skip3'},
|
||||||
|
want => '192.0.2.6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'queryv4 with fw-skip fallback',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
cfgxtra => {},
|
||||||
|
want => '192.0.2.5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'queryv6',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
cfgxtra => {'fwv6-skip' => 'skip4'},
|
||||||
|
want => '2001:db8::3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# Support for --usev6=<builtin> wasn't added until after --fwv6-skip was added, so fallback
|
||||||
|
# to the deprecated --fw-skip option was never needed.
|
||||||
|
desc => 'queryv6 ignores fw-skip',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
cfgxtra => {},
|
||||||
|
want => '2001:db8::1',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@skip_test_cases) {
|
||||||
|
my $h = "t/builtinfw_query.pl $tc->{desc}";
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
$tc->{useopt} => $skip_test_fw,
|
||||||
|
'fw-skip' => 'skip1',
|
||||||
|
%{$tc->{cfgxtra}},
|
||||||
|
};
|
||||||
|
my $got = $tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
|
||||||
|
is($got, $tc->{want}, $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
my $default_inputs_fw = 't/builtinfw_query.pl default inputs';
|
||||||
|
setbuiltinfw({
|
||||||
|
name => $default_inputs_fw,
|
||||||
|
query => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.1'; },
|
||||||
|
queryv4 => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.2'; },
|
||||||
|
queryv6 => sub { my %p = @_; push(@gotcalls, \%p); return '2001:db8::1'; },
|
||||||
|
});
|
||||||
|
my @default_inputs_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'use with default inputs',
|
||||||
|
getip => \&ddclient::get_ip,
|
||||||
|
useopt => 'use',
|
||||||
|
want => {use => $default_inputs_fw, fw => 'server', 'fw-skip' => 'skip',
|
||||||
|
'fw-login' => 'login', 'fw-password' => 'password', 'fw-ssl-validate' => 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4 with default inputs',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
want => {usev4 => $default_inputs_fw, fwv4 => 'serverv4', fw => 'server',
|
||||||
|
'fwv4-skip' => 'skipv4', 'fw-skip' => 'skip', 'fw-login' => 'login',
|
||||||
|
'fw-password' => 'password', 'fw-ssl-validate' => 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6 with default inputs',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
want => {usev6 => $default_inputs_fw, fwv6 => 'serverv6', 'fwv6-skip' => 'skipv6'},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for my $tc (@default_inputs_test_cases) {
|
||||||
|
my $h = "t/builtinfw_query.pl $tc->{desc}";
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
$tc->{useopt} => $default_inputs_fw,
|
||||||
|
'fw' => 'server',
|
||||||
|
'fwv4' => 'serverv4',
|
||||||
|
'fwv6' => 'serverv6',
|
||||||
|
'fw-login' => 'login',
|
||||||
|
'fw-password' => 'password',
|
||||||
|
'fw-ssl-validate' => 1,
|
||||||
|
'fw-skip' => 'skip',
|
||||||
|
'fwv4-skip' => 'skipv4',
|
||||||
|
'fwv6-skip' => 'skipv6',
|
||||||
|
};
|
||||||
|
@gotcalls = ();
|
||||||
|
$tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
|
||||||
|
is_deeply(\@gotcalls, [$tc->{want}], $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
my $custom_inputs_fw = 't/builtinfw_query.pl custom inputs';
|
||||||
|
setbuiltinfw({
|
||||||
|
name => $custom_inputs_fw,
|
||||||
|
query => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.1'; },
|
||||||
|
inputs => ['if'],
|
||||||
|
queryv4 => sub { my %p = @_; push(@gotcalls, \%p); return '192.0.2.2'; },
|
||||||
|
inputsv4 => ['ifv4'],
|
||||||
|
queryv6 => sub { my %p = @_; push(@gotcalls, \%p); return '2001:db8::1'; },
|
||||||
|
inputsv6 => ['ifv6'],
|
||||||
|
});
|
||||||
|
|
||||||
|
my @custom_inputs_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'use with custom inputs',
|
||||||
|
getip => \&ddclient::get_ip,
|
||||||
|
useopt => 'use',
|
||||||
|
want => {use => $custom_inputs_fw, if => 'eth0'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4 with custom inputs',
|
||||||
|
getip => \&ddclient::get_ipv4,
|
||||||
|
useopt => 'usev4',
|
||||||
|
want => {usev4 => $custom_inputs_fw, ifv4 => 'eth4'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6 with custom inputs',
|
||||||
|
getip => \&ddclient::get_ipv6,
|
||||||
|
useopt => 'usev6',
|
||||||
|
want => {usev6 => $custom_inputs_fw, ifv6 => 'eth6'},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@custom_inputs_test_cases) {
|
||||||
|
my $h = "t/builtinfw_query.pl $tc->{desc}";
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
$tc->{useopt} => $custom_inputs_fw,
|
||||||
|
'if' => 'eth0',
|
||||||
|
'ifv4' => 'eth4',
|
||||||
|
'ifv6' => 'eth6',
|
||||||
|
};
|
||||||
|
@gotcalls = ();
|
||||||
|
$tc->{getip}(ddclient::strategy_inputs($tc->{useopt}, $h));
|
||||||
|
is_deeply(\@gotcalls, [$tc->{want}], $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
53
t/check_value.pl
Normal file
53
t/check_value.pl
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use Test::More;
|
||||||
|
use strict;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
type => ddclient::T_FQDN(),
|
||||||
|
input => 'example.com',
|
||||||
|
want => 'example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_FQDN(),
|
||||||
|
input => 'example',
|
||||||
|
want_invalid => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'https://www.example.com',
|
||||||
|
want => 'https://www.example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'https://directnic.com/dns/gateway/ad133/',
|
||||||
|
want => 'https://directnic.com/dns/gateway/ad133/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'HTTPS://MixedCase.com/',
|
||||||
|
want => 'HTTPS://MixedCase.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'ftp://bad.protocol/',
|
||||||
|
want_invalid => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type => ddclient::T_URL(),
|
||||||
|
input => 'bad-url',
|
||||||
|
want_invalid => 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
my $got;
|
||||||
|
my $got_invalid = !(eval {
|
||||||
|
$got = ddclient::check_value($tc->{input},
|
||||||
|
ddclient::setv($tc->{type}, 0, 0, undef, undef));
|
||||||
|
1;
|
||||||
|
});
|
||||||
|
is($got_invalid, !!$tc->{want_invalid}, "$tc->{type}: $tc->{input}: validity");
|
||||||
|
is($got, $tc->{want}, "$tc->{type}: $tc->{input}: normalization") if !$tc->{want_invalid};
|
||||||
|
}
|
||||||
|
done_testing();
|
|
@ -1,12 +1,7 @@
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
use ddclient::t;
|
use ddclient::t;
|
||||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
|
||||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
|
||||||
|
|
||||||
# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid
|
|
||||||
# accidentally interfering with the Test Anything Protocol messages written by Test::More.)
|
|
||||||
#STDOUT->autoflush(1);
|
|
||||||
#$ddclient::globals{'debug'} = 1;
|
|
||||||
|
|
||||||
subtest "get_default_interface tests" => sub {
|
subtest "get_default_interface tests" => sub {
|
||||||
for my $sample (@ddclient::t::routing_samples) {
|
for my $sample (@ddclient::t::routing_samples) {
|
||||||
|
@ -39,23 +34,30 @@ subtest "get_ip_from_interface tests" => sub {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
subtest "Get default interface and IP for test system" => sub {
|
subtest "Get default interface and IP for test system (IPv4)" => sub {
|
||||||
my $interface = ddclient::get_default_interface(4);
|
my $interface = ddclient::get_default_interface(4);
|
||||||
if ($interface) {
|
plan(skip_all => 'no IPv4 interface') if !$interface;
|
||||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
||||||
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
my $ip1 = ddclient::get_ip_from_interface("default", 4);
|
||||||
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
|
||||||
is($ip1, $ip2, "Check IPv4 from default interface");
|
is($ip1, $ip2, "Check IPv4 from default interface");
|
||||||
|
SKIP: {
|
||||||
|
skip('default interface does not have an appropriate IPv4 addresses') if !$ip1;
|
||||||
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
|
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
|
||||||
}
|
}
|
||||||
$interface = ddclient::get_default_interface(6);
|
};
|
||||||
if ($interface) {
|
|
||||||
isnt($interface, "lo", "Check for loopback 'lo'");
|
subtest "Get default interface and IP for test system (IPv6)" => sub {
|
||||||
isnt($interface, "lo0", "Check for loopback 'lo0'");
|
my $interface = ddclient::get_default_interface(6);
|
||||||
my $ip1 = ddclient::get_ip_from_interface("default", 6);
|
plan(skip_all => 'no IPv6 interface') if !$interface;
|
||||||
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
|
isnt($interface, "lo", "Check for loopback 'lo'");
|
||||||
is($ip1, $ip2, "Check IPv6 from default interface");
|
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)");
|
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,114 +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 $has_io_socket_inet6 = eval { require IO::Socket::INET6; };
|
|
||||||
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 $has_curl = qx{ @CURL@ --version 2>/dev/null; } && $? == 0;
|
|
||||||
|
|
||||||
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 = (
|
|
||||||
# Fetch via IO::Socket::INET
|
|
||||||
{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 via IO::Socket::INET6
|
|
||||||
{ipv6_opt => 1, server_ipv => '4', client_ipv => ''},
|
|
||||||
{ipv6_opt => 1, server_ipv => '4', client_ipv => '4'},
|
|
||||||
{ipv6_opt => 1, server_ipv => '6', client_ipv => ''},
|
|
||||||
{ipv6_opt => 1, server_ipv => '6', client_ipv => '6'},
|
|
||||||
|
|
||||||
# Fetch via IO::Socket::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'},
|
|
||||||
|
|
||||||
# Fetch with curl
|
|
||||||
{ curl => 1, server_ipv => '4', client_ipv => '' },
|
|
||||||
{ curl => 1, server_ipv => '4', client_ipv => '4' },
|
|
||||||
{ curl => 1, server_ipv => '6', client_ipv => '' },
|
|
||||||
{ curl => 1, server_ipv => '6', client_ipv => '6' },
|
|
||||||
|
|
||||||
# Fetch with curl and ssl
|
|
||||||
{ curl => 1, ssl => 1, server_ipv => '4', client_ipv => '' },
|
|
||||||
{ curl => 1, ssl => 1, server_ipv => '4', client_ipv => '4' },
|
|
||||||
{ curl => 1, ssl => 1, server_ipv => '6', client_ipv => '' },
|
|
||||||
{ curl => 1, ssl => 1, server_ipv => '6', client_ipv => '6' },
|
|
||||||
);
|
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
|
||||||
$tc->{ipv6_opt} //= 0;
|
|
||||||
$tc->{ssl} //= 0;
|
|
||||||
$tc->{curl} //= 0;
|
|
||||||
SKIP: {
|
|
||||||
skip("IO::Socket::INET6 not available", 1)
|
|
||||||
if ($tc->{ipv6_opt} || $tc->{client_ipv} eq '6') && !$tc->{curl} && !$has_io_socket_inet6;
|
|
||||||
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;
|
|
||||||
skip("Curl not available on this system", 1) if $tc->{curl} && !$has_curl;
|
|
||||||
my $uri = $httpd{$tc->{server_ipv}}{$tc->{ssl} ? 'https' : 'http'}->endpoint();
|
|
||||||
my $name = sprintf("IPv%s client to %s%s%s",
|
|
||||||
$tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '',
|
|
||||||
$tc->{curl} ? ' (curl)' : '');
|
|
||||||
$ddclient::globals{'ipv6'} = $tc->{ipv6_opt};
|
|
||||||
$ddclient::globals{'curl'} = $tc->{curl};
|
|
||||||
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();
|
264
t/geturl_ssl.pl
264
t/geturl_ssl.pl
|
@ -1,264 +0,0 @@
|
||||||
use Test::More;
|
|
||||||
use Data::Dumper;
|
|
||||||
eval {
|
|
||||||
require HTTP::Request;
|
|
||||||
require HTTP::Response;
|
|
||||||
require IO::Socket::IP;
|
|
||||||
require IO::Socket::SSL;
|
|
||||||
require ddclient::Test::Fake::HTTPD;
|
|
||||||
} or plan(skip_all => $@);
|
|
||||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
|
||||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
|
||||||
|
|
||||||
$Data::Dumper::Sortkeys = 1;
|
|
||||||
|
|
||||||
my $httpd = ddclient::Test::Fake::HTTPD->new();
|
|
||||||
$httpd->run(sub {
|
|
||||||
my $req = shift;
|
|
||||||
# Echo back the full request.
|
|
||||||
my $resp = [ 200, [ 'Content-Type' => 'application/octet-stream' ], [ $req->as_string() ] ];
|
|
||||||
if ($req->method() ne 'GET') {
|
|
||||||
# TODO: Add support for CONNECT to test https via proxy.
|
|
||||||
$resp->[0] = 501; # 501 == Not Implemented
|
|
||||||
}
|
|
||||||
return $resp;
|
|
||||||
});
|
|
||||||
|
|
||||||
my $args;
|
|
||||||
|
|
||||||
{
|
|
||||||
package InterceptSocket;
|
|
||||||
require base;
|
|
||||||
base->import(qw(IO::Socket::IP));
|
|
||||||
|
|
||||||
sub new {
|
|
||||||
my ($class, %args) = @_;
|
|
||||||
$args = \%args;
|
|
||||||
return $class->SUPER::new(%args, PeerAddr => $httpd->host(), PeerPort => $httpd->port());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keys:
|
|
||||||
# * name: Display name.
|
|
||||||
# * params: Parameters to pass to geturl.
|
|
||||||
# * opt_ssl: Value to return from opt('ssl'). Defaults to 0.
|
|
||||||
# * opt_ssl_ca_dir: Value to return from opt('ssl_ca_dir'). Defaults to undef.
|
|
||||||
# * opt_ssl_ca_file: Value to return from opt('ssl_ca_file'). Defaults to undef.
|
|
||||||
# * want_args: Args that should be passed to the socket constructor minus MultiHomed, Proto,
|
|
||||||
# Timeout, and original_socket_class.
|
|
||||||
# * want_req_method: The HTTP method geturl is expected to use. Defaults to 'GET'.
|
|
||||||
# * want_req_uri: URI that geturl is expected to request.
|
|
||||||
# * todo: If defined, mark this test as expected to fail.
|
|
||||||
my @test_cases = (
|
|
||||||
{
|
|
||||||
name => 'https',
|
|
||||||
params => {
|
|
||||||
url => 'https://hostname',
|
|
||||||
},
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'hostname',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'http with ssl=true',
|
|
||||||
params => {
|
|
||||||
url => 'http://hostname',
|
|
||||||
},
|
|
||||||
opt_ssl => 1,
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'hostname',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'https with port',
|
|
||||||
params => {
|
|
||||||
url => 'https://hostname:123',
|
|
||||||
},
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'hostname',
|
|
||||||
PeerPort => '123',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'http with port and ssl=true',
|
|
||||||
params => {
|
|
||||||
url => 'https://hostname:123',
|
|
||||||
},
|
|
||||||
opt_ssl => 1,
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'hostname',
|
|
||||||
PeerPort => '123',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'https proxy, http URL',
|
|
||||||
params => {
|
|
||||||
proxy => 'https://proxy',
|
|
||||||
url => 'http://hostname',
|
|
||||||
},
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'proxy',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => 'http://hostname/',
|
|
||||||
todo => "broken",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'http proxy, https URL',
|
|
||||||
params => {
|
|
||||||
proxy => 'http://proxy',
|
|
||||||
url => 'https://hostname',
|
|
||||||
},
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'proxy',
|
|
||||||
PeerPort => '80',
|
|
||||||
SSL_startHandshake => 0,
|
|
||||||
},
|
|
||||||
want_req_method => 'CONNECT',
|
|
||||||
want_req_uri => 'hostname:443',
|
|
||||||
todo => "not yet supported; silently fails",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'https proxy, https URL',
|
|
||||||
params => {
|
|
||||||
proxy => 'https://proxy',
|
|
||||||
url => 'https://hostname',
|
|
||||||
},
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'proxy',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_method => 'CONNECT',
|
|
||||||
want_req_uri => 'hostname:443',
|
|
||||||
todo => "not yet supported; silently fails",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'http proxy, http URL, ssl=true',
|
|
||||||
params => {
|
|
||||||
proxy => 'http://proxy',
|
|
||||||
url => 'http://hostname',
|
|
||||||
},
|
|
||||||
opt_ssl => 1,
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'proxy',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_method => 'CONNECT',
|
|
||||||
want_req_uri => 'hostname:443',
|
|
||||||
todo => "not yet supported; silently fails",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'https proxy with port, http URL with port',
|
|
||||||
params => {
|
|
||||||
proxy => 'https://proxy:123',
|
|
||||||
url => 'http://hostname:456',
|
|
||||||
},
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'proxy',
|
|
||||||
PeerPort => '123',
|
|
||||||
},
|
|
||||||
want_req_uri => 'http://hostname:456/',
|
|
||||||
todo => "broken",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'http proxy with port, https URL with port',
|
|
||||||
params => {
|
|
||||||
proxy => 'http://proxy:123',
|
|
||||||
url => 'https://hostname:456',
|
|
||||||
},
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'proxy',
|
|
||||||
PeerPort => '123',
|
|
||||||
SSL_startHandshake => 0,
|
|
||||||
},
|
|
||||||
want_req_method => 'CONNECT',
|
|
||||||
want_req_uri => 'hostname:456',
|
|
||||||
todo => "not yet supported; silently fails",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'CA dir',
|
|
||||||
params => {
|
|
||||||
url => 'https://hostname',
|
|
||||||
},
|
|
||||||
opt_ssl_ca_dir => '/ca/dir',
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'hostname',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_ca_path => '/ca/dir',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'CA file',
|
|
||||||
params => {
|
|
||||||
url => 'https://hostname',
|
|
||||||
},
|
|
||||||
opt_ssl_ca_file => '/ca/file',
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'hostname',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_ca_file => '/ca/file',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name => 'CA dir and file',
|
|
||||||
params => {
|
|
||||||
url => 'https://hostname',
|
|
||||||
},
|
|
||||||
opt_ssl_ca_dir => '/ca/dir',
|
|
||||||
opt_ssl_ca_file => '/ca/file',
|
|
||||||
want_args => {
|
|
||||||
PeerAddr => 'hostname',
|
|
||||||
PeerPort => '443',
|
|
||||||
SSL_ca_file => '/ca/file',
|
|
||||||
SSL_ca_path => '/ca/dir',
|
|
||||||
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
||||||
},
|
|
||||||
want_req_uri => '/',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
|
||||||
$args = undef;
|
|
||||||
$ddclient::globals{'ssl'} = $tc->{opt_ssl} // 0;
|
|
||||||
$ddclient::globals{'ssl_ca_dir'} = $tc->{opt_ssl_ca_dir};
|
|
||||||
$ddclient::globals{'ssl_ca_file'} = $tc->{opt_ssl_ca_file};
|
|
||||||
my $resp_str = ddclient::geturl(_testonly_socket_class => 'InterceptSocket', %{$tc->{params}});
|
|
||||||
TODO: {
|
|
||||||
local $TODO = $tc->{todo};
|
|
||||||
subtest $tc->{name} => sub {
|
|
||||||
my %want_args = (
|
|
||||||
MultiHomed => 1,
|
|
||||||
Proto => 'tcp',
|
|
||||||
Timeout => ddclient::opt('timeout'),
|
|
||||||
original_socket_class => 'IO::Socket::SSL',
|
|
||||||
%{$tc->{want_args}},
|
|
||||||
);
|
|
||||||
is(Dumper($args), Dumper(\%want_args), "socket constructor args");
|
|
||||||
ok(defined($resp_str), "response is defined") or return;
|
|
||||||
ok(my $resp = HTTP::Response->parse($resp_str), "parse response") or return;
|
|
||||||
ok(my $req_str = $resp->decoded_content(), "decode request from response") or return;
|
|
||||||
ok(my $req = HTTP::Request->parse($req_str), "parse request") or return;
|
|
||||||
is($req->method(), $tc->{want_req_method} // 'GET', "request method");
|
|
||||||
is($req->uri(), $tc->{want_req_uri}, "request URI");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
done_testing();
|
|
113
t/group_hosts_by.pl
Normal file
113
t/group_hosts_by.pl
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
eval { require Data::Dumper; } or skip($@, 1);
|
||||||
|
Data::Dumper->import();
|
||||||
|
|
||||||
|
my $h1 = 'h1';
|
||||||
|
my $h2 = 'h2';
|
||||||
|
my $h3 = 'h3';
|
||||||
|
|
||||||
|
$ddclient::config{$h1} = {
|
||||||
|
common => 'common',
|
||||||
|
h1h2 => 'h1 and h2',
|
||||||
|
unique => 'h1',
|
||||||
|
falsy => 0,
|
||||||
|
maybeunset => 'unique',
|
||||||
|
};
|
||||||
|
$ddclient::config{$h2} = {
|
||||||
|
common => 'common',
|
||||||
|
h1h2 => 'h1 and h2',
|
||||||
|
unique => 'h2',
|
||||||
|
falsy => '',
|
||||||
|
maybeunset => undef, # should not be grouped with unset
|
||||||
|
};
|
||||||
|
$ddclient::config{$h3} = {
|
||||||
|
common => 'common',
|
||||||
|
h1h2 => 'unique',
|
||||||
|
unique => 'h3',
|
||||||
|
falsy => undef,
|
||||||
|
# maybeunset is intentionally not set
|
||||||
|
};
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'empty attribute set yields single group with all hosts',
|
||||||
|
groupby => [qw()],
|
||||||
|
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'common attribute yields single group with all hosts',
|
||||||
|
groupby => [qw(common)],
|
||||||
|
want => [{cfg => {common => 'common'}, hosts => [$h1, $h2, $h3]}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'subset share a value',
|
||||||
|
groupby => [qw(h1h2)],
|
||||||
|
want => [
|
||||||
|
{cfg => {h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
|
||||||
|
{cfg => {h1h2 => 'unique'}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'all unique',
|
||||||
|
groupby => [qw(unique)],
|
||||||
|
want => [
|
||||||
|
{cfg => {unique => 'h1'}, hosts => [$h1]},
|
||||||
|
{cfg => {unique => 'h2'}, hosts => [$h2]},
|
||||||
|
{cfg => {unique => 'h3'}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'combination',
|
||||||
|
groupby => [qw(common h1h2)],
|
||||||
|
want => [
|
||||||
|
{cfg => {common => 'common', h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
|
||||||
|
{cfg => {common => 'common', h1h2 => 'unique'}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'falsy values',
|
||||||
|
groupby => [qw(falsy)],
|
||||||
|
want => [
|
||||||
|
{cfg => {falsy => 0}, hosts => [$h1]},
|
||||||
|
{cfg => {falsy => ''}, hosts => [$h2]},
|
||||||
|
# undef intentionally becomes unset because undef always means "fall back to global or
|
||||||
|
# default".
|
||||||
|
{cfg => {}, hosts => [$h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'set, unset, undef',
|
||||||
|
groupby => [qw(maybeunset)],
|
||||||
|
want => [
|
||||||
|
{cfg => {maybeunset => 'unique'}, hosts => [$h1]},
|
||||||
|
# undef intentionally becomes unset because undef always means "fall back to global or
|
||||||
|
# default".
|
||||||
|
{cfg => {}, hosts => [$h2, $h3]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'missing attribute',
|
||||||
|
groupby => [qw(thisdoesnotexist)],
|
||||||
|
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
my @got = ddclient::group_hosts_by([$h1, $h2, $h3], @{$tc->{groupby}});
|
||||||
|
# @got is used as a set of sets. Sort everything to make comparison easier.
|
||||||
|
$_->{hosts} = [sort(@{$_->{hosts}})] for @got;
|
||||||
|
@got = sort({
|
||||||
|
for (my $i = 0; $i < @{$a->{hosts}} && $i < @{$b->{hosts}}; ++$i) {
|
||||||
|
my $x = $a->{hosts}[$i] cmp $b->{hosts}[$i];
|
||||||
|
return $x if $x != 0;
|
||||||
|
}
|
||||||
|
return @{$a->{hosts}} <=> @{$b->{hosts}};
|
||||||
|
} @got);
|
||||||
|
is_deeply(\@got, $tc->{want}, $tc->{desc})
|
||||||
|
or diag(Data::Dumper->new([\@got, $tc->{want}],
|
||||||
|
[qw(got want)])->Sortkeys(1)->Useqq(1)->Dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
74
t/header_ok.pl
Normal file
74
t/header_ok.pl
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
my $have_mock = eval { require Test::MockModule; };
|
||||||
|
|
||||||
|
my $failmsg;
|
||||||
|
my $module;
|
||||||
|
if ($have_mock) {
|
||||||
|
$module = Test::MockModule->new('ddclient');
|
||||||
|
# Note: 'mock' is used instead of 'redefine' because 'redefine' is not available in the versions
|
||||||
|
# of Test::MockModule distributed with old Debian and Ubuntu releases.
|
||||||
|
$module->mock('failed', sub { $failmsg //= ''; $failmsg .= sprintf(shift, @_) . "\n"; });
|
||||||
|
}
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'malformed not OK',
|
||||||
|
input => 'malformed',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/unexpected/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'HTTP/1.1 200 OK',
|
||||||
|
input => 'HTTP/1.1 200 OK',
|
||||||
|
want => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'HTTP/2 200 OK',
|
||||||
|
input => 'HTTP/2 200 OK',
|
||||||
|
want => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'HTTP/3 200 OK',
|
||||||
|
input => 'HTTP/3 200 OK',
|
||||||
|
want => 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => '401 not OK, fallback message',
|
||||||
|
input => 'HTTP/1.1 401 ',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/authentication failed/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => '403 not OK, fallback message',
|
||||||
|
input => 'HTTP/1.1 403 ',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/not authorized/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'other 4xx not OK',
|
||||||
|
input => 'HTTP/1.1 456 bad',
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/bad/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'only first line is logged on error',
|
||||||
|
input => "HTTP/1.1 404 not found\n\nbody",
|
||||||
|
want => 0,
|
||||||
|
wantmsg => qr/(?!body)/,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
subtest $tc->{desc} => sub {
|
||||||
|
$failmsg = '';
|
||||||
|
is(ddclient::header_ok($tc->{input}), $tc->{want}, 'return value matches');
|
||||||
|
SKIP: {
|
||||||
|
skip('Test::MockModule not available') if !$have_mock;
|
||||||
|
like($failmsg, $tc->{wantmsg} // qr/^$/, 'fail message matches');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
51
t/interval_expired.pl
Normal file
51
t/interval_expired.pl
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
|
my $h = 't/interval_expired.pl';
|
||||||
|
|
||||||
|
my $default_now = 1000000000;
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
now => 'inf',
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cache => '-inf',
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cache => undef, # Falsy cache value.
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
now => 0,
|
||||||
|
cache => 0, # Different kind of falsy cache value.
|
||||||
|
interval => 'inf',
|
||||||
|
want => 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
$tc->{now} //= $default_now;
|
||||||
|
# For convenience, $tc->{cache} is an offset from $tc->{now}, not an absolute time..
|
||||||
|
my $cachetime = $tc->{now} + $tc->{cache} if defined($tc->{cache});
|
||||||
|
$ddclient::config{$h} = {'interval' => $tc->{interval}};
|
||||||
|
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
$ddclient::cache{$h} = {'cached-time' => $cachetime} if defined($cachetime);
|
||||||
|
%ddclient::cache if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
$ddclient::now = $tc->{now};
|
||||||
|
$ddclient::now if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
my $desc = "now=$tc->{now}, cache=${\($cachetime // 'undef')}, interval=$tc->{interval}";
|
||||||
|
is(ddclient::interval_expired($h, 'cached-time', 'interval'), $tc->{want}, $desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
|
@ -1,8 +1,11 @@
|
||||||
# Copied from https://metacpan.org/pod/release/MASAKI/Test-Fake-HTTPD-0.08/lib/Test/Fake/HTTPD.pm
|
# Copied from https://metacpan.org/release/MASAKI/Test-Fake-HTTPD-0.09/source/lib/Test/Fake/HTTPD.pm
|
||||||
# and modified as follows:
|
# and modified as follows:
|
||||||
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/4 to add IPv6 support.
|
# * Added this comment block.
|
||||||
|
# * Patched with https://github.com/masaki/Test-Fake-HTTPD/pull/6 to fix server exit if TLS
|
||||||
|
# negotiation fails.
|
||||||
# * Changed package name to ddclient::Test::Fake::HTTPD.
|
# * Changed package name to ddclient::Test::Fake::HTTPD.
|
||||||
#
|
#
|
||||||
|
# Copyright: 2011-2020 NAKAGAWA Masaki <masaki@cpan.org>
|
||||||
# License: This library is free software; you can redistribute it and/or modify it under the same
|
# License: This library is free software; you can redistribute it and/or modify it under the same
|
||||||
# terms as Perl itself.
|
# terms as Perl itself.
|
||||||
|
|
||||||
|
@ -20,7 +23,7 @@ use Scalar::Util qw(blessed weaken);
|
||||||
use Carp qw(croak);
|
use Carp qw(croak);
|
||||||
use Exporter qw(import);
|
use Exporter qw(import);
|
||||||
|
|
||||||
our $VERSION = '0.08';
|
our $VERSION = '0.09';
|
||||||
$VERSION = eval $VERSION;
|
$VERSION = eval $VERSION;
|
||||||
|
|
||||||
our @EXPORT = qw(
|
our @EXPORT = qw(
|
||||||
|
@ -101,9 +104,10 @@ sub run {
|
||||||
$self->port || '<default>',
|
$self->port || '<default>',
|
||||||
$@ eq '' ? '' : ": $@")) unless $d;
|
$@ eq '' ? '' : ": $@")) unless $d;
|
||||||
|
|
||||||
$d->accept; # wait for port check from parent process
|
while (1) {
|
||||||
|
# accept can return undef if TLS handshake fails (e.g., port test or client rejects
|
||||||
while (my $c = $d->accept) {
|
# cert).
|
||||||
|
my $c = $d->accept or next;
|
||||||
while (my $req = $c->get_request) {
|
while (my $req = $c->get_request) {
|
||||||
my $res = $self->_to_http_res($app->($req));
|
my $res = $self->_to_http_res($app->($req));
|
||||||
$c->send_response($res);
|
$c->send_response($res);
|
||||||
|
@ -143,7 +147,7 @@ sub endpoint {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
my $uri = URI->new($self->scheme . ':');
|
my $uri = URI->new($self->scheme . ':');
|
||||||
my $host = $self->host;
|
my $host = $self->host;
|
||||||
$host = 'localhost' if !defined($host) || $host eq '0.0.0.0' || $host eq '::';
|
$host = 'localhost' if !defined($host) || $host eq '' || $host eq '0.0.0.0' || $host eq '::';
|
||||||
$uri->host($host);
|
$uri->host($host);
|
||||||
$uri->port($self->port);
|
$uri->port($self->port);
|
||||||
return $uri;
|
return $uri;
|
||||||
|
|
80
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem
Normal file
80
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
6c:bf:34:52:19:4d:c9:29:2b:a6:8b:41:59:aa:c6:c5:1f:a2:bb:10
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer: CN=Root Certification Authority
|
||||||
|
Validity
|
||||||
|
Not Before: Jan 8 08:24:32 2025 GMT
|
||||||
|
Not After : Jan 9 08:24:32 2125 GMT
|
||||||
|
Subject: CN=Root Certification Authority
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (2048 bit)
|
||||||
|
Modulus:
|
||||||
|
00:c3:3d:19:6b:72:0a:9e:87:c0:28:a1:ff:d0:08:
|
||||||
|
21:55:52:71:92:f2:98:36:75:fc:95:b4:0c:5e:c9:
|
||||||
|
98:b3:3c:a1:ee:cf:91:6f:07:bf:82:c9:d5:51:c0:
|
||||||
|
eb:f8:46:17:41:52:1d:c6:89:ec:63:dd:5c:30:87:
|
||||||
|
a7:b5:0d:dd:ae:bf:46:fd:de:1a:be:1d:69:83:0d:
|
||||||
|
fb:d9:5a:33:0b:8d:5f:63:76:fc:a8:b1:54:37:1e:
|
||||||
|
0b:12:44:93:90:39:1c:48:ee:f0:f2:12:fe:dc:fb:
|
||||||
|
58:a5:76:3b:e8:e8:94:44:1e:9d:03:22:5f:21:6a:
|
||||||
|
17:66:d1:4a:bf:12:d7:3c:15:76:11:76:09:ab:bf:
|
||||||
|
21:ef:0c:a5:a9:e0:08:99:63:19:26:e4:d8:5d:c2:
|
||||||
|
40:8b:98:e6:5d:df:b3:8c:63:e2:01:7c:5e:fb:55:
|
||||||
|
39:a8:67:78:80:d2:6b:61:b2:e2:2e:93:c0:9d:91:
|
||||||
|
0e:a1:79:4f:fc:38:94:ff:6f:65:18:8f:3e:0b:8c:
|
||||||
|
1f:cd:48:d7:46:5a:a2:76:d6:e0:bd:3c:aa:3d:44:
|
||||||
|
9e:50:e6:fd:e1:12:1a:ee:a1:9a:69:48:60:63:da:
|
||||||
|
41:ae:a7:3d:36:1b:95:fb:b7:f1:0d:60:cd:2f:e3:
|
||||||
|
b1:1f:b1:db:b4:98:a6:62:87:de:54:80:d1:45:43:
|
||||||
|
5b:25
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
E1:7C:D3:C3:9E:C7:F5:2C:DA:7C:D7:85:78:91:BA:26:88:61:F9:D4
|
||||||
|
X509v3 Authority Key Identifier:
|
||||||
|
E1:7C:D3:C3:9E:C7:F5:2C:DA:7C:D7:85:78:91:BA:26:88:61:F9:D4
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Certificate Sign, CRL Sign
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Signature Value:
|
||||||
|
9d:dc:49:c6:14:13:19:38:d9:14:b5:70:f0:3b:01:8e:d7:32:
|
||||||
|
a7:69:f0:21:68:ec:ad:8c:ee:53:7d:16:64:7d:3e:c2:d2:ac:
|
||||||
|
5a:54:17:55:84:43:1e:46:1d:42:01:fb:89:e0:db:ec:e8:f0:
|
||||||
|
3c:22:82:54:1d:38:12:21:45:3c:37:44:3b:2e:c9:4d:ed:8d:
|
||||||
|
6e:46:f5:a5:cc:ba:39:61:ab:df:cf:1f:d2:c9:40:e2:db:3f:
|
||||||
|
05:ea:83:14:93:5f:0e:3d:33:be:98:04:80:87:25:3a:6c:ff:
|
||||||
|
8e:87:6a:32:ed:1e:ec:54:90:9b:2a:6e:12:05:6a:9d:15:48:
|
||||||
|
3c:ea:c6:9e:ab:71:58:1e:34:95:3f:9b:9e:e3:e5:4b:fb:9e:
|
||||||
|
32:f2:d6:59:bf:8d:09:d6:e4:9e:9e:47:b9:d6:78:5f:f3:0c:
|
||||||
|
98:ab:56:f0:18:5d:63:8e:83:ee:c1:f2:84:da:0e:64:af:1c:
|
||||||
|
18:ff:b3:f9:15:0b:02:50:77:d1:0b:6e:ba:61:bc:9e:c3:37:
|
||||||
|
63:91:26:e8:ce:77:9a:47:8f:ef:38:8f:9c:7f:f1:ab:7b:65:
|
||||||
|
a5:96:b6:92:2e:c7:d3:c3:7a:54:0d:d6:76:f5:d6:88:13:3b:
|
||||||
|
17:e2:02:4e:3b:4d:10:95:0a:bb:47:e9:48:25:76:1d:7b:19:
|
||||||
|
5c:6f:b8:a1
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDQTCCAimgAwIBAgIUbL80UhlNySkrpotBWarGxR+iuxAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwJzElMCMGA1UEAwwcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAgFw0y
|
||||||
|
NTAxMDgwODI0MzJaGA8yMTI1MDEwOTA4MjQzMlowJzElMCMGA1UEAwwcUm9vdCBD
|
||||||
|
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBAMM9GWtyCp6HwCih/9AIIVVScZLymDZ1/JW0DF7JmLM8oe7PkW8Hv4LJ
|
||||||
|
1VHA6/hGF0FSHcaJ7GPdXDCHp7UN3a6/Rv3eGr4daYMN+9laMwuNX2N2/KixVDce
|
||||||
|
CxJEk5A5HEju8PIS/tz7WKV2O+jolEQenQMiXyFqF2bRSr8S1zwVdhF2Cau/Ie8M
|
||||||
|
pangCJljGSbk2F3CQIuY5l3fs4xj4gF8XvtVOahneIDSa2Gy4i6TwJ2RDqF5T/w4
|
||||||
|
lP9vZRiPPguMH81I10ZaonbW4L08qj1EnlDm/eESGu6hmmlIYGPaQa6nPTYblfu3
|
||||||
|
8Q1gzS/jsR+x27SYpmKH3lSA0UVDWyUCAwEAAaNjMGEwHQYDVR0OBBYEFOF808Oe
|
||||||
|
x/Us2nzXhXiRuiaIYfnUMB8GA1UdIwQYMBaAFOF808Oex/Us2nzXhXiRuiaIYfnU
|
||||||
|
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUA
|
||||||
|
A4IBAQCd3EnGFBMZONkUtXDwOwGO1zKnafAhaOytjO5TfRZkfT7C0qxaVBdVhEMe
|
||||||
|
Rh1CAfuJ4Nvs6PA8IoJUHTgSIUU8N0Q7LslN7Y1uRvWlzLo5Yavfzx/SyUDi2z8F
|
||||||
|
6oMUk18OPTO+mASAhyU6bP+Oh2oy7R7sVJCbKm4SBWqdFUg86saeq3FYHjSVP5ue
|
||||||
|
4+VL+54y8tZZv40J1uSenke51nhf8wyYq1bwGF1jjoPuwfKE2g5krxwY/7P5FQsC
|
||||||
|
UHfRC266YbyewzdjkSbozneaR4/vOI+cf/Gre2WllraSLsfTw3pUDdZ29daIEzsX
|
||||||
|
4gJOO00QlQq7R+lIJXYdexlcb7ih
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -560,3 +560,5 @@ EOF
|
||||||
want_ipv6_if => "en0",
|
want_ipv6_if => "en0",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
1;
|
||||||
|
|
161
t/lib/ddclient/t/HTTPD.pm
Normal file
161
t/lib/ddclient/t/HTTPD.pm
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package ddclient::t::HTTPD;
|
||||||
|
|
||||||
|
use v5.10.1;
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use parent qw(ddclient::Test::Fake::HTTPD);
|
||||||
|
|
||||||
|
use Exporter qw(import);
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { require 'ddclient'; }
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
our @EXPORT = qw(
|
||||||
|
httpd
|
||||||
|
httpd_ok httpd_required $httpd_supported $httpd_support_error
|
||||||
|
httpd_ipv6_ok httpd_ipv6_required $httpd_ipv6_supported $httpd_ipv6_support_error
|
||||||
|
httpd_ssl_ok httpd_ssl_required $httpd_ssl_supported $httpd_ssl_support_error
|
||||||
|
$ca_file $certdir $other_ca_file
|
||||||
|
$textplain
|
||||||
|
);
|
||||||
|
|
||||||
|
our $httpd_supported;
|
||||||
|
our $httpd_support_error;
|
||||||
|
BEGIN {
|
||||||
|
$httpd_supported = eval {
|
||||||
|
require parent; parent->import(qw(ddclient::Test::Fake::HTTPD));
|
||||||
|
require JSON::PP; JSON::PP->import();
|
||||||
|
1;
|
||||||
|
} or $httpd_support_error = $@;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_ok {
|
||||||
|
ok($httpd_supported, "HTTPD is supported") or diag($httpd_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_required {
|
||||||
|
plan(skip_all => $httpd_support_error) if !$httpd_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $httpd_ssl_supported = $httpd_supported;
|
||||||
|
our $httpd_ssl_support_error = $httpd_support_error;
|
||||||
|
$httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; }
|
||||||
|
or $httpd_ssl_support_error = $@
|
||||||
|
if $httpd_ssl_supported;
|
||||||
|
|
||||||
|
sub httpd_ssl_ok {
|
||||||
|
ok($httpd_ssl_supported, "SSL is supported") or diag($httpd_ssl_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_ssl_required {
|
||||||
|
plan(skip_all => $httpd_ssl_support_error) if !$httpd_ssl_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $httpd_ipv6_supported = $httpd_supported;
|
||||||
|
our $httpd_ipv6_support_error = $httpd_support_error;
|
||||||
|
$httpd_ipv6_supported = $ipv6_supported
|
||||||
|
or $httpd_ipv6_support_error = $ipv6_support_error
|
||||||
|
if $httpd_ipv6_supported;
|
||||||
|
$httpd_ipv6_supported = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); }
|
||||||
|
or $httpd_ipv6_support_error = $@
|
||||||
|
if $httpd_ipv6_supported;
|
||||||
|
|
||||||
|
sub httpd_ipv6_ok {
|
||||||
|
ok($httpd_ipv6_supported, "test HTTP server supports IPv6") or diag($httpd_ipv6_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub httpd_ipv6_required {
|
||||||
|
plan(skip_all => $httpd_ipv6_support_error) if !$httpd_ipv6_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $textplain = ['content-type' => 'text/plain; charset=utf-8'];
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my $class = shift;
|
||||||
|
my $self = $class->SUPER::new(@_);
|
||||||
|
$self->{_requests} = []; # Log of received requests.
|
||||||
|
$self->{_responses} = []; # Script of responses to play back.
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub run {
|
||||||
|
my ($self, $app) = @_;
|
||||||
|
$self->SUPER::run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
push(@{$self->{_requests}}, $req);
|
||||||
|
my $res = $app->($req) if defined($app);
|
||||||
|
return $res if defined($res);
|
||||||
|
if ($req->uri()->path() eq '/control') {
|
||||||
|
pop(@{$self->{_requests}});
|
||||||
|
if ($req->method() eq 'PUT') {
|
||||||
|
return [400, $textplain, ['content must be json']]
|
||||||
|
if $req->headers()->content_type() ne 'application/json';
|
||||||
|
eval { @{$self->{_responses}} = @{decode_json($req->content())}; 1; }
|
||||||
|
or return [400, $textplain, ['content is not valid json']];
|
||||||
|
@{$self->{_requests}} = ();
|
||||||
|
return [200, $textplain, ["successfully reset request log and response script"]];
|
||||||
|
} elsif ($req->method() eq 'GET') {
|
||||||
|
my @reqs = map($_->as_string(), @{$self->{_requests}});
|
||||||
|
return [200, ['content-type' => 'application/json'], [encode_json(\@reqs)]];
|
||||||
|
} else {
|
||||||
|
return [405, $textplain, ['unsupported method: ' . $req->method()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shift(@{$self->{_responses}}) // [500, $textplain, ["no more scripted responses"]];
|
||||||
|
});
|
||||||
|
diag("started server running at " . $self->endpoint());
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub reset {
|
||||||
|
my $self = shift;
|
||||||
|
my $ep = $self->endpoint();
|
||||||
|
my $got = ddclient::geturl(url => "$ep/control");
|
||||||
|
diag("http response:\n$got");
|
||||||
|
ddclient::header_ok($got)
|
||||||
|
or BAIL_OUT("failed to get log of requests from test http server at $ep");
|
||||||
|
$got =~ s/^.*?\n\n//s;
|
||||||
|
my @got = map(HTTP::Request->parse($_), @{decode_json($got)});
|
||||||
|
ddclient::header_ok(ddclient::geturl(
|
||||||
|
url => "$ep/control",
|
||||||
|
method => 'PUT',
|
||||||
|
headers => ['content-type: application/json'],
|
||||||
|
data => encode_json(\@_),
|
||||||
|
)) or BAIL_OUT("failed to reset the test http server at $ep");
|
||||||
|
return @got;
|
||||||
|
}
|
||||||
|
|
||||||
|
our $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
|
||||||
|
our $ca_file = "$certdir/dummy-ca-cert.pem";
|
||||||
|
our $other_ca_file = "$certdir/other-ca-cert.pem";
|
||||||
|
|
||||||
|
my %daemons;
|
||||||
|
|
||||||
|
sub httpd {
|
||||||
|
my ($ipv, $ssl) = @_;
|
||||||
|
$ipv //= '';
|
||||||
|
$ssl = !!$ssl;
|
||||||
|
return undef if !$httpd_supported;
|
||||||
|
return undef if $ipv eq '6' && !$httpd_ipv6_supported;
|
||||||
|
return undef if $ssl && !$httpd_ssl_supported;
|
||||||
|
if (!defined($daemons{$ipv}{$ssl})) {
|
||||||
|
my $host
|
||||||
|
= $ipv eq '4' ? '127.0.0.1'
|
||||||
|
: $ipv eq '6' ? '::1'
|
||||||
|
: $httpd_ipv6_supported ? '::1'
|
||||||
|
: '127.0.0.1';
|
||||||
|
$daemons{$ipv}{$ssl} = __PACKAGE__->new(
|
||||||
|
host => $host,
|
||||||
|
scheme => $ssl ? 'https' : 'http',
|
||||||
|
daemon_args => {
|
||||||
|
(V6Only => $ipv eq '6' ? 1 : 0) x ($host eq '::1'),
|
||||||
|
(SSL_cert_file => "$certdir/dummy-server-cert.pem",
|
||||||
|
SSL_key_file => "$certdir/dummy-server-key.pem") x $ssl,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $daemons{$ipv}{$ssl};
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
39
t/lib/ddclient/t/Logger.pm
Normal file
39
t/lib/ddclient/t/Logger.pm
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package ddclient::t::Logger;
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use parent qw(-norequire ddclient::Logger);
|
||||||
|
|
||||||
|
{
|
||||||
|
package ddclient::t::LoggerAbort;
|
||||||
|
use overload '""' => qw(stringify);
|
||||||
|
sub new {
|
||||||
|
my ($class, %args) = @_;
|
||||||
|
return bless(\%args, $class);
|
||||||
|
}
|
||||||
|
sub stringify {
|
||||||
|
return 'logged a FATAL message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my ($class, $parent, $labelre) = @_;
|
||||||
|
my $self = $class->SUPER::new(undef, $parent);
|
||||||
|
$self->{logs} = [];
|
||||||
|
$self->{_labelre} = $labelre;
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _log {
|
||||||
|
my ($self, $args) = @_;
|
||||||
|
my $lre = $self->{_labelre};
|
||||||
|
my $lbl = $args->{label};
|
||||||
|
push(@{$self->{logs}}, $args) if !defined($lre) || (defined($lbl) && $lbl =~ $lre);
|
||||||
|
return $self->SUPER::_log($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _abort {
|
||||||
|
my ($self) = @_;
|
||||||
|
push(@{$self->{logs}}, 'aborted');
|
||||||
|
die(ddclient::t::LoggerAbort->new());
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
30
t/lib/ddclient/t/ip.pm
Normal file
30
t/lib/ddclient/t/ip.pm
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package ddclient::t::ip;
|
||||||
|
|
||||||
|
use v5.10.1;
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use Exporter qw(import);
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
our @EXPORT = qw(ipv6_ok ipv6_required $ipv6_supported $ipv6_support_error);
|
||||||
|
|
||||||
|
our $ipv6_support_error;
|
||||||
|
our $ipv6_supported = eval {
|
||||||
|
require IO::Socket::IP;
|
||||||
|
my $ipv6_socket = IO::Socket::IP->new(
|
||||||
|
Domain => 'PF_INET6',
|
||||||
|
LocalHost => '::1',
|
||||||
|
Listen => 1,
|
||||||
|
);
|
||||||
|
defined($ipv6_socket);
|
||||||
|
} or $ipv6_support_error = $@;
|
||||||
|
|
||||||
|
sub ipv6_ok {
|
||||||
|
ok($ipv6_supported, "system supports IPv6") or diag($ipv6_support_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub ipv6_required {
|
||||||
|
plan(skip_all => $ipv6_support_error) if !$ipv6_supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
167
t/logmsg.pl
Normal file
167
t/logmsg.pl
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
use Test::More;
|
||||||
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'adds a newline',
|
||||||
|
args => ['xyz'],
|
||||||
|
want => "xyz\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'removes one trailing newline (before adding a newline)',
|
||||||
|
args => ["xyz \n\t\n\n"],
|
||||||
|
want => "xyz \n\t\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'accepts msg keyword parameter',
|
||||||
|
args => [msg => 'xyz'],
|
||||||
|
want => "xyz\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'msg keyword parameter trumps message parameter',
|
||||||
|
args => [msg => 'kw', 'pos'],
|
||||||
|
want => "kw\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'msg keyword parameter trumps message parameter',
|
||||||
|
args => [msg => 'kw', 'pos'],
|
||||||
|
want => "kw\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'email appends to email body',
|
||||||
|
args => [email => 1, 'foo'],
|
||||||
|
init_email => "preexisting message\n",
|
||||||
|
want_email => "preexisting message\nfoo\n",
|
||||||
|
want => "foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single-line label',
|
||||||
|
args => [label => 'LBL', 'foo'],
|
||||||
|
want => "LBL: > foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multi-line label',
|
||||||
|
args => [label => 'LBL', "foo\nbar"],
|
||||||
|
want => ("LBL: > foo\n" .
|
||||||
|
"LBL: bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single-line long label',
|
||||||
|
args => [label => 'VERY LONG LABEL', 'foo'],
|
||||||
|
want => "VERY LONG LABEL: > foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multi-line long label',
|
||||||
|
args => [label => 'VERY LONG LABEL', "foo\nbar"],
|
||||||
|
want => ("VERY LONG LABEL: > foo\n" .
|
||||||
|
"VERY LONG LABEL: bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single line, no label, single context',
|
||||||
|
args => ['foo'],
|
||||||
|
ctxs => ['only context'],
|
||||||
|
want => "[only context]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single line, no label, two contexts',
|
||||||
|
args => ['foo'],
|
||||||
|
ctxs => ['context one', 'context two'],
|
||||||
|
want => "[context one][context two]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single line, label, two contexts',
|
||||||
|
args => [label => 'LBL', 'foo'],
|
||||||
|
ctxs => ['context one', 'context two'],
|
||||||
|
want => "LBL: [context one][context two]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple lines, label, two contexts',
|
||||||
|
args => [label => 'LBL', "foo\nbar"],
|
||||||
|
ctxs => ['context one', 'context two'],
|
||||||
|
want => ("LBL: [context one][context two]> foo\n" .
|
||||||
|
"LBL: [context one][context two] bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'string ctx arg',
|
||||||
|
args => [label => 'LBL', ctx => 'three', "foo\nbar"],
|
||||||
|
ctxs => ['one', 'two'],
|
||||||
|
want => ("LBL: [one][two][three]> foo\n" .
|
||||||
|
"LBL: [one][two][three] bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'arrayref ctx arg',
|
||||||
|
args => [label => 'LBL', ctx => ['three', 'four'], "foo\nbar"],
|
||||||
|
ctxs => ['one', 'two'],
|
||||||
|
want => ("LBL: [one][two][three][four]> foo\n" .
|
||||||
|
"LBL: [one][two][three][four] bar\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'undef ctx',
|
||||||
|
args => [label => 'LBL', "foo"],
|
||||||
|
ctxs => ['one', undef],
|
||||||
|
want => "LBL: [one]> foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'arrayref ctx',
|
||||||
|
args => [label => 'LBL', "foo"],
|
||||||
|
ctxs => ['one', ['two', 'three']],
|
||||||
|
want => "LBL: [one][two][three]> foo\n",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
subtest $tc->{desc} => sub {
|
||||||
|
$tc->{wantemail} //= '';
|
||||||
|
my $output;
|
||||||
|
open(my $fh, '>', \$output);
|
||||||
|
local $ddclient::emailbody = $tc->{init_email} // '';
|
||||||
|
local $ddclient::_l = $ddclient::_l;
|
||||||
|
$ddclient::_l = ddclient::pushlogctx($_) for @{$tc->{ctxs} // []};
|
||||||
|
{
|
||||||
|
local *STDERR = $fh;
|
||||||
|
ddclient::logmsg(@{$tc->{args}});
|
||||||
|
}
|
||||||
|
close($fh);
|
||||||
|
is($output, $tc->{want}, 'output text matches');
|
||||||
|
is($ddclient::emailbody, $tc->{want_email} // '', 'email content matches');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my @logfmt_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'single argument is printed directly, not via sprintf',
|
||||||
|
args => ['%%'],
|
||||||
|
want => "DEBUG: > %%\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple arguments are formatted via sprintf',
|
||||||
|
args => ['%s', 'foo'],
|
||||||
|
want => "DEBUG: > foo\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'single argument with context',
|
||||||
|
args => [ctx => 'context', '%%'],
|
||||||
|
want => "DEBUG: [context]> %%\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple arguments with context',
|
||||||
|
args => [ctx => 'context', '%s', 'foo'],
|
||||||
|
want => "DEBUG: [context]> foo\n",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@logfmt_test_cases) {
|
||||||
|
my $got;
|
||||||
|
open(my $fh, '>', \$got);
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
%ddclient::globals if 0;
|
||||||
|
{
|
||||||
|
local *STDERR = $fh;
|
||||||
|
ddclient::debug(@{$tc->{args}});
|
||||||
|
}
|
||||||
|
close($fh);
|
||||||
|
is($got, $tc->{want}, $tc->{desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
|
@ -44,8 +44,20 @@ my @test_cases = (
|
||||||
tc('unquoted escaped backslash', "a=\\\\", { a => "\\" }, ""),
|
tc('unquoted escaped backslash', "a=\\\\", { a => "\\" }, ""),
|
||||||
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
|
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
|
||||||
tc('dquoted escaped dquote', "a=\"\\\"\"", { a => '"' }, ""),
|
tc('dquoted escaped dquote', "a=\"\\\"\"", { a => '"' }, ""),
|
||||||
|
tc('env: empty', "a_env=", {}, ""),
|
||||||
|
tc('env: unset', "a_env=UNSET", {}, ""),
|
||||||
|
tc('env: set', "a_env=TEST", { a => 'val' }, ""),
|
||||||
|
tc('env: single quoted', "a_env='TEST'", { a => 'val' }, ""),
|
||||||
|
tc('newline: quoted value', "a='1\n2'", { a => "1\n2" }, ""),
|
||||||
|
tc('newline: escaped value', "a=1\\\n2", { a => "1\n2" }, ""),
|
||||||
|
tc('newline: between vars', "a=1 \n b=2", { a => '1' }, "\n b=2"),
|
||||||
|
tc('newline: terminating', "a=1 \n", { a => '1' }, "\n"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
delete($ENV{''});
|
||||||
|
delete($ENV{UNSET});
|
||||||
|
$ENV{TEST} = 'val';
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
for my $tc (@test_cases) {
|
||||||
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
|
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
|
||||||
subtest $tc->{name} => sub {
|
subtest $tc->{name} => sub {
|
||||||
|
|
169
t/protocol_directnic.pl
Normal file
169
t/protocol_directnic.pl
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::Logger;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
ddclient::load_json_support('directnic');
|
||||||
|
|
||||||
|
httpd()->run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Test server received request:\n" . $req->as_string());
|
||||||
|
my $headers = ['content-type' => 'text/plain; charset=utf-8'];
|
||||||
|
if ($req->uri->as_string =~ m/\/dns\/gateway\/(abc|def)\/\?data=([^&]*)/) {
|
||||||
|
return [200, ['Content-Type' => 'application/json'], [encode_json({
|
||||||
|
result => 'success',
|
||||||
|
message => "Your record was updated to $2",
|
||||||
|
})]];
|
||||||
|
} elsif ($req->uri->as_string =~ m/\/dns\/gateway\/bad_token\/\?data=([^&]*)/) {
|
||||||
|
return [200, ['Content-Type' => 'application/json'], [encode_json({
|
||||||
|
result => 'error',
|
||||||
|
message => "There was an error updating your record.",
|
||||||
|
})]];
|
||||||
|
} elsif ($req->uri->as_string =~ m/\/bad\/path\/\?data=([^&]*)/) {
|
||||||
|
return [200, ['Content-Type' => 'application/json'], ['unexpected response body']];
|
||||||
|
}
|
||||||
|
return [400, $headers, ['unexpected request: ' . $req->uri()]]
|
||||||
|
});
|
||||||
|
|
||||||
|
my $hostname = httpd()->endpoint();
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'IPv4, good',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/dns/gateway/abc/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, failed',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/dns/gateway/bad_token/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'failed'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/There was an error updating your record/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, bad',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/bad/path/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'bad'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/response is not a JSON object:\nunexpected response body/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, unexpected response',
|
||||||
|
cfg => {h1 => {urlv4 => "$hostname/unexpected/path/", wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/400 Bad Request/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, no urlv4',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
wantrecap => {},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/missing urlv4 option/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv6, good',
|
||||||
|
cfg => {h1 => {urlv6 => "$hostname/dns/gateway/abc/", wantipv6 => '2001:db8::1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4 and IPv6, good',
|
||||||
|
cfg => {h1 => {
|
||||||
|
urlv4 => "$hostname/dns/gateway/abc/",
|
||||||
|
urlv6 => "$hostname/dns/gateway/def/",
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1',
|
||||||
|
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4 and IPv6, mixed success',
|
||||||
|
cfg => {h1 => {
|
||||||
|
urlv4 => "$hostname/dns/gateway/bad_token/",
|
||||||
|
urlv6 => "$hostname/dns/gateway/def/",
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
}},
|
||||||
|
wantips => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}},
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'failed',
|
||||||
|
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/There was an error updating your record/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Starting test: $tc->{desc}");
|
||||||
|
diag('==============================================================================');
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
|
||||||
|
local %ddclient::config = %{$tc->{cfg}};
|
||||||
|
local %ddclient::recap;
|
||||||
|
{
|
||||||
|
local $ddclient::_l = $l;
|
||||||
|
ddclient::nic_directnic_update(undef, sort(keys(%{$tc->{cfg}})));
|
||||||
|
}
|
||||||
|
is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
$tc->{wantlogs} //= [];
|
||||||
|
subtest("$tc->{desc}: logs" => sub {
|
||||||
|
my @got = @{$l->{logs}};
|
||||||
|
my @want = @{$tc->{wantlogs}};
|
||||||
|
for my $i (0..$#want) {
|
||||||
|
last if $i >= @got;
|
||||||
|
my $got = $got[$i];
|
||||||
|
my $want = $want[$i];
|
||||||
|
subtest("log $i" => sub {
|
||||||
|
is($got->{label}, $want->{label}, "label matches");
|
||||||
|
is_deeply($got->{ctx}, $want->{ctx}, "context matches");
|
||||||
|
like($got->{msg}, $want->{msg}, "message matches");
|
||||||
|
}) or diag(ddclient::repr(Values => [$got, $want], Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my @unexpected = @got[@want..$#got];
|
||||||
|
ok(@unexpected == 0, "no unexpected logs")
|
||||||
|
or diag(ddclient::repr(\@unexpected, Names => ['*unexpected']));
|
||||||
|
my @missing = @want[@got..$#want];
|
||||||
|
ok(@missing == 0, "no missing logs")
|
||||||
|
or diag(ddclient::repr(\@missing, Names => ['*missing']));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
242
t/protocol_dnsexit2.pl
Normal file
242
t/protocol_dnsexit2.pl
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::Logger;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
|
||||||
|
ddclient::load_json_support('dnsexit2');
|
||||||
|
|
||||||
|
httpd()->run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
return undef if $req->uri()->path() eq '/control';
|
||||||
|
return [200, ['Content-Type' => 'application/json'], [encode_json({
|
||||||
|
code => 0,
|
||||||
|
message => 'Success'
|
||||||
|
})]];
|
||||||
|
});
|
||||||
|
|
||||||
|
sub cmp_update {
|
||||||
|
my ($a, $b) = @_;
|
||||||
|
return $a->{name} cmp $b->{name} || $a->{type} cmp $b->{type};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub sort_updates {
|
||||||
|
my ($req) = @_;
|
||||||
|
return {
|
||||||
|
%$req,
|
||||||
|
update => [sort({ cmp_update($a, $b); } @{$req->{update}})],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub sort_reqs {
|
||||||
|
my @reqs = map(sort_updates($_), @_);
|
||||||
|
my @sorted = sort({
|
||||||
|
my $ret = $a->{domain} cmp $b->{domain};
|
||||||
|
$ret = @{$a->{update}} <=> @{$b->{update}} if !$ret;
|
||||||
|
my $i = 0;
|
||||||
|
while (!$ret && $i < @{$a->{update}} && $i < @{$b->{update}}) {
|
||||||
|
$ret = cmp_update($a->{update}[$i], $b->{update}[$i]);
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
} @reqs);
|
||||||
|
return @sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'both IPv4 and IPv6 are updated together',
|
||||||
|
cfg => {
|
||||||
|
'host.my.example.com' => {
|
||||||
|
ttl => 5,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
zone => 'my.example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'my.example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => 'host',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content => '2001:db8::1',
|
||||||
|
name => 'host',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'AAAA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'zone defaults to host',
|
||||||
|
cfg => {
|
||||||
|
'host.my.example.com' => {
|
||||||
|
ttl => 10,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'host.my.example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => '',
|
||||||
|
ttl => 10,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'two hosts, different zones',
|
||||||
|
cfg => {
|
||||||
|
'host1.example.com' => {
|
||||||
|
ttl => 5,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
# 'zone' intentionally not set, so it will default to 'host1.example.com'.
|
||||||
|
},
|
||||||
|
'host2.example.com' => {
|
||||||
|
ttl => 10,
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [
|
||||||
|
{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'host1.example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => '',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '2001:db8::1',
|
||||||
|
name => 'host2',
|
||||||
|
ttl => 10,
|
||||||
|
type => 'AAAA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'two hosts, same zone',
|
||||||
|
cfg => {
|
||||||
|
'host1.example.com' => {
|
||||||
|
ttl => 5,
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
'host2.example.com' => {
|
||||||
|
ttl => 10,
|
||||||
|
wantipv6 => '2001:db8::1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want => [
|
||||||
|
{
|
||||||
|
apikey => 'key',
|
||||||
|
domain => 'example.com',
|
||||||
|
update => [
|
||||||
|
{
|
||||||
|
content => '192.0.2.1',
|
||||||
|
name => 'host1',
|
||||||
|
ttl => 5,
|
||||||
|
type => 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content => '2001:db8::1',
|
||||||
|
name => 'host2',
|
||||||
|
ttl => 10,
|
||||||
|
type => 'AAAA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'host outside of zone',
|
||||||
|
cfg => {
|
||||||
|
'host.example' => {
|
||||||
|
wantipv4 => '192.0.2.1',
|
||||||
|
zone => 'example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want_fatal => qr{hostname does not end with the zone: example.com},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
subtest($tc->{desc} => sub {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
local %ddclient::config = ();
|
||||||
|
my @hosts = keys(%{$tc->{cfg}});
|
||||||
|
for my $h (@hosts) {
|
||||||
|
$ddclient::config{$h} = {
|
||||||
|
password => 'key',
|
||||||
|
path => '/update',
|
||||||
|
server => httpd()->endpoint(),
|
||||||
|
%{$tc->{cfg}{$h}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^FATAL$/);
|
||||||
|
my $err = do {
|
||||||
|
local $ddclient::_l = $l;
|
||||||
|
local $@;
|
||||||
|
(eval { ddclient::nic_dnsexit2_update(undef, @hosts); 1; })
|
||||||
|
? undef : ($@ // 'unknown error');
|
||||||
|
};
|
||||||
|
my @requests = httpd()->reset();
|
||||||
|
my @got;
|
||||||
|
for (my $i = 0; $i < @requests; $i++) {
|
||||||
|
subtest("request $i" => sub {
|
||||||
|
my $req = $requests[$i];
|
||||||
|
is($req->method(), 'POST', 'method is POST');
|
||||||
|
is($req->uri()->as_string(), '/update', 'path is /update');
|
||||||
|
is($req->header('content-type'), 'application/json', 'Content-Type is JSON');
|
||||||
|
is($req->header('accept'), 'application/json', 'Accept is JSON');
|
||||||
|
my $got = decode_json($req->content());
|
||||||
|
is(ref($got), 'HASH', 'request content is a JSON object');
|
||||||
|
is(ref($got->{update}), 'ARRAY', 'JSON object has array "update" property');
|
||||||
|
push(@got, $got);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@got = sort_reqs(@got);
|
||||||
|
my @want = sort_reqs(@{$tc->{want} // []});
|
||||||
|
is_deeply(\@got, \@want, 'request objects match');
|
||||||
|
subtest('expected (or lack of) error' => sub {
|
||||||
|
if (is(defined($err), defined($tc->{want_fatal}), 'error existence') && defined($err)) {
|
||||||
|
my @got = @{$l->{logs}};
|
||||||
|
if (is(scalar(@got), 2, 'logged two events')) {
|
||||||
|
is($got[0]->{label}, 'FATAL', 'first logged event is a FATAL message');
|
||||||
|
like($got[0]->{msg}, $tc->{want_fatal}, 'first logged event message matches');
|
||||||
|
is($got[1], 'aborted', 'second logged event is an "aborted" event');
|
||||||
|
isa_ok($err, qw(ddclient::t::LoggerAbort));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
279
t/protocol_dyndns2.pl
Normal file
279
t/protocol_dyndns2.pl
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
use MIME::Base64;
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::Logger;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
httpd()->run(sub {
|
||||||
|
my ($req) = @_;
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Test server received request:\n" . $req->as_string());
|
||||||
|
return undef if $req->uri()->path() eq '/control';
|
||||||
|
my $wantauthn = 'Basic ' . encode_base64('username:password', '');
|
||||||
|
return [401, [@$textplain, 'www-authenticate' => 'Basic realm="realm", charset="UTF-8"'],
|
||||||
|
['authentication required']] if ($req->header('authorization') // '') ne $wantauthn;
|
||||||
|
return [400, $textplain, ['invalid method: ' . $req->method()]] if $req->method() ne 'GET';
|
||||||
|
return undef;
|
||||||
|
});
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, good',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, nochg',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['nochg'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'WARNING', ctx => ['h1'], msg => qr/nochg/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, bad',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['nohost'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'nohost'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/nohost/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, single host, unexpected',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1'}},
|
||||||
|
resp => ['WAT'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'WAT'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/unexpected.*WAT/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, multiple hosts, multiple good',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good 192.0.2.1',
|
||||||
|
'good',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4, multiple hosts, mixed success',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h3 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good',
|
||||||
|
'nochg',
|
||||||
|
'dnserr',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h3 => {'status-ipv4' => 'dnserr'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'WARNING', ctx => ['h2'], msg => qr/nochg/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
{label => 'FAILED', ctx => ['h3'], msg => qr/dnserr/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv6, single host, good',
|
||||||
|
cfg => {h1 => {wantipv6 => '2001:db8::1'}},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1&myip=2001:db8::1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv6' => 'good', 'ipv6' => '2001:db8::1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'IPv4 and IPv6, single host, good',
|
||||||
|
cfg => {h1 => {wantipv4 => '192.0.2.1', wantipv6 => '2001:db8::1'}},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1&myip=192.0.2.1,2001:db8::1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1',
|
||||||
|
'status-ipv6' => 'good', 'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv6/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'excess status line',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good',
|
||||||
|
'good',
|
||||||
|
'WAT',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
{label => 'WARNING', ctx => ['h1,h2'], msg => qr/unexpected.*\nWAT$/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple hosts, single failure',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => ['abuse'],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'abuse'},
|
||||||
|
h2 => {'status-ipv4' => 'abuse'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'FAILED', ctx => ['h1'], msg => qr/abuse/},
|
||||||
|
{label => 'FAILED', ctx => ['h2'], msg => qr/abuse/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple hosts, single success',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => ['good'],
|
||||||
|
wantquery => 'hostname=h1,h2&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'WARNING', ctx => ['h1,h2'], msg => qr//},
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'multiple hosts, fewer results',
|
||||||
|
cfg => {
|
||||||
|
h1 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h2 => {wantipv4 => '192.0.2.1'},
|
||||||
|
h3 => {wantipv4 => '192.0.2.1'},
|
||||||
|
},
|
||||||
|
resp => [
|
||||||
|
'good',
|
||||||
|
'nochg',
|
||||||
|
],
|
||||||
|
wantquery => 'hostname=h1,h2,h3&myip=192.0.2.1',
|
||||||
|
wantrecap => {
|
||||||
|
h1 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h2 => {'status-ipv4' => 'good', 'ipv4' => '192.0.2.1', 'mtime' => $ddclient::now},
|
||||||
|
h3 => {'status-ipv4' => 'unknown'},
|
||||||
|
},
|
||||||
|
wantlogs => [
|
||||||
|
{label => 'SUCCESS', ctx => ['h1'], msg => qr/IPv4/},
|
||||||
|
{label => 'WARNING', ctx => ['h2'], msg => qr/nochg/},
|
||||||
|
{label => 'SUCCESS', ctx => ['h2'], msg => qr/IPv4/},
|
||||||
|
{label => 'FAILED', ctx => ['h3'], msg => qr/assuming failure/},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
diag('==============================================================================');
|
||||||
|
diag("Starting test: $tc->{desc}");
|
||||||
|
diag('==============================================================================');
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
|
||||||
|
local %ddclient::config;
|
||||||
|
local %ddclient::recap;
|
||||||
|
$ddclient::config{$_} = {
|
||||||
|
login => 'username',
|
||||||
|
password => 'password',
|
||||||
|
server => httpd()->endpoint(),
|
||||||
|
script => '/nic/update',
|
||||||
|
%{$tc->{cfg}{$_}},
|
||||||
|
} for keys(%{$tc->{cfg}});
|
||||||
|
httpd()->reset([200, $textplain, [map("$_\n", @{$tc->{resp}})]]);
|
||||||
|
{
|
||||||
|
local $ddclient::_l = $l;
|
||||||
|
ddclient::nic_dyndns2_update(undef, sort(keys(%{$tc->{cfg}})));
|
||||||
|
}
|
||||||
|
my @requests = httpd()->reset();
|
||||||
|
is(scalar(@requests), 1, "$tc->{desc}: single update request");
|
||||||
|
my $req = shift(@requests);
|
||||||
|
is($req->uri()->path(), '/nic/update', "$tc->{desc}: request path");
|
||||||
|
is($req->uri()->query(), $tc->{wantquery}, "$tc->{desc}: request query");
|
||||||
|
is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
$tc->{wantlogs} //= [];
|
||||||
|
subtest("$tc->{desc}: logs" => sub {
|
||||||
|
my @got = @{$l->{logs}};
|
||||||
|
my @want = @{$tc->{wantlogs}};
|
||||||
|
for my $i (0..$#want) {
|
||||||
|
last if $i >= @got;
|
||||||
|
my $got = $got[$i];
|
||||||
|
my $want = $want[$i];
|
||||||
|
subtest("log $i" => sub {
|
||||||
|
is($got->{label}, $want->{label}, "label matches");
|
||||||
|
is_deeply($got->{ctx}, $want->{ctx}, "context matches");
|
||||||
|
like($got->{msg}, $want->{msg}, "message matches");
|
||||||
|
}) or diag(ddclient::repr(Values => [$got, $want], Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my @unexpected = @got[@want..$#got];
|
||||||
|
ok(@unexpected == 0, "no unexpected logs")
|
||||||
|
or diag(ddclient::repr(\@unexpected, Names => ['*unexpected']));
|
||||||
|
my @missing = @want[@got..$#want];
|
||||||
|
ok(@missing == 0, "no missing logs")
|
||||||
|
or diag(ddclient::repr(\@missing, Names => ['*missing']));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
107
t/read_recap.pl
Normal file
107
t/read_recap.pl
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
use File::Temp;
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
local %ddclient::protocols = (
|
||||||
|
protocol_a => ddclient::Protocol->new(
|
||||||
|
recapvars => {
|
||||||
|
host => ddclient::T_STRING(),
|
||||||
|
var_a => ddclient::T_BOOL(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
protocol_b => ddclient::Protocol->new(
|
||||||
|
recapvars => {
|
||||||
|
host => ddclient::T_STRING(),
|
||||||
|
var_b => ddclient::T_NUMBER(),
|
||||||
|
},
|
||||||
|
cfgvars => {
|
||||||
|
var_b_non_recap => {type => ddclient::T_ANY()},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
local %ddclient::cfgvars = (merged => {map({ %{$ddclient::protocols{$_}{cfgvars} // {}}; }
|
||||||
|
sort(keys(%ddclient::protocols)))});
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => "ok value",
|
||||||
|
cachefile_lines => ["var_a=yes host_a"],
|
||||||
|
want => {host_a => {host => 'host_a', var_a => 1}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "unknown host",
|
||||||
|
cachefile_lines => ["var_a=yes host_c"],
|
||||||
|
want => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "unknown var",
|
||||||
|
cachefile_lines => ["var_b=123 host_a"],
|
||||||
|
want => {host_a => {host => 'host_a'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "invalid value",
|
||||||
|
cachefile_lines => ["var_a=wat host_a"],
|
||||||
|
want => {host_a => {host => 'host_a'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "multiple entries",
|
||||||
|
cachefile_lines => [
|
||||||
|
"var_a=yes host_a",
|
||||||
|
"var_b=123 host_b",
|
||||||
|
],
|
||||||
|
want => {
|
||||||
|
host_a => {host => 'host_a', var_a => 1},
|
||||||
|
host_b => {host => 'host_b', var_b => 123},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "non-recap vars are not loaded to %recap",
|
||||||
|
cachefile_lines => ["var_b_non_recap=foo host_b"],
|
||||||
|
want => {host_b => {host => 'host_b'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "non-recap vars are scrubbed from %recap",
|
||||||
|
cachefile_lines => ["var_b_non_recap=foo host_b"],
|
||||||
|
recap => {host_b => {host => 'host_b', var_b_non_recap => 'foo'}},
|
||||||
|
want => {host_b => {host => 'host_b'}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "unknown hosts are scrubbed from %recap",
|
||||||
|
cachefile_lines => ["host_a", "host_c"],
|
||||||
|
recap => {host_a => {host => 'host_a'}, host_c => {host => 'host_c'}},
|
||||||
|
want => {host_a => {host => 'host_a'}},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
my $cachef = File::Temp->new();
|
||||||
|
print($cachef join('', map("$_\n", "## $ddclient::program-$ddclient::version",
|
||||||
|
@{$tc->{cachefile_lines}})));
|
||||||
|
$cachef->close();
|
||||||
|
local $ddclient::globals{cache} = "$cachef";
|
||||||
|
local %ddclient::recap = %{$tc->{recap} // {}};
|
||||||
|
my %want_config = (
|
||||||
|
host_a => {protocol => 'protocol_a'},
|
||||||
|
host_b => {protocol => 'protocol_b'},
|
||||||
|
);
|
||||||
|
# Deep clone %want_config so we can check for changes.
|
||||||
|
local %ddclient::config;
|
||||||
|
$ddclient::config{$_} = {%{$want_config{$_}}} for keys(%want_config);
|
||||||
|
|
||||||
|
ddclient::read_recap($cachef->filename());
|
||||||
|
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_TODO};
|
||||||
|
is_deeply(\%ddclient::recap, $tc->{want}, "$tc->{desc}: %recap")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{want}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
is_deeply(\%ddclient::config, \%want_config, "$tc->{desc}: %config")
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::config, \%want_config],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
150
t/skip.pl
Normal file
150
t/skip.pl
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
httpd('4')->run(
|
||||||
|
sub { return [200, ['Content-Type' => 'text/plain'], ['127.0.0.1 skip 127.0.0.2']]; });
|
||||||
|
httpd('6')->run(
|
||||||
|
sub { return [200, ['Content-Type' => 'text/plain'], ['::1 skip ::2']]; })
|
||||||
|
if httpd('6');
|
||||||
|
|
||||||
|
my $builtinwebv4 = 't/skip.pl webv4';
|
||||||
|
my $builtinwebv6 = 't/skip.pl webv6';
|
||||||
|
my $builtinfw = 't/skip.pl fw';
|
||||||
|
|
||||||
|
$ddclient::builtinweb{$builtinwebv4} = {'url' => httpd('4')->endpoint(), 'skip' => 'skip'};
|
||||||
|
$ddclient::builtinweb{$builtinwebv6} = {'url' => httpd('6')->endpoint(), 'skip' => 'skip'}
|
||||||
|
if httpd('6');
|
||||||
|
$ddclient::builtinfw{$builtinfw} = {name => 'test', skip => 'skip'};
|
||||||
|
%ddclient::builtinfw if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
%ddclient::ip_strategies = (%ddclient::ip_strategies, ddclient::builtinfw_strategy($builtinfw));
|
||||||
|
%ddclient::ipv4_strategies =
|
||||||
|
(%ddclient::ipv4_strategies, ddclient::builtinfwv4_strategy($builtinfw));
|
||||||
|
%ddclient::ipv6_strategies =
|
||||||
|
(%ddclient::ipv6_strategies, ddclient::builtinfwv6_strategy($builtinfw));
|
||||||
|
|
||||||
|
sub run_test_case {
|
||||||
|
my %tc = @_;
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
my $h = 't/skip.pl';
|
||||||
|
$ddclient::config{$h} = $tc{cfg};
|
||||||
|
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
is(ddclient::get_ip(ddclient::strategy_inputs('use', $h)), $tc{want}, $tc{desc})
|
||||||
|
if ($tc{cfg}{use});
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc{want}, $tc{desc})
|
||||||
|
if ($tc{cfg}{usev4});
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc{want}, $tc{desc})
|
||||||
|
if ($tc{cfg}{usev6});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subtest "use=web web='$builtinwebv4'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "web-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'use' => 'web',
|
||||||
|
'web' => $builtinwebv4,
|
||||||
|
'web-skip' => '',
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'web-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'use' => 'web',
|
||||||
|
'web' => $builtinwebv4,
|
||||||
|
'web-skip' => undef,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "usev4=webv4 webv4='$builtinwebv4'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "webv4-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'usev4' => 'webv4',
|
||||||
|
'webv4' => $builtinwebv4,
|
||||||
|
'webv4-skip' => '',
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'webv4-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'usev4' => 'webv4',
|
||||||
|
'webv4' => $builtinwebv4,
|
||||||
|
'webv4-skip' => undef,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "usev6=webv6 webv6='$builtinwebv6'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "webv6-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
'webv6' => $builtinwebv6,
|
||||||
|
'webv6-skip' => '',
|
||||||
|
},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => '::1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'webv6-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
'webv6' => $builtinwebv6,
|
||||||
|
'webv6-skip' => undef,
|
||||||
|
},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => '::2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "use='$builtinfw'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "fw-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'fw' => httpd('4')->endpoint(),
|
||||||
|
'fw-skip' => '',
|
||||||
|
'use' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'fw-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'fw' => httpd('4')->endpoint(),
|
||||||
|
'fw-skip' => undef,
|
||||||
|
'use' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
subtest "usev4='$builtinfw'" => sub {
|
||||||
|
run_test_case(
|
||||||
|
desc => "fwv4-skip='' cancels built-in skip",
|
||||||
|
cfg => {
|
||||||
|
'fwv4' => httpd('4')->endpoint(),
|
||||||
|
'fwv4-skip' => '',
|
||||||
|
'usev4' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
);
|
||||||
|
run_test_case(
|
||||||
|
desc => 'fwv4-skip=undef uses built-in skip',
|
||||||
|
cfg => {
|
||||||
|
'fwv4' => httpd('4')->endpoint(),
|
||||||
|
'fwv4-skip' => undef,
|
||||||
|
'usev4' => $builtinfw,
|
||||||
|
},
|
||||||
|
want => '127.0.0.2',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
done_testing();
|
94
t/ssl-validate.pl
Normal file
94
t/ssl-validate.pl
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
httpd_ssl_required();
|
||||||
|
|
||||||
|
httpd('4', 1)->run(sub { return [200, $textplain, ['127.0.0.1']]; });
|
||||||
|
httpd('6', 1)->run(sub { return [200, $textplain, ['::1']]; }) if httpd('6', 1);
|
||||||
|
my $h = 't/ssl-validate.pl';
|
||||||
|
my %ep = (
|
||||||
|
'4' => httpd('4', 1)->endpoint(),
|
||||||
|
'6' => httpd('6', 1) ? httpd('6', 1)->endpoint() : undef,
|
||||||
|
);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'usev4=webv4 web-ssl-validate=no',
|
||||||
|
cfg => {'usev4' => 'webv4', 'web-ssl-validate' => 0, 'webv4' => $ep{'4'}},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=webv4 web-ssl-validate=yes',
|
||||||
|
cfg => {'usev4' => 'webv4', 'web-ssl-validate' => 1, 'webv4' => $ep{'4'}},
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6=webv6 web-ssl-validate=no',
|
||||||
|
cfg => {'usev6' => 'webv6', 'web-ssl-validate' => 0, 'webv6' => $ep{'6'}},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => '::1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6=webv6 web-ssl-validate=yes',
|
||||||
|
cfg => {'usev6' => 'webv6', 'web-ssl-validate' => 1, 'webv6' => $ep{'6'}},
|
||||||
|
ipv6 => 1,
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=cisco-asa fw-ssl-validate=no',
|
||||||
|
cfg => {'usev4' => 'cisco-asa', 'fw-ssl-validate' => 0,
|
||||||
|
# cisco-asa adds https:// to the URL. :-/
|
||||||
|
'fwv4' => substr($ep{'4'}, length('https://'))},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=cisco-asa fw-ssl-validate=yes',
|
||||||
|
cfg => {'usev4' => 'cisco-asa', 'fw-ssl-validate' => 1,
|
||||||
|
# cisco-asa adds https:// to the URL. :-/
|
||||||
|
'fwv4' => substr($ep{'4'}, length('https://'))},
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=fwv4 fw-ssl-validate=no',
|
||||||
|
cfg => {'usev4' => 'fwv4', 'fw-ssl-validate' => 0, 'fwv4' => $ep{'4'}},
|
||||||
|
want => '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4=fwv4 fw-ssl-validate=yes',
|
||||||
|
cfg => {'usev4' => 'fwv4', 'fw-ssl-validate' => 1, 'fwv4' => $ep{'4'}},
|
||||||
|
want => undef,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
# $ddclient::globals{'ssl_ca_file'} is intentionally NOT set to $ca_file so that we can
|
||||||
|
# test what happens when certificate validation fails. However, if curl can't find any CA
|
||||||
|
# certificates (which may be the case in some minimal test environments, such as Docker
|
||||||
|
# images and Debian package builder chroots), it will immediately close the connection
|
||||||
|
# after it sends the TLS client hello and before it receives the server hello (in Debian
|
||||||
|
# sid as of 2025-01-08, anyway). This confuses IO::Socket::SSL (used by
|
||||||
|
# Test::Fake::HTTPD), causing it to hang in the middle of the TLS handshake waiting for
|
||||||
|
# input that will never arrive. To work around this, the CA certificate file is explicitly
|
||||||
|
# set to an unrelated certificate so that curl has something to read.
|
||||||
|
local $ddclient::globals{'ssl_ca_file'} = $other_ca_file;
|
||||||
|
local $ddclient::config{$h} = $tc->{cfg};
|
||||||
|
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if ($tc->{cfg}{usev4});
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if ($tc->{cfg}{usev6});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
395
t/update_nics.pl
Normal file
395
t/update_nics.pl
Normal file
|
@ -0,0 +1,395 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
use File::Temp;
|
||||||
|
BEGIN { eval { require HTTP::Request; 1; } or plan(skip_all => $@); }
|
||||||
|
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||||
|
use List::Util qw(max);
|
||||||
|
use Scalar::Util qw(refaddr);
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
httpd('4')->run();
|
||||||
|
httpd('6')->run() if httpd('6');
|
||||||
|
local %ddclient::builtinweb = (
|
||||||
|
v4 => {url => "" . httpd('4')->endpoint()},
|
||||||
|
defined(httpd('6')) ? (v6 => {url => "" . httpd('6')->endpoint()}) : (),
|
||||||
|
);
|
||||||
|
|
||||||
|
# Sentinel value used by `mergecfg` that means "this hash entry should be deleted if it exists."
|
||||||
|
my $DOES_NOT_EXIST = [];
|
||||||
|
|
||||||
|
sub mergecfg {
|
||||||
|
my %ret;
|
||||||
|
for my $cfg (@_) {
|
||||||
|
next if !defined($cfg);
|
||||||
|
for my $h (keys(%$cfg)) {
|
||||||
|
if (refaddr($cfg->{$h}) == refaddr($DOES_NOT_EXIST)) {
|
||||||
|
delete($ret{$h});
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
$ret{$h} = {%{$ret{$h} // {}}, %{$cfg->{$h}}};
|
||||||
|
for my $k (keys(%{$ret{$h}})) {
|
||||||
|
my $a = refaddr($ret{$h}{$k});
|
||||||
|
delete($ret{$h}{$k}) if defined($a) && $a == refaddr($DOES_NOT_EXIST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return \%ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
local $ddclient::now = 1000;
|
||||||
|
our @updates;
|
||||||
|
local %ddclient::protocols = (
|
||||||
|
# The `legacy` protocol reads the legacy `wantip` property and sets the legacy `ip` and `status`
|
||||||
|
# properties. (Modern protocol implementations read `wantipv4` and `wantipv6` and set `ipv4`,
|
||||||
|
# `ipv6`, `status-ipv4`, and `status-ipv6`.) It always succeeds.
|
||||||
|
legacy => ddclient::LegacyProtocol->new(
|
||||||
|
update => sub {
|
||||||
|
my $self = shift;
|
||||||
|
ddclient::debug('in update');
|
||||||
|
push(@updates, [@_]);
|
||||||
|
for my $h (@_) {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($h);
|
||||||
|
ddclient::debug('updating host');
|
||||||
|
$ddclient::recap{$h}{status} = 'good';
|
||||||
|
$ddclient::recap{$h}{ip} = delete($ddclient::config{$h}{wantip});
|
||||||
|
$ddclient::recap{$h}{mtime} = $ddclient::now;
|
||||||
|
}
|
||||||
|
ddclient::debug('returning from update');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
my @test_cases = (
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, fresh, $desc",
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
{
|
||||||
|
desc => 'legacy, fresh, use=web (IPv6)',
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
'use' => 'web',
|
||||||
|
'web' => 'v6',
|
||||||
|
}},
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'legacy, fresh, usev6=webv6',
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
}},
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'legacy, fresh, usev4=webv4 usev6=webv6',
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
'usev4' => 'webv4',
|
||||||
|
'usev6' => 'webv6',
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, no change, not yet time, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, min-interval elapsed but no change, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, needs update, not yet time, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval'),
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'warned-min-interval' => $ddclient::now,
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, min-interval elapsed, needs update, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, previous failed update, not yet time to retry, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-error-interval'),
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - max(ddclient::opt('min-error-interval'),
|
||||||
|
ddclient::opt('min-interval')) - 1,
|
||||||
|
'status-ipv4' => 'failed',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'warned-min-error-interval' => $ddclient::now,
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
|
||||||
|
{
|
||||||
|
desc => "legacy, previous failed update, time to retry, $desc",
|
||||||
|
recap => {host => {
|
||||||
|
'atime' => $ddclient::now - ddclient::opt('min-error-interval') - 1,
|
||||||
|
'ipv4' => '192.0.2.2',
|
||||||
|
'mtime' => $ddclient::now - ddclient::opt('min-error-interval') - 2,
|
||||||
|
'status-ipv4' => 'failed',
|
||||||
|
}},
|
||||||
|
cfg => {host => {
|
||||||
|
'protocol' => 'legacy',
|
||||||
|
%cfg,
|
||||||
|
}},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['host']],
|
||||||
|
want_recap_changes => {host => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
}},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
map({
|
||||||
|
my %cfg = %{delete($_->{cfg})};
|
||||||
|
my $desc = join(' ', map("$_=$cfg{$_}", sort(keys(%cfg))));
|
||||||
|
{
|
||||||
|
desc => "deduplicates identical IP discovery, $desc",
|
||||||
|
cfg => {
|
||||||
|
hosta => {protocol => 'legacy', %cfg},
|
||||||
|
hostb => {protocol => 'legacy', %cfg},
|
||||||
|
},
|
||||||
|
want_reqs_webv4 => 1,
|
||||||
|
want_updates => [['hosta', 'hostb']],
|
||||||
|
want_recap_changes => {
|
||||||
|
hosta => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
},
|
||||||
|
hostb => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv4' => '192.0.2.1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv4' => 'good',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
%$_,
|
||||||
|
};
|
||||||
|
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
|
||||||
|
{
|
||||||
|
desc => "deduplicates identical IP discovery, usev6=webv6",
|
||||||
|
ipv6 => 1,
|
||||||
|
cfg => {
|
||||||
|
hosta => {protocol => 'legacy', usev6 => 'webv6'},
|
||||||
|
hostb => {protocol => 'legacy', usev6 => 'webv6'},
|
||||||
|
},
|
||||||
|
want_reqs_webv6 => 1,
|
||||||
|
want_updates => [['hosta', 'hostb']],
|
||||||
|
want_recap_changes => {
|
||||||
|
hosta => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
},
|
||||||
|
hostb => {
|
||||||
|
'atime' => $ddclient::now,
|
||||||
|
'ipv6' => '2001:db8::1',
|
||||||
|
'mtime' => $ddclient::now,
|
||||||
|
'status-ipv6' => 'good',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
subtest($tc->{desc} => sub {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
$tc->{"want_reqs_webv$ipv"} //= 0;
|
||||||
|
my $want = $tc->{"want_reqs_webv$ipv"};
|
||||||
|
next if !defined(httpd($ipv)) && $want == 0;
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
|
||||||
|
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
|
||||||
|
httpd($ipv)->reset(([200, $textplain, [$ip]]) x $want);
|
||||||
|
}
|
||||||
|
$tc->{recap}{$_}{host} //= $_ for keys(%{$tc->{recap} // {}});
|
||||||
|
# Deep copy `%{$tc->{recap}}` so that updates to `%ddclient::recap` don't mutate it.
|
||||||
|
local %ddclient::recap = %{mergecfg($tc->{recap})};
|
||||||
|
my $cachef = File::Temp->new();
|
||||||
|
# $cachef is an object that stringifies to a filename.
|
||||||
|
local $ddclient::globals{cache} = "$cachef";
|
||||||
|
$tc->{cfg} = {map({
|
||||||
|
($_ => {
|
||||||
|
host => $_,
|
||||||
|
web => 'v4',
|
||||||
|
webv4 => 'v4',
|
||||||
|
webv6 => 'v6',
|
||||||
|
%{$tc->{cfg}{$_}},
|
||||||
|
});
|
||||||
|
} keys(%{$tc->{cfg} // {}}))};
|
||||||
|
# Deep copy `%{$tc->{cfg}}` so that updates to `%ddclient::config` don't mutate it.
|
||||||
|
local %ddclient::config = %{mergecfg($tc->{cfg})};
|
||||||
|
local @updates;
|
||||||
|
|
||||||
|
ddclient::update_nics();
|
||||||
|
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
next if !defined(httpd($ipv));
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
|
||||||
|
my @gotreqs = httpd($ipv)->reset();
|
||||||
|
my $got = @gotreqs;
|
||||||
|
my $want = $tc->{"want_reqs_webv$ipv"};
|
||||||
|
is($got, $want, "number of requests to webv$ipv service");
|
||||||
|
}
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_updates_TODO};
|
||||||
|
is_deeply(\@updates, $tc->{want_updates} // [], 'got expected updates')
|
||||||
|
or diag(ddclient::repr(Values => [\@updates, $tc->{want_updates}],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my %want_recap = %{mergecfg($tc->{recap}, $tc->{want_recap_changes})};
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_recap_changes_TODO};
|
||||||
|
is_deeply(\%ddclient::recap, \%want_recap, 'recap matches')
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::recap, \%want_recap],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
my %want_cfg = %{mergecfg($tc->{cfg}, $tc->{want_cfg_changes})};
|
||||||
|
TODO: {
|
||||||
|
local $TODO = $tc->{want_cfg_changes_TODO};
|
||||||
|
is_deeply(\%ddclient::config, \%want_cfg, 'config matches')
|
||||||
|
or diag(ddclient::repr(Values => [\%ddclient::config, \%want_cfg],
|
||||||
|
Names => ['*got', '*want']));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
41
t/use_cmd.pl
Normal file
41
t/use_cmd.pl
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
|
||||||
|
local $ddclient::globals{debug} = 1;
|
||||||
|
local $ddclient::globals{verbose} = 1;
|
||||||
|
|
||||||
|
my @test_cases;
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
|
||||||
|
for my $use ('use', "usev$ipv") {
|
||||||
|
my @cmds = ();
|
||||||
|
push(@cmds, 'cmd') if $use eq 'use' || $ipv eq '6';
|
||||||
|
push(@cmds, "cmdv$ipv") if $use ne 'use';
|
||||||
|
for my $cmd (@cmds) {
|
||||||
|
my $cmdarg = "echo '$ip'";
|
||||||
|
push(
|
||||||
|
@test_cases,
|
||||||
|
{
|
||||||
|
desc => "$use=$cmd $cmd=\"$cmdarg\"",
|
||||||
|
cfg => {$use => $cmd, $cmd => $cmdarg},
|
||||||
|
want => $ip,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||||
|
my $h = 'test-host';
|
||||||
|
local $ddclient::config{$h} = $tc->{cfg};
|
||||||
|
is(ddclient::get_ip(ddclient::strategy_inputs('use', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{use};
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev4};
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev6};
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
87
t/use_web.pl
Normal file
87
t/use_web.pl
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use ddclient::t::HTTPD;
|
||||||
|
use ddclient::t::ip;
|
||||||
|
|
||||||
|
httpd_required();
|
||||||
|
|
||||||
|
my $builtinweb = 't/use_web.pl builtinweb';
|
||||||
|
my $h = 't/use_web.pl hostname';
|
||||||
|
|
||||||
|
my $headers = [
|
||||||
|
@$textplain,
|
||||||
|
'this-ipv4-should-be-ignored' => 'skip skip2 192.0.2.255',
|
||||||
|
'this-ipv6-should-be-ignored' => 'skip skip2 2001:db8::ff',
|
||||||
|
];
|
||||||
|
httpd('4')->run(sub { return [200, $headers, ['192.0.2.1 skip 192.0.2.2 skip2 192.0.2.3']]; });
|
||||||
|
httpd('6')->run(sub { return [200, $headers, ['2001:db8::1 skip 2001:db8::2 skip2 2001:db8::3']]; })
|
||||||
|
if httpd('6');
|
||||||
|
my %ep = (
|
||||||
|
'4' => httpd('4')->endpoint(),
|
||||||
|
'6' => httpd('6') ? httpd('6')->endpoint() : undef,
|
||||||
|
);
|
||||||
|
|
||||||
|
my @test_cases;
|
||||||
|
for my $ipv ('4', '6') {
|
||||||
|
my $ipv4 = $ipv eq '4';
|
||||||
|
for my $sfx ('', "v$ipv") {
|
||||||
|
push(
|
||||||
|
@test_cases,
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<url> IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $ep{$ipv}},
|
||||||
|
want => $ipv4 ? '192.0.2.1' : '2001:db8::1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<url> web$sfx-skip=skip IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $ep{$ipv}, "web$sfx-skip" => 'skip'},
|
||||||
|
# Note that "skip" should skip past the first "skip" and not past "skip2".
|
||||||
|
want => $ipv4 ? '192.0.2.2' : '2001:db8::2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<builtinweb> IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb},
|
||||||
|
biw => {url => $ep{$ipv}},
|
||||||
|
want => $ipv4 ? '192.0.2.1' : '2001:db8::1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<builtinweb w/skip> IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb},
|
||||||
|
biw => {url => $ep{$ipv}, skip => 'skip'},
|
||||||
|
# Note that "skip" should skip past the first "skip" and not past "skip2".
|
||||||
|
want => $ipv4 ? '192.0.2.2' : '2001:db8::2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => "use$sfx=web$sfx web$sfx=<builtinweb w/skip> web$sfx-skip=skip2 IPv$ipv",
|
||||||
|
ipv6 => !$ipv4,
|
||||||
|
cfg => {"use$sfx" => "web$sfx", "web$sfx" => $builtinweb, "web$sfx-skip" => 'skip2'},
|
||||||
|
biw => {url => $ep{$ipv}, skip => 'skip'},
|
||||||
|
want => $ipv4 ? '192.0.2.3' : '2001:db8::3',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
local $ddclient::builtinweb{$builtinweb} = $tc->{biw};
|
||||||
|
$ddclient::builtinweb if 0;
|
||||||
|
local $ddclient::config{$h} = $tc->{cfg};
|
||||||
|
$ddclient::config if 0;
|
||||||
|
SKIP: {
|
||||||
|
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
|
||||||
|
skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
|
||||||
|
is(ddclient::get_ip(ddclient::strategy_inputs('use', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{use};
|
||||||
|
is(ddclient::get_ipv4(ddclient::strategy_inputs('usev4', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev4};
|
||||||
|
is(ddclient::get_ipv6(ddclient::strategy_inputs('usev6', $h)), $tc->{want}, $tc->{desc})
|
||||||
|
if $tc->{cfg}{usev6};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
100
t/variable_defaults.pl
Normal file
100
t/variable_defaults.pl
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
use Test::More;
|
||||||
|
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
||||||
|
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||||
|
use re qw(is_regexp);
|
||||||
|
|
||||||
|
my %variable_collections = (
|
||||||
|
map({ ($_ => $ddclient::cfgvars{$_}) } grep($_ ne 'merged', keys(%ddclient::cfgvars))),
|
||||||
|
map({ ("protocol=$_" => $ddclient::protocols{$_}{cfgvars}); } keys(%ddclient::protocols)),
|
||||||
|
);
|
||||||
|
my %seen;
|
||||||
|
my @test_cases = (
|
||||||
|
map({
|
||||||
|
my $vcn = $_;
|
||||||
|
my $vc = $variable_collections{$_};
|
||||||
|
map({
|
||||||
|
my $def = $vc->{$_};
|
||||||
|
my $seen = exists($seen{$def});
|
||||||
|
$seen{$def} = undef;
|
||||||
|
({desc => "$vcn $_", def => $vc->{$_}}) x !$seen;
|
||||||
|
} sort(keys(%$vc)));
|
||||||
|
} sort(keys(%variable_collections))),
|
||||||
|
);
|
||||||
|
for my $tc (@test_cases) {
|
||||||
|
if ($tc->{def}{required}) {
|
||||||
|
is($tc->{def}{default}, undef, "'$tc->{desc}' (required) has no default");
|
||||||
|
} else {
|
||||||
|
# Preserve all existing variables in $cfgvars{merged} so that variables with dynamic
|
||||||
|
# defaults can reference them.
|
||||||
|
local %ddclient::cfgvars = (merged => {
|
||||||
|
%{$ddclient::cfgvars{merged}},
|
||||||
|
'var for test' => $tc->{def},
|
||||||
|
});
|
||||||
|
# Variables with dynamic defaults will need their own unit tests, but we can still check the
|
||||||
|
# clean-slate hostless default.
|
||||||
|
local %ddclient::config;
|
||||||
|
local %ddclient::opt;
|
||||||
|
local %ddclient::globals;
|
||||||
|
my $norm;
|
||||||
|
my $default = ddclient::default('var for test');
|
||||||
|
diag("'$tc->{desc}' default: " . ($default // '<undefined>'));
|
||||||
|
is($default, $tc->{def}{default}, "'$tc->{desc}' default() return value matches default")
|
||||||
|
if ref($tc->{def}{default}) ne 'CODE';
|
||||||
|
my $valid = eval { $norm = ddclient::check_value($default, $tc->{def}); 1; } or diag($@);
|
||||||
|
ok($valid, "'$tc->{desc}' (optional) has a valid default");
|
||||||
|
is($norm, $default, "'$tc->{desc}' default normalizes to itself") if $valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my @use_test_cases = (
|
||||||
|
{
|
||||||
|
desc => 'clean slate hostless default',
|
||||||
|
want => 'ip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usage string',
|
||||||
|
host => '<usage>',
|
||||||
|
want => qr/disabled.*ip|ip.*disabled/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev4 disables use by default',
|
||||||
|
host => 'host',
|
||||||
|
cfg => {usev4 => 'webv4'},
|
||||||
|
want => 'disabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'usev6 disables use by default',
|
||||||
|
host => 'host',
|
||||||
|
cfg => {usev4 => 'webv4'},
|
||||||
|
want => 'disabled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc => 'explicitly setting use re-enables it',
|
||||||
|
host => 'host',
|
||||||
|
cfg => {use => 'web', usev4 => 'webv4'},
|
||||||
|
want => 'web',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for my $tc (@use_test_cases) {
|
||||||
|
my $desc = "'use' dynamic default: $tc->{desc}";
|
||||||
|
local %ddclient::protocols = (protocol => ddclient::Protocol->new());
|
||||||
|
local %ddclient::cfgvars = (merged => {
|
||||||
|
'protocol' => $ddclient::cfgvars{'merged'}{'protocol'},
|
||||||
|
'use' => $ddclient::cfgvars{'protocol-common-defaults'}{'use'},
|
||||||
|
'usev4' => $ddclient::cfgvars{'merged'}{'usev4'},
|
||||||
|
'usev6' => $ddclient::cfgvars{'merged'}{'usev6'},
|
||||||
|
});
|
||||||
|
local %ddclient::config = (host => {protocol => 'protocol', %{$tc->{cfg} // {}}});
|
||||||
|
local %ddclient::opt;
|
||||||
|
local %ddclient::globals;
|
||||||
|
|
||||||
|
my $got = ddclient::opt('use', $tc->{host});
|
||||||
|
|
||||||
|
if (is_regexp($tc->{want})) {
|
||||||
|
like($got, $tc->{want}, $desc);
|
||||||
|
} else {
|
||||||
|
is($got, $tc->{want}, $desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
|
@ -4,6 +4,59 @@ use version;
|
||||||
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
|
||||||
eval { require 'ddclient'; } or BAIL_OUT($@);
|
eval { require 'ddclient'; } or BAIL_OUT($@);
|
||||||
|
|
||||||
is(ddclient->VERSION(), version->parse('v@PACKAGE_VERSION@'), "version matches Autoconf config");
|
ok(ddclient::parse_version($ddclient::VERSION),
|
||||||
|
"module's Perl version string is in opinionated form");
|
||||||
|
|
||||||
|
my $n = qr/0|[1-9]\d{0,2}/;
|
||||||
|
like($ddclient::version, qr/^$n\.$n\.$n(?:-alpha|-beta\.$n|-rc\.$n|\+r\.$n)?$/,
|
||||||
|
"human-readable version is in opinionated form");
|
||||||
|
|
||||||
|
my @tcs = (
|
||||||
|
['v1.0_0', '1-alpha'],
|
||||||
|
['v1.0.0_0', '1.0-alpha'],
|
||||||
|
['v1.2.3.0_0', '1.2.3-alpha'],
|
||||||
|
['v1.2.3.4.0_0', '1.2.3.4-alpha'],
|
||||||
|
['v1.0_1', '1-beta.1'],
|
||||||
|
['v1.0.0_1', '1.0-beta.1'],
|
||||||
|
['v1.2.3.0_1', '1.2.3-beta.1'],
|
||||||
|
['v1.2.3.4.0_1', '1.2.3.4-beta.1'],
|
||||||
|
['v1.2.3.0_899', '1.2.3-beta.899'],
|
||||||
|
['v1.0_901', '1-rc.1'],
|
||||||
|
['v1.0.0_901', '1.0-rc.1'],
|
||||||
|
['v1.2.3.0_901', '1.2.3-rc.1'],
|
||||||
|
['v1.2.3.4.0_901', '1.2.3.4-rc.1'],
|
||||||
|
['v1.2.3.0_998', '1.2.3-rc.98'],
|
||||||
|
['v1.999', '1'],
|
||||||
|
['v1.0.999', '1.0'],
|
||||||
|
['v1.2.3.999', '1.2.3'],
|
||||||
|
['v1.2.3.4.999', '1.2.3.4'],
|
||||||
|
['v1.999.1', '1+r.1'],
|
||||||
|
['v1.0.999.1', '1.0+r.1'],
|
||||||
|
['v1.2.3.999.1', '1.2.3+r.1'],
|
||||||
|
['v1.2.3.4.999.1', '1.2.3.4+r.1'],
|
||||||
|
['v1.2.3.999.999', '1.2.3+r.999'],
|
||||||
|
[$ddclient::VERSION, $ddclient::version],
|
||||||
|
);
|
||||||
|
|
||||||
|
subtest 'humanize_version' => sub {
|
||||||
|
for my $tc (@tcs) {
|
||||||
|
my ($pv, $want) = @$tc;
|
||||||
|
is(ddclient::humanize_version($pv), $want, "$pv -> $want");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest 'human-readable version can be translated back to Perl version' => sub {
|
||||||
|
for my $tc (@tcs) {
|
||||||
|
my ($want, $hv) = @$tc;
|
||||||
|
my $pv = "v$hv";
|
||||||
|
$pv =~ s/^(?!.*-)(.*?)(?:\+r\.(\d+))?$/"$1.999" . (defined($2) ? ".$2" : "")/e;
|
||||||
|
$pv =~ s/-alpha$/.0_0/;
|
||||||
|
$pv =~ s/-beta\.(\d+)$/.0_$1/;
|
||||||
|
$pv =~ s/-rc\.(\d+)$/'.0_' . (900 + $1)/e;
|
||||||
|
is($pv, $want, "$hv -> $want");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
is($ddclient::version, '@PACKAGE_VERSION@', "version matches version in Autoconf");
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|
|
@ -35,7 +35,7 @@ my @test_cases = (
|
||||||
|
|
||||||
for my $tc (@test_cases) {
|
for my $tc (@test_cases) {
|
||||||
$warning = undef;
|
$warning = undef;
|
||||||
ddclient::write_cache($tc->{f});
|
ddclient::write_recap($tc->{f});
|
||||||
subtest $tc->{name} => sub {
|
subtest $tc->{name} => sub {
|
||||||
if (defined($tc->{warning_regex})) {
|
if (defined($tc->{warning_regex})) {
|
||||||
like($warning, $tc->{warning_regex}, "expected warning message");
|
like($warning, $tc->{warning_regex}, "expected warning message");
|
Loading…
Reference in a new issue