Compare commits
615 Commits
update-ser
...
pyopenssl+
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65fa93f621 | ||
|
|
96e32a1eb1 | ||
|
|
353d092585 | ||
|
|
adedcc6416 | ||
|
|
bdfda3005e | ||
|
|
cb1226c938 | ||
|
|
82cfff8d35 | ||
|
|
5ff4fa91b0 | ||
|
|
be8638dad0 | ||
|
|
87215c7faf | ||
|
|
1651bdd86b | ||
|
|
11e0fa52d0 | ||
|
|
59a6d94b0a | ||
|
|
3edfe92069 | ||
|
|
245dbc7a95 | ||
|
|
6476663516 | ||
|
|
d151481dea | ||
|
|
9b9ba9b5fe | ||
|
|
9c0b27de68 | ||
|
|
f5aad1440f | ||
|
|
daa77b5023 | ||
|
|
d30afbea57 | ||
|
|
1f4297d0ed | ||
|
|
37d4b983c8 | ||
|
|
11dae13c2f | ||
|
|
d461655ec6 | ||
|
|
73458daac9 | ||
|
|
0b5468e992 | ||
|
|
8b5ac9e257 | ||
|
|
7d0ac47139 | ||
|
|
7527a6c959 | ||
|
|
6310d1e996 | ||
|
|
1c23fea076 | ||
|
|
3e155d443d | ||
|
|
a6ac2269b6 | ||
|
|
32fb5b3c47 | ||
|
|
a7f65eb5ac | ||
|
|
f65cb070b3 | ||
|
|
76bfe09393 | ||
|
|
ff66b641e3 | ||
|
|
3edb36c4cc | ||
|
|
fe840d5d46 | ||
|
|
41bf9c70f6 | ||
|
|
e8e3534335 | ||
|
|
ca42945264 | ||
|
|
a23d76beb0 | ||
|
|
fce5af50fd | ||
|
|
1dd7db12e0 | ||
|
|
78cf8ec4de | ||
|
|
4e1c22779e | ||
|
|
5700af594b | ||
|
|
ca12921a60 | ||
|
|
7fe64c3b9b | ||
|
|
5073090a20 | ||
|
|
b3d2ac5161 | ||
|
|
ad885afdb8 | ||
|
|
ee6f20d93d | ||
|
|
7352727a65 | ||
|
|
3d0e16ece3 | ||
|
|
b17c322483 | ||
|
|
63e0f56784 | ||
|
|
22858c6025 | ||
|
|
c1300a8e1b | ||
|
|
f3ff548a41 | ||
|
|
e6e323e3ff | ||
|
|
4edfb3ef65 | ||
|
|
92989956f9 | ||
|
|
39a008eb83 | ||
|
|
91b3c5d61c | ||
|
|
c3fa05ba74 | ||
|
|
5dc9dd8dea | ||
|
|
6c8652a0a6 | ||
|
|
b7f4b33ffb | ||
|
|
79b2ea19fb | ||
|
|
9cc5d18b97 | ||
|
|
47062dbfbf | ||
|
|
0e98904bec | ||
|
|
cbdc2ee23b | ||
|
|
cb8dd8a428 | ||
|
|
bc7763dd0f | ||
|
|
9403c1641d | ||
|
|
2c1964c639 | ||
|
|
eab7aa7bf1 | ||
|
|
bbf89781c9 | ||
|
|
c98bf18a77 | ||
|
|
0caaf872fb | ||
|
|
ac8d6f58bb | ||
|
|
1228f5da99 | ||
|
|
64a61fa6d4 | ||
|
|
1b595f26d8 | ||
|
|
a83afb6135 | ||
|
|
e37a8fbded | ||
|
|
d02ea812a5 | ||
|
|
28c117abe0 | ||
|
|
a1af42bc5f | ||
|
|
36b6328acd | ||
|
|
c699a50983 | ||
|
|
a5e26ca890 | ||
|
|
de1554f785 | ||
|
|
6e7d42f298 | ||
|
|
36e8d748a1 | ||
|
|
49619fc0ab | ||
|
|
6f662fa489 | ||
|
|
1eabb4bae3 | ||
|
|
35510bfc6c | ||
|
|
b837f218c9 | ||
|
|
a5e0e801be | ||
|
|
594fbe3ae8 | ||
|
|
2ddf47e3cc | ||
|
|
090a1bd44d | ||
|
|
1d783fd4b9 | ||
|
|
b0b8710835 | ||
|
|
54b4758c27 | ||
|
|
aad2663695 | ||
|
|
9264561944 | ||
|
|
36ebce4a5f | ||
|
|
7b17c84dd9 | ||
|
|
1e8c13ebf9 | ||
|
|
8dd68a6551 | ||
|
|
0dab41ee13 | ||
|
|
bfaf0296de | ||
|
|
b9dd40b350 | ||
|
|
6500b9095e | ||
|
|
ee02ed65af | ||
|
|
a3a3840e91 | ||
|
|
ca155b48ae | ||
|
|
3de3188dd6 | ||
|
|
92501eaf8f | ||
|
|
819f95c37d | ||
|
|
22da2447d5 | ||
|
|
139ef20650 | ||
|
|
e0f760099c | ||
|
|
19f74c3dc7 | ||
|
|
06174bc113 | ||
|
|
15932dcafc | ||
|
|
e8eeab3eab | ||
|
|
eb7a10b5c0 | ||
|
|
faa44070f5 | ||
|
|
7ccd6ec98e | ||
|
|
b42283f0b3 | ||
|
|
f4d615371e | ||
|
|
cb4b1897c9 | ||
|
|
e6a9fa8695 | ||
|
|
efd2ed1bdb | ||
|
|
3ef43e4d88 | ||
|
|
e501322ff1 | ||
|
|
38b1d9d6ba | ||
|
|
b32ec6ed30 | ||
|
|
a3b858db34 | ||
|
|
8f7209de14 | ||
|
|
251355cade | ||
|
|
85a859d63f | ||
|
|
5d1c6d28d5 | ||
|
|
b50abddb5f | ||
|
|
101eae4e05 | ||
|
|
4e2faffe89 | ||
|
|
d39a354a65 | ||
|
|
05ad539255 | ||
|
|
0c66de47cf | ||
|
|
a6f5189593 | ||
|
|
2708d28157 | ||
|
|
e28f3da974 | ||
|
|
19149a0d57 | ||
|
|
e178bbfdf5 | ||
|
|
cd2edeff1b | ||
|
|
405a8b4264 | ||
|
|
6e23b81dba | ||
|
|
0e9dd5e3d2 | ||
|
|
b1003b7250 | ||
|
|
d8057f0e17 | ||
|
|
4989668e0a | ||
|
|
7bff0a02e4 | ||
|
|
f6219ddf19 | ||
|
|
8943dffe0d | ||
|
|
b1b4650804 | ||
|
|
c131f4211d | ||
|
|
f2bc876b6e | ||
|
|
6262921315 | ||
|
|
68f9a1b300 | ||
|
|
8b3629ebd4 | ||
|
|
d19698251d | ||
|
|
ee7d5052fd | ||
|
|
dc91357418 | ||
|
|
c129ab2965 | ||
|
|
daee6e8eb3 | ||
|
|
cdc333491b | ||
|
|
94cadd33eb | ||
|
|
783b6e4746 | ||
|
|
20f8e69593 | ||
|
|
6fd312833f | ||
|
|
b3b2a65569 | ||
|
|
e9af000b5b | ||
|
|
a0d68338a2 | ||
|
|
0f833c6c40 | ||
|
|
704101c75b | ||
|
|
595e77eccb | ||
|
|
72daec4346 | ||
|
|
043db62a29 | ||
|
|
9974963887 | ||
|
|
fa7cb38e97 | ||
|
|
8428713032 | ||
|
|
9b0d2714c1 | ||
|
|
72cf844ecf | ||
|
|
fdb3c8df4b | ||
|
|
32676f02c3 | ||
|
|
7383fc6bf0 | ||
|
|
0a6d520d26 | ||
|
|
95e271bfcd | ||
|
|
c326c02108 | ||
|
|
a2222d5bdf | ||
|
|
148d68b99a | ||
|
|
b7113a35eb | ||
|
|
3f6a908821 | ||
|
|
3b39266813 | ||
|
|
9314911135 | ||
|
|
8a5abb6203 | ||
|
|
0672e63176 | ||
|
|
3855cfc08d | ||
|
|
83f7e72fef | ||
|
|
43f2bfd6f1 | ||
|
|
cdf93de338 | ||
|
|
dd600db436 | ||
|
|
2564566e1c | ||
|
|
08378203df | ||
|
|
cb076539ec | ||
|
|
cad95466b0 | ||
|
|
ab9851d97e | ||
|
|
552e60a126 | ||
|
|
6e13c2ccc7 | ||
|
|
a816cc8979 | ||
|
|
64e06d4201 | ||
|
|
d00a31622d | ||
|
|
742a57722b | ||
|
|
ad3c547e1f | ||
|
|
f169e7374b | ||
|
|
2304f7fcda | ||
|
|
a4760cfe56 | ||
|
|
87e1912bf9 | ||
|
|
80cd134847 | ||
|
|
7890de62ec | ||
|
|
1e1e7d8e97 | ||
|
|
2ac0b55208 | ||
|
|
6771b8e05b | ||
|
|
3877af6619 | ||
|
|
a875246a4b | ||
|
|
40c50231ed | ||
|
|
4ba153949d | ||
|
|
5025b4ea96 | ||
|
|
adc07ef933 | ||
|
|
3316eac178 | ||
|
|
8b16a56de8 | ||
|
|
453eafb11e | ||
|
|
c4ae376279 | ||
|
|
fccfbd14b1 | ||
|
|
c9ae365f66 | ||
|
|
9f20fa0ef9 | ||
|
|
95892cd4ab | ||
|
|
da028ca9c2 | ||
|
|
5bf1c51de7 | ||
|
|
afa7e3fb82 | ||
|
|
3a8de6d172 | ||
|
|
780a1b3a26 | ||
|
|
da6320f4d1 | ||
|
|
eec37f65a8 | ||
|
|
4b11fe1fda | ||
|
|
4ae2390c44 | ||
|
|
3cffe1449c | ||
|
|
868e5b831b | ||
|
|
d905886f4c | ||
|
|
09a28c7a27 | ||
|
|
8e4303af9f | ||
|
|
236f9630e0 | ||
|
|
15f1405fff | ||
|
|
4151737e17 | ||
|
|
f19ebab441 | ||
|
|
e2d6faa8a9 | ||
|
|
fb0d2ec3d6 | ||
|
|
d53ef1f7c2 | ||
|
|
9f6b147d6f | ||
|
|
e48c653245 | ||
|
|
a03c68fc83 | ||
|
|
b1bcccb04b | ||
|
|
a1f5dc27f2 | ||
|
|
0b215366b1 | ||
|
|
4304ff0d62 | ||
|
|
deb5b072d9 | ||
|
|
8440d0814d | ||
|
|
cfd4b8f363 | ||
|
|
c9a206ca89 | ||
|
|
0d3a157525 | ||
|
|
5d1df1cb4c | ||
|
|
dec97fc126 | ||
|
|
366c50e28e | ||
|
|
36dfd06503 | ||
|
|
250c0d6691 | ||
|
|
94bf97b812 | ||
|
|
1239d7a881 | ||
|
|
9b2862ebb0 | ||
|
|
1be1bd9211 | ||
|
|
20418cdd68 | ||
|
|
41e1976c17 | ||
|
|
722dac86d5 | ||
|
|
751f9843b4 | ||
|
|
9bd5b3dda2 | ||
|
|
307f45f88f | ||
|
|
802fcc99ee | ||
|
|
2d68c9b81e | ||
|
|
42ef252043 | ||
|
|
99d94cc7e8 | ||
|
|
02b128a128 | ||
|
|
372d4a046c | ||
|
|
a724dc659b | ||
|
|
907ee79715 | ||
|
|
c1471fe873 | ||
|
|
94829e35be | ||
|
|
19c6a5e6ee | ||
|
|
875c8d4c01 | ||
|
|
8d994ae30d | ||
|
|
be03a976d5 | ||
|
|
74448e9344 | ||
|
|
33583792fa | ||
|
|
430f9414a9 | ||
|
|
5636b55507 | ||
|
|
4bd9f4dac4 | ||
|
|
2d45b0b07a | ||
|
|
a0775f42ba | ||
|
|
cce23c86c7 | ||
|
|
68359086ff | ||
|
|
5940ee92ab | ||
|
|
9568f9d5b0 | ||
|
|
2f89a10f50 | ||
|
|
8851141dcf | ||
|
|
832941279b | ||
|
|
83ea820525 | ||
|
|
3eaf35f1e2 | ||
|
|
32e85e9a23 | ||
|
|
95c0c4a708 | ||
|
|
03b20d972c | ||
|
|
7fa3455dc6 | ||
|
|
552bfa5eb7 | ||
|
|
0ec0d79c35 | ||
|
|
4b870ef940 | ||
|
|
bf30226c69 | ||
|
|
f510f4bddf | ||
|
|
9c15fd354f | ||
|
|
726f3ce8b3 | ||
|
|
f40e04401f | ||
|
|
398bd4a2cd | ||
|
|
a024aaf59d | ||
|
|
261d063b10 | ||
|
|
a9e01ade4c | ||
|
|
5c7fc07ccf | ||
|
|
6dc8b66760 | ||
|
|
b39507c5af | ||
|
|
590ec375ec | ||
|
|
523cdc578d | ||
|
|
e0a5b1229f | ||
|
|
6253acf335 | ||
|
|
a708504d5b | ||
|
|
c443db0618 | ||
|
|
2d31598484 | ||
|
|
6b29d159a2 | ||
|
|
88ceaa38d5 | ||
|
|
e7db97df87 | ||
|
|
4a8e35289c | ||
|
|
58626c3197 | ||
|
|
56fb667e15 | ||
|
|
0153c04af3 | ||
|
|
db938dcc0e | ||
|
|
0e30621355 | ||
|
|
16b2539f72 | ||
|
|
b6afba0d64 | ||
|
|
b24d9dddc3 | ||
|
|
9996730fb1 | ||
|
|
2c502e6f8b | ||
|
|
f01aa1295f | ||
|
|
a26a78e84e | ||
|
|
5ca1b49e2e | ||
|
|
66953435c9 | ||
|
|
dde0bf0821 | ||
|
|
142bc33545 | ||
|
|
a339de80f4 | ||
|
|
d663f7981a | ||
|
|
72637b2cf6 | ||
|
|
d0ea5958f9 | ||
|
|
4805fb4b88 | ||
|
|
11b820c0e4 | ||
|
|
b21b66c0c0 | ||
|
|
218e15c9d4 | ||
|
|
a6c08a2e25 | ||
|
|
68dc678eed | ||
|
|
5f3be9b1cf | ||
|
|
2ae187b1d6 | ||
|
|
b4b5c44750 | ||
|
|
a29a99fb6f | ||
|
|
0f4c5c2305 | ||
|
|
60c6cc5f2a | ||
|
|
00e28592b6 | ||
|
|
b92df1b71c | ||
|
|
baf0d3343a | ||
|
|
cc3896d5d4 | ||
|
|
b94e268f83 | ||
|
|
b9177948d3 | ||
|
|
3b2e9e49be | ||
|
|
0efc02d6ee | ||
|
|
967a1830e6 | ||
|
|
90ffe2aac0 | ||
|
|
b342f40c2b | ||
|
|
83e37acc8b | ||
|
|
d1f3a2deef | ||
|
|
c9813a44d7 | ||
|
|
b98f541b91 | ||
|
|
50a1f6340f | ||
|
|
8c4ff5cb63 | ||
|
|
290f5b8ce7 | ||
|
|
48c5731a6b | ||
|
|
749f758adb | ||
|
|
2e8a8dfed5 | ||
|
|
25d1f6ec75 | ||
|
|
4c4b63437f | ||
|
|
02c7eca6da | ||
|
|
b72dfc0c08 | ||
|
|
7334fc3066 | ||
|
|
4e5740615c | ||
|
|
5a1d031f07 | ||
|
|
4715b2b12c | ||
|
|
5beaae3b65 | ||
|
|
dfd1cceb9b | ||
|
|
192f0f60da | ||
|
|
b395b72d1b | ||
|
|
a2dbf2fe4c | ||
|
|
86fe5ad362 | ||
|
|
89ae874f89 | ||
|
|
2a217189a6 | ||
|
|
481fb8413b | ||
|
|
d97a15861b | ||
|
|
a15fe57225 | ||
|
|
6c5a8423b8 | ||
|
|
a66500ea38 | ||
|
|
49cdfcec06 | ||
|
|
b50a71ff4e | ||
|
|
b37be61807 | ||
|
|
4a3fd19c93 | ||
|
|
66ba0b5276 | ||
|
|
61c2209110 | ||
|
|
694746409f | ||
|
|
1c258c0a2c | ||
|
|
6c4b3c08a7 | ||
|
|
c2a8ce59ae | ||
|
|
5bf4ad1f52 | ||
|
|
ae08dc6bea | ||
|
|
f89051cc2a | ||
|
|
74b22a596e | ||
|
|
e2d95b3719 | ||
|
|
dca274085d | ||
|
|
619e273ae5 | ||
|
|
baa563f359 | ||
|
|
64f2ddfa80 | ||
|
|
a5f23b5314 | ||
|
|
af38c30c9c | ||
|
|
887871833d | ||
|
|
5d07b70269 | ||
|
|
c43602c908 | ||
|
|
4d24eb83a8 | ||
|
|
7edceec8ac | ||
|
|
c6baa82ee4 | ||
|
|
e75bafa439 | ||
|
|
cc83e9ba52 | ||
|
|
5d355044c4 | ||
|
|
1f95ac9640 | ||
|
|
5928fae89e | ||
|
|
0bf2537a55 | ||
|
|
fd1cef3fa0 | ||
|
|
fee9c86233 | ||
|
|
5cc317408c | ||
|
|
2900d5122c | ||
|
|
c7a8d1cb7a | ||
|
|
0ba508ee2d | ||
|
|
1d37e94e17 | ||
|
|
843e156a51 | ||
|
|
ce88098ba6 | ||
|
|
1cde7f9b54 | ||
|
|
e42a222c5d | ||
|
|
87022782fb | ||
|
|
a58c652443 | ||
|
|
0ce1684ba6 | ||
|
|
af1e94be5a | ||
|
|
e88eac65da | ||
|
|
874c59012a | ||
|
|
b2977ad6a9 | ||
|
|
a435036a1e | ||
|
|
3a8e1d7a70 | ||
|
|
1210c04f14 | ||
|
|
6e2b6a0817 | ||
|
|
9a122626b9 | ||
|
|
d0629e62ea | ||
|
|
b8ce13f96a | ||
|
|
6db2858825 | ||
|
|
4d14423a21 | ||
|
|
959e943de8 | ||
|
|
9e42f6ed08 | ||
|
|
bd20b50879 | ||
|
|
28bb0eb6ac | ||
|
|
074fef773b | ||
|
|
3aeb62cf7e | ||
|
|
8f5b8558d2 | ||
|
|
47a5b7e3ba | ||
|
|
cdc8b94823 | ||
|
|
fedf970284 | ||
|
|
965027ce52 | ||
|
|
1cde095dc2 | ||
|
|
39a01190d5 | ||
|
|
9539f21390 | ||
|
|
9abef4c0bd | ||
|
|
146fce3878 | ||
|
|
904dc11b03 | ||
|
|
7c6c3efb0f | ||
|
|
c87b5d6a78 | ||
|
|
9a71b18b85 | ||
|
|
6da5de6b19 | ||
|
|
147f58bdbc | ||
|
|
fe17c873c0 | ||
|
|
613a8f5e88 | ||
|
|
f04e8259a9 | ||
|
|
828c00b758 | ||
|
|
1d47acddfd | ||
|
|
726afb8b95 | ||
|
|
a1d016d031 | ||
|
|
91f6be6b3b | ||
|
|
97cce82e5a | ||
|
|
622fc72dc1 | ||
|
|
42c63cb6dd | ||
|
|
31e320d0a7 | ||
|
|
7f9dadd681 | ||
|
|
ad40618897 | ||
|
|
e0edc1b7ec | ||
|
|
78a55c3823 | ||
|
|
9cafcf1caf | ||
|
|
8c6d28ce95 | ||
|
|
30ba7e9305 | ||
|
|
a6700e3172 | ||
|
|
0bd8134e5f | ||
|
|
ff5810d78f | ||
|
|
21ff3acf93 | ||
|
|
cebc6f9a20 | ||
|
|
749c4e39e0 | ||
|
|
6a1aa8e6b6 | ||
|
|
bdd4d01dc7 | ||
|
|
6a40e1964b | ||
|
|
82ef8b185a | ||
|
|
dd4f9d35ae | ||
|
|
127d49e837 | ||
|
|
a85fad98c0 | ||
|
|
1c3c69aaad | ||
|
|
3de8c2a651 | ||
|
|
803c39e585 | ||
|
|
51980e212f | ||
|
|
3df343495e | ||
|
|
45aeb5b003 | ||
|
|
67ee3b0488 | ||
|
|
2eba47a716 | ||
|
|
51f90ffafb | ||
|
|
8d6b6c358a | ||
|
|
9da4f93ae5 | ||
|
|
3cf61a54b7 | ||
|
|
43d457aa77 | ||
|
|
499f6c2fad | ||
|
|
9cd71642fb | ||
|
|
7a7329fa19 | ||
|
|
9ce047980a | ||
|
|
02abaf57bd | ||
|
|
3d7b53daf1 | ||
|
|
e99abfacfd | ||
|
|
a2ee328bc0 | ||
|
|
34cba3accf | ||
|
|
3712a45399 | ||
|
|
182e9b29e4 | ||
|
|
2540f1f1e8 | ||
|
|
eea1b0d8c5 | ||
|
|
6e1bcfdb2a | ||
|
|
d7e4d93190 | ||
|
|
0c4e332811 | ||
|
|
46ce09d36d | ||
|
|
269f15f9ee | ||
|
|
21e841fd13 | ||
|
|
a03db04ff4 | ||
|
|
d0bcc13059 | ||
|
|
17425e7337 | ||
|
|
7bdb63376c | ||
|
|
bdbc46fc84 | ||
|
|
0d43d2988a | ||
|
|
79924108c7 | ||
|
|
c033905b16 | ||
|
|
839c523048 | ||
|
|
7c81f23a07 | ||
|
|
e534a43d1a | ||
|
|
fcd1a98201 | ||
|
|
dc606eac7d | ||
|
|
372c96d9fd | ||
|
|
5a9f90dc30 | ||
|
|
ce0a6a1814 | ||
|
|
fa5acdf674 | ||
|
|
31db3b7034 | ||
|
|
30938260d4 | ||
|
|
7bd06a4d35 | ||
|
|
ace9d2383d | ||
|
|
ed0c024209 | ||
|
|
714cb17dcb | ||
|
|
6fb51d5422 | ||
|
|
4b5b9f164f | ||
|
|
3d9d5607bd | ||
|
|
aa417eec15 | ||
|
|
844ec79f01 | ||
|
|
f0b9ef2716 | ||
|
|
8857302347 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,7 +6,8 @@ dist*/
|
||||
/venv*/
|
||||
/kgs/
|
||||
/.tox/
|
||||
/releases/
|
||||
/releases*/
|
||||
/log*
|
||||
letsencrypt.log
|
||||
certbot.log
|
||||
letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64
|
||||
@@ -38,6 +39,8 @@ tests/letstest/venv/
|
||||
|
||||
# pytest cache
|
||||
.cache
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
|
||||
# docker files
|
||||
.docker
|
||||
|
||||
@@ -41,7 +41,7 @@ load-plugins=linter_plugin
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
|
||||
disable=fixme,locally-disabled,locally-enabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
|
||||
# abstract-class-not-used cannot be disabled locally (at least in
|
||||
# pylint 1.4.1), same for abstract-class-little-used
|
||||
|
||||
|
||||
28
.travis.yml
28
.travis.yml
@@ -13,15 +13,15 @@ before_script:
|
||||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27_install BOULDER_INTEGRATION=v1
|
||||
env: BOULDER_INTEGRATION=v1 INTEGRATION_TEST=all TOXENV=py27_install
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27_install BOULDER_INTEGRATION=v2
|
||||
env: BOULDER_INTEGRATION=v2 INTEGRATION_TEST=all TOXENV=py27_install
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "2.7"
|
||||
env: TOXENV=cover FYI="this also tests py27"
|
||||
env: TOXENV=py27-cover FYI="py27 tests + code coverage"
|
||||
- sudo: required
|
||||
env: TOXENV=nginx_compat
|
||||
services: docker
|
||||
@@ -29,16 +29,21 @@ matrix:
|
||||
addons:
|
||||
- python: "2.7"
|
||||
env: TOXENV=lint
|
||||
- python: "3.4"
|
||||
env: TOXENV=mypy
|
||||
- python: "3.5"
|
||||
env: TOXENV=mypy
|
||||
- python: "2.7"
|
||||
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
|
||||
env: TOXENV='py27-{acme,apache,certbot,dns,nginx,postfix}-oldest'
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.4"
|
||||
env: TOXENV=py34
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.7"
|
||||
dist: xenial
|
||||
env: TOXENV=py37
|
||||
sudo: required
|
||||
services: docker
|
||||
- sudo: required
|
||||
@@ -73,8 +78,6 @@ sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- augeas
|
||||
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
|
||||
- python-dev
|
||||
- python-virtualenv
|
||||
@@ -86,23 +89,20 @@ addons:
|
||||
# For certbot-nginx integration testing
|
||||
- nginx-light
|
||||
- openssl
|
||||
# for apacheconftest
|
||||
- apache2
|
||||
- libapache2-mod-wsgi
|
||||
- libapache2-mod-macro
|
||||
|
||||
install: "travis_retry $(command -v pip || command -v pip3) install tox coveralls"
|
||||
install: "travis_retry $(command -v pip || command -v pip3) install codecov tox"
|
||||
script:
|
||||
- travis_retry tox
|
||||
- '[ -z "${BOULDER_INTEGRATION+x}" ] || (travis_retry tests/boulder-fetch.sh && tests/tox-boulder-integration.sh)'
|
||||
|
||||
after_success: '[ "$TOXENV" == "cover" ] && coveralls'
|
||||
after_success: '[ "$TOXENV" == "py27-cover" ] && codecov'
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
irc:
|
||||
channels:
|
||||
- secure: "SGWZl3ownKx9xKVV2VnGt7DqkTmutJ89oJV9tjKhSs84kLijU6EYdPnllqISpfHMTxXflNZuxtGo0wTDYHXBuZL47w1O32W6nzuXdra5zC+i4sYQwYULUsyfOv9gJX8zWAULiK0Z3r0oho45U+FR5ZN6TPCidi8/eGU+EEPwaAw="
|
||||
on_cancel: never
|
||||
on_success: never
|
||||
on_failure: always
|
||||
use_notice: true
|
||||
|
||||
428
CHANGELOG.md
428
CHANGELOG.md
@@ -2,6 +2,432 @@
|
||||
|
||||
Certbot adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 0.30.0 - master
|
||||
|
||||
### Added
|
||||
|
||||
*
|
||||
|
||||
### Changed
|
||||
|
||||
*
|
||||
|
||||
### Fixed
|
||||
|
||||
*
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
package with changes other than its version number was:
|
||||
|
||||
*
|
||||
|
||||
More details about these changes can be found on our GitHub repo.
|
||||
|
||||
## 0.29.1 - 2018-12-05
|
||||
|
||||
### Added
|
||||
|
||||
*
|
||||
|
||||
### Changed
|
||||
|
||||
*
|
||||
|
||||
### Fixed
|
||||
|
||||
* The default work and log directories have been changed back to
|
||||
/var/lib/letsencrypt and /var/log/letsencrypt respectively.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
package with changes other than its version number was:
|
||||
|
||||
* certbot
|
||||
|
||||
More details about these changes can be found on our GitHub repo.
|
||||
|
||||
## 0.29.0 - 2018-12-05
|
||||
|
||||
### Added
|
||||
|
||||
* Noninteractive renewals with `certbot renew` (those not started from a
|
||||
terminal) now randomly sleep 1-480 seconds before beginning work in
|
||||
order to spread out load spikes on the server side.
|
||||
* Added External Account Binding support in cli and acme library.
|
||||
Command line arguments --eab-kid and --eab-hmac-key added.
|
||||
|
||||
### Changed
|
||||
|
||||
* Private key permissioning changes: Renewal preserves existing group mode
|
||||
& gid of previous private key material. Private keys for new
|
||||
lineages (i.e. new certs, not renewed) default to 0o600.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Update code and dependencies to clean up Resource and Deprecation Warnings.
|
||||
* Only depend on imgconverter extension for Sphinx >= 1.6
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
package with changes other than its version number was:
|
||||
|
||||
* acme
|
||||
* certbot
|
||||
* certbot-apache
|
||||
* certbot-dns-cloudflare
|
||||
* certbot-dns-digitalocean
|
||||
* certbot-dns-google
|
||||
* certbot-nginx
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/62?closed=1
|
||||
|
||||
## 0.28.0 - 2018-11-7
|
||||
|
||||
### Added
|
||||
|
||||
* `revoke` accepts `--cert-name`, and doesn't accept both `--cert-name` and `--cert-path`.
|
||||
* Use the ACMEv2 newNonce endpoint when a new nonce is needed, and newNonce is available in the directory.
|
||||
|
||||
### Changed
|
||||
|
||||
* Removed documentation mentions of `#letsencrypt` IRC on Freenode.
|
||||
* Write README to the base of (config-dir)/live directory
|
||||
* `--manual` will explicitly warn users that earlier challenges should remain in place when setting up subsequent challenges.
|
||||
* Warn when using deprecated acme.challenges.TLSSNI01
|
||||
* Log warning about TLS-SNI deprecation in Certbot
|
||||
* Stop preferring TLS-SNI in the Apache, Nginx, and standalone plugins
|
||||
* OVH DNS plugin now relies on Lexicon>=2.7.14 to support HTTP proxies
|
||||
* Default time the Linode plugin waits for DNS changes to propogate is now 1200 seconds.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Match Nginx parser update in allowing variable names to start with `${`.
|
||||
* Fix ranking of vhosts in Nginx so that all port-matching vhosts come first
|
||||
* Correct OVH integration tests on machines without internet access.
|
||||
* Stop caching the results of ipv6_info in http01.py
|
||||
* Test fix for Route53 plugin to prevent boto3 making outgoing connections.
|
||||
* The grammar used by Augeas parser in Apache plugin was updated to fix various parsing errors.
|
||||
* The CloudXNS, DNSimple, DNS Made Easy, Gehirn, Linode, LuaDNS, NS1, OVH, and
|
||||
Sakura Cloud DNS plugins are now compatible with Lexicon 3.0+.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
package with changes other than its version number was:
|
||||
|
||||
* acme
|
||||
* certbot
|
||||
* certbot-apache
|
||||
* certbot-dns-cloudxns
|
||||
* certbot-dns-dnsimple
|
||||
* certbot-dns-dnsmadeeasy
|
||||
* certbot-dns-gehirn
|
||||
* certbot-dns-linode
|
||||
* certbot-dns-luadns
|
||||
* certbot-dns-nsone
|
||||
* certbot-dns-ovh
|
||||
* certbot-dns-route53
|
||||
* certbot-dns-sakuracloud
|
||||
* certbot-nginx
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/59?closed=1
|
||||
|
||||
## 0.27.1 - 2018-09-06
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed parameter name in OpenSUSE overrides for default parameters in the
|
||||
Apache plugin. Certbot on OpenSUSE works again.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
package with changes other than its version number was:
|
||||
|
||||
* certbot-apache
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/60?closed=1
|
||||
|
||||
## 0.27.0 - 2018-09-05
|
||||
|
||||
### Added
|
||||
|
||||
* The Apache plugin now accepts the parameter --apache-ctl which can be
|
||||
used to configure the path to the Apache control script.
|
||||
|
||||
### Changed
|
||||
|
||||
* When using `acme.client.ClientV2` (or
|
||||
`acme.client.BackwardsCompatibleClientV2` with an ACME server that supports a
|
||||
newer version of the ACME protocol), an `acme.errors.ConflictError` will be
|
||||
raised if you try to create an ACME account with a key that has already been
|
||||
used. Previously, a JSON parsing error was raised in this scenario when using
|
||||
the library with Let's Encrypt's ACMEv2 endpoint.
|
||||
|
||||
### Fixed
|
||||
|
||||
* When Apache is not installed, Certbot's Apache plugin no longer prints
|
||||
messages about being unable to find apachectl to the terminal when the plugin
|
||||
is not selected.
|
||||
* If you're using the Apache plugin with the --apache-vhost-root flag set to a
|
||||
directory containing a disabled virtual host for the domain you're requesting
|
||||
a certificate for, the virtual host will now be temporarily enabled if
|
||||
necessary to pass the HTTP challenge.
|
||||
* The documentation for the Certbot package can now be built using Sphinx 1.6+.
|
||||
* You can now call `query_registration` without having to first call
|
||||
`new_account` on `acme.client.ClientV2` objects.
|
||||
* The requirement of `setuptools>=1.0` has been removed from `certbot-dns-ovh`.
|
||||
* Names in certbot-dns-sakuracloud's tests have been updated to refer to Sakura
|
||||
Cloud rather than NS1 whose plugin certbot-dns-sakuracloud was based on.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
package with changes other than its version number was:
|
||||
|
||||
* acme
|
||||
* certbot
|
||||
* certbot-apache
|
||||
* certbot-dns-ovh
|
||||
* certbot-dns-sakuracloud
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/57?closed=1
|
||||
|
||||
## 0.26.1 - 2018-07-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix a bug that was triggered when users who had previously manually set `--server` to get ACMEv2 certs tried to renew ACMEv1 certs.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was:
|
||||
|
||||
* certbot
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/58?closed=1
|
||||
|
||||
## 0.26.0 - 2018-07-11
|
||||
|
||||
### Added
|
||||
|
||||
* A new security enhancement which we're calling AutoHSTS has been added to
|
||||
Certbot's Apache plugin. This enhancement configures your webserver to send a
|
||||
HTTP Strict Transport Security header with a low max-age value that is slowly
|
||||
increased over time. The max-age value is not increased to a large value
|
||||
until you've successfully managed to renew your certificate. This enhancement
|
||||
can be requested with the --auto-hsts flag.
|
||||
* New official DNS plugins have been created for Gehirn Infrastracture Service,
|
||||
Linode, OVH, and Sakura Cloud. These plugins can be found on our Docker Hub
|
||||
page at https://hub.docker.com/u/certbot and on PyPI.
|
||||
* The ability to reuse ACME accounts from Let's Encrypt's ACMEv1 endpoint on
|
||||
Let's Encrypt's ACMEv2 endpoint has been added.
|
||||
* Certbot and its components now support Python 3.7.
|
||||
* Certbot's install subcommand now allows you to interactively choose which
|
||||
certificate to install from the list of certificates managed by Certbot.
|
||||
* Certbot now accepts the flag `--no-autorenew` which causes any obtained
|
||||
certificates to not be automatically renewed when it approaches expiration.
|
||||
* Support for parsing the TLS-ALPN-01 challenge has been added back to the acme
|
||||
library.
|
||||
|
||||
### Changed
|
||||
|
||||
* Certbot's default ACME server has been changed to Let's Encrypt's ACMEv2
|
||||
endpoint. By default, this server will now be used for both new certificate
|
||||
lineages and renewals.
|
||||
* The Nginx plugin is no longer marked labeled as an "Alpha" version.
|
||||
* The `prepare` method of Certbot's plugins is no longer called before running
|
||||
"Updater" enhancements that are run on every invocation of `certbot renew`.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
packages with functional changes were:
|
||||
|
||||
* acme
|
||||
* certbot
|
||||
* certbot-apache
|
||||
* certbot-dns-gehirn
|
||||
* certbot-dns-linode
|
||||
* certbot-dns-ovh
|
||||
* certbot-dns-sakuracloud
|
||||
* certbot-nginx
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/55?closed=1
|
||||
|
||||
## 0.25.1 - 2018-06-13
|
||||
|
||||
### Fixed
|
||||
|
||||
* TLS-ALPN-01 support has been removed from our acme library. Using our current
|
||||
dependencies, we are unable to provide a correct implementation of this
|
||||
challenge so we decided to remove it from the library until we can provide
|
||||
proper support.
|
||||
* Issues causing test failures when running the tests in the acme package with
|
||||
pytest<3.0 has been resolved.
|
||||
* certbot-nginx now correctly depends on acme>=0.25.0.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
packages with changes other than their version number were:
|
||||
|
||||
* acme
|
||||
* certbot-nginx
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/56?closed=1
|
||||
|
||||
## 0.25.0 - 2018-06-06
|
||||
|
||||
### Added
|
||||
|
||||
* Support for the ready status type was added to acme. Without this change,
|
||||
Certbot and acme users will begin encountering errors when using Let's
|
||||
Encrypt's ACMEv2 API starting on June 19th for the staging environment and
|
||||
July 5th for production. See
|
||||
https://community.letsencrypt.org/t/acmev2-order-ready-status/62866 for more
|
||||
information.
|
||||
* Certbot now accepts the flag --reuse-key which will cause the same key to be
|
||||
used in the certificate when the lineage is renewed rather than generating a
|
||||
new key.
|
||||
* You can now add multiple email addresses to your ACME account with Certbot by
|
||||
providing a comma separated list of emails to the --email flag.
|
||||
* Support for Let's Encrypt's upcoming TLS-ALPN-01 challenge was added to acme.
|
||||
For more information, see
|
||||
https://community.letsencrypt.org/t/tls-alpn-validation-method/63814/1.
|
||||
* acme now supports specifying the source address to bind to when sending
|
||||
outgoing connections. You still cannot specify this address using Certbot.
|
||||
* If you run Certbot against Let's Encrypt's ACMEv2 staging server but don't
|
||||
already have an account registered at that server URL, Certbot will
|
||||
automatically reuse your staging account from Let's Encrypt's ACMEv1 endpoint
|
||||
if it exists.
|
||||
* Interfaces were added to Certbot allowing plugins to be called at additional
|
||||
points. The `GenericUpdater` interface allows plugins to perform actions
|
||||
every time `certbot renew` is run, regardless of whether any certificates are
|
||||
due for renewal, and the `RenewDeployer` interface allows plugins to perform
|
||||
actions when a certificate is renewed. See `certbot.interfaces` for more
|
||||
information.
|
||||
|
||||
### Changed
|
||||
|
||||
* When running Certbot with --dry-run and you don't already have a staging
|
||||
account, the created account does not contain an email address even if one
|
||||
was provided to avoid expiration emails from Let's Encrypt's staging server.
|
||||
* certbot-nginx does a better job of automatically detecting the location of
|
||||
Nginx's configuration files when run on BSD based systems.
|
||||
* acme now requires and uses pytest when running tests with setuptools with
|
||||
`python setup.py test`.
|
||||
* `certbot config_changes` no longer waits for user input before exiting.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Misleading log output that caused users to think that Certbot's standalone
|
||||
plugin failed to bind to a port when performing a challenge has been
|
||||
corrected.
|
||||
* An issue where certbot-nginx would fail to enable HSTS if the server block
|
||||
already had an `add_header` directive has been resolved.
|
||||
* certbot-nginx now does a better job detecting the server block to base the
|
||||
configuration for TLS-SNI challenges on.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
packages with functional changes were:
|
||||
|
||||
* acme
|
||||
* certbot
|
||||
* certbot-apache
|
||||
* certbot-nginx
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/54?closed=1
|
||||
|
||||
## 0.24.0 - 2018-05-02
|
||||
|
||||
### Added
|
||||
|
||||
* certbot now has an enhance subcommand which allows you to configure security
|
||||
enhancements like HTTP to HTTPS redirects, OCSP stapling, and HSTS without
|
||||
reinstalling a certificate.
|
||||
* certbot-dns-rfc2136 now allows the user to specify the port to use to reach
|
||||
the DNS server in its credentials file.
|
||||
* acme now parses the wildcard field included in authorizations so it can be
|
||||
used by users of the library.
|
||||
|
||||
### Changed
|
||||
|
||||
* certbot-dns-route53 used to wait for each DNS update to propagate before
|
||||
sending the next one, but now it sends all updates before waiting which
|
||||
speeds up issuance for multiple domains dramatically.
|
||||
* Certbot's official Docker images are now based on Alpine Linux 3.7 rather
|
||||
than 3.4 because 3.4 has reached its end-of-life.
|
||||
* We've doubled the time Certbot will spend polling authorizations before
|
||||
timing out.
|
||||
* The level of the message logged when Certbot is being used with
|
||||
non-standard paths warning that crontabs for renewal included in Certbot
|
||||
packages from OS package managers may not work has been reduced. This stops
|
||||
the message from being written to stderr every time `certbot renew` runs.
|
||||
|
||||
### Fixed
|
||||
|
||||
* certbot-auto now works with Python 3.6.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
packages with changes other than their version number were:
|
||||
|
||||
* acme
|
||||
* certbot
|
||||
* certbot-apache
|
||||
* certbot-dns-digitalocean (only style improvements to tests)
|
||||
* certbot-dns-rfc2136
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/52?closed=1
|
||||
|
||||
## 0.23.0 - 2018-04-04
|
||||
|
||||
### Added
|
||||
|
||||
* Support for OpenResty was added to the Nginx plugin.
|
||||
|
||||
### Changed
|
||||
|
||||
* The timestamps in Certbot's logfiles now use the system's local time zone
|
||||
rather than UTC.
|
||||
* Certbot's DNS plugins that use Lexicon now rely on Lexicon>=2.2.1 to be able
|
||||
to create and delete multiple TXT records on a single domain.
|
||||
* certbot-dns-google's test suite now works without an internet connection.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Removed a small window that if during which an error occurred, Certbot
|
||||
wouldn't clean up performed challenges.
|
||||
* The parameters `default` and `ipv6only` are now removed from `listen`
|
||||
directives when creating a new server block in the Nginx plugin.
|
||||
* `server_name` directives enclosed in quotation marks in Nginx are now properly
|
||||
supported.
|
||||
* Resolved an issue preventing the Apache plugin from starting Apache when it's
|
||||
not currently running on RHEL and Gentoo based systems.
|
||||
|
||||
Despite us having broken lockstep, we are continuing to release new versions of
|
||||
all Certbot components during releases for the time being, however, the only
|
||||
packages with changes other than their version number were:
|
||||
|
||||
* certbot
|
||||
* certbot-apache
|
||||
* certbot-dns-cloudxns
|
||||
* certbot-dns-dnsimple
|
||||
* certbot-dns-dnsmadeeasy
|
||||
* certbot-dns-google
|
||||
* certbot-dns-luadns
|
||||
* certbot-dns-nsone
|
||||
* certbot-dns-rfc2136
|
||||
* certbot-nginx
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/50?closed=1
|
||||
|
||||
## 0.22.2 - 2018-03-19
|
||||
|
||||
### Fixed
|
||||
@@ -563,7 +989,7 @@ https://github.com/certbot/certbot/pulls?q=is%3Apr%20milestone%3A0.11.1%20is%3Ac
|
||||
|
||||
### Added
|
||||
|
||||
* When using the standalone plugin while running Certbot interactively
|
||||
* When using the standalone plugin while running Certbot interactively
|
||||
and a required port is bound by another process, Certbot will give you
|
||||
the option to retry to grab the port rather than immediately exiting.
|
||||
* You are now able to deactivate your account with the Let's Encrypt
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
To see the changes in a given release, view the issues closed in a given
|
||||
release's GitHub milestone:
|
||||
|
||||
- `Past releases <https://github.com/certbot/certbot/milestones?state=closed>`_
|
||||
- `Upcoming releases <https://github.com/certbot/certbot/milestones>`_
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM python:2-alpine
|
||||
FROM python:2-alpine3.7
|
||||
|
||||
ENTRYPOINT [ "certbot" ]
|
||||
EXPOSE 80 443
|
||||
VOLUME /etc/letsencrypt /var/lib/letsencrypt
|
||||
WORKDIR /opt/certbot
|
||||
|
||||
COPY CHANGES.rst README.rst setup.py src/
|
||||
COPY CHANGELOG.md README.rst setup.py src/
|
||||
COPY acme src/acme
|
||||
COPY certbot src/certbot
|
||||
|
||||
|
||||
@@ -16,6 +16,6 @@ RUN apt-get update && \
|
||||
/tmp/* \
|
||||
/var/tmp/*
|
||||
|
||||
RUN VENV_NAME="../venv" tools/venv.sh
|
||||
RUN VENV_NAME="../venv" python tools/venv.py
|
||||
|
||||
ENV PATH /opt/certbot/venv/bin:$PATH
|
||||
|
||||
@@ -34,7 +34,7 @@ RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only
|
||||
# Dockerfile we make sure we cache as much as possible
|
||||
|
||||
|
||||
COPY setup.py README.rst CHANGES.rst MANIFEST.in letsencrypt-auto-source/pieces/pipstrap.py /opt/certbot/src/
|
||||
COPY setup.py README.rst CHANGELOG.md MANIFEST.in letsencrypt-auto-source/pieces/pipstrap.py /opt/certbot/src/
|
||||
|
||||
# all above files are necessary for setup.py and venv setup, however,
|
||||
# package source code directory has to be copied separately to a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
include README.rst
|
||||
include CHANGES.rst
|
||||
include CHANGELOG.md
|
||||
include CONTRIBUTING.md
|
||||
include LICENSE.txt
|
||||
include linter_plugin.py
|
||||
|
||||
10
README.rst
10
README.rst
@@ -6,7 +6,7 @@ Anyone who has gone through the trouble of setting up a secure website knows wha
|
||||
|
||||
How you use Certbot depends on the configuration of your web server. The best way to get started is to use our `interactive guide <https://certbot.eff.org>`_. It generates instructions based on your configuration settings. In most cases, you’ll need `root or administrator access <https://certbot.eff.org/faq/#does-certbot-require-root-administrator-privileges>`_ to your web server to run Certbot.
|
||||
|
||||
If you’re using a hosted service and don’t have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Let’s Encrypt.
|
||||
Certbot is meant to be run directly on your web server, not on your personal computer. If you’re using a hosted service and don’t have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Let’s Encrypt.
|
||||
|
||||
Certbot is a fully-featured, extensible client for the Let's
|
||||
Encrypt CA (or any other CA that speaks the `ACME
|
||||
@@ -91,8 +91,6 @@ Main Website: https://certbot.eff.org
|
||||
|
||||
Let's Encrypt Website: https://letsencrypt.org
|
||||
|
||||
IRC Channel: #letsencrypt on `Freenode`_
|
||||
|
||||
Community: https://community.letsencrypt.org
|
||||
|
||||
ACME spec: http://ietf-wg-acme.github.io/acme/
|
||||
@@ -101,14 +99,12 @@ ACME working area in github: https://github.com/ietf-wg-acme/acme
|
||||
|
||||
|build-status| |coverage| |docs| |container|
|
||||
|
||||
.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt
|
||||
|
||||
.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master
|
||||
:target: https://travis-ci.org/certbot/certbot
|
||||
:alt: Travis CI status
|
||||
|
||||
.. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/certbot/certbot
|
||||
.. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/certbot/certbot
|
||||
:alt: Coverage status
|
||||
|
||||
.. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
include pytest.ini
|
||||
recursive-include docs *
|
||||
recursive-include examples *
|
||||
recursive-include acme/testdata *
|
||||
|
||||
@@ -4,11 +4,13 @@ import functools
|
||||
import hashlib
|
||||
import logging
|
||||
import socket
|
||||
import warnings
|
||||
|
||||
from cryptography.hazmat.primitives import hashes # type: ignore
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
import requests
|
||||
import six
|
||||
|
||||
from acme import errors
|
||||
from acme import crypto_util
|
||||
@@ -139,16 +141,16 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
|
||||
return True
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class KeyAuthorizationChallenge(_TokenChallenge):
|
||||
# pylint: disable=abstract-class-little-used,too-many-ancestors
|
||||
"""Challenge based on Key Authorization.
|
||||
|
||||
:param response_cls: Subclass of `KeyAuthorizationChallengeResponse`
|
||||
that will be used to generate `response`.
|
||||
|
||||
:param str typ: type of the challenge
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
typ = NotImplemented
|
||||
response_cls = NotImplemented
|
||||
thumbprint_hash_function = (
|
||||
KeyAuthorizationChallengeResponse.thumbprint_hash_function)
|
||||
@@ -477,7 +479,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse):
|
||||
try:
|
||||
cert = self.probe_cert(domain=domain, **kwargs)
|
||||
except errors.Error as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
logger.debug(str(error), exc_info=True)
|
||||
return False
|
||||
|
||||
return self.verify_cert(cert)
|
||||
@@ -492,6 +494,11 @@ class TLSSNI01(KeyAuthorizationChallenge):
|
||||
# boulder#962, ietf-wg-acme#22
|
||||
#n = jose.Field("n", encoder=int, decoder=int)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn("TLS-SNI-01 is deprecated, and will stop working soon.",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
super(TLSSNI01, self).__init__(*args, **kwargs)
|
||||
|
||||
def validation(self, account_key, **kwargs):
|
||||
"""Generate validation.
|
||||
|
||||
@@ -506,6 +513,21 @@ class TLSSNI01(KeyAuthorizationChallenge):
|
||||
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
|
||||
|
||||
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class TLSALPN01(KeyAuthorizationChallenge):
|
||||
"""ACME tls-alpn-01 challenge.
|
||||
|
||||
This class simply allows parsing the TLS-ALPN-01 challenge returned from
|
||||
the CA. Full TLS-ALPN-01 support is not currently provided.
|
||||
|
||||
"""
|
||||
typ = "tls-alpn-01"
|
||||
|
||||
def validation(self, account_key, **kwargs):
|
||||
"""Generate validation for the challenge."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class DNS(_TokenChallenge):
|
||||
"""ACME "dns" challenge."""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for acme.challenges."""
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
import josepy as jose
|
||||
import mock
|
||||
@@ -360,20 +361,29 @@ class TLSSNI01ResponseTest(unittest.TestCase):
|
||||
class TLSSNI01Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import TLSSNI01
|
||||
self.msg = TLSSNI01(
|
||||
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
self.jmsg = {
|
||||
'type': 'tls-sni-01',
|
||||
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
|
||||
}
|
||||
|
||||
def _msg(self):
|
||||
from acme.challenges import TLSSNI01
|
||||
with warnings.catch_warnings(record=True) as warn:
|
||||
warnings.simplefilter("always")
|
||||
msg = TLSSNI01(
|
||||
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
assert warn is not None # using a raw assert for mypy
|
||||
self.assertTrue(len(warn) == 1)
|
||||
self.assertTrue(issubclass(warn[-1].category, DeprecationWarning))
|
||||
self.assertTrue('deprecated' in str(warn[-1].message))
|
||||
return msg
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
self.assertEqual(self.jmsg, self._msg().to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import TLSSNI01
|
||||
self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg))
|
||||
self.assertEqual(self._msg(), TLSSNI01.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import TLSSNI01
|
||||
@@ -388,11 +398,43 @@ class TLSSNI01Test(unittest.TestCase):
|
||||
@mock.patch('acme.challenges.TLSSNI01Response.gen_cert')
|
||||
def test_validation(self, mock_gen_cert):
|
||||
mock_gen_cert.return_value = ('cert', 'key')
|
||||
self.assertEqual(('cert', 'key'), self.msg.validation(
|
||||
self.assertEqual(('cert', 'key'), self._msg().validation(
|
||||
KEY, cert_key=mock.sentinel.cert_key))
|
||||
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
|
||||
|
||||
|
||||
class TLSALPN01Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import TLSALPN01
|
||||
self.msg = TLSALPN01(
|
||||
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
self.jmsg = {
|
||||
'type': 'tls-alpn-01',
|
||||
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import TLSALPN01
|
||||
self.assertEqual(self.msg, TLSALPN01.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import TLSALPN01
|
||||
hash(TLSALPN01.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_invalid_token_length(self):
|
||||
from acme.challenges import TLSALPN01
|
||||
self.jmsg['token'] = jose.encode_b64jose(b'abcd')
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, TLSALPN01.from_json, self.jmsg)
|
||||
|
||||
def test_validation(self):
|
||||
self.assertRaises(NotImplementedError, self.msg.validation, KEY)
|
||||
|
||||
|
||||
class DNSTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -9,17 +9,20 @@ import time
|
||||
|
||||
import six
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
import re
|
||||
from requests_toolbelt.adapters.source import SourceAddressAdapter
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
import sys
|
||||
|
||||
from acme import crypto_util
|
||||
from acme import errors
|
||||
from acme import jws
|
||||
from acme import messages
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Dict, List, Set, Text
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,6 +33,7 @@ logger = logging.getLogger(__name__)
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
if sys.version_info < (2, 7, 9): # pragma: no cover
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() # type: ignore
|
||||
except AttributeError:
|
||||
import urllib3.contrib.pyopenssl # pylint: disable=import-error
|
||||
@@ -47,7 +51,6 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
|
||||
:ivar .ClientNetwork net: Client network.
|
||||
:ivar int acme_version: ACME protocol version. 1 or 2.
|
||||
"""
|
||||
|
||||
def __init__(self, directory, net, acme_version):
|
||||
"""Initialize.
|
||||
|
||||
@@ -87,6 +90,8 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
"""
|
||||
kwargs.setdefault('acme_version', self.acme_version)
|
||||
if hasattr(self.directory, 'newNonce'):
|
||||
kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce'))
|
||||
return self.net.post(*args, **kwargs)
|
||||
|
||||
def update_registration(self, regr, update=None):
|
||||
@@ -195,22 +200,6 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
|
||||
|
||||
def poll(self, authzr):
|
||||
"""Poll Authorization Resource for status.
|
||||
|
||||
:param authzr: Authorization Resource
|
||||
:type authzr: `.AuthorizationResource`
|
||||
|
||||
:returns: Updated Authorization Resource and HTTP response.
|
||||
|
||||
:rtype: (`.AuthorizationResource`, `requests.Response`)
|
||||
|
||||
"""
|
||||
response = self.net.get(authzr.uri)
|
||||
updated_authzr = self._authzr_from_response(
|
||||
response, authzr.body.identifier, authzr.uri)
|
||||
return updated_authzr, response
|
||||
|
||||
def _revoke(self, cert, rsn, url):
|
||||
"""Revoke certificate.
|
||||
|
||||
@@ -232,6 +221,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
|
||||
raise errors.ClientError(
|
||||
'Successful revocation must return HTTP OK status')
|
||||
|
||||
|
||||
class Client(ClientBase):
|
||||
"""ACME client for a v1 API.
|
||||
|
||||
@@ -384,6 +374,22 @@ class Client(ClientBase):
|
||||
body=jose.ComparableX509(OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, response.content)))
|
||||
|
||||
def poll(self, authzr):
|
||||
"""Poll Authorization Resource for status.
|
||||
|
||||
:param authzr: Authorization Resource
|
||||
:type authzr: `.AuthorizationResource`
|
||||
|
||||
:returns: Updated Authorization Resource and HTTP response.
|
||||
|
||||
:rtype: (`.AuthorizationResource`, `requests.Response`)
|
||||
|
||||
"""
|
||||
response = self.net.get(authzr.uri)
|
||||
updated_authzr = self._authzr_from_response(
|
||||
response, authzr.body.identifier, authzr.uri)
|
||||
return updated_authzr, response
|
||||
|
||||
def poll_and_request_issuance(
|
||||
self, csr, authzrs, mintime=5, max_attempts=10):
|
||||
"""Poll and request issuance.
|
||||
@@ -415,7 +421,7 @@ class Client(ClientBase):
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
assert max_attempts > 0
|
||||
attempts = collections.defaultdict(int)
|
||||
attempts = collections.defaultdict(int) # type: Dict[messages.AuthorizationResource, int]
|
||||
exhausted = set()
|
||||
|
||||
# priority queue with datetime.datetime (based on Retry-After) as key,
|
||||
@@ -529,7 +535,7 @@ class Client(ClientBase):
|
||||
:rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
|
||||
|
||||
"""
|
||||
chain = []
|
||||
chain = [] # type: List[jose.ComparableX509]
|
||||
uri = certr.cert_chain_uri
|
||||
while uri is not None and len(chain) < max_length:
|
||||
response, cert = self._get_cert(uri)
|
||||
@@ -575,16 +581,57 @@ class ClientV2(ClientBase):
|
||||
|
||||
:param .NewRegistration new_account:
|
||||
|
||||
:raises .ConflictError: in case the account already exists
|
||||
|
||||
:returns: Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
"""
|
||||
response = self._post(self.directory['newAccount'], new_account)
|
||||
# if account already exists
|
||||
if response.status_code == 200 and 'Location' in response.headers:
|
||||
raise errors.ConflictError(response.headers.get('Location'))
|
||||
# "Instance of 'Field' has no key/contact member" bug:
|
||||
# pylint: disable=no-member
|
||||
regr = self._regr_from_response(response)
|
||||
self.net.account = regr
|
||||
return regr
|
||||
|
||||
def query_registration(self, regr):
|
||||
"""Query server about registration.
|
||||
|
||||
:param messages.RegistrationResource: Existing Registration
|
||||
Resource.
|
||||
|
||||
"""
|
||||
self.net.account = regr
|
||||
updated_regr = super(ClientV2, self).query_registration(regr)
|
||||
self.net.account = updated_regr
|
||||
return updated_regr
|
||||
|
||||
def update_registration(self, regr, update=None):
|
||||
"""Update registration.
|
||||
|
||||
:param messages.RegistrationResource regr: Registration Resource.
|
||||
:param messages.Registration update: Updated body of the
|
||||
resource. If not provided, body will be taken from `regr`.
|
||||
|
||||
:returns: Updated Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
# https://github.com/certbot/certbot/issues/6155
|
||||
new_regr = self._get_v2_account(regr)
|
||||
return super(ClientV2, self).update_registration(new_regr, update)
|
||||
|
||||
def _get_v2_account(self, regr):
|
||||
self.net.account = None
|
||||
only_existing_reg = regr.body.update(only_return_existing=True)
|
||||
response = self._post(self.directory['newAccount'], only_existing_reg)
|
||||
updated_uri = response.headers['Location']
|
||||
new_regr = regr.update(uri=updated_uri)
|
||||
self.net.account = new_regr
|
||||
return new_regr
|
||||
|
||||
def new_order(self, csr_pem):
|
||||
"""Request a new Order object from the server.
|
||||
|
||||
@@ -606,13 +653,29 @@ class ClientV2(ClientBase):
|
||||
body = messages.Order.from_json(response.json())
|
||||
authorizations = []
|
||||
for url in body.authorizations:
|
||||
authorizations.append(self._authzr_from_response(self.net.get(url), uri=url))
|
||||
authorizations.append(self._authzr_from_response(self._post_as_get(url), uri=url))
|
||||
return messages.OrderResource(
|
||||
body=body,
|
||||
uri=response.headers.get('Location'),
|
||||
authorizations=authorizations,
|
||||
csr_pem=csr_pem)
|
||||
|
||||
def poll(self, authzr):
|
||||
"""Poll Authorization Resource for status.
|
||||
|
||||
:param authzr: Authorization Resource
|
||||
:type authzr: `.AuthorizationResource`
|
||||
|
||||
:returns: Updated Authorization Resource and HTTP response.
|
||||
|
||||
:rtype: (`.AuthorizationResource`, `requests.Response`)
|
||||
|
||||
"""
|
||||
response = self._post_as_get(authzr.uri)
|
||||
updated_authzr = self._authzr_from_response(
|
||||
response, authzr.body.identifier, authzr.uri)
|
||||
return updated_authzr, response
|
||||
|
||||
def poll_and_finalize(self, orderr, deadline=None):
|
||||
"""Poll authorizations and finalize the order.
|
||||
|
||||
@@ -636,7 +699,7 @@ class ClientV2(ClientBase):
|
||||
responses = []
|
||||
for url in orderr.body.authorizations:
|
||||
while datetime.datetime.now() < deadline:
|
||||
authzr = self._authzr_from_response(self.net.get(url), uri=url)
|
||||
authzr = self._authzr_from_response(self._post_as_get(url), uri=url)
|
||||
if authzr.body.status != messages.STATUS_PENDING:
|
||||
responses.append(authzr)
|
||||
break
|
||||
@@ -671,12 +734,12 @@ class ClientV2(ClientBase):
|
||||
self._post(orderr.body.finalize, wrapped_csr)
|
||||
while datetime.datetime.now() < deadline:
|
||||
time.sleep(1)
|
||||
response = self.net.get(orderr.uri)
|
||||
response = self._post_as_get(orderr.uri)
|
||||
body = messages.Order.from_json(response.json())
|
||||
if body.error is not None:
|
||||
raise errors.IssuanceError(body.error)
|
||||
if body.certificate is not None:
|
||||
certificate_response = self.net.get(body.certificate,
|
||||
certificate_response = self._post_as_get(body.certificate,
|
||||
content_type=DER_CONTENT_TYPE).text
|
||||
return orderr.update(body=body, fullchain_pem=certificate_response)
|
||||
raise errors.TimeoutError()
|
||||
@@ -694,6 +757,39 @@ class ClientV2(ClientBase):
|
||||
"""
|
||||
return self._revoke(cert, rsn, self.directory['revokeCert'])
|
||||
|
||||
def external_account_required(self):
|
||||
"""Checks if ACME server requires External Account Binding authentication."""
|
||||
if hasattr(self.directory, 'meta') and self.directory.meta.external_account_required:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _post_as_get(self, *args, **kwargs):
|
||||
"""
|
||||
Send GET request using the POST-as-GET protocol if needed.
|
||||
The request will be first issued using POST-as-GET for ACME v2. If the ACME CA servers do
|
||||
not support this yet and return an error, request will be retried using GET.
|
||||
For ACME v1, only GET request will be tried, as POST-as-GET is not supported.
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if self.acme_version >= 2:
|
||||
# We add an empty payload for POST-as-GET requests
|
||||
new_args = args[:1] + (None,) + args[1:]
|
||||
try:
|
||||
return self._post(*new_args, **kwargs) # pylint: disable=star-args
|
||||
except messages.Error as error:
|
||||
if error.code == 'malformed':
|
||||
logger.debug('Error during a POST-as-GET request, '
|
||||
'your ACME CA may not support it:\n%s', error)
|
||||
logger.debug('Retrying request with GET.')
|
||||
else: # pragma: no cover
|
||||
raise
|
||||
|
||||
# If POST-as-GET is not supported yet, we use a GET instead.
|
||||
return self.net.get(*args, **kwargs)
|
||||
|
||||
|
||||
class BackwardsCompatibleClientV2(object):
|
||||
"""ACME client wrapper that tends towards V2-style calls, but
|
||||
@@ -723,12 +819,7 @@ class BackwardsCompatibleClientV2(object):
|
||||
self.client = ClientV2(directory, net=net)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in vars(self.client):
|
||||
return getattr(self.client, name)
|
||||
elif name in dir(ClientBase):
|
||||
return getattr(self.client, name)
|
||||
else:
|
||||
raise AttributeError()
|
||||
return getattr(self.client, name)
|
||||
|
||||
def new_account_and_tos(self, regr, check_tos_cb=None):
|
||||
"""Combined register and agree_tos for V1, new_account for V2
|
||||
@@ -835,6 +926,15 @@ class BackwardsCompatibleClientV2(object):
|
||||
else:
|
||||
return 1
|
||||
|
||||
def external_account_required(self):
|
||||
"""Checks if the server requires an external account for ACMEv2 servers.
|
||||
|
||||
Always return False for ACMEv1 servers, as it doesn't use External Account Binding."""
|
||||
if self.acme_version == 1:
|
||||
return False
|
||||
else:
|
||||
return self.client.external_account_required()
|
||||
|
||||
|
||||
class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
"""Wrapper around requests that signs POSTs for authentication.
|
||||
@@ -856,18 +956,28 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
:param bool verify_ssl: Whether to verify certificates on SSL connections.
|
||||
:param str user_agent: String to send as User-Agent header.
|
||||
:param float timeout: Timeout for requests.
|
||||
:param source_address: Optional source address to bind to when making requests.
|
||||
:type source_address: str or tuple(str, int)
|
||||
"""
|
||||
def __init__(self, key, account=None, alg=jose.RS256, verify_ssl=True,
|
||||
user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT):
|
||||
user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT,
|
||||
source_address=None):
|
||||
# pylint: disable=too-many-arguments
|
||||
self.key = key
|
||||
self.account = account
|
||||
self.alg = alg
|
||||
self.verify_ssl = verify_ssl
|
||||
self._nonces = set()
|
||||
self._nonces = set() # type: Set[Text]
|
||||
self.user_agent = user_agent
|
||||
self.session = requests.Session()
|
||||
self._default_timeout = timeout
|
||||
adapter = HTTPAdapter()
|
||||
|
||||
if source_address is not None:
|
||||
adapter = SourceAddressAdapter(source_address)
|
||||
|
||||
self.session.mount("http://", adapter)
|
||||
self.session.mount("https://", adapter)
|
||||
|
||||
def __del__(self):
|
||||
# Try to close the session, but don't show exceptions to the
|
||||
@@ -888,7 +998,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
:rtype: `josepy.JWS`
|
||||
|
||||
"""
|
||||
jobj = obj.json_dumps(indent=2).encode()
|
||||
jobj = obj.json_dumps(indent=2).encode() if obj else b''
|
||||
logger.debug('JWS payload:\n%s', jobj)
|
||||
kwargs = {
|
||||
"alg": self.alg,
|
||||
@@ -897,6 +1007,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
if acme_version == 2:
|
||||
kwargs["url"] = url
|
||||
# newAccount and revokeCert work without the kid
|
||||
# newAccount must not have kid
|
||||
if self.account is not None:
|
||||
kwargs["kid"] = self.account["uri"]
|
||||
kwargs["key"] = self.key
|
||||
@@ -1017,7 +1128,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
if response.headers.get("Content-Type") == DER_CONTENT_TYPE:
|
||||
debug_content = base64.b64encode(response.content)
|
||||
else:
|
||||
debug_content = response.content
|
||||
debug_content = response.content.decode("utf-8")
|
||||
logger.debug('Received response:\nHTTP %d\n%s\n\n%s',
|
||||
response.status_code,
|
||||
"\n".join(["{0}: {1}".format(k, v)
|
||||
@@ -1052,10 +1163,15 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
else:
|
||||
raise errors.MissingNonce(response)
|
||||
|
||||
def _get_nonce(self, url):
|
||||
def _get_nonce(self, url, new_nonce_url):
|
||||
if not self._nonces:
|
||||
logger.debug('Requesting fresh nonce')
|
||||
self._add_nonce(self.head(url))
|
||||
if new_nonce_url is None:
|
||||
response = self.head(url)
|
||||
else:
|
||||
# request a new nonce from the acme newNonce endpoint
|
||||
response = self._check_response(self.head(new_nonce_url), content_type=None)
|
||||
self._add_nonce(response)
|
||||
return self._nonces.pop()
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
@@ -1076,8 +1192,13 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE,
|
||||
acme_version=1, **kwargs):
|
||||
data = self._wrap_in_jws(obj, self._get_nonce(url), url, acme_version)
|
||||
try:
|
||||
new_nonce_url = kwargs.pop('new_nonce_url')
|
||||
except KeyError:
|
||||
new_nonce_url = None
|
||||
data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url, acme_version)
|
||||
kwargs.setdefault('headers', {'Content-Type': content_type})
|
||||
response = self._send_request('POST', url, data=data, **kwargs)
|
||||
response = self._check_response(response, content_type=content_type)
|
||||
self._add_nonce(response)
|
||||
return self._check_response(response, content_type=content_type)
|
||||
return response
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for acme.client."""
|
||||
# pylint: disable=too-many-lines
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
@@ -17,6 +18,7 @@ from acme import jws as acme_jws
|
||||
from acme import messages
|
||||
from acme import messages_test
|
||||
from acme import test_util
|
||||
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
|
||||
CERT_DER = test_util.load_vector('cert.der')
|
||||
@@ -61,7 +63,8 @@ class ClientTestBase(unittest.TestCase):
|
||||
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
|
||||
reg = messages.Registration(
|
||||
contact=self.contact, key=KEY.public_key())
|
||||
self.new_reg = messages.NewRegistration(**dict(reg))
|
||||
the_arg = dict(reg) # type: Dict
|
||||
self.new_reg = messages.NewRegistration(**the_arg) # pylint: disable=star-args
|
||||
self.regr = messages.RegistrationResource(
|
||||
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1')
|
||||
|
||||
@@ -132,12 +135,18 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
|
||||
client = self._init()
|
||||
self.assertEqual(client.acme_version, 2)
|
||||
|
||||
def test_query_registration_client_v2(self):
|
||||
self.response.json.return_value = DIRECTORY_V2.to_json()
|
||||
client = self._init()
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.assertEqual(self.regr, client.query_registration(self.regr))
|
||||
|
||||
def test_forwarding(self):
|
||||
self.response.json.return_value = DIRECTORY_V1.to_json()
|
||||
client = self._init()
|
||||
self.assertEqual(client.directory, client.client.directory)
|
||||
self.assertEqual(client.key, KEY)
|
||||
self.assertEqual(client.update_registration, client.client.update_registration)
|
||||
self.assertEqual(client.deactivate_registration, client.client.deactivate_registration)
|
||||
self.assertRaises(AttributeError, client.__getattr__, 'nonexistent')
|
||||
self.assertRaises(AttributeError, client.__getattr__, 'new_account_and_tos')
|
||||
self.assertRaises(AttributeError, client.__getattr__, 'new_account')
|
||||
@@ -268,6 +277,44 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
|
||||
client.revoke(messages_test.CERT, self.rsn)
|
||||
mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn)
|
||||
|
||||
def test_update_registration(self):
|
||||
self.response.json.return_value = DIRECTORY_V1.to_json()
|
||||
with mock.patch('acme.client.Client') as mock_client:
|
||||
client = self._init()
|
||||
client.update_registration(mock.sentinel.regr, None)
|
||||
mock_client().update_registration.assert_called_once_with(mock.sentinel.regr, None)
|
||||
|
||||
# newNonce present means it will pick acme_version 2
|
||||
def test_external_account_required_true(self):
|
||||
self.response.json.return_value = messages.Directory({
|
||||
'newNonce': 'http://letsencrypt-test.com/acme/new-nonce',
|
||||
'meta': messages.Directory.Meta(external_account_required=True),
|
||||
}).to_json()
|
||||
|
||||
client = self._init()
|
||||
|
||||
self.assertTrue(client.external_account_required())
|
||||
|
||||
# newNonce present means it will pick acme_version 2
|
||||
def test_external_account_required_false(self):
|
||||
self.response.json.return_value = messages.Directory({
|
||||
'newNonce': 'http://letsencrypt-test.com/acme/new-nonce',
|
||||
'meta': messages.Directory.Meta(external_account_required=False),
|
||||
}).to_json()
|
||||
|
||||
client = self._init()
|
||||
|
||||
self.assertFalse(client.external_account_required())
|
||||
|
||||
def test_external_account_required_false_v1(self):
|
||||
self.response.json.return_value = messages.Directory({
|
||||
'meta': messages.Directory.Meta(external_account_required=False),
|
||||
}).to_json()
|
||||
|
||||
client = self._init()
|
||||
|
||||
self.assertFalse(client.external_account_required())
|
||||
|
||||
|
||||
class ClientTest(ClientTestBase):
|
||||
"""Tests for acme.client.Client."""
|
||||
@@ -650,7 +697,7 @@ class ClientTest(ClientTestBase):
|
||||
def test_revocation_payload(self):
|
||||
obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn)
|
||||
self.assertTrue('reason' in obj.to_partial_json().keys())
|
||||
self.assertEquals(self.rsn, obj.to_partial_json()['reason'])
|
||||
self.assertEqual(self.rsn, obj.to_partial_json()['reason'])
|
||||
|
||||
def test_revoke_bad_status_raises_error(self):
|
||||
self.response.status_code = http_client.METHOD_NOT_ALLOWED
|
||||
@@ -697,6 +744,11 @@ class ClientV2Test(ClientTestBase):
|
||||
|
||||
self.assertEqual(self.regr, self.client.new_account(self.new_reg))
|
||||
|
||||
def test_new_account_conflict(self):
|
||||
self.response.status_code = http_client.OK
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.assertRaises(errors.ConflictError, self.client.new_account, self.new_reg)
|
||||
|
||||
def test_new_order(self):
|
||||
order_response = copy.deepcopy(self.response)
|
||||
order_response.status_code = http_client.CREATED
|
||||
@@ -710,9 +762,10 @@ class ClientV2Test(ClientTestBase):
|
||||
authz_response2 = self.response
|
||||
authz_response2.json.return_value = self.authz2.to_json()
|
||||
authz_response2.headers['Location'] = self.authzr2.uri
|
||||
self.net.get.side_effect = (authz_response, authz_response2)
|
||||
|
||||
self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr)
|
||||
with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get:
|
||||
mock_post_as_get.side_effect = (authz_response, authz_response2)
|
||||
self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr)
|
||||
|
||||
@mock.patch('acme.client.datetime')
|
||||
def test_poll_and_finalize(self, mock_datetime):
|
||||
@@ -785,7 +838,62 @@ class ClientV2Test(ClientTestBase):
|
||||
def test_revoke(self):
|
||||
self.client.revoke(messages_test.CERT, self.rsn)
|
||||
self.net.post.assert_called_once_with(
|
||||
self.directory["revokeCert"], mock.ANY, acme_version=2)
|
||||
self.directory["revokeCert"], mock.ANY, acme_version=2,
|
||||
new_nonce_url=DIRECTORY_V2['newNonce'])
|
||||
|
||||
def test_update_registration(self):
|
||||
# "Instance of 'Field' has no to_json/update member" bug:
|
||||
# pylint: disable=no-member
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.assertEqual(self.regr, self.client.update_registration(self.regr))
|
||||
self.assertNotEqual(self.client.net.account, None)
|
||||
self.assertEqual(self.client.net.post.call_count, 2)
|
||||
self.assertTrue(DIRECTORY_V2.newAccount in self.net.post.call_args_list[0][0])
|
||||
|
||||
self.response.json.return_value = self.regr.body.update(
|
||||
contact=()).to_json()
|
||||
|
||||
def test_external_account_required_true(self):
|
||||
self.client.directory = messages.Directory({
|
||||
'meta': messages.Directory.Meta(external_account_required=True)
|
||||
})
|
||||
|
||||
self.assertTrue(self.client.external_account_required())
|
||||
|
||||
def test_external_account_required_false(self):
|
||||
self.client.directory = messages.Directory({
|
||||
'meta': messages.Directory.Meta(external_account_required=False)
|
||||
})
|
||||
|
||||
self.assertFalse(self.client.external_account_required())
|
||||
|
||||
def test_external_account_required_default(self):
|
||||
self.assertFalse(self.client.external_account_required())
|
||||
|
||||
def test_post_as_get(self):
|
||||
with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client:
|
||||
mock_client.return_value = self.authzr2
|
||||
|
||||
self.client.poll(self.authzr2) # pylint: disable=protected-access
|
||||
|
||||
self.client.net.post.assert_called_once_with(
|
||||
self.authzr2.uri, None, acme_version=2,
|
||||
new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce')
|
||||
self.client.net.get.assert_not_called()
|
||||
|
||||
class FakeError(messages.Error): # pylint: disable=too-many-ancestors
|
||||
"""Fake error to reproduce a malformed request ACME error"""
|
||||
def __init__(self): # pylint: disable=super-init-not-called
|
||||
pass
|
||||
@property
|
||||
def code(self):
|
||||
return 'malformed'
|
||||
self.client.net.post.side_effect = FakeError()
|
||||
|
||||
self.client.poll(self.authzr2) # pylint: disable=protected-access
|
||||
|
||||
self.client.net.get.assert_called_once_with(self.authzr2.uri)
|
||||
|
||||
|
||||
class MockJSONDeSerializable(jose.JSONDeSerializable):
|
||||
@@ -1005,8 +1113,8 @@ class ClientNetworkTest(unittest.TestCase):
|
||||
|
||||
# Requests Library Exceptions
|
||||
except requests.exceptions.ConnectionError as z: #pragma: no cover
|
||||
self.assertEqual("('Connection aborted.', "
|
||||
"error(111, 'Connection refused'))", str(z))
|
||||
self.assertTrue("('Connection aborted.', error(111, 'Connection refused'))"
|
||||
== str(z) or "[WinError 10061]" in str(z))
|
||||
|
||||
class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
||||
"""Tests for acme.client.ClientNetwork which mock out response."""
|
||||
@@ -1019,7 +1127,10 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
||||
self.response = mock.MagicMock(ok=True, status_code=http_client.OK)
|
||||
self.response.headers = {}
|
||||
self.response.links = {}
|
||||
self.checked_response = mock.MagicMock()
|
||||
self.response.checked = False
|
||||
self.acmev1_nonce_response = mock.MagicMock(ok=False,
|
||||
status_code=http_client.METHOD_NOT_ALLOWED)
|
||||
self.acmev1_nonce_response.headers = {}
|
||||
self.obj = mock.MagicMock()
|
||||
self.wrapped_obj = mock.MagicMock()
|
||||
self.content_type = mock.sentinel.content_type
|
||||
@@ -1031,13 +1142,21 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
||||
|
||||
def send_request(*args, **kwargs):
|
||||
# pylint: disable=unused-argument,missing-docstring
|
||||
self.assertFalse("new_nonce_url" in kwargs)
|
||||
method = args[0]
|
||||
uri = args[1]
|
||||
if method == 'HEAD' and uri != "new_nonce_uri":
|
||||
response = self.acmev1_nonce_response
|
||||
else:
|
||||
response = self.response
|
||||
|
||||
if self.available_nonces:
|
||||
self.response.headers = {
|
||||
response.headers = {
|
||||
self.net.REPLAY_NONCE_HEADER:
|
||||
self.available_nonces.pop().decode()}
|
||||
else:
|
||||
self.response.headers = {}
|
||||
return self.response
|
||||
response.headers = {}
|
||||
return response
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.net._send_request = self.send_request = mock.MagicMock(
|
||||
@@ -1049,28 +1168,39 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
||||
# pylint: disable=missing-docstring
|
||||
self.assertEqual(self.response, response)
|
||||
self.assertEqual(self.content_type, content_type)
|
||||
return self.checked_response
|
||||
self.assertTrue(self.response.ok)
|
||||
self.response.checked = True
|
||||
return self.response
|
||||
|
||||
def test_head(self):
|
||||
self.assertEqual(self.response, self.net.head(
|
||||
self.assertEqual(self.acmev1_nonce_response, self.net.head(
|
||||
'http://example.com/', 'foo', bar='baz'))
|
||||
self.send_request.assert_called_once_with(
|
||||
'HEAD', 'http://example.com/', 'foo', bar='baz')
|
||||
|
||||
def test_head_v2(self):
|
||||
self.assertEqual(self.response, self.net.head(
|
||||
'new_nonce_uri', 'foo', bar='baz'))
|
||||
self.send_request.assert_called_once_with(
|
||||
'HEAD', 'new_nonce_uri', 'foo', bar='baz')
|
||||
|
||||
def test_get(self):
|
||||
self.assertEqual(self.checked_response, self.net.get(
|
||||
self.assertEqual(self.response, self.net.get(
|
||||
'http://example.com/', content_type=self.content_type, bar='baz'))
|
||||
self.assertTrue(self.response.checked)
|
||||
self.send_request.assert_called_once_with(
|
||||
'GET', 'http://example.com/', bar='baz')
|
||||
|
||||
def test_post_no_content_type(self):
|
||||
self.content_type = self.net.JOSE_CONTENT_TYPE
|
||||
self.assertEqual(self.checked_response, self.net.post('uri', self.obj))
|
||||
self.assertEqual(self.response, self.net.post('uri', self.obj))
|
||||
self.assertTrue(self.response.checked)
|
||||
|
||||
def test_post(self):
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.checked_response, self.net.post(
|
||||
self.assertEqual(self.response, self.net.post(
|
||||
'uri', self.obj, content_type=self.content_type))
|
||||
self.assertTrue(self.response.checked)
|
||||
self.net._wrap_in_jws.assert_called_once_with(
|
||||
self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1)
|
||||
|
||||
@@ -1102,7 +1232,7 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
||||
def test_post_not_retried(self):
|
||||
check_response = mock.MagicMock()
|
||||
check_response.side_effect = [messages.Error.with_code('malformed'),
|
||||
self.checked_response]
|
||||
self.response]
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = check_response
|
||||
@@ -1110,13 +1240,12 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
||||
self.obj, content_type=self.content_type)
|
||||
|
||||
def test_post_successful_retry(self):
|
||||
check_response = mock.MagicMock()
|
||||
check_response.side_effect = [messages.Error.with_code('badNonce'),
|
||||
self.checked_response]
|
||||
post_once = mock.MagicMock()
|
||||
post_once.side_effect = [messages.Error.with_code('badNonce'),
|
||||
self.response]
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = check_response
|
||||
self.assertEqual(self.checked_response, self.net.post(
|
||||
self.assertEqual(self.response, self.net.post(
|
||||
'uri', self.obj, content_type=self.content_type))
|
||||
|
||||
def test_head_get_post_error_passthrough(self):
|
||||
@@ -1127,6 +1256,51 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
||||
self.assertRaises(requests.exceptions.RequestException,
|
||||
self.net.post, 'uri', obj=self.obj)
|
||||
|
||||
def test_post_bad_nonce_head(self):
|
||||
# pylint: disable=protected-access
|
||||
# regression test for https://github.com/certbot/certbot/issues/6092
|
||||
bad_response = mock.MagicMock(ok=False, status_code=http_client.SERVICE_UNAVAILABLE)
|
||||
self.net._send_request = mock.MagicMock()
|
||||
self.net._send_request.return_value = bad_response
|
||||
self.content_type = None
|
||||
check_response = mock.MagicMock()
|
||||
self.net._check_response = check_response
|
||||
self.assertRaises(errors.ClientError, self.net.post, 'uri',
|
||||
self.obj, content_type=self.content_type, acme_version=2,
|
||||
new_nonce_url='new_nonce_uri')
|
||||
self.assertEqual(check_response.call_count, 1)
|
||||
|
||||
def test_new_nonce_uri_removed(self):
|
||||
self.content_type = None
|
||||
self.net.post('uri', self.obj, content_type=None,
|
||||
acme_version=2, new_nonce_url='new_nonce_uri')
|
||||
|
||||
|
||||
class ClientNetworkSourceAddressBindingTest(unittest.TestCase):
|
||||
"""Tests that if ClientNetwork has a source IP set manually, the underlying library has
|
||||
used the provided source address."""
|
||||
|
||||
def setUp(self):
|
||||
self.source_address = "8.8.8.8"
|
||||
|
||||
def test_source_address_set(self):
|
||||
from acme.client import ClientNetwork
|
||||
net = ClientNetwork(key=None, alg=None, source_address=self.source_address)
|
||||
for adapter in net.session.adapters.values():
|
||||
self.assertTrue(self.source_address in adapter.source_address)
|
||||
|
||||
def test_behavior_assumption(self):
|
||||
"""This is a test that guardrails the HTTPAdapter behavior so that if the default for
|
||||
a Session() changes, the assumptions here aren't violated silently."""
|
||||
from acme.client import ClientNetwork
|
||||
# Source address not specified, so the default adapter type should be bound -- this
|
||||
# test should fail if the default adapter type is changed by requests
|
||||
net = ClientNetwork(key=None, alg=None)
|
||||
session = requests.Session()
|
||||
for scheme in session.adapters.keys():
|
||||
client_network_adapter = net.session.adapters.get(scheme)
|
||||
default_adapter = session.adapters.get(scheme)
|
||||
self.assertEqual(client_network_adapter.__class__, default_adapter.__class__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -6,11 +6,14 @@ import os
|
||||
import re
|
||||
import socket
|
||||
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
|
||||
import josepy as jose
|
||||
|
||||
|
||||
from acme import errors
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Callable, Union, Tuple, Optional
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -25,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni
|
||||
# should be changed to use "set_options" to disable SSLv2 and SSLv3,
|
||||
# in case it's used for things other than probing/serving!
|
||||
_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD # type: ignore
|
||||
_DEFAULT_TLSSNI01_SSL_METHOD = SSL.SSLv23_METHOD # type: ignore
|
||||
|
||||
|
||||
class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
@@ -64,9 +67,9 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
logger.debug("Server name (%s) not recognized, dropping SSL",
|
||||
server_name)
|
||||
return
|
||||
new_context = OpenSSL.SSL.Context(self.method)
|
||||
new_context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
|
||||
new_context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
|
||||
new_context = SSL.Context(self.method)
|
||||
new_context.set_options(SSL.OP_NO_SSLv2)
|
||||
new_context.set_options(SSL.OP_NO_SSLv3)
|
||||
new_context.use_privatekey(key)
|
||||
new_context.use_certificate(cert)
|
||||
connection.set_context(new_context)
|
||||
@@ -89,18 +92,18 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
||||
def accept(self): # pylint: disable=missing-docstring
|
||||
sock, addr = self.sock.accept()
|
||||
|
||||
context = OpenSSL.SSL.Context(self.method)
|
||||
context.set_options(OpenSSL.SSL.OP_NO_SSLv2)
|
||||
context.set_options(OpenSSL.SSL.OP_NO_SSLv3)
|
||||
context = SSL.Context(self.method)
|
||||
context.set_options(SSL.OP_NO_SSLv2)
|
||||
context.set_options(SSL.OP_NO_SSLv3)
|
||||
context.set_tlsext_servername_callback(self._pick_certificate_cb)
|
||||
|
||||
ssl_sock = self.FakeConnection(OpenSSL.SSL.Connection(context, sock))
|
||||
ssl_sock = self.FakeConnection(SSL.Connection(context, sock))
|
||||
ssl_sock.set_accept_state()
|
||||
|
||||
logger.debug("Performing handshake with %s", addr)
|
||||
try:
|
||||
ssl_sock.do_handshake()
|
||||
except OpenSSL.SSL.Error as error:
|
||||
except SSL.Error as error:
|
||||
# _pick_certificate_cb might have returned without
|
||||
# creating SSL context (wrong server name)
|
||||
raise socket.error(error)
|
||||
@@ -128,30 +131,33 @@ def probe_sni(name, host, port=443, timeout=300,
|
||||
:rtype: OpenSSL.crypto.X509
|
||||
|
||||
"""
|
||||
context = OpenSSL.SSL.Context(method)
|
||||
context = SSL.Context(method)
|
||||
context.set_timeout(timeout)
|
||||
|
||||
socket_kwargs = {'source_address': source_address}
|
||||
|
||||
host_protocol_agnostic = None if host == '::' or host == '0' else host
|
||||
|
||||
try:
|
||||
# pylint: disable=star-args
|
||||
logger.debug("Attempting to connect to %s:%d%s.", host_protocol_agnostic, port,
|
||||
" from {0}:{1}".format(source_address[0], source_address[1]) if \
|
||||
socket_kwargs else "")
|
||||
sock = socket.create_connection((host_protocol_agnostic, port), **socket_kwargs)
|
||||
logger.debug(
|
||||
"Attempting to connect to %s:%d%s.", host, port,
|
||||
" from {0}:{1}".format(
|
||||
source_address[0],
|
||||
source_address[1]
|
||||
) if socket_kwargs else ""
|
||||
)
|
||||
socket_tuple = (host, port) # type: Tuple[str, int]
|
||||
sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore
|
||||
except socket.error as error:
|
||||
raise errors.Error(error)
|
||||
|
||||
with contextlib.closing(sock) as client:
|
||||
client_ssl = OpenSSL.SSL.Connection(context, client)
|
||||
client_ssl = SSL.Connection(context, client)
|
||||
client_ssl.set_connect_state()
|
||||
client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13
|
||||
try:
|
||||
client_ssl.do_handshake()
|
||||
client_ssl.shutdown()
|
||||
except OpenSSL.SSL.Error as error:
|
||||
except SSL.Error as error:
|
||||
raise errors.Error(error)
|
||||
return client_ssl.get_peer_certificate()
|
||||
|
||||
@@ -164,18 +170,18 @@ def make_csr(private_key_pem, domains, must_staple=False):
|
||||
OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
|
||||
:returns: buffer PEM-encoded Certificate Signing Request.
|
||||
"""
|
||||
private_key = OpenSSL.crypto.load_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, private_key_pem)
|
||||
csr = OpenSSL.crypto.X509Req()
|
||||
private_key = crypto.load_privatekey(
|
||||
crypto.FILETYPE_PEM, private_key_pem)
|
||||
csr = crypto.X509Req()
|
||||
extensions = [
|
||||
OpenSSL.crypto.X509Extension(
|
||||
crypto.X509Extension(
|
||||
b'subjectAltName',
|
||||
critical=False,
|
||||
value=', '.join('DNS:' + d for d in domains).encode('ascii')
|
||||
),
|
||||
]
|
||||
if must_staple:
|
||||
extensions.append(OpenSSL.crypto.X509Extension(
|
||||
extensions.append(crypto.X509Extension(
|
||||
b"1.3.6.1.5.5.7.1.24",
|
||||
critical=False,
|
||||
value=b"DER:30:03:02:01:05"))
|
||||
@@ -183,8 +189,8 @@ def make_csr(private_key_pem, domains, must_staple=False):
|
||||
csr.set_pubkey(private_key)
|
||||
csr.set_version(2)
|
||||
csr.sign(private_key, 'sha256')
|
||||
return OpenSSL.crypto.dump_certificate_request(
|
||||
OpenSSL.crypto.FILETYPE_PEM, csr)
|
||||
return crypto.dump_certificate_request(
|
||||
crypto.FILETYPE_PEM, csr)
|
||||
|
||||
def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
|
||||
common_name = loaded_cert_or_req.get_subject().CN
|
||||
@@ -221,11 +227,12 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
|
||||
parts_separator = ", "
|
||||
prefix = "DNS" + part_separator
|
||||
|
||||
if isinstance(cert_or_req, OpenSSL.crypto.X509):
|
||||
func = OpenSSL.crypto.dump_certificate
|
||||
if isinstance(cert_or_req, crypto.X509):
|
||||
# pylint: disable=line-too-long
|
||||
func = crypto.dump_certificate # type: Union[Callable[[int, crypto.X509Req], bytes], Callable[[int, crypto.X509], bytes]]
|
||||
else:
|
||||
func = OpenSSL.crypto.dump_certificate_request
|
||||
text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
|
||||
func = crypto.dump_certificate_request
|
||||
text = func(crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
|
||||
# WARNING: this function does not support multiple SANs extensions.
|
||||
# Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
|
||||
match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
|
||||
@@ -252,12 +259,12 @@ def gen_ss_cert(key, domains, not_before=None,
|
||||
|
||||
"""
|
||||
assert domains, "Must provide one or more hostnames for the cert."
|
||||
cert = OpenSSL.crypto.X509()
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
|
||||
cert.set_version(2)
|
||||
|
||||
extensions = [
|
||||
OpenSSL.crypto.X509Extension(
|
||||
crypto.X509Extension(
|
||||
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
|
||||
]
|
||||
|
||||
@@ -266,7 +273,7 @@ def gen_ss_cert(key, domains, not_before=None,
|
||||
cert.set_issuer(cert.get_subject())
|
||||
|
||||
if force_san or len(domains) > 1:
|
||||
extensions.append(OpenSSL.crypto.X509Extension(
|
||||
extensions.append(crypto.X509Extension(
|
||||
b"subjectAltName",
|
||||
critical=False,
|
||||
value=b", ".join(b"DNS:" + d.encode() for d in domains)
|
||||
@@ -281,7 +288,7 @@ def gen_ss_cert(key, domains, not_before=None,
|
||||
cert.sign(key, "sha256")
|
||||
return cert
|
||||
|
||||
def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
|
||||
def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM):
|
||||
"""Dump certificate chain into a bundle.
|
||||
|
||||
:param list chain: List of `OpenSSL.crypto.X509` (or wrapped in
|
||||
@@ -298,7 +305,7 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
|
||||
if isinstance(cert, jose.ComparableX509):
|
||||
# pylint: disable=protected-access
|
||||
cert = cert.wrapped
|
||||
return OpenSSL.crypto.dump_certificate(filetype, cert)
|
||||
return crypto.dump_certificate(filetype, cert)
|
||||
|
||||
# assumes that OpenSSL.crypto.dump_certificate includes ending
|
||||
# newline character
|
||||
|
||||
@@ -13,6 +13,7 @@ import OpenSSL
|
||||
|
||||
from acme import errors
|
||||
from acme import test_util
|
||||
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
|
||||
class SSLSocketAndProbeSNITest(unittest.TestCase):
|
||||
@@ -41,28 +42,38 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
|
||||
self.server_thread = threading.Thread(
|
||||
# pylint: disable=no-member
|
||||
target=self.server.handle_request)
|
||||
self.server_thread.start()
|
||||
time.sleep(1) # TODO: avoid race conditions in other way
|
||||
|
||||
def tearDown(self):
|
||||
self.server_thread.join()
|
||||
if self.server_thread.is_alive():
|
||||
# The thread may have already terminated.
|
||||
self.server_thread.join() # pragma: no cover
|
||||
|
||||
def _probe(self, name):
|
||||
from acme.crypto_util import probe_sni
|
||||
return jose.ComparableX509(probe_sni(
|
||||
name, host='127.0.0.1', port=self.port))
|
||||
|
||||
def _start_server(self):
|
||||
self.server_thread.start()
|
||||
time.sleep(1) # TODO: avoid race conditions in other way
|
||||
|
||||
def test_probe_ok(self):
|
||||
self._start_server()
|
||||
self.assertEqual(self.cert, self._probe(b'foo'))
|
||||
|
||||
def test_probe_not_recognized_name(self):
|
||||
self._start_server()
|
||||
self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
|
||||
# TODO: py33/py34 tox hangs forever on do_handshake in second probe
|
||||
#def probe_connection_error(self):
|
||||
# self._probe(b'foo')
|
||||
# #time.sleep(1) # TODO: avoid race conditions in other way
|
||||
# self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
def test_probe_connection_error(self):
|
||||
# pylint has a hard time with six
|
||||
self.server.server_close() # pylint: disable=no-member
|
||||
original_timeout = socket.getdefaulttimeout()
|
||||
try:
|
||||
socket.setdefaulttimeout(1)
|
||||
self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
finally:
|
||||
socket.setdefaulttimeout(original_timeout)
|
||||
|
||||
|
||||
class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
|
||||
@@ -165,7 +176,7 @@ class RandomSnTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.cert_count = 5
|
||||
self.serial_num = []
|
||||
self.serial_num = [] # type: List[int]
|
||||
self.key = OpenSSL.crypto.PKey()
|
||||
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
|
||||
|
||||
@@ -198,8 +209,8 @@ class MakeCSRTest(unittest.TestCase):
|
||||
# have a get_extensions() method, so we skip this test if the method
|
||||
# isn't available.
|
||||
if hasattr(csr, 'get_extensions'):
|
||||
self.assertEquals(len(csr.get_extensions()), 1)
|
||||
self.assertEquals(csr.get_extensions()[0].get_data(),
|
||||
self.assertEqual(len(csr.get_extensions()), 1)
|
||||
self.assertEqual(csr.get_extensions()[0].get_data(),
|
||||
OpenSSL.crypto.X509Extension(
|
||||
b'subjectAltName',
|
||||
critical=False,
|
||||
@@ -216,7 +227,7 @@ class MakeCSRTest(unittest.TestCase):
|
||||
# have a get_extensions() method, so we skip this test if the method
|
||||
# isn't available.
|
||||
if hasattr(csr, 'get_extensions'):
|
||||
self.assertEquals(len(csr.get_extensions()), 2)
|
||||
self.assertEqual(len(csr.get_extensions()), 2)
|
||||
# NOTE: Ideally we would filter by the TLS Feature OID, but
|
||||
# OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID,
|
||||
# and the shortname field is just "UNDEF"
|
||||
|
||||
@@ -110,6 +110,8 @@ class ConflictError(ClientError):
|
||||
|
||||
In the version of ACME implemented by Boulder, this is used to find an
|
||||
account if you only have the private key, but don't know the account URL.
|
||||
|
||||
Also used in V2 of the ACME client for the same purpose.
|
||||
"""
|
||||
def __init__(self, location):
|
||||
self.location = location
|
||||
|
||||
16
acme/acme/magic_typing.py
Normal file
16
acme/acme/magic_typing.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Shim class to not have to depend on typing module in prod."""
|
||||
import sys
|
||||
|
||||
class TypingClass(object):
|
||||
"""Ignore import errors by getting anything"""
|
||||
def __getattr__(self, name):
|
||||
return None
|
||||
|
||||
try:
|
||||
# mypy doesn't respect modifying sys.modules
|
||||
from typing import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
# pylint: disable=unused-import
|
||||
from typing import Collection, IO # type: ignore
|
||||
# pylint: enable=unused-import
|
||||
except ImportError:
|
||||
sys.modules[__name__] = TypingClass()
|
||||
41
acme/acme/magic_typing_test.py
Normal file
41
acme/acme/magic_typing_test.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Tests for acme.magic_typing."""
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
class MagicTypingTest(unittest.TestCase):
|
||||
"""Tests for acme.magic_typing."""
|
||||
def test_import_success(self):
|
||||
try:
|
||||
import typing as temp_typing
|
||||
except ImportError: # pragma: no cover
|
||||
temp_typing = None # pragma: no cover
|
||||
typing_class_mock = mock.MagicMock()
|
||||
text_mock = mock.MagicMock()
|
||||
typing_class_mock.Text = text_mock
|
||||
sys.modules['typing'] = typing_class_mock
|
||||
if 'acme.magic_typing' in sys.modules:
|
||||
del sys.modules['acme.magic_typing'] # pragma: no cover
|
||||
from acme.magic_typing import Text # pylint: disable=no-name-in-module
|
||||
self.assertEqual(Text, text_mock)
|
||||
del sys.modules['acme.magic_typing']
|
||||
sys.modules['typing'] = temp_typing
|
||||
|
||||
def test_import_failure(self):
|
||||
try:
|
||||
import typing as temp_typing
|
||||
except ImportError: # pragma: no cover
|
||||
temp_typing = None # pragma: no cover
|
||||
sys.modules['typing'] = None
|
||||
if 'acme.magic_typing' in sys.modules:
|
||||
del sys.modules['acme.magic_typing'] # pragma: no cover
|
||||
from acme.magic_typing import Text # pylint: disable=no-name-in-module
|
||||
self.assertTrue(Text is None)
|
||||
del sys.modules['acme.magic_typing']
|
||||
sys.modules['typing'] = temp_typing
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,6 +1,7 @@
|
||||
"""ACME protocol messages."""
|
||||
import collections
|
||||
import six
|
||||
import json
|
||||
|
||||
import josepy as jose
|
||||
|
||||
@@ -8,6 +9,7 @@ from acme import challenges
|
||||
from acme import errors
|
||||
from acme import fields
|
||||
from acme import util
|
||||
from acme import jws
|
||||
|
||||
OLD_ERROR_PREFIX = "urn:acme:error:"
|
||||
ERROR_PREFIX = "urn:ietf:params:acme:error:"
|
||||
@@ -27,6 +29,7 @@ ERROR_CODES = {
|
||||
'tls': 'The server experienced a TLS error during domain verification',
|
||||
'unauthorized': 'The client lacks sufficient authorization',
|
||||
'unknownHost': 'The server could not resolve a domain name',
|
||||
'externalAccountRequired': 'The server requires external account binding',
|
||||
}
|
||||
|
||||
ERROR_TYPE_DESCRIPTIONS = dict(
|
||||
@@ -145,6 +148,7 @@ STATUS_PROCESSING = Status('processing')
|
||||
STATUS_VALID = Status('valid')
|
||||
STATUS_INVALID = Status('invalid')
|
||||
STATUS_REVOKED = Status('revoked')
|
||||
STATUS_READY = Status('ready')
|
||||
|
||||
|
||||
class IdentifierType(_Constant):
|
||||
@@ -175,6 +179,7 @@ class Directory(jose.JSONDeSerializable):
|
||||
_terms_of_service_v2 = jose.Field('termsOfService', omitempty=True)
|
||||
website = jose.Field('website', omitempty=True)
|
||||
caa_identities = jose.Field('caaIdentities', omitempty=True)
|
||||
external_account_required = jose.Field('externalAccountRequired', omitempty=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items())
|
||||
@@ -257,6 +262,24 @@ class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body."""
|
||||
|
||||
|
||||
class ExternalAccountBinding(object):
|
||||
"""ACME External Account Binding"""
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, account_public_key, kid, hmac_key, directory):
|
||||
"""Create External Account Binding Resource from contact details, kid and hmac."""
|
||||
|
||||
key_json = json.dumps(account_public_key.to_partial_json()).encode()
|
||||
decoded_hmac_key = jose.b64.b64decode(hmac_key)
|
||||
url = directory["newAccount"]
|
||||
|
||||
eab = jws.JWS.sign(key_json, jose.jwk.JWKOct(key=decoded_hmac_key),
|
||||
jose.jwa.HS256, None,
|
||||
url, kid)
|
||||
|
||||
return eab.to_partial_json()
|
||||
|
||||
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
@@ -273,19 +296,25 @@ class Registration(ResourceBody):
|
||||
agreement = jose.Field('agreement', omitempty=True)
|
||||
status = jose.Field('status', omitempty=True)
|
||||
terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True)
|
||||
only_return_existing = jose.Field('onlyReturnExisting', omitempty=True)
|
||||
external_account_binding = jose.Field('externalAccountBinding', omitempty=True)
|
||||
|
||||
phone_prefix = 'tel:'
|
||||
email_prefix = 'mailto:'
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, phone=None, email=None, **kwargs):
|
||||
def from_data(cls, phone=None, email=None, external_account_binding=None, **kwargs):
|
||||
"""Create registration resource from contact details."""
|
||||
details = list(kwargs.pop('contact', ()))
|
||||
if phone is not None:
|
||||
details.append(cls.phone_prefix + phone)
|
||||
if email is not None:
|
||||
details.append(cls.email_prefix + email)
|
||||
details.extend([cls.email_prefix + mail for mail in email.split(',')])
|
||||
kwargs['contact'] = tuple(details)
|
||||
|
||||
if external_account_binding:
|
||||
kwargs['external_account_binding'] = external_account_binding
|
||||
|
||||
return cls(**kwargs)
|
||||
|
||||
def _filter_contact(self, prefix):
|
||||
@@ -435,6 +464,7 @@ class Authorization(ResourceBody):
|
||||
# be absent'... then acme-spec gives example with 'expires'
|
||||
# present... That's confusing!
|
||||
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||
wildcard = jose.Field('wildcard', omitempty=True)
|
||||
|
||||
@challenges.decoder
|
||||
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
@@ -520,7 +550,7 @@ class Order(ResourceBody):
|
||||
"""
|
||||
identifiers = jose.Field('identifiers', omitempty=True)
|
||||
status = jose.Field('status', decoder=Status.from_json,
|
||||
omitempty=True, default=STATUS_PENDING)
|
||||
omitempty=True)
|
||||
authorizations = jose.Field('authorizations', omitempty=True)
|
||||
certificate = jose.Field('certificate', omitempty=True)
|
||||
finalize = jose.Field('finalize', omitempty=True)
|
||||
@@ -550,4 +580,3 @@ class OrderResource(ResourceWithURI):
|
||||
class NewOrder(Order):
|
||||
"""New order."""
|
||||
resource_type = 'new-order'
|
||||
resource = fields.Resource(resource_type)
|
||||
|
||||
@@ -6,6 +6,7 @@ import mock
|
||||
|
||||
from acme import challenges
|
||||
from acme import test_util
|
||||
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
|
||||
CERT = test_util.load_comparable_cert('cert.der')
|
||||
@@ -85,7 +86,7 @@ class ConstantTest(unittest.TestCase):
|
||||
from acme.messages import _Constant
|
||||
|
||||
class MockConstant(_Constant): # pylint: disable=missing-docstring
|
||||
POSSIBLE_NAMES = {}
|
||||
POSSIBLE_NAMES = {} # type: Dict
|
||||
|
||||
self.MockConstant = MockConstant # pylint: disable=invalid-name
|
||||
self.const_a = MockConstant('a')
|
||||
@@ -173,6 +174,24 @@ class DirectoryTest(unittest.TestCase):
|
||||
self.assertTrue(result)
|
||||
|
||||
|
||||
class ExternalAccountBindingTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from acme.messages import Directory
|
||||
self.key = jose.jwk.JWKRSA(key=KEY.public_key())
|
||||
self.kid = "kid-for-testing"
|
||||
self.hmac_key = "hmac-key-for-testing"
|
||||
self.dir = Directory({
|
||||
'newAccount': 'http://url/acme/new-account',
|
||||
})
|
||||
|
||||
def test_from_data(self):
|
||||
from acme.messages import ExternalAccountBinding
|
||||
eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir)
|
||||
|
||||
self.assertEqual(len(eab), 3)
|
||||
self.assertEqual(sorted(eab.keys()), sorted(['protected', 'payload', 'signature']))
|
||||
|
||||
|
||||
class RegistrationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.Registration."""
|
||||
|
||||
@@ -204,6 +223,22 @@ class RegistrationTest(unittest.TestCase):
|
||||
'mailto:admin@foo.com',
|
||||
))
|
||||
|
||||
def test_new_registration_from_data_with_eab(self):
|
||||
from acme.messages import NewRegistration, ExternalAccountBinding, Directory
|
||||
key = jose.jwk.JWKRSA(key=KEY.public_key())
|
||||
kid = "kid-for-testing"
|
||||
hmac_key = "hmac-key-for-testing"
|
||||
directory = Directory({
|
||||
'newAccount': 'http://url/acme/new-account',
|
||||
})
|
||||
eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory)
|
||||
reg = NewRegistration.from_data(email='admin@foo.com', external_account_binding=eab)
|
||||
self.assertEqual(reg.contact, (
|
||||
'mailto:admin@foo.com',
|
||||
))
|
||||
self.assertEqual(sorted(reg.external_account_binding.keys()),
|
||||
sorted(['protected', 'payload', 'signature']))
|
||||
|
||||
def test_phones(self):
|
||||
self.assertEqual(('1234',), self.reg.phones)
|
||||
|
||||
@@ -423,6 +458,19 @@ class OrderResourceTest(unittest.TestCase):
|
||||
'authorizations': None,
|
||||
})
|
||||
|
||||
class NewOrderTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.NewOrder."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import NewOrder
|
||||
self.reg = NewOrder(
|
||||
identifiers=mock.sentinel.identifiers)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.reg.to_json(), {
|
||||
'identifiers': mock.sentinel.identifiers,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -16,6 +16,7 @@ import OpenSSL
|
||||
|
||||
from acme import challenges
|
||||
from acme import crypto_util
|
||||
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -66,8 +67,8 @@ class BaseDualNetworkedServers(object):
|
||||
|
||||
def __init__(self, ServerClass, server_address, *remaining_args, **kwargs):
|
||||
port = server_address[1]
|
||||
self.threads = []
|
||||
self.servers = []
|
||||
self.threads = [] # type: List[threading.Thread]
|
||||
self.servers = [] # type: List[ACMEServerMixin]
|
||||
|
||||
# Must try True first.
|
||||
# Ubuntu, for example, will fail to bind to IPv4 if we've already bound
|
||||
@@ -82,9 +83,22 @@ class BaseDualNetworkedServers(object):
|
||||
new_address = (server_address[0],) + (port,) + server_address[2:]
|
||||
new_args = (new_address,) + remaining_args
|
||||
server = ServerClass(*new_args, **kwargs) # pylint: disable=star-args
|
||||
except socket.error:
|
||||
logger.debug("Failed to bind to %s:%s using %s", new_address[0],
|
||||
logger.debug(
|
||||
"Successfully bound to %s:%s using %s", new_address[0],
|
||||
new_address[1], "IPv6" if ip_version else "IPv4")
|
||||
except socket.error:
|
||||
if self.servers:
|
||||
# Already bound using IPv6.
|
||||
logger.debug(
|
||||
"Certbot wasn't able to bind to %s:%s using %s, this " +
|
||||
"is often expected due to the dual stack nature of " +
|
||||
"IPv6 socket implementations.",
|
||||
new_address[0], new_address[1],
|
||||
"IPv6" if ip_version else "IPv4")
|
||||
else:
|
||||
logger.debug(
|
||||
"Failed to bind to %s:%s using %s", new_address[0],
|
||||
new_address[1], "IPv6" if ip_version else "IPv4")
|
||||
else:
|
||||
self.servers.append(server)
|
||||
# If two servers are set up and port 0 was passed in, ensure we always
|
||||
@@ -189,7 +203,7 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.simple_http_resources = kwargs.pop("simple_http_resources", set())
|
||||
socketserver.BaseRequestHandler.__init__(self, *args, **kwargs)
|
||||
BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def log_message(self, format, *args): # pylint: disable=redefined-builtin
|
||||
"""Log arbitrary message."""
|
||||
@@ -262,7 +276,7 @@ def simple_tls_sni_01_server(cli_args, forever=True):
|
||||
|
||||
certs = {}
|
||||
|
||||
_, hosts, _ = next(os.walk('.'))
|
||||
_, hosts, _ = next(os.walk('.')) # type: ignore # https://github.com/python/mypy/issues/465
|
||||
for host in hosts:
|
||||
with open(os.path.join(host, "cert.pem")) as cert_file:
|
||||
cert_contents = cert_file.read()
|
||||
|
||||
@@ -4,10 +4,10 @@ import shutil
|
||||
import socket
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
from six.moves import queue # pylint: disable=import-error
|
||||
from six.moves import socketserver # type: ignore # pylint: disable=import-error
|
||||
|
||||
import josepy as jose
|
||||
@@ -16,8 +16,8 @@ import requests
|
||||
|
||||
from acme import challenges
|
||||
from acme import crypto_util
|
||||
from acme import errors
|
||||
from acme import test_util
|
||||
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
|
||||
class TLSServerTest(unittest.TestCase):
|
||||
@@ -48,7 +48,7 @@ class TLSSNI01ServerTest(unittest.TestCase):
|
||||
test_util.load_cert('rsa2048_cert.pem'),
|
||||
)}
|
||||
from acme.standalone import TLSSNI01Server
|
||||
self.server = TLSSNI01Server(("", 0), certs=self.certs)
|
||||
self.server = TLSSNI01Server(('localhost', 0), certs=self.certs)
|
||||
# pylint: disable=no-member
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.start()
|
||||
@@ -72,7 +72,7 @@ class HTTP01ServerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.account_key = jose.JWK.load(
|
||||
test_util.load_vector('rsa1024_key.pem'))
|
||||
self.resources = set()
|
||||
self.resources = set() # type: Set
|
||||
|
||||
from acme.standalone import HTTP01Server
|
||||
self.server = HTTP01Server(('', 0), resources=self.resources)
|
||||
@@ -133,8 +133,11 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
|
||||
self.address_family = socket.AF_INET
|
||||
socketserver.TCPServer.__init__(self, *args, **kwargs)
|
||||
if ipv6:
|
||||
# NB: On Windows, socket.IPPROTO_IPV6 constant may be missing.
|
||||
# We use the corresponding value (41) instead.
|
||||
level = getattr(socket, "IPPROTO_IPV6", 41)
|
||||
# pylint: disable=no-member
|
||||
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
self.socket.setsockopt(level, socket.IPV6_V6ONLY, 1)
|
||||
try:
|
||||
self.server_bind()
|
||||
self.server_activate()
|
||||
@@ -147,15 +150,15 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
|
||||
mock_bind.side_effect = socket.error
|
||||
from acme.standalone import BaseDualNetworkedServers
|
||||
self.assertRaises(socket.error, BaseDualNetworkedServers,
|
||||
BaseDualNetworkedServersTest.SingleProtocolServer,
|
||||
("", 0),
|
||||
socketserver.BaseRequestHandler)
|
||||
BaseDualNetworkedServersTest.SingleProtocolServer,
|
||||
('', 0),
|
||||
socketserver.BaseRequestHandler)
|
||||
|
||||
def test_ports_equal(self):
|
||||
from acme.standalone import BaseDualNetworkedServers
|
||||
servers = BaseDualNetworkedServers(
|
||||
BaseDualNetworkedServersTest.SingleProtocolServer,
|
||||
("", 0),
|
||||
('', 0),
|
||||
socketserver.BaseRequestHandler)
|
||||
socknames = servers.getsocknames()
|
||||
prev_port = None
|
||||
@@ -177,7 +180,7 @@ class TLSSNI01DualNetworkedServersTest(unittest.TestCase):
|
||||
test_util.load_cert('rsa2048_cert.pem'),
|
||||
)}
|
||||
from acme.standalone import TLSSNI01DualNetworkedServers
|
||||
self.servers = TLSSNI01DualNetworkedServers(("", 0), certs=self.certs)
|
||||
self.servers = TLSSNI01DualNetworkedServers(('localhost', 0), certs=self.certs)
|
||||
self.servers.serve_forever()
|
||||
|
||||
def tearDown(self):
|
||||
@@ -201,7 +204,7 @@ class HTTP01DualNetworkedServersTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.account_key = jose.JWK.load(
|
||||
test_util.load_vector('rsa1024_key.pem'))
|
||||
self.resources = set()
|
||||
self.resources = set() # type: Set
|
||||
|
||||
from acme.standalone import HTTP01DualNetworkedServers
|
||||
self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources)
|
||||
@@ -245,6 +248,7 @@ class HTTP01DualNetworkedServersTest(unittest.TestCase):
|
||||
self.assertFalse(self._test_http01(add=False))
|
||||
|
||||
|
||||
@test_util.broken_on_windows
|
||||
class TestSimpleTLSSNI01Server(unittest.TestCase):
|
||||
"""Tests for acme.standalone.simple_tls_sni_01_server."""
|
||||
|
||||
@@ -260,10 +264,9 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
|
||||
os.path.join(localhost_dir, 'key.pem'))
|
||||
|
||||
from acme.standalone import simple_tls_sni_01_server
|
||||
self.port = 1234
|
||||
self.thread = threading.Thread(
|
||||
target=simple_tls_sni_01_server, kwargs={
|
||||
'cli_args': ('xxx', '--port', str(self.port)),
|
||||
'cli_args': ('filename',),
|
||||
'forever': False,
|
||||
},
|
||||
)
|
||||
@@ -275,25 +278,20 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
|
||||
self.thread.join()
|
||||
shutil.rmtree(self.test_cwd)
|
||||
|
||||
def test_it(self):
|
||||
max_attempts = 5
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
cert = crypto_util.probe_sni(
|
||||
b'localhost', b'0.0.0.0', self.port)
|
||||
except errors.Error:
|
||||
self.assertTrue(attempt + 1 < max_attempts, "Timeout!")
|
||||
time.sleep(1) # wait until thread starts
|
||||
else:
|
||||
self.assertEqual(jose.ComparableX509(cert),
|
||||
test_util.load_comparable_cert(
|
||||
'rsa2048_cert.pem'))
|
||||
break
|
||||
@mock.patch('acme.standalone.logger')
|
||||
def test_it(self, mock_logger):
|
||||
# Use a Queue because mock objects aren't thread safe.
|
||||
q = queue.Queue() # type: queue.Queue[int]
|
||||
# Add port number to the queue.
|
||||
mock_logger.info.side_effect = lambda *args: q.put(args[-1])
|
||||
self.thread.start()
|
||||
|
||||
if attempt == 0:
|
||||
# the first attempt is always meant to fail, so we can test
|
||||
# the socket failure code-path for probe_sni, as well
|
||||
self.thread.start()
|
||||
# After the timeout, an exception is raised if the queue is empty.
|
||||
port = q.get(timeout=5)
|
||||
cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', port)
|
||||
self.assertEqual(jose.ComparableX509(cert),
|
||||
test_util.load_comparable_cert(
|
||||
'rsa2048_cert.pem'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
|
||||
|
||||
def vector_path(*names):
|
||||
@@ -39,8 +40,8 @@ def _guess_loader(filename, loader_pem, loader_der):
|
||||
def load_cert(*names):
|
||||
"""Load certificate."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
|
||||
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
|
||||
return crypto.load_certificate(loader, load_vector(*names))
|
||||
|
||||
|
||||
def load_comparable_cert(*names):
|
||||
@@ -51,8 +52,8 @@ def load_comparable_cert(*names):
|
||||
def load_csr(*names):
|
||||
"""Load certificate request."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
|
||||
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
|
||||
return crypto.load_certificate_request(loader, load_vector(*names))
|
||||
|
||||
|
||||
def load_comparable_csr(*names):
|
||||
@@ -71,8 +72,8 @@ def load_rsa_private_key(*names):
|
||||
def load_pyopenssl_private_key(*names):
|
||||
"""Load pyOpenSSL private key."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
|
||||
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
|
||||
return crypto.load_privatekey(loader, load_vector(*names))
|
||||
|
||||
|
||||
def skip_unless(condition, reason): # pragma: no cover
|
||||
@@ -94,3 +95,11 @@ def skip_unless(condition, reason): # pragma: no cover
|
||||
return lambda cls: cls
|
||||
else:
|
||||
return lambda cls: None
|
||||
|
||||
def broken_on_windows(function):
|
||||
"""Decorator to skip temporarily a broken test on Windows."""
|
||||
reason = 'Test is broken and ignored on windows but should be fixed.'
|
||||
return unittest.skipIf(
|
||||
sys.platform == 'win32'
|
||||
and os.environ.get('SKIP_BROKEN_TESTS_ON_WINDOWS', 'true') == 'true',
|
||||
reason)(function)
|
||||
|
||||
2
acme/pytest.ini
Normal file
2
acme/pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
norecursedirs = .* build dist CVS _darcs {arch} *.egg
|
||||
@@ -1,10 +1,9 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
from setuptools.command.test import test as TestCommand
|
||||
import sys
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
@@ -19,6 +18,7 @@ install_requires = [
|
||||
'pyrfc3339',
|
||||
'pytz',
|
||||
'requests[security]>=2.4.1', # security extras added in 2.4.1
|
||||
'requests-toolbelt>=0.3.0',
|
||||
'setuptools',
|
||||
'six>=1.9.0', # needed for python_2_unicode_compatible
|
||||
]
|
||||
@@ -34,6 +34,19 @@ docs_extras = [
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
class PyTest(TestCommand):
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
TestCommand.initialize_options(self)
|
||||
self.pytest_args = ''
|
||||
|
||||
def run_tests(self):
|
||||
import shlex
|
||||
# import here, cause outside the eggs aren't loaded
|
||||
import pytest
|
||||
errno = pytest.main(shlex.split(self.pytest_args))
|
||||
sys.exit(errno)
|
||||
|
||||
setup(
|
||||
name='acme',
|
||||
@@ -45,7 +58,7 @@ setup(
|
||||
license='Apache License 2.0',
|
||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
@@ -55,6 +68,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
@@ -66,5 +80,7 @@ setup(
|
||||
'dev': dev_extras,
|
||||
'docs': docs_extras,
|
||||
},
|
||||
tests_require=["pytest"],
|
||||
test_suite='acme',
|
||||
cmdclass={"test": PyTest},
|
||||
)
|
||||
|
||||
31
appveyor.yml
Normal file
31
appveyor.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
image: Visual Studio 2015
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- TOXENV: py35
|
||||
- TOXENV: py37-cover
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^\d+\.\d+\.x$/ # Version branches like X.X.X
|
||||
- /^test-.*$/
|
||||
|
||||
install:
|
||||
# Use Python 3.7 by default
|
||||
- "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%"
|
||||
# Check env
|
||||
- "python --version"
|
||||
# Upgrade pip to avoid warnings
|
||||
- "python -m pip install --upgrade pip"
|
||||
# Ready to install tox and coverage
|
||||
- "pip install tox codecov"
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
# Test env is set by TOXENV env variable
|
||||
- tox
|
||||
|
||||
on_success:
|
||||
- if exist .coverage codecov
|
||||
@@ -1,4 +1,5 @@
|
||||
""" Utility functions for certbot-apache plugin """
|
||||
import binascii
|
||||
import os
|
||||
|
||||
from certbot import util
|
||||
@@ -98,3 +99,8 @@ def parse_define_file(filepath, varname):
|
||||
var_parts = v[2:].partition("=")
|
||||
return_vars[var_parts[0]] = var_parts[2]
|
||||
return return_vars
|
||||
|
||||
|
||||
def unique_id():
|
||||
""" Returns an unique id to be used as a VirtualHost identifier"""
|
||||
return binascii.hexlify(os.urandom(16)).decode("utf-8")
|
||||
|
||||
@@ -44,67 +44,134 @@ autoload xfm
|
||||
*****************************************************************)
|
||||
let dels (s:string) = del s s
|
||||
|
||||
(* The continuation sequence that indicates that we should consider the
|
||||
* next line part of the current line *)
|
||||
let cont = /\\\\\r?\n/
|
||||
|
||||
(* Whitespace within a line: space, tab, and the continuation sequence *)
|
||||
let ws = /[ \t]/ | cont
|
||||
|
||||
(* Any possible character - '.' does not match \n *)
|
||||
let any = /(.|\n)/
|
||||
|
||||
(* Any character preceded by a backslash *)
|
||||
let esc_any = /\\\\(.|\n)/
|
||||
|
||||
(* Newline sequence - both for Unix and DOS newlines *)
|
||||
let nl = /\r?\n/
|
||||
|
||||
(* Whitespace at the end of a line *)
|
||||
let eol = del (ws* . nl) "\n"
|
||||
|
||||
(* deal with continuation lines *)
|
||||
let sep_spc = del /([ \t]+|[ \t]*\\\\\r?\n[ \t]*)+/ " "
|
||||
let sep_osp = del /([ \t]*|[ \t]*\\\\\r?\n[ \t]*)*/ ""
|
||||
let sep_eq = del /[ \t]*=[ \t]*/ "="
|
||||
let sep_spc = del ws+ " "
|
||||
let sep_osp = del ws* ""
|
||||
let sep_eq = del (ws* . "=" . ws*) "="
|
||||
|
||||
let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/
|
||||
let word = /[a-z][a-z0-9._-]*/i
|
||||
|
||||
let eol = Util.doseol
|
||||
let empty = Util.empty_dos
|
||||
(* A complete line that is either just whitespace or a comment that only
|
||||
* contains whitespace *)
|
||||
let empty = [ del (ws* . /#?/ . ws* . nl) "\n" ]
|
||||
|
||||
let indent = Util.indent
|
||||
|
||||
let comment_val_re = /([^ \t\r\n](.|\\\\\r?\n)*[^ \\\t\r\n]|[^ \t\r\n])/
|
||||
let comment = [ label "#comment" . del /[ \t]*#[ \t]*/ "# "
|
||||
. store comment_val_re . eol ]
|
||||
(* A comment that is not just whitespace. We define it in terms of the
|
||||
* things that are not allowed as part of such a comment:
|
||||
* 1) Starts with whitespace
|
||||
* 2) Ends with whitespace, a backslash or \r
|
||||
* 3) Unescaped newlines
|
||||
*)
|
||||
let comment =
|
||||
let comment_start = del (ws* . "#" . ws* ) "# " in
|
||||
let unesc_eol = /[^\]?/ . nl in
|
||||
let w = /[^\t\n\r \\]/ in
|
||||
let r = /[\r\\]/ in
|
||||
let s = /[\t\r ]/ in
|
||||
(*
|
||||
* we'd like to write
|
||||
* let b = /\\\\/ in
|
||||
* let t = /[\t\n\r ]/ in
|
||||
* let x = b . (t? . (s|w)* ) in
|
||||
* but the definition of b depends on commit 244c0edd in 1.9.0 and
|
||||
* would make the lens unusable with versions before 1.9.0. So we write
|
||||
* x out which works in older versions, too
|
||||
*)
|
||||
let x = /\\\\[\t\n\r ]?[^\n\\]*/ in
|
||||
let line = ((r . s* . w|w|r) . (s|w)* . x*|(r.s* )?).w.(s*.w)* in
|
||||
[ label "#comment" . comment_start . store line . eol ]
|
||||
|
||||
(* borrowed from shellvars.aug *)
|
||||
let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ \t\r\n])|\\\\"|\\\\'|\\\\ /
|
||||
let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'|\\\\ /
|
||||
let char_arg_wl = /([^\\ '"},\t\r\n]|[^ '"},\t\r\n]+[^\\ '"},\t\r\n])/
|
||||
|
||||
let cdot = /\\\\./
|
||||
let cl = /\\\\\n/
|
||||
let dquot =
|
||||
let no_dquot = /[^"\\\r\n]/
|
||||
in /"/ . (no_dquot|cdot|cl)* . /"/
|
||||
in /"/ . (no_dquot|esc_any)* . /"/
|
||||
let dquot_msg =
|
||||
let no_dquot = /([^ \t"\\\r\n]|[^"\\\r\n]+[^ \t"\\\r\n])/
|
||||
in /"/ . (no_dquot|cdot|cl)*
|
||||
in /"/ . (no_dquot|esc_any)* . no_dquot
|
||||
|
||||
let squot =
|
||||
let no_squot = /[^'\\\r\n]/
|
||||
in /'/ . (no_squot|cdot|cl)* . /'/
|
||||
in /'/ . (no_squot|esc_any)* . /'/
|
||||
let comp = /[<>=]?=/
|
||||
|
||||
(******************************************************************
|
||||
* Attributes
|
||||
*****************************************************************)
|
||||
|
||||
let arg_dir = [ label "arg" . store (char_arg_dir+|dquot|squot) ]
|
||||
(* The arguments for a directive come in two flavors: quoted with single or
|
||||
* double quotes, or bare. Bare arguments may not start with a single or
|
||||
* double quote; since we also treat "word lists" special, i.e. lists
|
||||
* enclosed in curly braces, bare arguments may not start with those,
|
||||
* either.
|
||||
*
|
||||
* Bare arguments may not contain unescaped spaces, but we allow escaping
|
||||
* with '\\'. Quoted arguments can contain anything, though the quote must
|
||||
* be escaped with '\\'.
|
||||
*)
|
||||
let bare = /([^{"' \t\n\r]|\\\\.)([^ \t\n\r]|\\\\.)*[^ \t\n\r\\]|[^{"' \t\n\r\\]/
|
||||
|
||||
let arg_quoted = [ label "arg" . store (dquot|squot) ]
|
||||
let arg_bare = [ label "arg" . store bare ]
|
||||
|
||||
(* message argument starts with " but ends at EOL *)
|
||||
let arg_dir_msg = [ label "arg" . store dquot_msg ]
|
||||
let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ]
|
||||
let arg_wl = [ label "arg" . store (char_arg_wl+|dquot|squot) ]
|
||||
|
||||
(* comma-separated wordlist as permitted in the SSLRequire directive *)
|
||||
let arg_wordlist =
|
||||
let wl_start = Util.del_str "{" in
|
||||
let wl_end = Util.del_str "}" in
|
||||
let wl_start = dels "{" in
|
||||
let wl_end = dels "}" in
|
||||
let wl_sep = del /[ \t]*,[ \t]*/ ", "
|
||||
in [ label "wordlist" . wl_start . arg_wl . (wl_sep . arg_wl)* . wl_end ]
|
||||
|
||||
let argv (l:lens) = l . (sep_spc . l)*
|
||||
|
||||
(* the arguments of a directive. We use this once we have parsed the name
|
||||
* of the directive, and the space right after it. When dir_args is used,
|
||||
* we also know that we have at least one argument. We need to be careful
|
||||
* with the spacing between arguments: quoted arguments and word lists do
|
||||
* not need to have space between them, but bare arguments do.
|
||||
*
|
||||
* Apache apparently is also happy if the last argument starts with a double
|
||||
* quote, but has no corresponding closing duoble quote, which is what
|
||||
* arg_dir_msg handles
|
||||
*)
|
||||
let dir_args =
|
||||
let arg_nospc = arg_quoted|arg_wordlist in
|
||||
(arg_bare . sep_spc | arg_nospc . sep_osp)* . (arg_bare|arg_nospc|arg_dir_msg)
|
||||
|
||||
let directive =
|
||||
(* arg_dir_msg may be the last or only argument *)
|
||||
let dir_args = (argv (arg_dir|arg_wordlist) . (sep_spc . arg_dir_msg)?) | arg_dir_msg
|
||||
in [ indent . label "directive" . store word . (sep_spc . dir_args)? . eol ]
|
||||
[ indent . label "directive" . store word . (sep_spc . dir_args)? . eol ]
|
||||
|
||||
let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ]
|
||||
|
||||
let section (body:lens) =
|
||||
(* opt_eol includes empty lines *)
|
||||
let opt_eol = del /([ \t]*#?\r?\n)*/ "\n" in
|
||||
let opt_eol = del /([ \t]*#?[ \t]*\r?\n)*/ "\n" in
|
||||
let inner = (sep_spc . argv arg_sec)? . sep_osp .
|
||||
dels ">" . opt_eol . ((body|comment) . (body|empty|comment)*)? .
|
||||
indent . dels "</" in
|
||||
@@ -133,6 +200,7 @@ let filter = (incl "/etc/apache2/apache2.conf") .
|
||||
(incl "/etc/httpd/conf.d/*.conf") .
|
||||
(incl "/etc/httpd/httpd.conf") .
|
||||
(incl "/etc/httpd/conf/httpd.conf") .
|
||||
(incl "/etc/httpd/conf.modules.d/*.conf") .
|
||||
Util.stdexcl
|
||||
|
||||
let xfm = transform lns filter
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Apache Configuration based off of Augeas Configurator."""
|
||||
# pylint: disable=too-many-lines
|
||||
import copy
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
@@ -13,13 +14,16 @@ import zope.component
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
from acme.magic_typing import Any, DefaultDict, Dict, List, Set, Union # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
|
||||
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
|
||||
from certbot.plugins import common
|
||||
from certbot.plugins.util import path_surgery
|
||||
from certbot.plugins.enhancements import AutoHSTSEnhancement
|
||||
|
||||
from certbot_apache import apache_util
|
||||
from certbot_apache import augeas_configurator
|
||||
@@ -87,55 +91,79 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
"""
|
||||
|
||||
description = "Apache Web Server plugin - Beta"
|
||||
description = "Apache Web Server plugin"
|
||||
|
||||
OS_DEFAULTS = dict(
|
||||
server_root="/etc/apache2",
|
||||
vhost_root="/etc/apache2/sites-available",
|
||||
vhost_files="*",
|
||||
logs_root="/var/log/apache2",
|
||||
ctl="apache2ctl",
|
||||
version_cmd=['apache2ctl', '-v'],
|
||||
apache_cmd="apache2ctl",
|
||||
restart_cmd=['apache2ctl', 'graceful'],
|
||||
conftest_cmd=['apache2ctl', 'configtest'],
|
||||
enmod=None,
|
||||
dismod=None,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
handle_mods=False,
|
||||
handle_modules=False,
|
||||
handle_sites=False,
|
||||
challenge_location="/etc/apache2",
|
||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||
"certbot_apache", "options-ssl-apache.conf")
|
||||
)
|
||||
|
||||
def constant(self, key):
|
||||
"""Get constant for OS_DEFAULTS"""
|
||||
return self.OS_DEFAULTS.get(key)
|
||||
def option(self, key):
|
||||
"""Get a value from options"""
|
||||
return self.options.get(key)
|
||||
|
||||
def _prepare_options(self):
|
||||
"""
|
||||
Set the values possibly changed by command line parameters to
|
||||
OS_DEFAULTS constant dictionary
|
||||
"""
|
||||
opts = ["enmod", "dismod", "le_vhost_ext", "server_root", "vhost_root",
|
||||
"logs_root", "challenge_location", "handle_modules", "handle_sites",
|
||||
"ctl"]
|
||||
for o in opts:
|
||||
# Config options use dashes instead of underscores
|
||||
if self.conf(o.replace("_", "-")) is not None:
|
||||
self.options[o] = self.conf(o.replace("_", "-"))
|
||||
else:
|
||||
self.options[o] = self.OS_DEFAULTS[o]
|
||||
|
||||
# Special cases
|
||||
self.options["version_cmd"][0] = self.option("ctl")
|
||||
self.options["restart_cmd"][0] = self.option("ctl")
|
||||
self.options["conftest_cmd"][0] = self.option("ctl")
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
# When adding, modifying or deleting command line arguments, be sure to
|
||||
# include the changes in the list used in method _prepare_options() to
|
||||
# ensure consistent behavior.
|
||||
add("enmod", default=cls.OS_DEFAULTS["enmod"],
|
||||
help="Path to the Apache 'a2enmod' binary.")
|
||||
help="Path to the Apache 'a2enmod' binary")
|
||||
add("dismod", default=cls.OS_DEFAULTS["dismod"],
|
||||
help="Path to the Apache 'a2dismod' binary.")
|
||||
help="Path to the Apache 'a2dismod' binary")
|
||||
add("le-vhost-ext", default=cls.OS_DEFAULTS["le_vhost_ext"],
|
||||
help="SSL vhost configuration extension.")
|
||||
help="SSL vhost configuration extension")
|
||||
add("server-root", default=cls.OS_DEFAULTS["server_root"],
|
||||
help="Apache server root directory.")
|
||||
help="Apache server root directory")
|
||||
add("vhost-root", default=None,
|
||||
help="Apache server VirtualHost configuration root")
|
||||
add("logs-root", default=cls.OS_DEFAULTS["logs_root"],
|
||||
help="Apache server logs directory")
|
||||
add("challenge-location",
|
||||
default=cls.OS_DEFAULTS["challenge_location"],
|
||||
help="Directory path for challenge configuration.")
|
||||
add("handle-modules", default=cls.OS_DEFAULTS["handle_mods"],
|
||||
help="Let installer handle enabling required modules for you." +
|
||||
help="Directory path for challenge configuration")
|
||||
add("handle-modules", default=cls.OS_DEFAULTS["handle_modules"],
|
||||
help="Let installer handle enabling required modules for you " +
|
||||
"(Only Ubuntu/Debian currently)")
|
||||
add("handle-sites", default=cls.OS_DEFAULTS["handle_sites"],
|
||||
help="Let installer handle enabling sites for you." +
|
||||
help="Let installer handle enabling sites for you " +
|
||||
"(Only Ubuntu/Debian currently)")
|
||||
util.add_deprecated_argument(add, argument_name="ctl", nargs=1)
|
||||
add("ctl", default=cls.OS_DEFAULTS["ctl"],
|
||||
help="Full path to Apache control script")
|
||||
util.add_deprecated_argument(
|
||||
add, argument_name="init-script", nargs=1)
|
||||
|
||||
@@ -150,20 +178,23 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
super(ApacheConfigurator, self).__init__(*args, **kwargs)
|
||||
|
||||
# Add name_server association dict
|
||||
self.assoc = dict()
|
||||
self.assoc = dict() # type: Dict[str, obj.VirtualHost]
|
||||
# Outstanding challenges
|
||||
self._chall_out = set()
|
||||
self._chall_out = set() # type: Set[KeyAuthorizationAnnotatedChallenge]
|
||||
# List of vhosts configured per wildcard domain on this run.
|
||||
# used by deploy_cert() and enhance()
|
||||
self._wildcard_vhosts = dict()
|
||||
self._wildcard_vhosts = dict() # type: Dict[str, List[obj.VirtualHost]]
|
||||
# Maps enhancements to vhosts we've enabled the enhancement for
|
||||
self._enhanced_vhosts = defaultdict(set)
|
||||
self._enhanced_vhosts = defaultdict(set) # type: DefaultDict[str, Set[obj.VirtualHost]]
|
||||
# Temporary state for AutoHSTS enhancement
|
||||
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
|
||||
|
||||
# These will be set in the prepare function
|
||||
self._prepared = False
|
||||
self.parser = None
|
||||
self.version = version
|
||||
self.vhosts = None
|
||||
self.vhostroot = None
|
||||
self.options = copy.deepcopy(self.OS_DEFAULTS)
|
||||
self._enhance_func = {"redirect": self._enable_redirect,
|
||||
"ensure-http-header": self._set_http_header,
|
||||
"staple-ocsp": self._enable_ocsp_stapling}
|
||||
@@ -195,12 +226,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
except ImportError:
|
||||
raise errors.NoInstallationError("Problem in Augeas installation")
|
||||
|
||||
self._prepare_options()
|
||||
|
||||
# Verify Apache is installed
|
||||
restart_cmd = self.constant("restart_cmd")[0]
|
||||
if not util.exe_exists(restart_cmd):
|
||||
if not path_surgery(restart_cmd):
|
||||
raise errors.NoInstallationError(
|
||||
'Cannot find Apache control command {0}'.format(restart_cmd))
|
||||
self._verify_exe_availability(self.option("ctl"))
|
||||
|
||||
# Make sure configuration is valid
|
||||
self.config_test()
|
||||
@@ -220,12 +249,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"version 1.2.0 or higher, please make sure you have you have "
|
||||
"those installed.")
|
||||
|
||||
# Parse vhost-root if defined on cli
|
||||
if not self.conf("vhost-root"):
|
||||
self.vhostroot = self.constant("vhost_root")
|
||||
else:
|
||||
self.vhostroot = os.path.abspath(self.conf("vhost-root"))
|
||||
|
||||
self.parser = self.get_parser()
|
||||
|
||||
# Check for errors in parsing files with Augeas
|
||||
@@ -239,11 +262,19 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
# Prevent two Apache plugins from modifying a config at once
|
||||
try:
|
||||
util.lock_dir_until_exit(self.conf("server-root"))
|
||||
util.lock_dir_until_exit(self.option("server_root"))
|
||||
except (OSError, errors.LockError):
|
||||
logger.debug("Encountered error:", exc_info=True)
|
||||
raise errors.PluginError(
|
||||
"Unable to lock %s", self.conf("server-root"))
|
||||
"Unable to lock %s", self.option("server_root"))
|
||||
self._prepared = True
|
||||
|
||||
def _verify_exe_availability(self, exe):
|
||||
"""Checks availability of Apache executable"""
|
||||
if not util.exe_exists(exe):
|
||||
if not path_surgery(exe):
|
||||
raise errors.NoInstallationError(
|
||||
'Cannot find Apache executable {0}'.format(exe))
|
||||
|
||||
def _check_aug_version(self):
|
||||
""" Checks that we have recent enough version of libaugeas.
|
||||
@@ -262,8 +293,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
def get_parser(self):
|
||||
"""Initializes the ApacheParser"""
|
||||
# If user provided vhost_root value in command line, use it
|
||||
return parser.ApacheParser(
|
||||
self.aug, self.conf("server-root"), self.conf("vhost-root"),
|
||||
self.aug, self.option("server_root"), self.conf("vhost-root"),
|
||||
self.version, configurator=self)
|
||||
|
||||
def _wildcard_domain(self, domain):
|
||||
@@ -323,7 +355,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# Returned objects are guaranteed to be ssl vhosts
|
||||
return self._choose_vhosts_wildcard(domain, create_if_no_ssl)
|
||||
else:
|
||||
return [self.choose_vhost(domain)]
|
||||
return [self.choose_vhost(domain, create_if_no_ssl)]
|
||||
|
||||
def _vhosts_for_wildcard(self, domain):
|
||||
"""
|
||||
@@ -475,20 +507,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if chain_path is not None:
|
||||
self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path
|
||||
|
||||
def choose_vhost(self, target_name, temp=False):
|
||||
def choose_vhost(self, target_name, create_if_no_ssl=True):
|
||||
"""Chooses a virtual host based on the given domain name.
|
||||
|
||||
If there is no clear virtual host to be selected, the user is prompted
|
||||
with all available choices.
|
||||
|
||||
The returned vhost is guaranteed to have TLS enabled unless temp is
|
||||
True. If temp is True, there is no such guarantee and the result is
|
||||
not cached.
|
||||
The returned vhost is guaranteed to have TLS enabled unless
|
||||
create_if_no_ssl is set to False, in which case there is no such guarantee
|
||||
and the result is not cached.
|
||||
|
||||
:param str target_name: domain name
|
||||
:param bool temp: whether the vhost is only used temporarily
|
||||
:param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS
|
||||
counterpart, should one get created
|
||||
|
||||
:returns: ssl vhost associated with name
|
||||
:returns: vhost associated with name
|
||||
:rtype: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
:raises .errors.PluginError: If no vhost is available or chosen
|
||||
@@ -501,7 +534,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# Try to find a reasonable vhost
|
||||
vhost = self._find_best_vhost(target_name)
|
||||
if vhost is not None:
|
||||
if temp:
|
||||
if not create_if_no_ssl:
|
||||
return vhost
|
||||
if not vhost.ssl:
|
||||
vhost = self.make_vhost_ssl(vhost)
|
||||
@@ -510,7 +543,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.assoc[target_name] = vhost
|
||||
return vhost
|
||||
|
||||
return self._choose_vhost_from_list(target_name, temp)
|
||||
# Negate create_if_no_ssl value to indicate if we want a SSL vhost
|
||||
# to get created if a non-ssl vhost is selected.
|
||||
return self._choose_vhost_from_list(target_name, temp=not create_if_no_ssl)
|
||||
|
||||
def _choose_vhost_from_list(self, target_name, temp=False):
|
||||
# Select a vhost from a list
|
||||
@@ -656,7 +691,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
all_names = set()
|
||||
all_names = set() # type: Set[str]
|
||||
|
||||
vhost_macro = []
|
||||
|
||||
@@ -797,8 +832,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
"""
|
||||
# Search base config, and all included paths for VirtualHosts
|
||||
file_paths = {}
|
||||
internal_paths = defaultdict(set)
|
||||
file_paths = {} # type: Dict[str, str]
|
||||
internal_paths = defaultdict(set) # type: DefaultDict[str, Set[str]]
|
||||
vhs = []
|
||||
# Make a list of parser paths because the parser_paths
|
||||
# dictionary may be modified during the loop.
|
||||
@@ -1027,7 +1062,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:param boolean temp: If the change is temporary
|
||||
"""
|
||||
|
||||
if self.conf("handle-modules"):
|
||||
if self.option("handle_modules"):
|
||||
if self.version >= (2, 4) and ("socache_shmcb_module" not in
|
||||
self.parser.modules):
|
||||
self.enable_mod("socache_shmcb", temp=temp)
|
||||
@@ -1056,7 +1091,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
Duplicates vhost and adds default ssl options
|
||||
New vhost will reside as (nonssl_vhost.path) +
|
||||
``self.constant("le_vhost_ext")``
|
||||
``self.option("le_vhost_ext")``
|
||||
|
||||
.. note:: This function saves the configuration
|
||||
|
||||
@@ -1155,18 +1190,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
|
||||
if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
|
||||
# Defined by user on CLI
|
||||
|
||||
fp = os.path.join(os.path.realpath(self.vhostroot),
|
||||
fp = os.path.join(os.path.realpath(self.option("vhost_root")),
|
||||
os.path.basename(non_ssl_vh_fp))
|
||||
else:
|
||||
# Use non-ssl filepath
|
||||
fp = os.path.realpath(non_ssl_vh_fp)
|
||||
|
||||
if fp.endswith(".conf"):
|
||||
return fp[:-(len(".conf"))] + self.conf("le_vhost_ext")
|
||||
return fp[:-(len(".conf"))] + self.option("le_vhost_ext")
|
||||
else:
|
||||
return fp + self.conf("le_vhost_ext")
|
||||
return fp + self.option("le_vhost_ext")
|
||||
|
||||
def _sift_rewrite_rule(self, line):
|
||||
"""Decides whether a line should be copied to a SSL vhost.
|
||||
@@ -1236,7 +1269,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if not self.parser.parsed_in_current(ssl_fp):
|
||||
self.parser.parse_file(ssl_fp)
|
||||
except IOError:
|
||||
logger.fatal("Error writing/reading to file in make_vhost_ssl")
|
||||
logger.critical("Error writing/reading to file in make_vhost_ssl", exc_info=True)
|
||||
raise errors.PluginError("Unable to write/read in make_vhost_ssl")
|
||||
|
||||
if sift:
|
||||
@@ -1324,7 +1357,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
try:
|
||||
span_val = self.aug.span(vhost.path)
|
||||
except ValueError:
|
||||
logger.fatal("Error while reading the VirtualHost %s from "
|
||||
logger.critical("Error while reading the VirtualHost %s from "
|
||||
"file %s", vhost.name, vhost.filep, exc_info=True)
|
||||
raise errors.PluginError("Unable to read VirtualHost from file")
|
||||
span_filep = span_val[0]
|
||||
@@ -1467,6 +1500,67 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if need_to_save:
|
||||
self.save()
|
||||
|
||||
def find_vhost_by_id(self, id_str):
|
||||
"""
|
||||
Searches through VirtualHosts and tries to match the id in a comment
|
||||
|
||||
:param str id_str: Id string for matching
|
||||
|
||||
:returns: The matched VirtualHost or None
|
||||
:rtype: :class:`~certbot_apache.obj.VirtualHost` or None
|
||||
|
||||
:raises .errors.PluginError: If no VirtualHost is found
|
||||
"""
|
||||
|
||||
for vh in self.vhosts:
|
||||
if self._find_vhost_id(vh) == id_str:
|
||||
return vh
|
||||
msg = "No VirtualHost with ID {} was found.".format(id_str)
|
||||
logger.warning(msg)
|
||||
raise errors.PluginError(msg)
|
||||
|
||||
def _find_vhost_id(self, vhost):
|
||||
"""Tries to find the unique ID from the VirtualHost comments. This is
|
||||
used for keeping track of VirtualHost directive over time.
|
||||
|
||||
:param vhost: Virtual host to add the id
|
||||
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
:returns: The unique ID or None
|
||||
:rtype: str or None
|
||||
"""
|
||||
|
||||
# Strip the {} off from the format string
|
||||
search_comment = constants.MANAGED_COMMENT_ID.format("")
|
||||
|
||||
id_comment = self.parser.find_comments(search_comment, vhost.path)
|
||||
if id_comment:
|
||||
# Use the first value, multiple ones shouldn't exist
|
||||
comment = self.parser.get_arg(id_comment[0])
|
||||
return comment.split(" ")[-1]
|
||||
return None
|
||||
|
||||
def add_vhost_id(self, vhost):
|
||||
"""Adds an unique ID to the VirtualHost as a comment for mapping back
|
||||
to it on later invocations, as the config file order might have changed.
|
||||
If ID already exists, returns that instead.
|
||||
|
||||
:param vhost: Virtual host to add or find the id
|
||||
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
:returns: The unique ID for vhost
|
||||
:rtype: str or None
|
||||
"""
|
||||
|
||||
vh_id = self._find_vhost_id(vhost)
|
||||
if vh_id:
|
||||
return vh_id
|
||||
|
||||
id_string = apache_util.unique_id()
|
||||
comment = constants.MANAGED_COMMENT_ID.format(id_string)
|
||||
self.parser.add_comment(vhost.path, comment)
|
||||
return id_string
|
||||
|
||||
def _escape(self, fp):
|
||||
fp = fp.replace(",", "\\,")
|
||||
fp = fp.replace("[", "\\[")
|
||||
@@ -1505,7 +1599,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
raise errors.PluginError(
|
||||
"Unsupported enhancement: {0}".format(enhancement))
|
||||
|
||||
vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)
|
||||
matched_vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)
|
||||
# We should be handling only SSL vhosts for enhancements
|
||||
vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
|
||||
|
||||
if not vhosts:
|
||||
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
|
||||
"domain {0} for enabling enhancement \"{1}\". The requested "
|
||||
"enhancement was not configured.")
|
||||
msg_enhancement = enhancement
|
||||
if options:
|
||||
msg_enhancement += ": " + options
|
||||
msg = msg_tmpl.format(domain, msg_enhancement)
|
||||
logger.warning(msg)
|
||||
raise errors.PluginError(msg)
|
||||
try:
|
||||
for vhost in vhosts:
|
||||
func(vhost, options)
|
||||
@@ -1513,6 +1620,78 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
logger.warning("Failed %s for %s", enhancement, domain)
|
||||
raise
|
||||
|
||||
def _autohsts_increase(self, vhost, id_str, nextstep):
|
||||
"""Increase the AutoHSTS max-age value
|
||||
|
||||
:param vhost: Virtual host object to modify
|
||||
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
:param str id_str: The unique ID string of VirtualHost
|
||||
|
||||
:param int nextstep: Next AutoHSTS max-age value index
|
||||
|
||||
"""
|
||||
nextstep_value = constants.AUTOHSTS_STEPS[nextstep]
|
||||
self._autohsts_write(vhost, nextstep_value)
|
||||
self._autohsts[id_str] = {"laststep": nextstep, "timestamp": time.time()}
|
||||
|
||||
def _autohsts_write(self, vhost, nextstep_value):
|
||||
"""
|
||||
Write the new HSTS max-age value to the VirtualHost file
|
||||
"""
|
||||
|
||||
hsts_dirpath = None
|
||||
header_path = self.parser.find_dir("Header", None, vhost.path)
|
||||
if header_path:
|
||||
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
|
||||
for match in header_path:
|
||||
if re.search(pat, self.aug.get(match).lower()):
|
||||
hsts_dirpath = match
|
||||
if not hsts_dirpath:
|
||||
err_msg = ("Certbot was unable to find the existing HSTS header "
|
||||
"from the VirtualHost at path {0}.").format(vhost.filep)
|
||||
raise errors.PluginError(err_msg)
|
||||
|
||||
# Prepare the HSTS header value
|
||||
hsts_maxage = "\"max-age={0}\"".format(nextstep_value)
|
||||
|
||||
# Update the header
|
||||
# Our match statement was for string strict-transport-security, but
|
||||
# we need to update the value instead. The next index is for the value
|
||||
hsts_dirpath = hsts_dirpath.replace("arg[3]", "arg[4]")
|
||||
self.aug.set(hsts_dirpath, hsts_maxage)
|
||||
note_msg = ("Increasing HSTS max-age value to {0} for VirtualHost "
|
||||
"in {1}\n".format(nextstep_value, vhost.filep))
|
||||
logger.debug(note_msg)
|
||||
self.save_notes += note_msg
|
||||
self.save(note_msg)
|
||||
|
||||
def _autohsts_fetch_state(self):
|
||||
"""
|
||||
Populates the AutoHSTS state from the pluginstorage
|
||||
"""
|
||||
try:
|
||||
self._autohsts = self.storage.fetch("autohsts")
|
||||
except KeyError:
|
||||
self._autohsts = dict()
|
||||
|
||||
def _autohsts_save_state(self):
|
||||
"""
|
||||
Saves the state of AutoHSTS object to pluginstorage
|
||||
"""
|
||||
self.storage.put("autohsts", self._autohsts)
|
||||
self.storage.save()
|
||||
|
||||
def _autohsts_vhost_in_lineage(self, vhost, lineage):
|
||||
"""
|
||||
Searches AutoHSTS managed VirtualHosts that belong to the lineage.
|
||||
Matches the private key path.
|
||||
"""
|
||||
|
||||
return bool(
|
||||
self.parser.find_dir("SSLCertificateKeyFile",
|
||||
lineage.key_path, vhost.path))
|
||||
|
||||
def _enable_ocsp_stapling(self, ssl_vhost, unused_options):
|
||||
"""Enables OCSP Stapling
|
||||
|
||||
@@ -1754,7 +1933,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# There can be other RewriteRule directive lines in vhost config.
|
||||
# rewrite_args_dict keys are directive ids and the corresponding value
|
||||
# for each is a list of arguments to that directive.
|
||||
rewrite_args_dict = defaultdict(list)
|
||||
rewrite_args_dict = defaultdict(list) # type: DefaultDict[str, List[str]]
|
||||
pat = r'(.*directive\[\d+\]).*'
|
||||
for match in rewrite_path:
|
||||
m = re.match(pat, match)
|
||||
@@ -1848,7 +2027,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if ssl_vhost.aliases:
|
||||
serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases)
|
||||
|
||||
rewrite_rule_args = []
|
||||
rewrite_rule_args = [] # type: List[str]
|
||||
if self.get_version() >= (2, 3, 9):
|
||||
rewrite_rule_args = constants.REWRITE_HTTPS_ARGS_WITH_END
|
||||
else:
|
||||
@@ -1869,7 +2048,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
addr in self._get_proposed_addrs(ssl_vhost)),
|
||||
servername, serveralias,
|
||||
" ".join(rewrite_rule_args),
|
||||
self.conf("logs-root")))
|
||||
self.option("logs_root")))
|
||||
|
||||
def _write_out_redirect(self, ssl_vhost, text):
|
||||
# This is the default name
|
||||
@@ -1881,7 +2060,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
|
||||
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.name
|
||||
|
||||
redirect_filepath = os.path.join(self.vhostroot,
|
||||
redirect_filepath = os.path.join(self.option("vhost_root"),
|
||||
redirect_filename)
|
||||
|
||||
# Register the new file that will be created
|
||||
@@ -2000,10 +2179,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:raises .errors.MisconfigurationError: If reload fails
|
||||
|
||||
"""
|
||||
error = ""
|
||||
try:
|
||||
util.run_script(self.constant("restart_cmd"))
|
||||
util.run_script(self.option("restart_cmd"))
|
||||
except errors.SubprocessError as err:
|
||||
raise errors.MisconfigurationError(str(err))
|
||||
logger.info("Unable to restart apache using %s",
|
||||
self.option("restart_cmd"))
|
||||
alt_restart = self.option("restart_cmd_alt")
|
||||
if alt_restart:
|
||||
logger.debug("Trying alternative restart command: %s",
|
||||
alt_restart)
|
||||
# There is an alternative restart command available
|
||||
# This usually is "restart" verb while original is "graceful"
|
||||
try:
|
||||
util.run_script(self.option(
|
||||
"restart_cmd_alt"))
|
||||
return
|
||||
except errors.SubprocessError as secerr:
|
||||
error = str(secerr)
|
||||
else:
|
||||
error = str(err)
|
||||
raise errors.MisconfigurationError(error)
|
||||
|
||||
def config_test(self): # pylint: disable=no-self-use
|
||||
"""Check the configuration of Apache for errors.
|
||||
@@ -2012,7 +2208,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
"""
|
||||
try:
|
||||
util.run_script(self.constant("conftest_cmd"))
|
||||
util.run_script(self.option("conftest_cmd"))
|
||||
except errors.SubprocessError as err:
|
||||
raise errors.MisconfigurationError(str(err))
|
||||
|
||||
@@ -2028,11 +2224,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
"""
|
||||
try:
|
||||
stdout, _ = util.run_script(self.constant("version_cmd"))
|
||||
stdout, _ = util.run_script(self.option("version_cmd"))
|
||||
except errors.SubprocessError:
|
||||
raise errors.PluginError(
|
||||
"Unable to run %s -v" %
|
||||
self.constant("version_cmd"))
|
||||
self.option("version_cmd"))
|
||||
|
||||
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
|
||||
matches = regex.findall(stdout)
|
||||
@@ -2057,7 +2253,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
###########################################################################
|
||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||
"""Return list of challenge preferences."""
|
||||
return [challenges.TLSSNI01, challenges.HTTP01]
|
||||
return [challenges.HTTP01, challenges.TLSSNI01]
|
||||
|
||||
def perform(self, achalls):
|
||||
"""Perform the configuration related challenge.
|
||||
@@ -2122,4 +2318,181 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# certbot for unprivileged users via setuid), this function will need
|
||||
# to be modified.
|
||||
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
|
||||
self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
|
||||
self.option("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
|
||||
|
||||
def enable_autohsts(self, _unused_lineage, domains):
|
||||
"""
|
||||
Enable the AutoHSTS enhancement for defined domains
|
||||
|
||||
:param _unused_lineage: Certificate lineage object, unused
|
||||
:type _unused_lineage: certbot.storage.RenewableCert
|
||||
|
||||
:param domains: List of domains in certificate to enhance
|
||||
:type domains: str
|
||||
"""
|
||||
|
||||
self._autohsts_fetch_state()
|
||||
_enhanced_vhosts = []
|
||||
for d in domains:
|
||||
matched_vhosts = self.choose_vhosts(d, create_if_no_ssl=False)
|
||||
# We should be handling only SSL vhosts for AutoHSTS
|
||||
vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
|
||||
|
||||
if not vhosts:
|
||||
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
|
||||
"domain {0} for enabling AutoHSTS enhancement.")
|
||||
msg = msg_tmpl.format(d)
|
||||
logger.warning(msg)
|
||||
raise errors.PluginError(msg)
|
||||
for vh in vhosts:
|
||||
try:
|
||||
self._enable_autohsts_domain(vh)
|
||||
_enhanced_vhosts.append(vh)
|
||||
except errors.PluginEnhancementAlreadyPresent:
|
||||
if vh in _enhanced_vhosts:
|
||||
continue
|
||||
msg = ("VirtualHost for domain {0} in file {1} has a " +
|
||||
"String-Transport-Security header present, exiting.")
|
||||
raise errors.PluginEnhancementAlreadyPresent(
|
||||
msg.format(d, vh.filep))
|
||||
if _enhanced_vhosts:
|
||||
note_msg = "Enabling AutoHSTS"
|
||||
self.save(note_msg)
|
||||
logger.info(note_msg)
|
||||
self.restart()
|
||||
|
||||
# Save the current state to pluginstorage
|
||||
self._autohsts_save_state()
|
||||
|
||||
def _enable_autohsts_domain(self, ssl_vhost):
|
||||
"""Do the initial AutoHSTS deployment to a vhost
|
||||
|
||||
:param ssl_vhost: The VirtualHost object to deploy the AutoHSTS
|
||||
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost` or None
|
||||
|
||||
:raises errors.PluginEnhancementAlreadyPresent: When already enhanced
|
||||
|
||||
"""
|
||||
# This raises the exception
|
||||
self._verify_no_matching_http_header(ssl_vhost,
|
||||
"Strict-Transport-Security")
|
||||
|
||||
if "headers_module" not in self.parser.modules:
|
||||
self.enable_mod("headers")
|
||||
# Prepare the HSTS header value
|
||||
hsts_header = constants.HEADER_ARGS["Strict-Transport-Security"][:-1]
|
||||
initial_maxage = constants.AUTOHSTS_STEPS[0]
|
||||
hsts_header.append("\"max-age={0}\"".format(initial_maxage))
|
||||
|
||||
# Add ID to the VirtualHost for mapping back to it later
|
||||
uniq_id = self.add_vhost_id(ssl_vhost)
|
||||
self.save_notes += "Adding unique ID {0} to VirtualHost in {1}\n".format(
|
||||
uniq_id, ssl_vhost.filep)
|
||||
# Add the actual HSTS header
|
||||
self.parser.add_dir(ssl_vhost.path, "Header", hsts_header)
|
||||
note_msg = ("Adding gradually increasing HSTS header with initial value "
|
||||
"of {0} to VirtualHost in {1}\n".format(
|
||||
initial_maxage, ssl_vhost.filep))
|
||||
self.save_notes += note_msg
|
||||
|
||||
# Save the current state to pluginstorage
|
||||
self._autohsts[uniq_id] = {"laststep": 0, "timestamp": time.time()}
|
||||
|
||||
def update_autohsts(self, _unused_domain):
|
||||
"""
|
||||
Increase the AutoHSTS values of VirtualHosts that the user has enabled
|
||||
this enhancement for.
|
||||
|
||||
:param _unused_domain: Not currently used
|
||||
:type _unused_domain: Not Available
|
||||
|
||||
"""
|
||||
self._autohsts_fetch_state()
|
||||
if not self._autohsts:
|
||||
# No AutoHSTS enabled for any domain
|
||||
return
|
||||
curtime = time.time()
|
||||
save_and_restart = False
|
||||
for id_str, config in list(self._autohsts.items()):
|
||||
if config["timestamp"] + constants.AUTOHSTS_FREQ > curtime:
|
||||
# Skip if last increase was < AUTOHSTS_FREQ ago
|
||||
continue
|
||||
nextstep = config["laststep"] + 1
|
||||
if nextstep < len(constants.AUTOHSTS_STEPS):
|
||||
# If installer hasn't been prepared yet, do it now
|
||||
if not self._prepared:
|
||||
self.prepare()
|
||||
# Have not reached the max value yet
|
||||
try:
|
||||
vhost = self.find_vhost_by_id(id_str)
|
||||
except errors.PluginError:
|
||||
msg = ("Could not find VirtualHost with ID {0}, disabling "
|
||||
"AutoHSTS for this VirtualHost").format(id_str)
|
||||
logger.warning(msg)
|
||||
# Remove the orphaned AutoHSTS entry from pluginstorage
|
||||
self._autohsts.pop(id_str)
|
||||
continue
|
||||
self._autohsts_increase(vhost, id_str, nextstep)
|
||||
msg = ("Increasing HSTS max-age value for VirtualHost with id "
|
||||
"{0}").format(id_str)
|
||||
self.save_notes += msg
|
||||
save_and_restart = True
|
||||
|
||||
if save_and_restart:
|
||||
self.save("Increased HSTS max-age values")
|
||||
self.restart()
|
||||
|
||||
self._autohsts_save_state()
|
||||
|
||||
def deploy_autohsts(self, lineage):
|
||||
"""
|
||||
Checks if autohsts vhost has reached maximum auto-increased value
|
||||
and changes the HSTS max-age to a high value.
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: certbot.storage.RenewableCert
|
||||
"""
|
||||
self._autohsts_fetch_state()
|
||||
if not self._autohsts:
|
||||
# No autohsts enabled for any vhost
|
||||
return
|
||||
|
||||
vhosts = []
|
||||
affected_ids = []
|
||||
# Copy, as we are removing from the dict inside the loop
|
||||
for id_str, config in list(self._autohsts.items()):
|
||||
if config["laststep"]+1 >= len(constants.AUTOHSTS_STEPS):
|
||||
# max value reached, try to make permanent
|
||||
try:
|
||||
vhost = self.find_vhost_by_id(id_str)
|
||||
except errors.PluginError:
|
||||
msg = ("VirtualHost with id {} was not found, unable to "
|
||||
"make HSTS max-age permanent.").format(id_str)
|
||||
logger.warning(msg)
|
||||
self._autohsts.pop(id_str)
|
||||
continue
|
||||
if self._autohsts_vhost_in_lineage(vhost, lineage):
|
||||
vhosts.append(vhost)
|
||||
affected_ids.append(id_str)
|
||||
|
||||
save_and_restart = False
|
||||
for vhost in vhosts:
|
||||
self._autohsts_write(vhost, constants.AUTOHSTS_PERMANENT)
|
||||
msg = ("Strict-Transport-Security max-age value for "
|
||||
"VirtualHost in {0} was made permanent.").format(vhost.filep)
|
||||
logger.debug(msg)
|
||||
self.save_notes += msg+"\n"
|
||||
save_and_restart = True
|
||||
|
||||
if save_and_restart:
|
||||
self.save("Made HSTS max-age permanent")
|
||||
self.restart()
|
||||
|
||||
for id_str in affected_ids:
|
||||
self._autohsts.pop(id_str)
|
||||
|
||||
# Update AutoHSTS storage (We potentially removed vhosts from managed)
|
||||
self._autohsts_save_state()
|
||||
|
||||
|
||||
AutoHSTSEnhancement.register(ApacheConfigurator) # pylint: disable=no-member
|
||||
|
||||
@@ -48,3 +48,16 @@ UIR_ARGS = ["always", "set", "Content-Security-Policy",
|
||||
|
||||
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
|
||||
"Upgrade-Insecure-Requests": UIR_ARGS}
|
||||
|
||||
AUTOHSTS_STEPS = [60, 300, 900, 3600, 21600, 43200, 86400]
|
||||
"""AutoHSTS increase steps: 1min, 5min, 15min, 1h, 6h, 12h, 24h"""
|
||||
|
||||
AUTOHSTS_PERMANENT = 31536000
|
||||
"""Value for the last max-age of HSTS"""
|
||||
|
||||
AUTOHSTS_FREQ = 172800
|
||||
"""Minimum time since last increase to perform a new one: 48h"""
|
||||
|
||||
MANAGED_COMMENT = "DO NOT REMOVE - Managed by Certbot"
|
||||
MANAGED_COMMENT_ID = MANAGED_COMMENT+", VirtualHost id: {0}"
|
||||
"""Managed by Certbot comments and the VirtualHost identification template"""
|
||||
|
||||
@@ -113,8 +113,7 @@ def _vhost_menu(domain, vhosts):
|
||||
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
|
||||
"We were unable to find a vhost with a ServerName "
|
||||
"or Address of {0}.{1}Which virtual host would you "
|
||||
"like to choose?\n(note: conf files with multiple "
|
||||
"vhosts are not yet supported)".format(domain, os.linesep),
|
||||
"like to choose?".format(domain, os.linesep),
|
||||
choices, force_interactive=True)
|
||||
except errors.MissingCommandlineFlag:
|
||||
msg = (
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
|
||||
from certbot import errors
|
||||
|
||||
from certbot.plugins import common
|
||||
from certbot_apache.obj import VirtualHost # pylint: disable=unused-import
|
||||
from certbot_apache.parser import get_aug_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,7 +53,7 @@ class ApacheHttp01(common.TLSSNI01):
|
||||
self.challenge_dir = os.path.join(
|
||||
self.configurator.config.work_dir,
|
||||
"http_challenges")
|
||||
self.moded_vhosts = set()
|
||||
self.moded_vhosts = set() # type: Set[VirtualHost]
|
||||
|
||||
def perform(self):
|
||||
"""Perform all HTTP-01 challenges."""
|
||||
@@ -171,4 +173,9 @@ class ApacheHttp01(common.TLSSNI01):
|
||||
self.configurator.parser.add_dir(
|
||||
vhost.path, "Include", self.challenge_conf_post)
|
||||
|
||||
if not vhost.enabled:
|
||||
self.configurator.parser.add_dir(
|
||||
get_aug_path(self.configurator.parser.loc["default"]),
|
||||
"Include", vhost.filep)
|
||||
|
||||
self.moded_vhosts.add(vhost)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Module contains classes used by the Apache Configurator."""
|
||||
import re
|
||||
|
||||
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
|
||||
from certbot.plugins import common
|
||||
|
||||
|
||||
@@ -140,7 +141,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
|
||||
def get_names(self):
|
||||
"""Return a set of all names."""
|
||||
all_names = set()
|
||||
all_names = set() # type: Set[str]
|
||||
all_names.update(self.aliases)
|
||||
# Strip out any scheme:// and <port> field from servername
|
||||
if self.name is not None:
|
||||
@@ -251,7 +252,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
|
||||
# already_found acts to keep everything very conservative.
|
||||
# Don't allow multiple ip:ports in same set.
|
||||
already_found = set()
|
||||
already_found = set() # type: Set[str]
|
||||
|
||||
for addr in vhost.addrs:
|
||||
for local_addr in self.addrs:
|
||||
|
||||
@@ -16,14 +16,14 @@ class ArchConfigurator(configurator.ApacheConfigurator):
|
||||
vhost_root="/etc/httpd/conf",
|
||||
vhost_files="*.conf",
|
||||
logs_root="/var/log/httpd",
|
||||
ctl="apachectl",
|
||||
version_cmd=['apachectl', '-v'],
|
||||
apache_cmd="apachectl",
|
||||
restart_cmd=['apachectl', 'graceful'],
|
||||
conftest_cmd=['apachectl', 'configtest'],
|
||||
enmod=None,
|
||||
dismod=None,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
handle_mods=False,
|
||||
handle_modules=False,
|
||||
handle_sites=False,
|
||||
challenge_location="/etc/httpd/conf",
|
||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||
|
||||
@@ -18,24 +18,33 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
|
||||
vhost_root="/etc/httpd/conf.d",
|
||||
vhost_files="*.conf",
|
||||
logs_root="/var/log/httpd",
|
||||
ctl="apachectl",
|
||||
version_cmd=['apachectl', '-v'],
|
||||
apache_cmd="apachectl",
|
||||
restart_cmd=['apachectl', 'graceful'],
|
||||
restart_cmd_alt=['apachectl', 'restart'],
|
||||
conftest_cmd=['apachectl', 'configtest'],
|
||||
enmod=None,
|
||||
dismod=None,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
handle_mods=False,
|
||||
handle_modules=False,
|
||||
handle_sites=False,
|
||||
challenge_location="/etc/httpd/conf.d",
|
||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||
"certbot_apache", "centos-options-ssl-apache.conf")
|
||||
)
|
||||
|
||||
def _prepare_options(self):
|
||||
"""
|
||||
Override the options dictionary initialization in order to support
|
||||
alternative restart cmd used in CentOS.
|
||||
"""
|
||||
super(CentOSConfigurator, self)._prepare_options()
|
||||
self.options["restart_cmd_alt"][0] = self.option("ctl")
|
||||
|
||||
def get_parser(self):
|
||||
"""Initializes the ApacheParser"""
|
||||
return CentOSParser(
|
||||
self.aug, self.conf("server-root"), self.conf("vhost-root"),
|
||||
self.aug, self.option("server_root"), self.option("vhost_root"),
|
||||
self.version, configurator=self)
|
||||
|
||||
|
||||
@@ -46,10 +55,10 @@ class CentOSParser(parser.ApacheParser):
|
||||
self.sysconfig_filep = "/etc/sysconfig/httpd"
|
||||
super(CentOSParser, self).__init__(*args, **kwargs)
|
||||
|
||||
def update_runtime_variables(self, *args, **kwargs):
|
||||
def update_runtime_variables(self):
|
||||
""" Override for update_runtime_variables for custom parsing """
|
||||
# Opportunistic, works if SELinux not enforced
|
||||
super(CentOSParser, self).update_runtime_variables(*args, **kwargs)
|
||||
super(CentOSParser, self).update_runtime_variables()
|
||||
self.parse_sysconfig_var()
|
||||
|
||||
def parse_sysconfig_var(self):
|
||||
|
||||
@@ -16,14 +16,14 @@ class DarwinConfigurator(configurator.ApacheConfigurator):
|
||||
vhost_root="/etc/apache2/other",
|
||||
vhost_files="*.conf",
|
||||
logs_root="/var/log/apache2",
|
||||
version_cmd=['/usr/sbin/httpd', '-v'],
|
||||
apache_cmd="/usr/sbin/httpd",
|
||||
ctl="apachectl",
|
||||
version_cmd=['apachectl', '-v'],
|
||||
restart_cmd=['apachectl', 'graceful'],
|
||||
conftest_cmd=['apachectl', 'configtest'],
|
||||
enmod=None,
|
||||
dismod=None,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
handle_mods=False,
|
||||
handle_modules=False,
|
||||
handle_sites=False,
|
||||
challenge_location="/etc/apache2/other",
|
||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||
|
||||
@@ -23,14 +23,14 @@ class DebianConfigurator(configurator.ApacheConfigurator):
|
||||
vhost_root="/etc/apache2/sites-available",
|
||||
vhost_files="*",
|
||||
logs_root="/var/log/apache2",
|
||||
ctl="apache2ctl",
|
||||
version_cmd=['apache2ctl', '-v'],
|
||||
apache_cmd="apache2ctl",
|
||||
restart_cmd=['apache2ctl', 'graceful'],
|
||||
conftest_cmd=['apache2ctl', 'configtest'],
|
||||
enmod="a2enmod",
|
||||
dismod="a2dismod",
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
handle_mods=True,
|
||||
handle_modules=True,
|
||||
handle_sites=True,
|
||||
challenge_location="/etc/apache2",
|
||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||
@@ -134,11 +134,11 @@ class DebianConfigurator(configurator.ApacheConfigurator):
|
||||
# Generate reversal command.
|
||||
# Try to be safe here... check that we can probably reverse before
|
||||
# applying enmod command
|
||||
if not util.exe_exists(self.conf("dismod")):
|
||||
if not util.exe_exists(self.option("dismod")):
|
||||
raise errors.MisconfigurationError(
|
||||
"Unable to find a2dismod, please make sure a2enmod and "
|
||||
"a2dismod are configured correctly for certbot.")
|
||||
|
||||
self.reverter.register_undo_command(
|
||||
temp, [self.conf("dismod"), "-f", mod_name])
|
||||
util.run_script([self.conf("enmod"), mod_name])
|
||||
temp, [self.option("dismod"), "-f", mod_name])
|
||||
util.run_script([self.option("enmod"), mod_name])
|
||||
|
||||
@@ -18,24 +18,33 @@ class GentooConfigurator(configurator.ApacheConfigurator):
|
||||
vhost_root="/etc/apache2/vhosts.d",
|
||||
vhost_files="*.conf",
|
||||
logs_root="/var/log/apache2",
|
||||
version_cmd=['/usr/sbin/apache2', '-v'],
|
||||
apache_cmd="apache2ctl",
|
||||
ctl="apache2ctl",
|
||||
version_cmd=['apache2ctl', '-v'],
|
||||
restart_cmd=['apache2ctl', 'graceful'],
|
||||
restart_cmd_alt=['apache2ctl', 'restart'],
|
||||
conftest_cmd=['apache2ctl', 'configtest'],
|
||||
enmod=None,
|
||||
dismod=None,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
handle_mods=False,
|
||||
handle_modules=False,
|
||||
handle_sites=False,
|
||||
challenge_location="/etc/apache2/vhosts.d",
|
||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||
"certbot_apache", "options-ssl-apache.conf")
|
||||
)
|
||||
|
||||
def _prepare_options(self):
|
||||
"""
|
||||
Override the options dictionary initialization in order to support
|
||||
alternative restart cmd used in Gentoo.
|
||||
"""
|
||||
super(GentooConfigurator, self)._prepare_options()
|
||||
self.options["restart_cmd_alt"][0] = self.option("ctl")
|
||||
|
||||
def get_parser(self):
|
||||
"""Initializes the ApacheParser"""
|
||||
return GentooParser(
|
||||
self.aug, self.conf("server-root"), self.conf("vhost-root"),
|
||||
self.aug, self.option("server_root"), self.option("vhost_root"),
|
||||
self.version, configurator=self)
|
||||
|
||||
|
||||
@@ -60,7 +69,7 @@ class GentooParser(parser.ApacheParser):
|
||||
|
||||
def update_modules(self):
|
||||
"""Get loaded modules from httpd process, and add them to DOM"""
|
||||
mod_cmd = [self.configurator.constant("apache_cmd"), "modules"]
|
||||
mod_cmd = [self.configurator.option("ctl"), "modules"]
|
||||
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
|
||||
for mod in matches:
|
||||
self.add_mod(mod.strip())
|
||||
|
||||
@@ -16,14 +16,14 @@ class OpenSUSEConfigurator(configurator.ApacheConfigurator):
|
||||
vhost_root="/etc/apache2/vhosts.d",
|
||||
vhost_files="*.conf",
|
||||
logs_root="/var/log/apache2",
|
||||
ctl="apache2ctl",
|
||||
version_cmd=['apache2ctl', '-v'],
|
||||
apache_cmd="apache2ctl",
|
||||
restart_cmd=['apache2ctl', 'graceful'],
|
||||
conftest_cmd=['apache2ctl', 'configtest'],
|
||||
enmod="a2enmod",
|
||||
dismod="a2dismod",
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
handle_mods=False,
|
||||
handle_modules=False,
|
||||
handle_sites=False,
|
||||
challenge_location="/etc/apache2/vhosts.d",
|
||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||
|
||||
@@ -9,12 +9,14 @@ import sys
|
||||
|
||||
import six
|
||||
|
||||
from acme.magic_typing import Dict, List, Set # pylint: disable=unused-import, no-name-in-module
|
||||
from certbot import errors
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApacheParser(object):
|
||||
# pylint: disable=too-many-public-methods
|
||||
"""Class handles the fine details of parsing the Apache Configuration.
|
||||
|
||||
.. todo:: Make parsing general... remove sites-available etc...
|
||||
@@ -38,9 +40,9 @@ class ApacheParser(object):
|
||||
# issues with aug.load() after adding new files / defines to parse tree
|
||||
self.configurator = configurator
|
||||
|
||||
self.modules = set()
|
||||
self.parser_paths = {}
|
||||
self.variables = {}
|
||||
self.modules = set() # type: Set[str]
|
||||
self.parser_paths = {} # type: Dict[str, List[str]]
|
||||
self.variables = {} # type: Dict[str, str]
|
||||
|
||||
self.aug = aug
|
||||
# Find configuration root and make sure augeas can parse it.
|
||||
@@ -67,7 +69,7 @@ class ApacheParser(object):
|
||||
# Must also attempt to parse additional virtual host root
|
||||
if vhostroot:
|
||||
self.parse_file(os.path.abspath(vhostroot) + "/" +
|
||||
self.configurator.constant("vhost_files"))
|
||||
self.configurator.option("vhost_files"))
|
||||
|
||||
# check to see if there were unparsed define statements
|
||||
if version < (2, 4):
|
||||
@@ -119,7 +121,7 @@ class ApacheParser(object):
|
||||
the iteration issue. Else... parse and enable mods at same time.
|
||||
|
||||
"""
|
||||
mods = set()
|
||||
mods = set() # type: Set[str]
|
||||
matches = self.find_dir("LoadModule")
|
||||
iterator = iter(matches)
|
||||
# Make sure prev_size != cur_size for do: while: iteration
|
||||
@@ -150,7 +152,7 @@ class ApacheParser(object):
|
||||
"""Get Defines from httpd process"""
|
||||
|
||||
variables = dict()
|
||||
define_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D",
|
||||
define_cmd = [self.configurator.option("ctl"), "-t", "-D",
|
||||
"DUMP_RUN_CFG"]
|
||||
matches = self.parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
|
||||
try:
|
||||
@@ -177,7 +179,7 @@ class ApacheParser(object):
|
||||
# configuration files
|
||||
_ = self.find_dir("Include")
|
||||
|
||||
inc_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D",
|
||||
inc_cmd = [self.configurator.option("ctl"), "-t", "-D",
|
||||
"DUMP_INCLUDES"]
|
||||
matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
|
||||
if matches:
|
||||
@@ -188,7 +190,7 @@ class ApacheParser(object):
|
||||
def update_modules(self):
|
||||
"""Get loaded modules from httpd process, and add them to DOM"""
|
||||
|
||||
mod_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D",
|
||||
mod_cmd = [self.configurator.option("ctl"), "-t", "-D",
|
||||
"DUMP_MODULES"]
|
||||
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
|
||||
for mod in matches:
|
||||
@@ -349,6 +351,37 @@ class ApacheParser(object):
|
||||
else:
|
||||
self.aug.set(first_dir + "/arg", args)
|
||||
|
||||
def add_comment(self, aug_conf_path, comment):
|
||||
"""Adds the comment to the augeas path
|
||||
|
||||
:param str aug_conf_path: Augeas configuration path to add directive
|
||||
:param str comment: Comment content
|
||||
|
||||
"""
|
||||
self.aug.set(aug_conf_path + "/#comment[last() + 1]", comment)
|
||||
|
||||
def find_comments(self, arg, start=None):
|
||||
"""Finds a comment with specified content from the provided DOM path
|
||||
|
||||
:param str arg: Comment content to search
|
||||
:param str start: Beginning Augeas path to begin looking
|
||||
|
||||
:returns: List of augeas paths containing the comment content
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
if not start:
|
||||
start = get_aug_path(self.root)
|
||||
|
||||
comments = self.aug.match("%s//*[label() = '#comment']" % start)
|
||||
|
||||
results = []
|
||||
for comment in comments:
|
||||
c_content = self.aug.get(comment)
|
||||
if c_content and arg in c_content:
|
||||
results.append(comment)
|
||||
return results
|
||||
|
||||
def find_dir(self, directive, arg=None, start=None, exclude=True):
|
||||
"""Finds directive in the configuration.
|
||||
|
||||
@@ -408,7 +441,7 @@ class ApacheParser(object):
|
||||
else:
|
||||
arg_suffix = "/*[self::arg=~regexp('%s')]" % case_i(arg)
|
||||
|
||||
ordered_matches = []
|
||||
ordered_matches = [] # type: List[str]
|
||||
|
||||
# TODO: Wildcards should be included in alphabetical order
|
||||
# https://httpd.apache.org/docs/2.4/mod/core.html#include
|
||||
|
||||
@@ -46,6 +46,7 @@ function Cleanup() {
|
||||
|
||||
# if our environment asks us to enable modules, do our best!
|
||||
if [ "$1" = --debian-modules ] ; then
|
||||
sudo apt-get install -y apache2
|
||||
sudo apt-get install -y libapache2-mod-wsgi
|
||||
sudo apt-get install -y libapache2-mod-macro
|
||||
|
||||
|
||||
187
certbot-apache/certbot_apache/tests/autohsts_test.py
Normal file
187
certbot-apache/certbot_apache/tests/autohsts_test.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# pylint: disable=too-many-public-methods,too-many-lines
|
||||
"""Test for certbot_apache.configurator AutoHSTS functionality"""
|
||||
import re
|
||||
import unittest
|
||||
import mock
|
||||
# six is used in mock.patch()
|
||||
import six # pylint: disable=unused-import
|
||||
|
||||
from certbot import errors
|
||||
from certbot_apache import constants
|
||||
from certbot_apache.tests import util
|
||||
|
||||
|
||||
class AutoHSTSTest(util.ApacheTest):
|
||||
"""Tests for AutoHSTS feature"""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(AutoHSTSTest, self).setUp()
|
||||
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
|
||||
self.config.parser.modules.add("headers_module")
|
||||
self.config.parser.modules.add("mod_headers.c")
|
||||
self.config.parser.modules.add("ssl_module")
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
|
||||
self.vh_truth = util.get_vh_truth(
|
||||
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
|
||||
|
||||
def get_autohsts_value(self, vh_path):
|
||||
""" Get value from Strict-Transport-Security header """
|
||||
header_path = self.config.parser.find_dir("Header", None, vh_path)
|
||||
if header_path:
|
||||
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
|
||||
for head in header_path:
|
||||
if re.search(pat, self.config.parser.aug.get(head).lower()):
|
||||
return self.config.parser.aug.get(head.replace("arg[3]",
|
||||
"arg[4]"))
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||
def test_autohsts_enable_headers_mod(self, mock_enable, _restart):
|
||||
self.config.parser.modules.discard("headers_module")
|
||||
self.config.parser.modules.discard("mod_header.c")
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
self.assertTrue(mock_enable.called)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
def test_autohsts_deploy_already_exists(self, _restart):
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
self.assertRaises(errors.PluginEnhancementAlreadyPresent,
|
||||
self.config.enable_autohsts,
|
||||
mock.MagicMock(), ["ocspvhost.com"])
|
||||
|
||||
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.prepare")
|
||||
def test_autohsts_increase(self, mock_prepare, _mock_restart):
|
||||
self.config._prepared = False
|
||||
maxage = "\"max-age={0}\""
|
||||
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
||||
inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])
|
||||
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
# Verify initial value
|
||||
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
initial_val)
|
||||
# Increase
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
# Verify increased value
|
||||
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
inc_val)
|
||||
self.assertTrue(mock_prepare.called)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator._autohsts_increase")
|
||||
def test_autohsts_increase_noop(self, mock_increase, _restart):
|
||||
maxage = "\"max-age={0}\""
|
||||
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
# Verify initial value
|
||||
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
initial_val)
|
||||
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
# Freq not patched, so value shouldn't increase
|
||||
self.assertFalse(mock_increase.called)
|
||||
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
||||
def test_autohsts_increase_no_header(self, _restart):
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
# Remove the header
|
||||
dir_locs = self.config.parser.find_dir("Header", None,
|
||||
self.vh_truth[7].path)
|
||||
dir_loc = "/".join(dir_locs[0].split("/")[:-1])
|
||||
self.config.parser.aug.remove(dir_loc)
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.config.update_autohsts,
|
||||
mock.MagicMock())
|
||||
|
||||
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
def test_autohsts_increase_and_make_permanent(self, _mock_restart):
|
||||
maxage = "\"max-age={0}\""
|
||||
max_val = maxage.format(constants.AUTOHSTS_PERMANENT)
|
||||
mock_lineage = mock.MagicMock()
|
||||
mock_lineage.key_path = "/etc/apache2/ssl/key-certbot_15.pem"
|
||||
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
||||
for i in range(len(constants.AUTOHSTS_STEPS)-1):
|
||||
# Ensure that value is not made permanent prematurely
|
||||
self.config.deploy_autohsts(mock_lineage)
|
||||
self.assertNotEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
max_val)
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
# Value should match pre-permanent increment step
|
||||
cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])
|
||||
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
cur_val)
|
||||
# Ensure that the value is raised to max
|
||||
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
maxage.format(constants.AUTOHSTS_STEPS[-1]))
|
||||
# Make permanent
|
||||
self.config.deploy_autohsts(mock_lineage)
|
||||
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
||||
max_val)
|
||||
|
||||
def test_autohsts_update_noop(self):
|
||||
with mock.patch("time.time") as mock_time:
|
||||
# Time mock is used to make sure that the execution does not
|
||||
# continue when no autohsts entries exist in pluginstorage
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
self.assertFalse(mock_time.called)
|
||||
|
||||
def test_autohsts_make_permanent_noop(self):
|
||||
self.config.storage.put = mock.MagicMock()
|
||||
self.config.deploy_autohsts(mock.MagicMock())
|
||||
# Make sure that the execution does not continue when no entries in store
|
||||
self.assertFalse(self.config.storage.put.called)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.select_vhost")
|
||||
def test_autohsts_no_ssl_vhost(self, mock_select):
|
||||
mock_select.return_value = self.vh_truth[0]
|
||||
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.config.enable_autohsts,
|
||||
mock.MagicMock(), "invalid.example.com")
|
||||
self.assertTrue(
|
||||
"Certbot was not able to find SSL" in mock_log.call_args[0][0])
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.add_vhost_id")
|
||||
def test_autohsts_dont_enhance_twice(self, mock_id, _restart):
|
||||
mock_id.return_value = "1234567"
|
||||
self.config.enable_autohsts(mock.MagicMock(),
|
||||
["ocspvhost.com", "ocspvhost.com"])
|
||||
self.assertEqual(mock_id.call_count, 1)
|
||||
|
||||
def test_autohsts_remove_orphaned(self):
|
||||
# pylint: disable=protected-access
|
||||
self.config._autohsts_fetch_state()
|
||||
self.config._autohsts["orphan_id"] = {"laststep": 0, "timestamp": 0}
|
||||
|
||||
self.config._autohsts_save_state()
|
||||
self.config.update_autohsts(mock.MagicMock())
|
||||
self.assertFalse("orphan_id" in self.config._autohsts)
|
||||
# Make sure it's removed from the pluginstorage file as well
|
||||
self.config._autohsts = None
|
||||
self.config._autohsts_fetch_state()
|
||||
self.assertFalse(self.config._autohsts)
|
||||
|
||||
def test_autohsts_make_permanent_vhost_not_found(self):
|
||||
# pylint: disable=protected-access
|
||||
self.config._autohsts_fetch_state()
|
||||
self.config._autohsts["orphan_id"] = {"laststep": 999, "timestamp": 0}
|
||||
self.config._autohsts_save_state()
|
||||
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
|
||||
self.config.deploy_autohsts(mock.MagicMock())
|
||||
self.assertTrue(mock_log.called)
|
||||
self.assertTrue(
|
||||
"VirtualHost with id orphan_id was not" in mock_log.call_args[0][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -4,6 +4,8 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
|
||||
from certbot_apache import obj
|
||||
from certbot_apache import override_centos
|
||||
from certbot_apache.tests import util
|
||||
@@ -79,9 +81,9 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
|
||||
mock_osi.return_value = ("centos", "7")
|
||||
self.config.parser.update_runtime_variables()
|
||||
|
||||
self.assertEquals(mock_get.call_count, 3)
|
||||
self.assertEquals(len(self.config.parser.modules), 4)
|
||||
self.assertEquals(len(self.config.parser.variables), 2)
|
||||
self.assertEqual(mock_get.call_count, 3)
|
||||
self.assertEqual(len(self.config.parser.modules), 4)
|
||||
self.assertEqual(len(self.config.parser.variables), 2)
|
||||
self.assertTrue("TEST2" in self.config.parser.variables.keys())
|
||||
self.assertTrue("mod_another.c" in self.config.parser.modules)
|
||||
|
||||
@@ -121,5 +123,19 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
|
||||
self.assertTrue("MOCK_NOSEP" in self.config.parser.variables.keys())
|
||||
self.assertEqual("NOSEP_VAL", self.config.parser.variables["NOSEP_TWO"])
|
||||
|
||||
@mock.patch("certbot_apache.configurator.util.run_script")
|
||||
def test_alt_restart_works(self, mock_run_script):
|
||||
mock_run_script.side_effect = [None, errors.SubprocessError, None]
|
||||
self.config.restart()
|
||||
self.assertEqual(mock_run_script.call_count, 3)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.util.run_script")
|
||||
def test_alt_restart_errors(self, mock_run_script):
|
||||
mock_run_script.side_effect = [None,
|
||||
errors.SubprocessError,
|
||||
errors.SubprocessError]
|
||||
self.assertRaises(errors.MisconfigurationError, self.config.restart)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -115,9 +115,22 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# Weak test..
|
||||
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
|
||||
|
||||
def test_add_parser_arguments_all_configurators(self): # pylint: disable=no-self-use
|
||||
from certbot_apache.entrypoint import OVERRIDE_CLASSES
|
||||
for cls in OVERRIDE_CLASSES.values():
|
||||
cls.add_parser_arguments(mock.MagicMock())
|
||||
|
||||
def test_all_configurators_defaults_defined(self):
|
||||
from certbot_apache.entrypoint import OVERRIDE_CLASSES
|
||||
from certbot_apache.configurator import ApacheConfigurator
|
||||
parameters = set(ApacheConfigurator.OS_DEFAULTS.keys())
|
||||
for cls in OVERRIDE_CLASSES.values():
|
||||
self.assertTrue(parameters.issubset(set(cls.OS_DEFAULTS.keys())))
|
||||
|
||||
def test_constant(self):
|
||||
self.assertEqual(self.config.constant("server_root"), "/etc/apache2")
|
||||
self.assertEqual(self.config.constant("nonexistent"), None)
|
||||
self.assertTrue("debian_apache_2_4/multiple_vhosts/apache" in
|
||||
self.config.option("server_root"))
|
||||
self.assertEqual(self.config.option("nonexistent"), None)
|
||||
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_get_all_names(self, mock_getutility):
|
||||
@@ -246,7 +259,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
@mock.patch("certbot_apache.display_ops.select_vhost")
|
||||
def test_choose_vhost_select_vhost_with_temp(self, mock_select):
|
||||
mock_select.return_value = self.vh_truth[0]
|
||||
chosen_vhost = self.config.choose_vhost("none.com", temp=True)
|
||||
chosen_vhost = self.config.choose_vhost("none.com", create_if_no_ssl=False)
|
||||
self.assertEqual(self.vh_truth[0], chosen_vhost)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.select_vhost")
|
||||
@@ -353,14 +366,11 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
|
||||
self.config.parser.find_dir = mock_find_dir
|
||||
mock_add.reset_mock()
|
||||
|
||||
self.config._add_dummy_ssl_directives(self.vh_truth[0]) # pylint: disable=protected-access
|
||||
tried_to_add = []
|
||||
for a in mock_add.call_args_list:
|
||||
tried_to_add.append(a[0][1] == "Include" and
|
||||
a[0][2] == self.config.mod_ssl_conf)
|
||||
# Include shouldn't be added, as patched find_dir "finds" existing one
|
||||
self.assertFalse(any(tried_to_add))
|
||||
if a[0][1] == "Include" and a[0][2] == self.config.mod_ssl_conf:
|
||||
self.fail("Include shouldn't be added, as patched find_dir 'finds' existing one") \
|
||||
# pragma: no cover
|
||||
|
||||
def test_deploy_cert(self):
|
||||
self.config.parser.modules.add("ssl_module")
|
||||
@@ -654,22 +664,10 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.assertEqual(ssl_vhost_slink.name, "nonsym.link")
|
||||
|
||||
def test_make_vhost_ssl_nonexistent_vhost_path(self):
|
||||
def conf_side_effect(arg):
|
||||
""" Mock function for ApacheConfigurator.conf """
|
||||
confvars = {
|
||||
"vhost-root": "/tmp/nonexistent",
|
||||
"le_vhost_ext": "-le-ssl.conf",
|
||||
"handle-sites": True}
|
||||
return confvars[arg]
|
||||
|
||||
with mock.patch(
|
||||
"certbot_apache.configurator.ApacheConfigurator.conf"
|
||||
) as mock_conf:
|
||||
mock_conf.side_effect = conf_side_effect
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
|
||||
self.assertEqual(os.path.dirname(ssl_vhost.filep),
|
||||
os.path.dirname(os.path.realpath(
|
||||
self.vh_truth[1].filep)))
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
|
||||
self.assertEqual(os.path.dirname(ssl_vhost.filep),
|
||||
os.path.dirname(os.path.realpath(
|
||||
self.vh_truth[1].filep)))
|
||||
|
||||
def test_make_vhost_ssl(self):
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
|
||||
@@ -936,6 +934,22 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
errors.PluginError,
|
||||
self.config.enhance, "certbot.demo", "unknown_enhancement")
|
||||
|
||||
def test_enhance_no_ssl_vhost(self):
|
||||
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
|
||||
self.assertRaises(errors.PluginError, self.config.enhance,
|
||||
"certbot.demo", "redirect")
|
||||
# Check that correct logger.warning was printed
|
||||
self.assertTrue("not able to find" in mock_log.call_args[0][0])
|
||||
self.assertTrue("\"redirect\"" in mock_log.call_args[0][0])
|
||||
|
||||
mock_log.reset_mock()
|
||||
|
||||
self.assertRaises(errors.PluginError, self.config.enhance,
|
||||
"certbot.demo", "ensure-http-header", "Test")
|
||||
# Check that correct logger.warning was printed
|
||||
self.assertTrue("not able to find" in mock_log.call_args[0][0])
|
||||
self.assertTrue("Test" in mock_log.call_args[0][0])
|
||||
|
||||
@mock.patch("certbot.util.exe_exists")
|
||||
def test_ocsp_stapling(self, mock_exe):
|
||||
self.config.parser.update_runtime_variables = mock.Mock()
|
||||
@@ -945,6 +959,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
mock_exe.return_value = True
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "staple-ocsp")
|
||||
|
||||
# Get the ssl vhost for certbot.demo
|
||||
@@ -971,6 +986,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
mock_exe.return_value = True
|
||||
|
||||
# Checking the case with already enabled ocsp stapling configuration
|
||||
self.config.choose_vhost("ocspvhost.com")
|
||||
self.config.enhance("ocspvhost.com", "staple-ocsp")
|
||||
|
||||
# Get the ssl vhost for letsencrypt.demo
|
||||
@@ -995,6 +1011,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
self.config.parser.modules.add("socache_shmcb_module")
|
||||
self.config.get_version = mock.Mock(return_value=(2, 2, 0))
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.config.enhance, "certbot.demo", "staple-ocsp")
|
||||
@@ -1020,6 +1037,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
mock_exe.return_value = True
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
|
||||
@@ -1039,7 +1057,8 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# skip the enable mod
|
||||
self.config.parser.modules.add("headers_module")
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
# This will create an ssl vhost for encryption-example.demo
|
||||
self.config.choose_vhost("encryption-example.demo")
|
||||
self.config.enhance("encryption-example.demo", "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
|
||||
@@ -1058,6 +1077,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
mock_exe.return_value = True
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
|
||||
@@ -1079,7 +1099,8 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# skip the enable mod
|
||||
self.config.parser.modules.add("headers_module")
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
# This will create an ssl vhost for encryption-example.demo
|
||||
self.config.choose_vhost("encryption-example.demo")
|
||||
self.config.enhance("encryption-example.demo", "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
|
||||
@@ -1097,6 +1118,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.config.get_version = mock.Mock(return_value=(2, 2))
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "redirect")
|
||||
|
||||
# These are not immediately available in find_dir even with save() and
|
||||
@@ -1147,6 +1169,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.config.save()
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "redirect")
|
||||
|
||||
# These are not immediately available in find_dir even with save() and
|
||||
@@ -1213,6 +1236,9 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
|
||||
|
||||
# Creates ssl vhost for the domain
|
||||
self.config.choose_vhost("red.blue.purple.com")
|
||||
|
||||
self.config.enhance("red.blue.purple.com", "redirect")
|
||||
verify_no_redirect = ("certbot_apache.configurator."
|
||||
"ApacheConfigurator._verify_no_certbot_redirect")
|
||||
@@ -1224,7 +1250,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# Skip the enable mod
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
self.config.get_version = mock.Mock(return_value=(2, 3, 9))
|
||||
|
||||
self.config.choose_vhost("red.blue.purple.com")
|
||||
self.config.enhance("red.blue.purple.com", "redirect")
|
||||
# Clear state about enabling redirect on this run
|
||||
# pylint: disable=protected-access
|
||||
@@ -1376,11 +1402,11 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
vhs = self.config._choose_vhosts_wildcard("*.certbot.demo",
|
||||
create_ssl=True)
|
||||
# Check that the dialog was called with one vh: certbot.demo
|
||||
self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[3])
|
||||
self.assertEquals(len(mock_select_vhs.call_args_list), 1)
|
||||
self.assertEqual(mock_select_vhs.call_args[0][0][0], self.vh_truth[3])
|
||||
self.assertEqual(len(mock_select_vhs.call_args_list), 1)
|
||||
|
||||
# And the actual returned values
|
||||
self.assertEquals(len(vhs), 1)
|
||||
self.assertEqual(len(vhs), 1)
|
||||
self.assertTrue(vhs[0].name == "certbot.demo")
|
||||
self.assertTrue(vhs[0].ssl)
|
||||
|
||||
@@ -1395,7 +1421,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
vhs = self.config._choose_vhosts_wildcard("*.certbot.demo",
|
||||
create_ssl=False)
|
||||
self.assertFalse(mock_makessl.called)
|
||||
self.assertEquals(vhs[0], self.vh_truth[1])
|
||||
self.assertEqual(vhs[0], self.vh_truth[1])
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator._vhosts_for_wildcard")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.make_vhost_ssl")
|
||||
@@ -1408,15 +1434,15 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
mock_select_vhs.return_value = [self.vh_truth[7]]
|
||||
vhs = self.config._choose_vhosts_wildcard("whatever",
|
||||
create_ssl=True)
|
||||
self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[7])
|
||||
self.assertEquals(len(mock_select_vhs.call_args_list), 1)
|
||||
self.assertEqual(mock_select_vhs.call_args[0][0][0], self.vh_truth[7])
|
||||
self.assertEqual(len(mock_select_vhs.call_args_list), 1)
|
||||
# Ensure that make_vhost_ssl was not called, vhost.ssl == true
|
||||
self.assertFalse(mock_makessl.called)
|
||||
|
||||
# And the actual returned values
|
||||
self.assertEquals(len(vhs), 1)
|
||||
self.assertEqual(len(vhs), 1)
|
||||
self.assertTrue(vhs[0].ssl)
|
||||
self.assertEquals(vhs[0], self.vh_truth[7])
|
||||
self.assertEqual(vhs[0], self.vh_truth[7])
|
||||
|
||||
|
||||
def test_deploy_cert_wildcard(self):
|
||||
@@ -1429,7 +1455,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.config.deploy_cert("*.wildcard.example.org", "/tmp/path",
|
||||
"/tmp/path", "/tmp/path", "/tmp/path")
|
||||
self.assertTrue(mock_dep.called)
|
||||
self.assertEquals(len(mock_dep.call_args_list), 1)
|
||||
self.assertEqual(len(mock_dep.call_args_list), 1)
|
||||
self.assertEqual(self.vh_truth[7], mock_dep.call_args_list[0][0][0])
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.select_vhost_multiple")
|
||||
@@ -1446,6 +1472,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# pylint: disable=protected-access
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
self.config.parser.modules.add("headers_module")
|
||||
self.vh_truth[3].ssl = True
|
||||
self.config._wildcard_vhosts["*.certbot.demo"] = [self.vh_truth[3]]
|
||||
self.config.enhance("*.certbot.demo", "ensure-http-header",
|
||||
"Upgrade-Insecure-Requests")
|
||||
@@ -1453,6 +1480,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator._choose_vhosts_wildcard")
|
||||
def test_enhance_wildcard_no_install(self, mock_choose):
|
||||
self.vh_truth[3].ssl = True
|
||||
mock_choose.return_value = [self.vh_truth[3]]
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
self.config.parser.modules.add("headers_module")
|
||||
@@ -1460,6 +1488,21 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
"Upgrade-Insecure-Requests")
|
||||
self.assertTrue(mock_choose.called)
|
||||
|
||||
def test_add_vhost_id(self):
|
||||
for vh in [self.vh_truth[0], self.vh_truth[1], self.vh_truth[2]]:
|
||||
vh_id = self.config.add_vhost_id(vh)
|
||||
self.assertEqual(vh, self.config.find_vhost_by_id(vh_id))
|
||||
|
||||
def test_find_vhost_by_id_404(self):
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.config.find_vhost_by_id,
|
||||
"nonexistent")
|
||||
|
||||
def test_add_vhost_id_already_exists(self):
|
||||
first_id = self.config.add_vhost_id(self.vh_truth[0])
|
||||
second_id = self.config.add_vhost_id(self.vh_truth[0])
|
||||
self.assertEqual(first_id, second_id)
|
||||
|
||||
|
||||
class AugeasVhostsTest(util.ApacheTest):
|
||||
"""Test vhosts with illegal names dependent on augeas version."""
|
||||
@@ -1541,7 +1584,7 @@ class AugeasVhostsTest(util.ApacheTest):
|
||||
broken_vhost)
|
||||
|
||||
class MultiVhostsTest(util.ApacheTest):
|
||||
"""Test vhosts with illegal names dependent on augeas version."""
|
||||
"""Test configuration with multiple virtualhosts in a single file."""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
@@ -1608,7 +1651,8 @@ class MultiVhostsTest(util.ApacheTest):
|
||||
self.assertTrue(self.config.parser.find_dir(
|
||||
"RewriteEngine", "on", ssl_vhost.path, False))
|
||||
|
||||
conf_text = open(ssl_vhost.filep).read()
|
||||
with open(ssl_vhost.filep) as the_file:
|
||||
conf_text = the_file.read()
|
||||
commented_rewrite_rule = ("# RewriteRule \"^/secrets/(.+)\" "
|
||||
"\"https://new.example.com/docs/$1\" [R,L]")
|
||||
uncommented_rewrite_rule = ("RewriteRule \"^/docs/(.+)\" "
|
||||
@@ -1624,7 +1668,8 @@ class MultiVhostsTest(util.ApacheTest):
|
||||
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[3])
|
||||
|
||||
conf_lines = open(ssl_vhost.filep).readlines()
|
||||
with open(ssl_vhost.filep) as the_file:
|
||||
conf_lines = the_file.readlines()
|
||||
conf_line_set = [l.strip() for l in conf_lines]
|
||||
not_commented_cond1 = ("RewriteCond "
|
||||
"%{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f")
|
||||
@@ -1661,7 +1706,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
|
||||
self.config.updated_mod_ssl_conf_digest)
|
||||
|
||||
def _current_ssl_options_hash(self):
|
||||
return crypto_util.sha256sum(self.config.constant("MOD_SSL_CONF_SRC"))
|
||||
return crypto_util.sha256sum(self.config.option("MOD_SSL_CONF_SRC"))
|
||||
|
||||
def _assert_current_file(self):
|
||||
self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
|
||||
@@ -1697,7 +1742,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
|
||||
self.assertEqual(crypto_util.sha256sum(
|
||||
self.config.constant("MOD_SSL_CONF_SRC")),
|
||||
self.config.option("MOD_SSL_CONF_SRC")),
|
||||
self._current_ssl_options_hash())
|
||||
self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf),
|
||||
self._current_ssl_options_hash())
|
||||
@@ -1713,7 +1758,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
|
||||
"%s has been manually modified; updated file "
|
||||
"saved to %s. We recommend updating %s for security purposes.")
|
||||
self.assertEqual(crypto_util.sha256sum(
|
||||
self.config.constant("MOD_SSL_CONF_SRC")),
|
||||
self.config.option("MOD_SSL_CONF_SRC")),
|
||||
self._current_ssl_options_hash())
|
||||
# only print warning once
|
||||
with mock.patch("certbot.plugins.common.logger") as mock_logger:
|
||||
|
||||
@@ -20,7 +20,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(MultipleVhostsTestDebian, self).setUp()
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, None, self.config_dir, self.work_dir,
|
||||
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
|
||||
os_info="debian")
|
||||
self.config = self.mock_deploy_cert(self.config)
|
||||
self.vh_truth = util.get_vh_truth(self.temp_dir,
|
||||
@@ -161,6 +161,8 @@ class MultipleVhostsTestDebian(util.ApacheTest):
|
||||
self.config.parser.modules.add("mod_ssl.c")
|
||||
self.config.get_version = mock.Mock(return_value=(2, 4, 7))
|
||||
mock_exe.return_value = True
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "staple-ocsp")
|
||||
self.assertTrue("socache_shmcb_module" in self.config.parser.modules)
|
||||
|
||||
@@ -172,6 +174,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
|
||||
mock_exe.return_value = True
|
||||
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "ensure-http-header",
|
||||
"Strict-Transport-Security")
|
||||
self.assertTrue("headers_module" in self.config.parser.modules)
|
||||
@@ -183,6 +186,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
|
||||
mock_exe.return_value = True
|
||||
self.config.get_version = mock.Mock(return_value=(2, 2))
|
||||
# This will create an ssl vhost for certbot.demo
|
||||
self.config.choose_vhost("certbot.demo")
|
||||
self.config.enhance("certbot.demo", "redirect")
|
||||
self.assertTrue("rewrite_module" in self.config.parser.modules)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
|
||||
from certbot_apache import override_gentoo
|
||||
from certbot_apache import obj
|
||||
from certbot_apache.tests import util
|
||||
@@ -115,13 +117,19 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
|
||||
self.config.parser.modules = set()
|
||||
|
||||
with mock.patch("certbot.util.get_os_info") as mock_osi:
|
||||
# Make sure we have the have the CentOS httpd constants
|
||||
# Make sure we have the have the Gentoo httpd constants
|
||||
mock_osi.return_value = ("gentoo", "123")
|
||||
self.config.parser.update_runtime_variables()
|
||||
|
||||
self.assertEquals(mock_get.call_count, 1)
|
||||
self.assertEquals(len(self.config.parser.modules), 4)
|
||||
self.assertEqual(mock_get.call_count, 1)
|
||||
self.assertEqual(len(self.config.parser.modules), 4)
|
||||
self.assertTrue("mod_another.c" in self.config.parser.modules)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.util.run_script")
|
||||
def test_alt_restart_works(self, mock_run_script):
|
||||
mock_run_script.side_effect = [None, errors.SubprocessError, None]
|
||||
self.config.restart()
|
||||
self.assertEqual(mock_run_script.call_count, 3)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -4,47 +4,27 @@ import os
|
||||
import unittest
|
||||
|
||||
from acme import challenges
|
||||
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
|
||||
|
||||
from certbot import achallenges
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import acme_util
|
||||
|
||||
from certbot_apache.parser import get_aug_path
|
||||
from certbot_apache.tests import util
|
||||
|
||||
|
||||
NUM_ACHALLS = 3
|
||||
|
||||
|
||||
class ApacheHttp01TestMeta(type):
|
||||
"""Generates parmeterized tests for testing perform."""
|
||||
def __new__(mcs, name, bases, class_dict):
|
||||
|
||||
def _gen_test(num_achalls, minor_version):
|
||||
def _test(self):
|
||||
achalls = self.achalls[:num_achalls]
|
||||
vhosts = self.vhosts[:num_achalls]
|
||||
self.config.version = (2, minor_version)
|
||||
self.common_perform_test(achalls, vhosts)
|
||||
return _test
|
||||
|
||||
for i in range(1, NUM_ACHALLS + 1):
|
||||
for j in (2, 4):
|
||||
test_name = "test_perform_{0}_{1}".format(i, j)
|
||||
class_dict[test_name] = _gen_test(i, j)
|
||||
return type.__new__(mcs, name, bases, class_dict)
|
||||
|
||||
|
||||
class ApacheHttp01Test(util.ApacheTest):
|
||||
"""Test for certbot_apache.http_01.ApacheHttp01."""
|
||||
|
||||
__metaclass__ = ApacheHttp01TestMeta
|
||||
|
||||
def setUp(self, *args, **kwargs):
|
||||
super(ApacheHttp01Test, self).setUp(*args, **kwargs)
|
||||
|
||||
self.account_key = self.rsa512jwk
|
||||
self.achalls = []
|
||||
self.achalls = [] # type: List[achallenges.KeyAuthorizationAnnotatedChallenge]
|
||||
vh_truth = util.get_vh_truth(
|
||||
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
|
||||
# Takes the vhosts for encryption-example.demo, certbot.demo, and
|
||||
@@ -71,7 +51,7 @@ class ApacheHttp01Test(util.ApacheTest):
|
||||
self.assertFalse(self.http.perform())
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||
def test_enable_modules_22(self, mock_enmod):
|
||||
def test_enable_modules_apache_2_2(self, mock_enmod):
|
||||
self.config.version = (2, 2)
|
||||
self.config.parser.modules.remove("authz_host_module")
|
||||
self.config.parser.modules.remove("mod_authz_host.c")
|
||||
@@ -80,7 +60,7 @@ class ApacheHttp01Test(util.ApacheTest):
|
||||
self.assertEqual(enmod_calls[0][0][0], "authz_host")
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||
def test_enable_modules_24(self, mock_enmod):
|
||||
def test_enable_modules_apache_2_4(self, mock_enmod):
|
||||
self.config.parser.modules.remove("authz_core_module")
|
||||
self.config.parser.modules.remove("mod_authz_core.c")
|
||||
|
||||
@@ -137,6 +117,46 @@ class ApacheHttp01Test(util.ApacheTest):
|
||||
self.config.config.http01_port = 12345
|
||||
self.assertRaises(errors.PluginError, self.http.perform)
|
||||
|
||||
def test_perform_1_achall_apache_2_2(self):
|
||||
self.combinations_perform_test(num_achalls=1, minor_version=2)
|
||||
|
||||
def test_perform_1_achall_apache_2_4(self):
|
||||
self.combinations_perform_test(num_achalls=1, minor_version=4)
|
||||
|
||||
def test_perform_2_achall_apache_2_2(self):
|
||||
self.combinations_perform_test(num_achalls=2, minor_version=2)
|
||||
|
||||
def test_perform_2_achall_apache_2_4(self):
|
||||
self.combinations_perform_test(num_achalls=2, minor_version=4)
|
||||
|
||||
def test_perform_3_achall_apache_2_2(self):
|
||||
self.combinations_perform_test(num_achalls=3, minor_version=2)
|
||||
|
||||
def test_perform_3_achall_apache_2_4(self):
|
||||
self.combinations_perform_test(num_achalls=3, minor_version=4)
|
||||
|
||||
def test_activate_disabled_vhost(self):
|
||||
vhosts = [v for v in self.config.vhosts if v.name == "certbot.demo"]
|
||||
achalls = [
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.HTTP01(token=((b'a' * 16))),
|
||||
"pending"),
|
||||
domain="certbot.demo", account_key=self.account_key)]
|
||||
vhosts[0].enabled = False
|
||||
self.common_perform_test(achalls, vhosts)
|
||||
matches = self.config.parser.find_dir(
|
||||
"Include", vhosts[0].filep,
|
||||
get_aug_path(self.config.parser.loc["default"]))
|
||||
self.assertEqual(len(matches), 1)
|
||||
|
||||
def combinations_perform_test(self, num_achalls, minor_version):
|
||||
"""Test perform with the given achall count and Apache version."""
|
||||
achalls = self.achalls[:num_achalls]
|
||||
vhosts = self.vhosts[:num_achalls]
|
||||
self.config.version = (2, minor_version)
|
||||
self.common_perform_test(achalls, vhosts)
|
||||
|
||||
def common_perform_test(self, achalls, vhosts):
|
||||
"""Tests perform with the given achalls."""
|
||||
challenge_dir = self.http.challenge_dir
|
||||
|
||||
@@ -84,7 +84,7 @@ class BasicParserTest(util.ParserTest):
|
||||
self.assertEqual(self.parser.aug.get(match), str(i + 1))
|
||||
|
||||
def test_empty_arg(self):
|
||||
self.assertEquals(None,
|
||||
self.assertEqual(None,
|
||||
self.parser.get_arg("/files/whatever/nonexistent"))
|
||||
|
||||
def test_add_dir_to_ifmodssl(self):
|
||||
@@ -282,11 +282,11 @@ class BasicParserTest(util.ParserTest):
|
||||
self.assertRaises(
|
||||
errors.PluginError, self.parser.update_runtime_variables)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.constant")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.option")
|
||||
@mock.patch("certbot_apache.parser.subprocess.Popen")
|
||||
def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_const):
|
||||
def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_opt):
|
||||
mock_popen.side_effect = OSError
|
||||
mock_const.return_value = "nonexistent"
|
||||
mock_opt.return_value = "nonexistent"
|
||||
self.assertRaises(
|
||||
errors.MisconfigurationError,
|
||||
self.parser.update_runtime_variables)
|
||||
@@ -299,6 +299,13 @@ class BasicParserTest(util.ParserTest):
|
||||
errors.MisconfigurationError,
|
||||
self.parser.update_runtime_variables)
|
||||
|
||||
def test_add_comment(self):
|
||||
from certbot_apache.parser import get_aug_path
|
||||
self.parser.add_comment(get_aug_path(self.parser.loc["name"]), "123456")
|
||||
comm = self.parser.find_comments("123456")
|
||||
self.assertEqual(len(comm), 1)
|
||||
self.assertTrue(self.parser.loc["name"] in comm[0])
|
||||
|
||||
|
||||
class ParserInitTest(util.ApacheTest):
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
|
||||
@@ -87,7 +87,6 @@ class ParserTest(ApacheTest):
|
||||
def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-locals
|
||||
config_path, vhost_path,
|
||||
config_dir, work_dir, version=(2, 4, 7),
|
||||
conf=None,
|
||||
os_info="generic",
|
||||
conf_vhost_path=None):
|
||||
"""Create an Apache Configurator with the specified options.
|
||||
@@ -98,9 +97,10 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc
|
||||
backups = os.path.join(work_dir, "backups")
|
||||
mock_le_config = mock.MagicMock(
|
||||
apache_server_root=config_path,
|
||||
apache_vhost_root=conf_vhost_path,
|
||||
apache_vhost_root=None,
|
||||
apache_le_vhost_ext="-le-ssl.conf",
|
||||
apache_challenge_location=config_path,
|
||||
apache_enmod=None,
|
||||
backup_dir=backups,
|
||||
config_dir=config_dir,
|
||||
http01_port=80,
|
||||
@@ -108,37 +108,25 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
work_dir=work_dir)
|
||||
|
||||
orig_os_constant = configurator.ApacheConfigurator(mock_le_config,
|
||||
name="apache",
|
||||
version=version).constant
|
||||
|
||||
def mock_os_constant(key, vhost_path=vhost_path):
|
||||
"""Mock default vhost path"""
|
||||
if key == "vhost_root":
|
||||
return vhost_path
|
||||
else:
|
||||
return orig_os_constant(key)
|
||||
|
||||
with mock.patch("certbot_apache.configurator.ApacheConfigurator.constant") as mock_cons:
|
||||
mock_cons.side_effect = mock_os_constant
|
||||
with mock.patch("certbot_apache.configurator.util.run_script"):
|
||||
with mock.patch("certbot_apache.configurator.util."
|
||||
"exe_exists") as mock_exe_exists:
|
||||
mock_exe_exists.return_value = True
|
||||
with mock.patch("certbot_apache.parser.ApacheParser."
|
||||
"update_runtime_variables"):
|
||||
try:
|
||||
config_class = entrypoint.OVERRIDE_CLASSES[os_info]
|
||||
except KeyError:
|
||||
config_class = configurator.ApacheConfigurator
|
||||
config = config_class(config=mock_le_config, name="apache",
|
||||
version=version)
|
||||
# This allows testing scripts to set it a bit more
|
||||
# quickly
|
||||
if conf is not None:
|
||||
config.conf = conf # pragma: no cover
|
||||
|
||||
config.prepare()
|
||||
with mock.patch("certbot_apache.configurator.util.run_script"):
|
||||
with mock.patch("certbot_apache.configurator.util."
|
||||
"exe_exists") as mock_exe_exists:
|
||||
mock_exe_exists.return_value = True
|
||||
with mock.patch("certbot_apache.parser.ApacheParser."
|
||||
"update_runtime_variables"):
|
||||
try:
|
||||
config_class = entrypoint.OVERRIDE_CLASSES[os_info]
|
||||
except KeyError:
|
||||
config_class = configurator.ApacheConfigurator
|
||||
config = config_class(config=mock_le_config, name="apache",
|
||||
version=version)
|
||||
if not conf_vhost_path:
|
||||
config_class.OS_DEFAULTS["vhost_root"] = vhost_path
|
||||
else:
|
||||
# Custom virtualhost path was requested
|
||||
config.config.apache_vhost_root = conf_vhost_path
|
||||
config.config.apache_ctl = config_class.OS_DEFAULTS["ctl"]
|
||||
config.prepare()
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
|
||||
from certbot.plugins import common
|
||||
from certbot.errors import PluginError, MissingCommandlineFlag
|
||||
|
||||
@@ -93,7 +94,7 @@ class ApacheTlsSni01(common.TLSSNI01):
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
addrs = set()
|
||||
addrs = set() # type: Set[obj.Addr]
|
||||
config_text = "<IfModule mod_ssl.c>\n"
|
||||
|
||||
for achall in self.achalls:
|
||||
@@ -123,7 +124,8 @@ class ApacheTlsSni01(common.TLSSNI01):
|
||||
self.configurator.config.tls_sni_01_port)))
|
||||
|
||||
try:
|
||||
vhost = self.configurator.choose_vhost(achall.domain, temp=True)
|
||||
vhost = self.configurator.choose_vhost(achall.domain,
|
||||
create_if_no_ssl=False)
|
||||
except (PluginError, MissingCommandlineFlag):
|
||||
# We couldn't find the virtualhost for this domain, possibly
|
||||
# because it's a new vhost that's not configured yet
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
acme[dev]==0.21.1
|
||||
certbot[dev]==0.21.1
|
||||
acme[dev]==0.25.0
|
||||
certbot[dev]==0.26.0
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
install_requires = [
|
||||
'acme>=0.21.1',
|
||||
'certbot>=0.21.1',
|
||||
'acme>=0.25.0',
|
||||
'certbot>=0.26.0',
|
||||
'mock',
|
||||
'python-augeas',
|
||||
'setuptools',
|
||||
@@ -33,7 +31,7 @@ setup(
|
||||
license='Apache License 2.0',
|
||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Plugins',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
@@ -45,6 +43,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
236
certbot-auto
236
certbot-auto
@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
|
||||
LE_AUTO_VERSION="0.22.2"
|
||||
LE_AUTO_VERSION="0.29.1"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
@@ -195,7 +195,7 @@ if [ "$1" = "--cb-auto-has-root" ]; then
|
||||
else
|
||||
SetRootAuthMechanism
|
||||
if [ -n "$SUDO" ]; then
|
||||
echo "Requesting to rerun $0 with root privileges..."
|
||||
say "Requesting to rerun $0 with root privileges..."
|
||||
$SUDO "$0" --cb-auto-has-root "$@"
|
||||
exit 0
|
||||
fi
|
||||
@@ -593,8 +593,7 @@ BootstrapArchCommon() {
|
||||
# - ArchLinux (x86_64)
|
||||
#
|
||||
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
|
||||
# only "virtualenv2" binary, not "virtualenv" necessary in
|
||||
# ./tools/_venv_common.sh
|
||||
# only "virtualenv2" binary, not "virtualenv".
|
||||
|
||||
deps="
|
||||
python2
|
||||
@@ -912,6 +911,35 @@ OldVenvExists() {
|
||||
[ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ]
|
||||
}
|
||||
|
||||
# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2.
|
||||
# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated
|
||||
# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1
|
||||
# is outdated, and "UP_TO_DATE" if not.
|
||||
# This function relies only on installed python environment (2.x or 3.x) by certbot-auto.
|
||||
CompareVersions() {
|
||||
"$1" - "$2" "$3" << "UNLIKELY_EOF"
|
||||
import sys
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
try:
|
||||
current = StrictVersion(sys.argv[1])
|
||||
except ValueError:
|
||||
sys.stdout.write('UNOFFICIAL')
|
||||
sys.exit()
|
||||
|
||||
try:
|
||||
remote = StrictVersion(sys.argv[2])
|
||||
except ValueError:
|
||||
sys.stdout.write('UP_TO_DATE')
|
||||
sys.exit()
|
||||
|
||||
if current < remote:
|
||||
sys.stdout.write('OUTDATED')
|
||||
else:
|
||||
sys.stdout.write('UP_TO_DATE')
|
||||
UNLIKELY_EOF
|
||||
}
|
||||
|
||||
if [ "$1" = "--le-auto-phase2" ]; then
|
||||
# Phase 2: Create venv, install LE, and run.
|
||||
|
||||
@@ -1017,79 +1045,66 @@ pycparser==2.14 \
|
||||
asn1crypto==0.22.0 \
|
||||
--hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \
|
||||
--hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a
|
||||
cffi==1.10.0 \
|
||||
--hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \
|
||||
--hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \
|
||||
--hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \
|
||||
--hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \
|
||||
--hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \
|
||||
--hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \
|
||||
--hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \
|
||||
--hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \
|
||||
--hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \
|
||||
--hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \
|
||||
--hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \
|
||||
--hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \
|
||||
--hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \
|
||||
--hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \
|
||||
--hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \
|
||||
--hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \
|
||||
--hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \
|
||||
--hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \
|
||||
--hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \
|
||||
--hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \
|
||||
--hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \
|
||||
--hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \
|
||||
--hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \
|
||||
--hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \
|
||||
--hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \
|
||||
--hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \
|
||||
--hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \
|
||||
--hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \
|
||||
--hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \
|
||||
--hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \
|
||||
--hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \
|
||||
--hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \
|
||||
--hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \
|
||||
--hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \
|
||||
--hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \
|
||||
--hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5
|
||||
cffi==1.11.5 \
|
||||
--hash=sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50 \
|
||||
--hash=sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596 \
|
||||
--hash=sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef \
|
||||
--hash=sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743 \
|
||||
--hash=sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f \
|
||||
--hash=sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31 \
|
||||
--hash=sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04 \
|
||||
--hash=sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6 \
|
||||
--hash=sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3 \
|
||||
--hash=sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6 \
|
||||
--hash=sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b \
|
||||
--hash=sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca \
|
||||
--hash=sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e \
|
||||
--hash=sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb \
|
||||
--hash=sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd \
|
||||
--hash=sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1 \
|
||||
--hash=sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917 \
|
||||
--hash=sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359 \
|
||||
--hash=sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f \
|
||||
--hash=sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95 \
|
||||
--hash=sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801 \
|
||||
--hash=sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257 \
|
||||
--hash=sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184 \
|
||||
--hash=sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc \
|
||||
--hash=sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085 \
|
||||
--hash=sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93 \
|
||||
--hash=sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2 \
|
||||
--hash=sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30 \
|
||||
--hash=sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5 \
|
||||
--hash=sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e \
|
||||
--hash=sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b \
|
||||
--hash=sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4
|
||||
ConfigArgParse==0.12.0 \
|
||||
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339
|
||||
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \
|
||||
--no-binary ConfigArgParse
|
||||
configobj==5.0.6 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
|
||||
cryptography==2.0.2 \
|
||||
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
|
||||
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
|
||||
--hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \
|
||||
--hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \
|
||||
--hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \
|
||||
--hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \
|
||||
--hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \
|
||||
--hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \
|
||||
--hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \
|
||||
--hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \
|
||||
--hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \
|
||||
--hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \
|
||||
--hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \
|
||||
--hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \
|
||||
--hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \
|
||||
--hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \
|
||||
--hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \
|
||||
--hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \
|
||||
--hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \
|
||||
--hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \
|
||||
--hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \
|
||||
--hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \
|
||||
--hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \
|
||||
--hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \
|
||||
--hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \
|
||||
--hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \
|
||||
--hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \
|
||||
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
|
||||
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
|
||||
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
|
||||
enum34==1.1.2 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
|
||||
--no-binary configobj
|
||||
cryptography==2.2.2 \
|
||||
--hash=sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd \
|
||||
--hash=sha256:5251e7de0de66810833606439ca65c9b9e45da62196b0c88bfadf27740aac09f \
|
||||
--hash=sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04 \
|
||||
--hash=sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f \
|
||||
--hash=sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd \
|
||||
--hash=sha256:64b5c67acc9a7c83fbb4b69166f3105a0ab722d27934fac2cb26456718eec2ba \
|
||||
--hash=sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb \
|
||||
--hash=sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2 \
|
||||
--hash=sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037 \
|
||||
--hash=sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd \
|
||||
--hash=sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531 \
|
||||
--hash=sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63 \
|
||||
--hash=sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e \
|
||||
--hash=sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351 \
|
||||
--hash=sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a \
|
||||
--hash=sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563 \
|
||||
--hash=sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab \
|
||||
--hash=sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471 \
|
||||
--hash=sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887
|
||||
enum34==1.1.2 ; python_version < '3.4' \
|
||||
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
|
||||
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
|
||||
funcsigs==1.0.2 \
|
||||
@@ -1101,9 +1116,9 @@ idna==2.5 \
|
||||
ipaddress==1.0.16 \
|
||||
--hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \
|
||||
--hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0
|
||||
josepy==1.0.1 \
|
||||
--hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \
|
||||
--hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc
|
||||
josepy==1.1.0 \
|
||||
--hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \
|
||||
--hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086
|
||||
linecache2==1.0.0 \
|
||||
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
|
||||
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
|
||||
@@ -1112,7 +1127,8 @@ mock==1.3.0 \
|
||||
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
|
||||
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
|
||||
ordereddict==1.1 \
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f \
|
||||
--no-binary ordereddict
|
||||
packaging==16.8 \
|
||||
--hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \
|
||||
--hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e
|
||||
@@ -1138,7 +1154,8 @@ pyRFC3339==1.0 \
|
||||
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
|
||||
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
|
||||
python-augeas==0.5.0 \
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 \
|
||||
--no-binary python-augeas
|
||||
pytz==2015.7 \
|
||||
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
|
||||
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
|
||||
@@ -1153,9 +1170,9 @@ pytz==2015.7 \
|
||||
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
|
||||
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
|
||||
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
|
||||
requests==2.12.1 \
|
||||
--hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \
|
||||
--hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e
|
||||
requests==2.20.0 \
|
||||
--hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \
|
||||
--hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279
|
||||
six==1.10.0 \
|
||||
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
|
||||
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
|
||||
@@ -1166,9 +1183,11 @@ unittest2==1.1.0 \
|
||||
--hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \
|
||||
--hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579
|
||||
zope.component==4.2.2 \
|
||||
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a
|
||||
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a \
|
||||
--no-binary zope.component
|
||||
zope.event==4.1.0 \
|
||||
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786
|
||||
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 \
|
||||
--no-binary zope.event
|
||||
zope.interface==4.1.3 \
|
||||
--hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \
|
||||
--hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \
|
||||
@@ -1187,6 +1206,18 @@ zope.interface==4.1.3 \
|
||||
--hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \
|
||||
--hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \
|
||||
--hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392
|
||||
requests-toolbelt==0.8.0 \
|
||||
--hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \
|
||||
--hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5
|
||||
chardet==3.0.2 \
|
||||
--hash=sha256:4f7832e7c583348a9eddd927ee8514b3bf717c061f57b21dbe7697211454d9bb \
|
||||
--hash=sha256:6ebf56457934fdce01fb5ada5582762a84eed94cad43ed877964aebbdd8174c0
|
||||
urllib3==1.21.1 \
|
||||
--hash=sha256:8ed6d5c1ff9d6ba84677310060d6a3a78ca3072ce0684cb3c645023009c114b1 \
|
||||
--hash=sha256:b14486978518ca0901a76ba973d7821047409d7f726f22156b24e83fd71382a5
|
||||
certifi==2017.4.17 \
|
||||
--hash=sha256:f4318671072f030a33c7ca6acaef720ddd50ff124d1388e50c1bda4cbd6d7010 \
|
||||
--hash=sha256:f7527ebf7461582ce95f7a9e03dd141ce810d40590834f4ec20cddd54234c10a
|
||||
|
||||
# Contains the requirements for the letsencrypt package.
|
||||
#
|
||||
@@ -1199,31 +1230,29 @@ letsencrypt==0.7.0 \
|
||||
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
|
||||
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
|
||||
|
||||
certbot==0.22.2 \
|
||||
--hash=sha256:c8c63bdf0fed6258bdbc892454314ec37bcd1c35a7f62524a083d93ccdfc420d \
|
||||
--hash=sha256:e6e3639293e78397f31f7d99e3c63aff82d91e2b0d50d146ee3c77f830464bef
|
||||
acme==0.22.2 \
|
||||
--hash=sha256:59a55244612ee305d2caa6bb4cddd400fb60ec841bf011ed29a2899832a682c2 \
|
||||
--hash=sha256:0ecd0ea369f53d5bc744d6e72717f9af2e1ceb558d109dbd433148851027adb4
|
||||
certbot-apache==0.22.2 \
|
||||
--hash=sha256:b5340d4b9190358fde8eb6a5be0def37e32014b5142ee79ef5d2319ccbbde754 \
|
||||
--hash=sha256:3cd26912bb5732d917ddf7aad2fe870090d4ece9a408b2c2de8e9723ec99c759
|
||||
certbot-nginx==0.22.2 \
|
||||
--hash=sha256:91feef0d879496835d355e82841f92e5ecb5abbf6f23ea0ee5bbb8f5a92b278a \
|
||||
--hash=sha256:b10bf04c1a20cf878d5e0d1877deb0e0780bc31b0ffda08ce7199bbc39d0753b
|
||||
certbot==0.29.1 \
|
||||
--hash=sha256:2ba2c60fd1969e75d3e5048d3f7d95afd0949670b39a6a0037ba4a594e9f26a5 \
|
||||
--hash=sha256:6fc604d207c48b95dea3458bb33a11b17aa625628eb197927ffee8b458f62692
|
||||
acme==0.29.1 \
|
||||
--hash=sha256:4be3848f8813c455021f13519642d8ec2746b78d4d0bc2ae04c3dcb1d8862f60 \
|
||||
--hash=sha256:a2e203ade83cd1eaf19112004a63073830211cf7759d437f634babb08c49b47c
|
||||
certbot-apache==0.29.1 \
|
||||
--hash=sha256:8d8b6b7c5f333cf5297153c6a1eacc09b4a5c73e8f93544800b3ad016d5e34d0 \
|
||||
--hash=sha256:c3af1c66c86cfeef7dac4fe9b16c7c755ebd12bc526408c27781bd34b9de8128
|
||||
certbot-nginx==0.29.1 \
|
||||
--hash=sha256:5ba3a7d93d3ce317fb8b3d0222c708fb79e96c7a9b1ba56e12e46892c2d12869 \
|
||||
--hash=sha256:0c1205ebb91eef4b7d15293c6778ffc962d09563b315120b2d226348d751e38d
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py"
|
||||
#!/usr/bin/env python
|
||||
"""A small script that can act as a trust root for installing pip >=8
|
||||
|
||||
Embed this in your project, and your VCS checkout is all you have to trust. In
|
||||
a post-peep era, this lets you claw your way to a hash-checking version of pip,
|
||||
with which you can install the rest of your dependencies safely. All it assumes
|
||||
is Python 2.6 or better and *some* version of pip already installed. If
|
||||
anything goes wrong, it will exit with a non-zero status code.
|
||||
|
||||
"""
|
||||
# This is here so embedded copies are MIT-compliant:
|
||||
# Copyright (c) 2016 Erik Rose
|
||||
@@ -1350,10 +1379,8 @@ def hashed_download(url, temp, digest):
|
||||
|
||||
def get_index_base():
|
||||
"""Return the URL to the dir containing the "packages" folder.
|
||||
|
||||
Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the
|
||||
end if it's there; that is likely to give us the right dir.
|
||||
|
||||
"""
|
||||
env_var = environ.get('PIP_INDEX_URL', '').rstrip('/')
|
||||
if env_var:
|
||||
@@ -1643,7 +1670,12 @@ UNLIKELY_EOF
|
||||
error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates."
|
||||
elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
|
||||
error "WARNING: unable to check for updates."
|
||||
elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then
|
||||
fi
|
||||
|
||||
LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"`
|
||||
if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then
|
||||
say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION"
|
||||
elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then
|
||||
say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."
|
||||
|
||||
# Now we drop into Python so we don't have to install even more
|
||||
|
||||
@@ -14,7 +14,7 @@ RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only
|
||||
# the above is not likely to change, so by putting it further up the
|
||||
# Dockerfile we make sure we cache as much as possible
|
||||
|
||||
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini .pylintrc /opt/certbot/src/
|
||||
COPY setup.py README.rst CHANGELOG.md MANIFEST.in linter_plugin.py tox.cover.py tox.ini .pylintrc /opt/certbot/src/
|
||||
|
||||
# all above files are necessary for setup.py, however, package source
|
||||
# code directory has to be copied separately to a subdirectory...
|
||||
@@ -35,7 +35,8 @@ RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \
|
||||
/opt/certbot/venv/bin/pip install -U setuptools && \
|
||||
/opt/certbot/venv/bin/pip install -U pip
|
||||
ENV PATH /opt/certbot/venv/bin:$PATH
|
||||
RUN /opt/certbot/src/tools/pip_install_editable.sh \
|
||||
RUN /opt/certbot/venv/bin/python \
|
||||
/opt/certbot/src/tools/pip_install_editable.py \
|
||||
/opt/certbot/src/acme \
|
||||
/opt/certbot/src \
|
||||
/opt/certbot/src/certbot-apache \
|
||||
|
||||
@@ -59,9 +59,6 @@ class Proxy(configurators_common.Proxy):
|
||||
setattr(self.le_config, "apache_" + k,
|
||||
entrypoint.ENTRYPOINT.OS_DEFAULTS[k])
|
||||
|
||||
# An alias
|
||||
self.le_config.apache_handle_modules = self.le_config.apache_handle_mods
|
||||
|
||||
self._configurator = entrypoint.ENTRYPOINT(
|
||||
config=configuration.NamespaceConfig(self.le_config),
|
||||
name="apache")
|
||||
|
||||
@@ -15,6 +15,7 @@ from six.moves import xrange # pylint: disable=import-error,redefined-builtin
|
||||
from acme import challenges
|
||||
from acme import crypto_util
|
||||
from acme import messages
|
||||
from acme.magic_typing import List, Tuple # pylint: disable=unused-import, no-name-in-module
|
||||
from certbot import achallenges
|
||||
from certbot import errors as le_errors
|
||||
from certbot.tests import acme_util
|
||||
@@ -52,9 +53,8 @@ def test_authenticator(plugin, config, temp_dir):
|
||||
|
||||
try:
|
||||
responses = plugin.perform(achalls)
|
||||
except le_errors.Error as error:
|
||||
logger.error("Performing challenges on %s caused an error:", config)
|
||||
logger.exception(error)
|
||||
except le_errors.Error:
|
||||
logger.error("Performing challenges on %s caused an error:", config, exc_info=True)
|
||||
return False
|
||||
|
||||
success = True
|
||||
@@ -82,9 +82,8 @@ def test_authenticator(plugin, config, temp_dir):
|
||||
if success:
|
||||
try:
|
||||
plugin.cleanup(achalls)
|
||||
except le_errors.Error as error:
|
||||
logger.error("Challenge cleanup for %s caused an error:", config)
|
||||
logger.exception(error)
|
||||
except le_errors.Error:
|
||||
logger.error("Challenge cleanup for %s caused an error:", config, exc_info=True)
|
||||
success = False
|
||||
|
||||
if _dirs_are_unequal(config, backup):
|
||||
@@ -147,9 +146,8 @@ def test_deploy_cert(plugin, temp_dir, domains):
|
||||
try:
|
||||
plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path)
|
||||
plugin.save() # Needed by the Apache plugin
|
||||
except le_errors.Error as error:
|
||||
logger.error("**** Plugin failed to deploy certificate for %s:", domain)
|
||||
logger.exception(error)
|
||||
except le_errors.Error:
|
||||
logger.error("**** Plugin failed to deploy certificate for %s:", domain, exc_info=True)
|
||||
return False
|
||||
|
||||
if not _save_and_restart(plugin, "deployed"):
|
||||
@@ -179,7 +177,7 @@ def test_enhancements(plugin, domains):
|
||||
"enhancements")
|
||||
return False
|
||||
|
||||
domains_and_info = [(domain, []) for domain in domains]
|
||||
domains_and_info = [(domain, []) for domain in domains] # type: List[Tuple[str, List[bool]]]
|
||||
|
||||
for domain, info in domains_and_info:
|
||||
try:
|
||||
@@ -192,10 +190,9 @@ def test_enhancements(plugin, domains):
|
||||
# Don't immediately fail because a redirect may already be enabled
|
||||
logger.warning("*** Plugin failed to enable redirect for %s:", domain)
|
||||
logger.warning("%s", error)
|
||||
except le_errors.Error as error:
|
||||
except le_errors.Error:
|
||||
logger.error("*** An error occurred while enabling redirect for %s:",
|
||||
domain)
|
||||
logger.exception(error)
|
||||
domain, exc_info=True)
|
||||
|
||||
if not _save_and_restart(plugin, "enhanced"):
|
||||
return False
|
||||
@@ -222,9 +219,8 @@ def _save_and_restart(plugin, title=None):
|
||||
plugin.save(title)
|
||||
plugin.restart()
|
||||
return True
|
||||
except le_errors.Error as error:
|
||||
logger.error("*** Plugin failed to save and restart server:")
|
||||
logger.exception(error)
|
||||
except le_errors.Error:
|
||||
logger.error("*** Plugin failed to save and restart server:", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
@@ -232,9 +228,8 @@ def test_rollback(plugin, config, backup):
|
||||
"""Tests the rollback checkpoints function"""
|
||||
try:
|
||||
plugin.rollback_checkpoints(1337)
|
||||
except le_errors.Error as error:
|
||||
logger.error("*** Plugin raised an exception during rollback:")
|
||||
logger.exception(error)
|
||||
except le_errors.Error:
|
||||
logger.error("*** Plugin raised an exception during rollback:", exc_info=True)
|
||||
return False
|
||||
|
||||
if _dirs_are_unequal(config, backup):
|
||||
@@ -263,21 +258,21 @@ def _dirs_are_unequal(dir1, dir2):
|
||||
logger.error("The following files and directories are only "
|
||||
"present in one directory")
|
||||
if dircmp.left_only:
|
||||
logger.error(dircmp.left_only)
|
||||
logger.error(str(dircmp.left_only))
|
||||
else:
|
||||
logger.error(dircmp.right_only)
|
||||
logger.error(str(dircmp.right_only))
|
||||
return True
|
||||
elif dircmp.common_funny or dircmp.funny_files:
|
||||
logger.error("The following files and directories could not be "
|
||||
"compared:")
|
||||
if dircmp.common_funny:
|
||||
logger.error(dircmp.common_funny)
|
||||
logger.error(str(dircmp.common_funny))
|
||||
else:
|
||||
logger.error(dircmp.funny_files)
|
||||
logger.error(str(dircmp.funny_files))
|
||||
return True
|
||||
elif dircmp.diff_files:
|
||||
logger.error("The following files differ:")
|
||||
logger.error(dircmp.diff_files)
|
||||
logger.error(str(dircmp.diff_files))
|
||||
return True
|
||||
|
||||
for subdir in dircmp.subdirs.itervalues():
|
||||
@@ -354,9 +349,8 @@ def main():
|
||||
success = test_authenticator(plugin, config, temp_dir)
|
||||
if success and args.install:
|
||||
success = test_installer(args, plugin, config, temp_dir)
|
||||
except errors.Error as error:
|
||||
logger.error("Tests on %s raised:", config)
|
||||
logger.exception(error)
|
||||
except errors.Error:
|
||||
logger.error("Tests on %s raised:", config, exc_info=True)
|
||||
success = False
|
||||
|
||||
if success:
|
||||
|
||||
@@ -26,12 +26,12 @@ def create_le_config(parent_dir):
|
||||
config = copy.deepcopy(constants.CLI_DEFAULTS)
|
||||
|
||||
le_dir = os.path.join(parent_dir, "certbot")
|
||||
config["config_dir"] = os.path.join(le_dir, "config")
|
||||
config["work_dir"] = os.path.join(le_dir, "work")
|
||||
config["logs_dir"] = os.path.join(le_dir, "logs_dir")
|
||||
os.makedirs(config["config_dir"])
|
||||
os.mkdir(config["work_dir"])
|
||||
os.mkdir(config["logs_dir"])
|
||||
os.mkdir(le_dir)
|
||||
for dir_name in ("config", "logs", "work"):
|
||||
full_path = os.path.join(le_dir, dir_name)
|
||||
os.mkdir(full_path)
|
||||
full_name = dir_name + "_dir"
|
||||
config[full_name] = full_path
|
||||
|
||||
config["domains"] = None
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class Validator(object):
|
||||
try:
|
||||
presented_cert = crypto_util.probe_sni(name, host, port)
|
||||
except acme_errors.Error as error:
|
||||
logger.exception(error)
|
||||
logger.exception(str(error))
|
||||
return False
|
||||
|
||||
return presented_cert.digest("sha256") == cert.digest("sha256")
|
||||
@@ -86,8 +86,7 @@ class Validator(object):
|
||||
return False
|
||||
|
||||
try:
|
||||
_, max_age_value = max_age[0]
|
||||
max_age_value = int(max_age_value)
|
||||
max_age_value = int(max_age[0][1])
|
||||
except ValueError:
|
||||
logger.error("Server responded with invalid HSTS header field")
|
||||
return False
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'certbot',
|
||||
@@ -46,6 +46,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
|
||||
@@ -122,7 +122,7 @@ class _CloudflareClient(object):
|
||||
self.cf.zones.dns_records.delete(zone_id, record_id)
|
||||
logger.debug('Successfully deleted TXT record.')
|
||||
except CloudFlare.exceptions.CloudFlareAPIError as e:
|
||||
logger.warn('Encountered CloudFlareAPIError deleting TXT record: %s', e)
|
||||
logger.warning('Encountered CloudFlareAPIError deleting TXT record: %s', e)
|
||||
else:
|
||||
logger.debug('TXT record not found; no cleanup needed.')
|
||||
else:
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
@@ -44,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -70,6 +70,7 @@ class _CloudXNSLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
super(_CloudXNSLexiconClient, self).__init__()
|
||||
|
||||
self.provider = cloudxns.Provider({
|
||||
'provider_name': 'cloudxns',
|
||||
'auth_username': api_key,
|
||||
'auth_token': secret_key,
|
||||
'ttl': ttl,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
@@ -44,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -134,7 +134,7 @@ class _DigitalOceanClient(object):
|
||||
logger.debug('Removing TXT record with id: %s', record.id)
|
||||
record.destroy()
|
||||
except digitalocean.Error as e:
|
||||
logger.warn('Error deleting TXT record %s using the DigitalOcean API: %s',
|
||||
logger.warning('Error deleting TXT record %s using the DigitalOcean API: %s',
|
||||
record.id, e)
|
||||
|
||||
def _find_domain(self, domain_name):
|
||||
|
||||
@@ -50,7 +50,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
||||
|
||||
|
||||
class DigitalOceanClientTest(unittest.TestCase):
|
||||
id = 1
|
||||
|
||||
id_num = 1
|
||||
record_prefix = "_acme-challenge"
|
||||
record_name = record_prefix + "." + DOMAIN
|
||||
record_content = "bar"
|
||||
@@ -70,7 +71,7 @@ class DigitalOceanClientTest(unittest.TestCase):
|
||||
|
||||
domain_mock = mock.MagicMock()
|
||||
domain_mock.name = DOMAIN
|
||||
domain_mock.create_new_domain_record.return_value = {'domain_record': {'id': self.id}}
|
||||
domain_mock.create_new_domain_record.return_value = {'domain_record': {'id': self.id_num}}
|
||||
|
||||
self.manager.get_all_domains.return_value = [wrong_domain_mock, domain_mock]
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
@@ -45,6 +43,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -66,6 +66,7 @@ class _DNSimpleLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
super(_DNSimpleLexiconClient, self).__init__()
|
||||
|
||||
self.provider = dnsimple.Provider({
|
||||
'provider_name': 'dnssimple',
|
||||
'auth_token': token,
|
||||
'ttl': ttl,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
@@ -44,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
@@ -72,6 +72,7 @@ class _DNSMadeEasyLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
super(_DNSMadeEasyLexiconClient, self).__init__()
|
||||
|
||||
self.provider = dnsmadeeasy.Provider({
|
||||
'provider_name': 'dnsmadeeasy',
|
||||
'auth_username': api_key,
|
||||
'auth_token': secret_key,
|
||||
'ttl': ttl,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
@@ -44,6 +42,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
5
certbot-dns-gehirn/Dockerfile
Normal file
5
certbot-dns-gehirn/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM certbot/certbot
|
||||
|
||||
COPY . src/certbot-dns-gehirn
|
||||
|
||||
RUN pip install --no-cache-dir --editable src/certbot-dns-gehirn
|
||||
190
certbot-dns-gehirn/LICENSE.txt
Normal file
190
certbot-dns-gehirn/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2018 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
3
certbot-dns-gehirn/MANIFEST.in
Normal file
3
certbot-dns-gehirn/MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
1
certbot-dns-gehirn/README.rst
Normal file
1
certbot-dns-gehirn/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
Gehirn Infrastracture Service DNS Authenticator plugin for Certbot
|
||||
88
certbot-dns-gehirn/certbot_dns_gehirn/__init__.py
Normal file
88
certbot-dns-gehirn/certbot_dns_gehirn/__init__.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
The `~certbot_dns_gehirn.dns_gehirn` plugin automates the process of completing
|
||||
a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently
|
||||
removing, TXT records using the Gehirn Infrastracture Service DNS API.
|
||||
|
||||
|
||||
Named Arguments
|
||||
---------------
|
||||
|
||||
======================================== =====================================
|
||||
``--dns-gehirn-credentials`` Gehirn Infrastracture Service
|
||||
credentials_ INI file.
|
||||
(Required)
|
||||
``--dns-gehirn-propagation-seconds`` The number of seconds to wait for DNS
|
||||
to propagate before asking the ACME
|
||||
server to verify the DNS record.
|
||||
(Default: 30)
|
||||
======================================== =====================================
|
||||
|
||||
|
||||
Credentials
|
||||
-----------
|
||||
|
||||
Use of this plugin requires a configuration file containing
|
||||
Gehirn Infrastracture Service DNS API credentials,
|
||||
obtained from your Gehirn Infrastracture Service
|
||||
`dashboard <https://gis.gehirn.jp/>`_.
|
||||
|
||||
.. code-block:: ini
|
||||
:name: credentials.ini
|
||||
:caption: Example credentials file:
|
||||
|
||||
# Gehirn Infrastracture Service API credentials used by Certbot
|
||||
dns_gehirn_api_token = 00000000-0000-0000-0000-000000000000
|
||||
dns_gehirn_api_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw
|
||||
|
||||
The path to this file can be provided interactively or using the
|
||||
``--dns-gehirn-credentials`` command-line argument. Certbot records the path
|
||||
to this file for use during renewal, but does not store the file's contents.
|
||||
|
||||
.. caution::
|
||||
You should protect these API credentials as you would the password to your
|
||||
Gehirn Infrastracture Service account. Users who can read this file can use
|
||||
these credentials to issue arbitrary API calls on your behalf. Users who can
|
||||
cause Certbot to run using these credentials can complete a ``dns-01``
|
||||
challenge to acquire new certificates or revoke existing certificates for
|
||||
associated domains, even if those domains aren't being managed by this server.
|
||||
|
||||
Certbot will emit a warning if it detects that the credentials file can be
|
||||
accessed by other users on your system. The warning reads "Unsafe permissions
|
||||
on credentials configuration file", followed by the path to the credentials
|
||||
file. This warning will be emitted each time Certbot uses the credentials file,
|
||||
including for renewal, and cannot be silenced except by addressing the issue
|
||||
(e.g., by using a command like ``chmod 600`` to restrict access to the file).
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a certificate for ``example.com``
|
||||
|
||||
certbot certonly \\
|
||||
--dns-gehirn \\
|
||||
--dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\
|
||||
-d example.com
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a single certificate for both ``example.com`` and
|
||||
``www.example.com``
|
||||
|
||||
certbot certonly \\
|
||||
--dns-gehirn \\
|
||||
--dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\
|
||||
-d example.com \\
|
||||
-d www.example.com
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: To acquire a certificate for ``example.com``, waiting 60 seconds
|
||||
for DNS propagation
|
||||
|
||||
certbot certonly \\
|
||||
--dns-gehirn \\
|
||||
--dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\
|
||||
--dns-gehirn-propagation-seconds 60 \\
|
||||
-d example.com
|
||||
|
||||
"""
|
||||
85
certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn.py
Normal file
85
certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""DNS Authenticator for Gehirn Infrastracture Service DNS."""
|
||||
import logging
|
||||
|
||||
import zope.interface
|
||||
from lexicon.providers import gehirn
|
||||
|
||||
from certbot import interfaces
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DASHBOARD_URL = "https://gis.gehirn.jp/"
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
"""DNS Authenticator for Gehirn Infrastracture Service DNS
|
||||
|
||||
This Authenticator uses the Gehirn Infrastracture Service API to fulfill
|
||||
a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certificates using a DNS TXT record ' + \
|
||||
'(if you are using Gehirn Infrastracture Service for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.credentials = None
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
|
||||
super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=30)
|
||||
add('credentials', help='Gehirn Infrastracture Service credentials file.')
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the Gehirn Infrastracture Service API.'
|
||||
|
||||
def _setup_credentials(self):
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'Gehirn Infrastracture Service credentials file',
|
||||
{
|
||||
'api-token': 'API token for Gehirn Infrastracture Service ' + \
|
||||
'API obtained from {0}'.format(DASHBOARD_URL),
|
||||
'api-secret': 'API secret for Gehirn Infrastracture Service ' + \
|
||||
'API obtained from {0}'.format(DASHBOARD_URL),
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain, validation_name, validation):
|
||||
self._get_gehirn_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain, validation_name, validation):
|
||||
self._get_gehirn_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_gehirn_client(self):
|
||||
return _GehirnLexiconClient(
|
||||
self.credentials.conf('api-token'),
|
||||
self.credentials.conf('api-secret'),
|
||||
self.ttl
|
||||
)
|
||||
|
||||
|
||||
class _GehirnLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the Gehirn Infrastracture Service via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, api_token, api_secret, ttl):
|
||||
super(_GehirnLexiconClient, self).__init__()
|
||||
|
||||
self.provider = gehirn.Provider({
|
||||
'provider_name': 'gehirn',
|
||||
'auth_token': api_token,
|
||||
'auth_secret': api_secret,
|
||||
'ttl': ttl,
|
||||
})
|
||||
|
||||
def _handle_http_error(self, e, domain_name):
|
||||
if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:')):
|
||||
return # Expected errors when zone name guess is wrong
|
||||
return super(_GehirnLexiconClient, self)._handle_http_error(e, domain_name)
|
||||
55
certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn_test.py
Normal file
55
certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn_test.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Tests for certbot_dns_gehirn.dns_gehirn."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from certbot.plugins import dns_test_common
|
||||
from certbot.plugins import dns_test_common_lexicon
|
||||
from certbot.plugins.dns_test_common import DOMAIN
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
API_TOKEN = '00000000-0000-0000-0000-000000000000'
|
||||
API_SECRET = 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw'
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
|
||||
from certbot_dns_gehirn.dns_gehirn import Authenticator
|
||||
|
||||
path = os.path.join(self.tempdir, 'file.ini')
|
||||
dns_test_common.write(
|
||||
{"gehirn_api_token": API_TOKEN, "gehirn_api_secret": API_SECRET},
|
||||
path
|
||||
)
|
||||
|
||||
self.config = mock.MagicMock(gehirn_credentials=path,
|
||||
gehirn_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "gehirn")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_gehirn_client | pylint: disable=protected-access
|
||||
self.auth._get_gehirn_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class GehirnLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN))
|
||||
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: {0}.'.format(DOMAIN))
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_gehirn.dns_gehirn import _GehirnLexiconClient
|
||||
|
||||
self.client = _GehirnLexiconClient(API_TOKEN, API_SECRET, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
1
certbot-dns-gehirn/docs/.gitignore
vendored
Normal file
1
certbot-dns-gehirn/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-dns-gehirn/docs/Makefile
Normal file
20
certbot-dns-gehirn/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-dns-gehirn
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
8
certbot-dns-gehirn/docs/api.rst
Normal file
8
certbot-dns-gehirn/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-dns-gehirn/docs/api/dns_gehirn.rst
Normal file
5
certbot-dns-gehirn/docs/api/dns_gehirn.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_dns_gehirn.dns_gehirn`
|
||||
------------------------------------
|
||||
|
||||
.. automodule:: certbot_dns_gehirn.dns_gehirn
|
||||
:members:
|
||||
180
certbot-dns-gehirn/docs/conf.py
Normal file
180
certbot-dns-gehirn/docs/conf.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# certbot-dns-gehirn documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed May 10 18:30:40 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', 'private-members']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'certbot-dns-gehirn'
|
||||
copyright = u'2018, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
|
||||
# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
|
||||
# on_rtd is whether we are on readthedocs.org
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
# otherwise, readthedocs.org uses their theme by default, so no need to specify it
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-dns-gehirndoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-dns-gehirn.tex', u'certbot-dns-gehirn Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-dns-gehirn', u'certbot-dns-gehirn Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-dns-gehirn', u'certbot-dns-gehirn Documentation',
|
||||
author, 'certbot-dns-gehirn', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
28
certbot-dns-gehirn/docs/index.rst
Normal file
28
certbot-dns-gehirn/docs/index.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. certbot-dns-gehirn documentation master file, created by
|
||||
sphinx-quickstart on Wed May 10 18:30:40 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-dns-gehirn's documentation!
|
||||
==============================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
.. automodule:: certbot_dns_gehirn
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-dns-gehirn/docs/make.bat
Normal file
36
certbot-dns-gehirn/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-dns-gehirn
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
12
certbot-dns-gehirn/readthedocs.org.requirements.txt
Normal file
12
certbot-dns-gehirn/readthedocs.org.requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
# readthedocs.org gives no way to change the install command to "pip
|
||||
# install -e .[docs]" (that would in turn install documentation
|
||||
# dependencies), but it allows to specify a requirements.txt file at
|
||||
# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)
|
||||
|
||||
# Although ReadTheDocs certainly doesn't need to install the project
|
||||
# in --editable mode (-e), just "pip install .[docs]" does not work as
|
||||
# expected and "pip install -e .[docs]" must be used instead
|
||||
|
||||
-e acme
|
||||
-e .
|
||||
-e certbot-dns-gehirn[docs]
|
||||
2
certbot-dns-gehirn/setup.cfg
Normal file
2
certbot-dns-gehirn/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
64
certbot-dns-gehirn/setup.py
Normal file
64
certbot-dns-gehirn/setup.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
'acme>=0.21.1',
|
||||
'certbot>=0.21.1',
|
||||
'dns-lexicon>=2.1.22',
|
||||
'mock',
|
||||
'setuptools',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='certbot-dns-gehirn',
|
||||
version=version,
|
||||
description="Gehirn Infrastracture Service DNS Authenticator plugin for Certbot",
|
||||
url='https://github.com/certbot/certbot',
|
||||
author="Certbot Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Plugins',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'certbot.plugins': [
|
||||
'dns-gehirn = certbot_dns_gehirn.dns_gehirn:Authenticator',
|
||||
],
|
||||
},
|
||||
test_suite='certbot_dns_gehirn',
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
recursive-include certbot_dns_google/testdata *
|
||||
|
||||
@@ -98,7 +98,7 @@ Examples
|
||||
|
||||
certbot certonly \\
|
||||
--dns-google \\
|
||||
--dns-google-credentials ~/.secrets/certbot/google.ini \\
|
||||
--dns-google-credentials ~/.secrets/certbot/google.json \\
|
||||
--dns-google-propagation-seconds 120 \\
|
||||
-d example.com
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ class _GoogleClient(object):
|
||||
try:
|
||||
zone_id = self._find_managed_zone_id(domain)
|
||||
except errors.PluginError as e:
|
||||
logger.warn('Error finding zone. Skipping cleanup.')
|
||||
logger.warning('Error finding zone. Skipping cleanup.')
|
||||
return
|
||||
|
||||
record_contents = self.get_existing_txt_rrset(zone_id, record_name)
|
||||
@@ -219,7 +219,7 @@ class _GoogleClient(object):
|
||||
request = changes.create(project=self.project_id, managedZone=zone_id, body=data)
|
||||
request.execute()
|
||||
except googleapiclient_errors.Error as e:
|
||||
logger.warn('Encountered error deleting TXT record: %s', e)
|
||||
logger.warning('Encountered error deleting TXT record: %s', e)
|
||||
|
||||
def get_existing_txt_rrset(self, zone_id, record_name):
|
||||
"""
|
||||
|
||||
@@ -276,9 +276,9 @@ class GoogleClientTest(unittest.TestCase):
|
||||
[{'managedZones': [{'id': self.zone}]}])
|
||||
# Record name mocked in setUp
|
||||
found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org")
|
||||
self.assertEquals(found, ["\"example-txt-contents\""])
|
||||
self.assertEqual(found, ["\"example-txt-contents\""])
|
||||
not_found = client.get_existing_txt_rrset(self.zone, "nonexistent.tld")
|
||||
self.assertEquals(not_found, None)
|
||||
self.assertEqual(not_found, None)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.23.0.dev0'
|
||||
version = '0.30.0.dev0'
|
||||
|
||||
# Remember to update local-oldest-requirements.txt when changing the minimum
|
||||
# acme/certbot version.
|
||||
@@ -49,6 +47,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
5
certbot-dns-linode/Dockerfile
Normal file
5
certbot-dns-linode/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM certbot/certbot
|
||||
|
||||
COPY . src/certbot-dns-linode
|
||||
|
||||
RUN pip install --no-cache-dir --editable src/certbot-dns-linode
|
||||
190
certbot-dns-linode/LICENSE.txt
Normal file
190
certbot-dns-linode/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user