Compare commits
645 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19a6b94680 | ||
|
|
65a72b8d60 | ||
|
|
7f61cab101 | ||
|
|
37caef38ad | ||
|
|
9cc01db1d5 | ||
|
|
9172440f79 | ||
|
|
e0eb3a4413 | ||
|
|
ae0f16bf35 | ||
|
|
6c9ed162e3 | ||
|
|
3849b52cdf | ||
|
|
9ecfcb5814 | ||
|
|
54ad09f755 | ||
|
|
6ee4dc165b | ||
|
|
8e2eb89696 | ||
|
|
9d397cc8be | ||
|
|
cbfb0755b3 | ||
|
|
d8d127ee9d | ||
|
|
0ed5430e80 | ||
|
|
878c1f52fa | ||
|
|
6900452b49 | ||
|
|
b97e0e512d | ||
|
|
f740ff517f | ||
|
|
e5989fe023 | ||
|
|
4323156fbe | ||
|
|
3c721901c5 | ||
|
|
5c2c50839a | ||
|
|
fd54c2ffac | ||
|
|
7e483e6091 | ||
|
|
80c48e9acd | ||
|
|
b98f1c0dd0 | ||
|
|
b53874a0b8 | ||
|
|
c4c9adb8bf | ||
|
|
eed265faf1 | ||
|
|
3dc6dd403d | ||
|
|
deb9ba0c43 | ||
|
|
fa33ff499d | ||
|
|
2ed4967744 | ||
|
|
ad360e81cb | ||
|
|
f95f5188b4 | ||
|
|
17d1efa395 | ||
|
|
732cbc5e92 | ||
|
|
5d2d0955b1 | ||
|
|
20feacea12 | ||
|
|
575bf2b73b | ||
|
|
934e6e2bd0 | ||
|
|
fbb9a47e8f | ||
|
|
368132daae | ||
|
|
3d54d04017 | ||
|
|
5b1494b3ce | ||
|
|
ebf2a820cc | ||
|
|
9caa4cd1d4 | ||
|
|
91fd80d44f | ||
|
|
f932e553b0 | ||
|
|
0dd4953197 | ||
|
|
aaea6aa1f3 | ||
|
|
ab4a0aea70 | ||
|
|
29f923537e | ||
|
|
24aa416740 | ||
|
|
08e517ff00 | ||
|
|
29f65389bd | ||
|
|
960a1964c7 | ||
|
|
760d54ba85 | ||
|
|
b1b21d3efc | ||
|
|
5acd7f6fb6 | ||
|
|
a31f1c7f5e | ||
|
|
660ae7333b | ||
|
|
6cf699b25f | ||
|
|
7e5dea51a5 | ||
|
|
92446c3399 | ||
|
|
d9eb927b0a | ||
|
|
39ad7597fa | ||
|
|
83d1bda56a | ||
|
|
20bc5aa6c7 | ||
|
|
dd730f6beb | ||
|
|
33fb1a6bf3 | ||
|
|
a1344245cd | ||
|
|
fe2ca6bed3 | ||
|
|
19a3c7874a | ||
|
|
83e40836eb | ||
|
|
4304c9443a | ||
|
|
b72f8e796c | ||
|
|
7c15f52368 | ||
|
|
adf569eb62 | ||
|
|
340801e743 | ||
|
|
80f96b5b26 | ||
|
|
6646ec888f | ||
|
|
e6cab51031 | ||
|
|
3449a4d6af | ||
|
|
588de02be6 | ||
|
|
422e011d31 | ||
|
|
56a1f8158a | ||
|
|
a3c375ede5 | ||
|
|
754e76d9b9 | ||
|
|
feb7bfc724 | ||
|
|
f84dc771c4 | ||
|
|
d8a52c0be3 | ||
|
|
b50739e064 | ||
|
|
f8e320e2bd | ||
|
|
bac74dc650 | ||
|
|
103f28f6ba | ||
|
|
72fb20abf3 | ||
|
|
d9efc3d4d8 | ||
|
|
836d18f07e | ||
|
|
485ae3514c | ||
|
|
2fa6489153 | ||
|
|
e02d9716f5 | ||
|
|
154409b1df | ||
|
|
98177a5b1e | ||
|
|
8d44171875 | ||
|
|
5b576112d1 | ||
|
|
e1b372c33b | ||
|
|
ce49dce8c6 | ||
|
|
09c9b42cab | ||
|
|
370a12e88a | ||
|
|
c2f1f5c549 | ||
|
|
090e03fac1 | ||
|
|
b745712791 | ||
|
|
7ee753ac85 | ||
|
|
eea6b8ab5d | ||
|
|
a135f5742c | ||
|
|
04adbb45d8 | ||
|
|
62efb22f37 | ||
|
|
5e98f05036 | ||
|
|
292a7ecbe3 | ||
|
|
4cea45bd87 | ||
|
|
7f0b075529 | ||
|
|
8c7ff5e0e8 | ||
|
|
afea5a1623 | ||
|
|
c60dd7f151 | ||
|
|
92f9371156 | ||
|
|
c5714ec6d9 | ||
|
|
dd16386317 | ||
|
|
7cf1f75eb9 | ||
|
|
cf28a00ccd | ||
|
|
9e48474f11 | ||
|
|
c327c0c995 | ||
|
|
bb567da8c6 | ||
|
|
3e3373b8c7 | ||
|
|
7d45db89bf | ||
|
|
849c145926 | ||
|
|
36a773df0b | ||
|
|
b2abf1490b | ||
|
|
fd5aa63ef3 | ||
|
|
7e024cbcf7 | ||
|
|
7050ee2354 | ||
|
|
024e97d632 | ||
|
|
61479cd5fb | ||
|
|
aaa322464d | ||
|
|
02bfbd5019 | ||
|
|
282f8b4e02 | ||
|
|
3393bde820 | ||
|
|
2277c87908 | ||
|
|
2ea0c48853 | ||
|
|
28cbefde04 | ||
|
|
4e13843c78 | ||
|
|
a929f8429d | ||
|
|
40f3f06ed3 | ||
|
|
0a79c4c717 | ||
|
|
712d789992 | ||
|
|
8de8b1a32e | ||
|
|
c2f565c73a | ||
|
|
1df036a811 | ||
|
|
9910a4fc7b | ||
|
|
9933afc8b7 | ||
|
|
1d89d4b0bc | ||
|
|
a8a84d58d9 | ||
|
|
80fee524a8 | ||
|
|
4ca2ee52b7 | ||
|
|
6638be81a0 | ||
|
|
6ced0e5e43 | ||
|
|
e36518dbe5 | ||
|
|
70d8bb60e7 | ||
|
|
3c919f6b23 | ||
|
|
0cb566d2f3 | ||
|
|
e137b6baaa | ||
|
|
58faeea838 | ||
|
|
47200e9f35 | ||
|
|
5ad8cc668f | ||
|
|
e7704171f7 | ||
|
|
c43718652a | ||
|
|
461a12e909 | ||
|
|
afb682e3eb | ||
|
|
31384fbce5 | ||
|
|
c7cfd7a8a0 | ||
|
|
717e50fd5c | ||
|
|
2ffc7d19f1 | ||
|
|
d857021df5 | ||
|
|
2ee864ccaf | ||
|
|
018dfed128 | ||
|
|
90e4545210 | ||
|
|
4a4b16b010 | ||
|
|
8701303012 | ||
|
|
9788aefcc1 | ||
|
|
ed08ef1604 | ||
|
|
adce70a5e5 | ||
|
|
d5978f64e1 | ||
|
|
45215debcc | ||
|
|
919eef55a1 | ||
|
|
8c529eed46 | ||
|
|
7909c2cd46 | ||
|
|
b1ac396bf1 | ||
|
|
d5eb4a1900 | ||
|
|
b8eb27441c | ||
|
|
de1494710a | ||
|
|
e3b05ac77f | ||
|
|
32c8e9482c | ||
|
|
4d3c86dba1 | ||
|
|
28449c348e | ||
|
|
bb9cf7b93c | ||
|
|
eb861083ad | ||
|
|
b133505086 | ||
|
|
0f0cae713a | ||
|
|
56cfce86e4 | ||
|
|
e950322232 | ||
|
|
14de21ee64 | ||
|
|
22712eae96 | ||
|
|
86d1033324 | ||
|
|
b4c4dc2c2e | ||
|
|
671f0142bc | ||
|
|
ab4bdc7be6 | ||
|
|
0859e60b23 | ||
|
|
e69c2d8b0c | ||
|
|
186e058f3d | ||
|
|
ed5af59040 | ||
|
|
0da312f755 | ||
|
|
dc646d9a45 | ||
|
|
109e01bb60 | ||
|
|
657fad06fb | ||
|
|
3e014c876d | ||
|
|
d14dd51359 | ||
|
|
70f876c445 | ||
|
|
9d8d51d88d | ||
|
|
57037f20cc | ||
|
|
4f2f509819 | ||
|
|
474fd77970 | ||
|
|
d2fad719fa | ||
|
|
6a3955a1d6 | ||
|
|
dceb33006a | ||
|
|
a096a43c56 | ||
|
|
8114a33d20 | ||
|
|
9f3adddd41 | ||
|
|
05f74ab654 | ||
|
|
0317118cd9 | ||
|
|
461de8d269 | ||
|
|
b258e92620 | ||
|
|
f6148ef1fb | ||
|
|
457da594be | ||
|
|
891a43ae67 | ||
|
|
bc65c0a786 | ||
|
|
3eeb1f77aa | ||
|
|
91be6826b9 | ||
|
|
f87eee3b9f | ||
|
|
b4e17691c4 | ||
|
|
cce372aeba | ||
|
|
b5a8a9e08a | ||
|
|
35632da284 | ||
|
|
02a9b0d16c | ||
|
|
d1809e0f7d | ||
|
|
abb4a7c0f9 | ||
|
|
cd6fa8b15c | ||
|
|
ecfcada802 | ||
|
|
f8ec5ae253 | ||
|
|
832ba5c8fb | ||
|
|
6fda0d6896 | ||
|
|
a8edaf4dfa | ||
|
|
e11b7802c2 | ||
|
|
aa0c5972fb | ||
|
|
fa8e91cdcd | ||
|
|
e5d902663b | ||
|
|
042535536e | ||
|
|
6d3063437c | ||
|
|
3db4d04e4c | ||
|
|
96f9eab5cd | ||
|
|
1e641b83c1 | ||
|
|
3791d92d67 | ||
|
|
2bcab76f5a | ||
|
|
f5493c542b | ||
|
|
68eb4198f1 | ||
|
|
ef94607728 | ||
|
|
4ccadbd2be | ||
|
|
0643063b80 | ||
|
|
d6c6ab932a | ||
|
|
46004d2db8 | ||
|
|
620d1d4092 | ||
|
|
f30afac47e | ||
|
|
1779e34773 | ||
|
|
28f535f41c | ||
|
|
e921f58d2f | ||
|
|
301f6cc273 | ||
|
|
f04e497999 | ||
|
|
8db438d76b | ||
|
|
af75e607ec | ||
|
|
cd4b9527c3 | ||
|
|
ea8dc446ae | ||
|
|
ba6270990b | ||
|
|
df55f1066c | ||
|
|
b562d661db | ||
|
|
aede78a0ec | ||
|
|
7e8c3fbab7 | ||
|
|
6621601155 | ||
|
|
1fbd585a46 | ||
|
|
5a51c14de5 | ||
|
|
27a4c81c6d | ||
|
|
fdc25dc0d7 | ||
|
|
99522fb49a | ||
|
|
671d273e2f | ||
|
|
2851a33eb2 | ||
|
|
937e3fac19 | ||
|
|
47cb00857c | ||
|
|
7904e05b4a | ||
|
|
c4fe19f2e6 | ||
|
|
9db57f0517 | ||
|
|
64afebecd4 | ||
|
|
4c324960e6 | ||
|
|
164b90a22f | ||
|
|
dc735a8aa2 | ||
|
|
f516b0931f | ||
|
|
2297121eff | ||
|
|
0c2684d1cf | ||
|
|
4b4c5dba73 | ||
|
|
7b9d70e093 | ||
|
|
71a289b009 | ||
|
|
ddf98ff593 | ||
|
|
db8043ecb6 | ||
|
|
ea756cf0a4 | ||
|
|
8446a6d813 | ||
|
|
a18aaeacf7 | ||
|
|
58a43b3785 | ||
|
|
dd06f12582 | ||
|
|
d7dcc01c9f | ||
|
|
66f56740b7 | ||
|
|
5bd042d9bc | ||
|
|
60921d9adf | ||
|
|
3a668f1f8b | ||
|
|
609fe5f838 | ||
|
|
e29e528aa2 | ||
|
|
e06c9e07fe | ||
|
|
03ee28c380 | ||
|
|
7474235283 | ||
|
|
4eb6ae92d1 | ||
|
|
0eeba2783b | ||
|
|
8498fa75e3 | ||
|
|
5556bc3a4b | ||
|
|
998200e570 | ||
|
|
420b835b09 | ||
|
|
cc3534cf00 | ||
|
|
062a267a6f | ||
|
|
98ee7fcd8c | ||
|
|
e5ec9f4b62 | ||
|
|
b66bb21777 | ||
|
|
f7e026a132 | ||
|
|
c12e56823e | ||
|
|
042f8e9030 | ||
|
|
9308950550 | ||
|
|
a43c5b0824 | ||
|
|
ebdf9f4b5b | ||
|
|
511be1f3ae | ||
|
|
ceb0454277 | ||
|
|
c553b017af | ||
|
|
c553f63127 | ||
|
|
152cb9b23a | ||
|
|
5eff50e8fe | ||
|
|
cddf01ee68 | ||
|
|
d968c4bbb9 | ||
|
|
1a738403fc | ||
|
|
71428b0181 | ||
|
|
4b538bae97 | ||
|
|
a4bb4fd708 | ||
|
|
99bb7b70e0 | ||
|
|
443f6358c9 | ||
|
|
0bae4851c1 | ||
|
|
66710d33c6 | ||
|
|
73fe1422f3 | ||
|
|
47f3cf7ba5 | ||
|
|
d5ef57542a | ||
|
|
cc3531b9d5 | ||
|
|
95722f46f4 | ||
|
|
5950e1cae7 | ||
|
|
d176ab4886 | ||
|
|
cbccf1024b | ||
|
|
643148d283 | ||
|
|
ac15c8fc0e | ||
|
|
3751fcd4c9 | ||
|
|
02466ea0bd | ||
|
|
6f6606d76d | ||
|
|
59f22ab17e | ||
|
|
2db9343e0f | ||
|
|
36b3a53ab2 | ||
|
|
dc8c42a820 | ||
|
|
2bd5d0bd8e | ||
|
|
8c152371a1 | ||
|
|
f489c59ca3 | ||
|
|
d2897cefaa | ||
|
|
9747d40734 | ||
|
|
e123ec4089 | ||
|
|
f358a0f226 | ||
|
|
003ea9310b | ||
|
|
d602df4c70 | ||
|
|
c9ac5ae963 | ||
|
|
daa990ee96 | ||
|
|
49487419d2 | ||
|
|
0fca7dc3ce | ||
|
|
0a946f55ca | ||
|
|
508fe69cf8 | ||
|
|
3e4a8f230f | ||
|
|
a62230c195 | ||
|
|
1173fb1e90 | ||
|
|
529648a30c | ||
|
|
82b6b9ccb2 | ||
|
|
71244a4eb8 | ||
|
|
32fd424295 | ||
|
|
5746042d68 | ||
|
|
e76fb235aa | ||
|
|
47e13312b1 | ||
|
|
55e05afe0e | ||
|
|
aebce2f241 | ||
|
|
aa3207fca5 | ||
|
|
ce8df34b49 | ||
|
|
8aa8c5d8ae | ||
|
|
e7628bdbdd | ||
|
|
b9dd4a35db | ||
|
|
040b2e8a53 | ||
|
|
af25254628 | ||
|
|
0c673a54cd | ||
|
|
9f1f36774d | ||
|
|
6ec697b010 | ||
|
|
f344c58f26 | ||
|
|
263b0fa455 | ||
|
|
a634c8f2d1 | ||
|
|
336faa46b2 | ||
|
|
52a167c647 | ||
|
|
458756e895 | ||
|
|
76dd23174a | ||
|
|
0ad667b00a | ||
|
|
699ab168c1 | ||
|
|
5ccb4a6f08 | ||
|
|
2c10cedb34 | ||
|
|
3b357ef301 | ||
|
|
f686fb0d95 | ||
|
|
3f739daaea | ||
|
|
c9d415f9db | ||
|
|
51249c304c | ||
|
|
83f9a551e7 | ||
|
|
6c450d3249 | ||
|
|
8b828b3f34 | ||
|
|
146d106991 | ||
|
|
b15b2edcdf | ||
|
|
f08f765529 | ||
|
|
fbde7cbd93 | ||
|
|
d10e80bf83 | ||
|
|
a269d8374e | ||
|
|
49ea196160 | ||
|
|
ddb1bc87bb | ||
|
|
07a45b4530 | ||
|
|
dcd1023a39 | ||
|
|
52522f27e9 | ||
|
|
e4ec4e1404 | ||
|
|
717d203622 | ||
|
|
c23f6172b5 | ||
|
|
b04d4cb5c5 | ||
|
|
d66bc33761 | ||
|
|
f710c00c0d | ||
|
|
e68862b633 | ||
|
|
6094a4af79 | ||
|
|
146ac49703 | ||
|
|
909b75eb40 | ||
|
|
dcdb25d92d | ||
|
|
3dfdd3c31c | ||
|
|
25d5a99b3d | ||
|
|
e1466737e3 | ||
|
|
370a28c10e | ||
|
|
af919c2f6e | ||
|
|
dc3ab544c8 | ||
|
|
708bebe6e3 | ||
|
|
3722e922ff | ||
|
|
8b5d5a8bf9 | ||
|
|
038b1c9ca4 | ||
|
|
b10ff5b6ee | ||
|
|
6950d779e0 | ||
|
|
4c31db427a | ||
|
|
540981e071 | ||
|
|
ed8b9cd5e9 | ||
|
|
3dea83ce31 | ||
|
|
cfb46a7c2b | ||
|
|
dd2f44bfe9 | ||
|
|
09d0e79bcd | ||
|
|
a9c1ca5c97 | ||
|
|
1af6fb3a58 | ||
|
|
b7bde7b685 | ||
|
|
718028ca62 | ||
|
|
98b2863547 | ||
|
|
b1e60456cf | ||
|
|
a38004c1ed | ||
|
|
5a355b276e | ||
|
|
a52721bd7e | ||
|
|
6df399fe02 | ||
|
|
6d8ede9575 | ||
|
|
7145aa60ca | ||
|
|
b1181c0f9a | ||
|
|
bace2a7c25 | ||
|
|
a70b4373de | ||
|
|
b7c12e6d91 | ||
|
|
6a88dd476e | ||
|
|
5fbd774266 | ||
|
|
bdec010d2e | ||
|
|
05a00b7b78 | ||
|
|
eaf23c3034 | ||
|
|
276a8b35e5 | ||
|
|
466d659f6e | ||
|
|
84e26381b5 | ||
|
|
469b5a5f69 | ||
|
|
ad77ebd2f9 | ||
|
|
b75543c3bc | ||
|
|
0677275742 | ||
|
|
0c3724e0ad | ||
|
|
803083d23c | ||
|
|
f4f8067a12 | ||
|
|
caa9f084d6 | ||
|
|
81407b65d1 | ||
|
|
8a24293fd7 | ||
|
|
8f1886a585 | ||
|
|
0a64e5fa67 | ||
|
|
7a70603971 | ||
|
|
0d5e00e744 | ||
|
|
91ba1433af | ||
|
|
12e56d14f2 | ||
|
|
7326119f52 | ||
|
|
136983cf14 | ||
|
|
105a1b80ae | ||
|
|
b8000ca533 | ||
|
|
c3e374e6e5 | ||
|
|
a9b6e87249 | ||
|
|
61de5422bf | ||
|
|
f96697f619 | ||
|
|
b4560d6370 | ||
|
|
a7bcde8d82 | ||
|
|
34bb4d54c2 | ||
|
|
e0116a1a03 | ||
|
|
12fec7939d | ||
|
|
ff8e02cceb | ||
|
|
8122bed97f | ||
|
|
991c3dbb76 | ||
|
|
399c23623d | ||
|
|
2232f21b48 | ||
|
|
e41c084381 | ||
|
|
520b27e0dc | ||
|
|
ace7e0247a | ||
|
|
9ae414b1c6 | ||
|
|
cb8c8186f1 | ||
|
|
82f86d9556 | ||
|
|
cfb1034450 | ||
|
|
2a07442a85 | ||
|
|
68c1eff81d | ||
|
|
baec15dfc6 | ||
|
|
6eb9817296 | ||
|
|
b9d5d33aaa | ||
|
|
560519894c | ||
|
|
9f434b0968 | ||
|
|
07066dde87 | ||
|
|
074c8f7cd0 | ||
|
|
e9df2355f4 | ||
|
|
45547d6f94 | ||
|
|
4a421d5b14 | ||
|
|
7b9825eb40 | ||
|
|
5cde165f0b | ||
|
|
305824ff1a | ||
|
|
86ddb72227 | ||
|
|
cca33478e4 | ||
|
|
a8f41d3c48 | ||
|
|
a25a15ca6e | ||
|
|
a39dac4dbd | ||
|
|
eab0c3be60 | ||
|
|
b4ee3d0dfc | ||
|
|
2f03e18c59 | ||
|
|
232cd7215e | ||
|
|
86b1e9959b | ||
|
|
fd130f86fd | ||
|
|
2669f509e1 | ||
|
|
d3619ad60f | ||
|
|
c26417d769 | ||
|
|
2942d39dfe | ||
|
|
df65b0509e | ||
|
|
fbde35483b | ||
|
|
7370f8b83b | ||
|
|
8b0ca1da2e | ||
|
|
466f2b1a02 | ||
|
|
f1d6cce88c | ||
|
|
ad7ababb4c | ||
|
|
72fa623674 | ||
|
|
e5d117c134 | ||
|
|
f8944a1331 | ||
|
|
e850855154 | ||
|
|
06eacee90c | ||
|
|
b1e100982e | ||
|
|
8f30158b00 | ||
|
|
576f7db978 | ||
|
|
137e043dfe | ||
|
|
15467fc233 | ||
|
|
eec0fcdcf1 | ||
|
|
a41dee015e | ||
|
|
d1a74713ef | ||
|
|
813e9e71d7 | ||
|
|
3d08dce26e | ||
|
|
4739d75f4a | ||
|
|
30cd62664b | ||
|
|
ae6b0fb111 | ||
|
|
9ec48f6ab8 | ||
|
|
f68565f444 | ||
|
|
ce5aae3795 | ||
|
|
bd00c09da0 | ||
|
|
44326c3abe | ||
|
|
2f3db7d982 | ||
|
|
9e4e3044b4 | ||
|
|
23bf0d07f7 | ||
|
|
6e61f8bcfb | ||
|
|
c7b28feb07 | ||
|
|
624eade9f2 | ||
|
|
d727a77289 | ||
|
|
c0c2cb328c | ||
|
|
8cc80deff8 | ||
|
|
5312c11472 | ||
|
|
f07ce6f47d | ||
|
|
259e797ea5 | ||
|
|
d77addd2dc | ||
|
|
c2420edb5a | ||
|
|
e1396bb107 | ||
|
|
b0def03790 | ||
|
|
9f371df372 | ||
|
|
986fd4b010 | ||
|
|
4cd7b02cb7 | ||
|
|
67bff28255 | ||
|
|
43e90503ca | ||
|
|
25dae3d1ec | ||
|
|
ec81572670 | ||
|
|
71f6d5769a | ||
|
|
49b96592a0 | ||
|
|
af0875ac4c | ||
|
|
48192b9002 | ||
|
|
ca289d4604 | ||
|
|
6529fd2fdb | ||
|
|
e84f5e8b0a | ||
|
|
df9f561fd3 | ||
|
|
06603759fd | ||
|
|
a4bd29e6bf | ||
|
|
458486dd6b |
21
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
> 感谢您支持certd,请按如下规范提交issue
|
||||||
|
> 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues)
|
||||||
|
|
||||||
|
|
||||||
|
## 一、问题描述
|
||||||
|
`请在此处简要描述你所遇到的问题,必要时请贴出相关截图辅助理解和定位`
|
||||||
|
|
||||||
|
### 复现步骤
|
||||||
|
`请描述复现问题的详细步骤`
|
||||||
|
`如果非示例页面的问题,最好能提供最小复现示例的代码、或者仓库链接`
|
||||||
|
|
||||||
|
|
||||||
|
### 报错截图
|
||||||
|
`请贴出报错日志截图`
|
||||||
|
|
||||||
|
### 效果截图
|
||||||
|
`请贴出效果截图`
|
||||||
|
#### 1. 期望效果
|
||||||
|
|
||||||
|
#### 2. 实际效果
|
||||||
|
|
||||||
35
.github/workflows/sync-to-gitee.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: sync-to-gitee
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['v2']
|
||||||
|
pull_request:
|
||||||
|
branches: ['v2']
|
||||||
|
# schedule:
|
||||||
|
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
|
||||||
|
# - cron: '17 19 * * *'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout work repo # 1. 检出当前仓库(certd-sync-work)
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email
|
||||||
|
run: |
|
||||||
|
git config --global user.name "xiaojunnuo"
|
||||||
|
git config --global user.email "xiaojunnuo@qq.com"
|
||||||
|
|
||||||
|
- name: Set git token # 3. 给git命令设置token,用于push到目标仓库
|
||||||
|
uses: de-vri-es/setup-git-credentials@v2
|
||||||
|
with: # token 格式为: username:password
|
||||||
|
credentials: https://${{secrets.PUSH_TOKEN_GITEE}}@gitee.com
|
||||||
|
|
||||||
|
- name: push to gitee # 4. 执行同步
|
||||||
|
run: |
|
||||||
|
git remote add upstream https://gitee.com/certd/certd
|
||||||
|
git push --set-upstream upstream v2
|
||||||
|
|
||||||
4
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
./packages/core/lego
|
||||||
# IntelliJ project files
|
# IntelliJ project files
|
||||||
.vscode/
|
.vscode/
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -30,3 +31,6 @@ gen
|
|||||||
/packages/test
|
/packages/test
|
||||||
/test/own
|
/test/own
|
||||||
/pnpm-lock.yaml
|
/pnpm-lock.yaml
|
||||||
|
|
||||||
|
docker/image/workspace
|
||||||
|
/packages/core/lego
|
||||||
|
|||||||
184
CHANGELOG.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复无法强制取消任务的bug ([9cc01db](https://github.com/certd/certd/commit/9cc01db1d569a5c45bb3e731f35d85df324a8e62))
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/certd/certd/commit/e0eb3a441384d474fe2923c69b25318264bdc9df))
|
||||||
|
* 支持windows文件上传 ([7f61cab](https://github.com/certd/certd/commit/7f61cab101fa13b4e88234e9ad47434e6130fed2))
|
||||||
|
|
||||||
|
## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复修改密码功能异常问题 ([f740ff5](https://github.com/certd/certd/commit/f740ff517f521dce361284c2c54bccc68aee0ea2))
|
||||||
|
|
||||||
|
## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 日志高度越界 ([c4c9adb](https://github.com/certd/certd/commit/c4c9adb8bfd513f57252e523794e3799a9b220f8))
|
||||||
|
* 修复邮箱设置页面SMTP拼写错误的问题 ([b98f1c0](https://github.com/certd/certd/commit/b98f1c0dd0bc6c6b4f814c578692afdf6d90b88d))
|
||||||
|
* 修复logo问题 ([7e483e6](https://github.com/certd/certd/commit/7e483e60913d509b113148c735fe13ba1d72dddf))
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 增加警告,修复一些样式错乱问题 ([fd54c2f](https://github.com/certd/certd/commit/fd54c2ffac492222e85ff2f5f49a9ee5cfc73588))
|
||||||
|
* ssh登录支持openssh格式私钥、支持私钥密码 ([5c2c508](https://github.com/certd/certd/commit/5c2c50839a9076004f9034d754ac6deb531acdfb))
|
||||||
|
|
||||||
|
## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复aliyun域名超过100个找不到域名的bug ([5b1494b](https://github.com/certd/certd/commit/5b1494b3ce93d1026dc56ee741342fbb8bf7be24))
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 增加系统设置,可以关闭自助注册功能 ([20feace](https://github.com/certd/certd/commit/20feacea12d43386540db6a600f391d786be4014))
|
||||||
|
* 增加cloudflare access token说明 ([934e6e2](https://github.com/certd/certd/commit/934e6e2bd05387cd50ffab95f230933543954098))
|
||||||
|
* 支持重置管理员密码,忘记密码的补救方案 ([732cbc5](https://github.com/certd/certd/commit/732cbc5e927b526850724594830392b2f10c6705))
|
||||||
|
* 支持cloudflare域名 ([fbb9a47](https://github.com/certd/certd/commit/fbb9a47e8f7bb805289b9ee64bd46ffee0f01c06))
|
||||||
|
|
||||||
|
## [1.20.10](https://github.com/certd/certd/compare/v1.20.9...v1.20.10) (2024-05-30)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 增加权限相关helper说明 ([83e4083](https://github.com/certd/certd/commit/83e40836ebff10bec60efe8933183e1ba1c22bf9))
|
||||||
|
* 增加权限相关helper说明 ([4304c94](https://github.com/certd/certd/commit/4304c9443ad9248f63dd6d8c512d8d6f32f90d37))
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 上传到主机插件支持复制到本机路径 ([92446c3](https://github.com/certd/certd/commit/92446c339936f98f08f654b8971a7393d8435224))
|
||||||
|
* 优化文件下载包名 ([d9eb927](https://github.com/certd/certd/commit/d9eb927b0a1445feab08b1958aa9ea80637a5ae6))
|
||||||
|
* 增加任务复制功能 ([39ad759](https://github.com/certd/certd/commit/39ad7597fa0e19cc1f7631bbd6fea0a9e05a62c9))
|
||||||
|
|
||||||
|
## [1.20.9](https://github.com/certd/certd/compare/v1.20.8...v1.20.9) (2024-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.20.8](https://github.com/certd/certd/compare/v1.20.7...v1.20.8) (2024-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.20.7](https://github.com/certd/certd/compare/v1.20.6...v1.20.7) (2024-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.20.6](https://github.com/certd/certd/compare/v1.20.5...v1.20.6) (2024-03-21)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 调整按钮图标到居中位置 ([836d18f](https://github.com/certd/certd/commit/836d18f07e22d00faf2f213bc3301a6672b5bafc))
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 插件贡献文档及示例 ([72fb20a](https://github.com/certd/certd/commit/72fb20abf3ba5bdd862575d2907703a52fd7eb17))
|
||||||
|
|
||||||
|
## [1.20.5](https://github.com/certd/certd/compare/v1.20.2...v1.20.5) (2024-03-11)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复腾讯云cdn部署无法选择端点的bug ([154409b](https://github.com/certd/certd/commit/154409b1dfee3ea1caae740ad9c1f99a6e7a9814))
|
||||||
|
|
||||||
|
## [1.20.2](https://github.com/certd/certd/compare/v1.2.1...v1.20.2) (2024-02-28)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 临时修复阿里云domainlist接口返回域名列表不全的问题,后续还需要增加翻页查询 ([849c145](https://github.com/certd/certd/commit/849c145926984762bd9dbec87bd91cd047fc0855))
|
||||||
|
|
||||||
|
## [1.2.1](https://github.com/certd/certd/compare/v1.2.0...v1.2.1) (2023-12-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复邮箱设置无效的bug ([aaa3224](https://github.com/certd/certd/commit/aaa322464d0f65e924d1850995540d396ee24d25))
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
# [1.2.0](https://github.com/certd/certd/compare/v1.1.6...v1.2.0) (2023-10-27)
|
||||||
|
|
||||||
|
* 🔱: [client] sync upgrade with 2 commits [trident-sync] ([aa3207f](https://github.com/certd/certd/commit/aa3207fca5f15f7c3da789989d99c8ae7d1c4551))
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* search支持自定义布局,search.layout、search.collapse转移到 search.container之下。如果想使用原来的search组件,请配置search.is=fs-search-v1
|
||||||
|
|
||||||
|
## [1.1.6](https://github.com/certd/certd/compare/v1.1.5...v1.1.6) (2023-07-10)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复上传证书到腾讯云失败的bug ([e950322](https://github.com/certd/certd/commit/e950322232e19d1263b8552eefa5b0150fd7864e))
|
||||||
|
|
||||||
|
## [1.1.5](https://github.com/certd/certd/compare/v1.1.4...v1.1.5) (2023-07-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.1.4](https://github.com/certd/certd/compare/v1.1.3...v1.1.4) (2023-07-03)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 成功图标转动的问题 ([f87eee3](https://github.com/certd/certd/commit/f87eee3b9ff1ef9874e79a81fe0ed7104cb9ee8c))
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* cancel task ([bc65c0a](https://github.com/certd/certd/commit/bc65c0a786360c087fe95cad93ec6a87804cc5ee))
|
||||||
|
* flush log ([891a43a](https://github.com/certd/certd/commit/891a43ae6716ff98ed06643f7da2e35199ee195c))
|
||||||
|
* flush logger ([91be682](https://github.com/certd/certd/commit/91be6826b902e0f302b1a6cbdb1d24e15914c18d))
|
||||||
|
* timeout ([3eeb1f7](https://github.com/certd/certd/commit/3eeb1f77aa2922f3545f3d2067f561d95621d54f))
|
||||||
|
|
||||||
|
## [1.1.3](https://github.com/certd/certd/compare/v1.1.2...v1.1.3) (2023-07-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.1.2](https://github.com/certd/certd/compare/v1.1.1...v1.1.2) (2023-07-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.1.1](https://github.com/certd/certd/compare/v1.1.0...v1.1.1) (2023-06-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
# [1.1.0](https://github.com/certd/certd/compare/v1.0.6...v1.1.0) (2023-06-28)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复access选择类型trigger ([2851a33](https://github.com/certd/certd/commit/2851a33eb2510f038fadb55da29512597a4ba512))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 权限控制 ([27a4c81](https://github.com/certd/certd/commit/27a4c81c6d70e70abb3892c3ea58d4719988808a))
|
||||||
|
* 邮件通知 ([937e3fa](https://github.com/certd/certd/commit/937e3fac19cd03b8aa91db8ba03fda7fcfbacea2))
|
||||||
|
* cert download ([5a51c14](https://github.com/certd/certd/commit/5a51c14de521cb8075a80d2ae41a16e6d5281259))
|
||||||
|
* config merge ([fdc25dc](https://github.com/certd/certd/commit/fdc25dc0d795555cffacc4572648ec158988fbbb))
|
||||||
|
* save files ([99522fb](https://github.com/certd/certd/commit/99522fb49adb42c1dfdf7bec3dd52d641158285b))
|
||||||
|
* save files ([671d273](https://github.com/certd/certd/commit/671d273e2f9136d16896536b0ca127cf372f1619))
|
||||||
|
|
||||||
|
## [1.0.6](https://github.com/certd/certd/compare/v1.0.5...v1.0.6) (2023-05-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.0.5](https://github.com/certd/certd/compare/v1.0.4...v1.0.5) (2023-05-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.0.4](https://github.com/certd/certd/compare/v1.0.3...v1.0.4) (2023-05-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.0.3](https://github.com/certd/certd/compare/v1.0.2...v1.0.3) (2023-05-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.0.2](https://github.com/certd/certd/compare/v1.0.1...v1.0.2) (2023-05-24)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/certd/certd/compare/v1.0.0...v1.0.1) (2023-05-24)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package root
|
||||||
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
174
README.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# CertD
|
||||||
|
|
||||||
|
CertD 是一个免费全自动申请和部署SSL证书的工具。
|
||||||
|
后缀D取自linux守护进程的命名风格,意为证书守护进程。
|
||||||
|
|
||||||
|
## 一、特性
|
||||||
|
本项目不仅支持证书申请过程自动化,还可以自动化部署证书,让你的证书永不过期。
|
||||||
|
|
||||||
|
* 全自动申请证书(支持阿里云、腾讯云、华为云、Cloudflare注册的域名)
|
||||||
|
* 全自动部署证书(目前支持服务器上传部署、部署到阿里云、腾讯云等)
|
||||||
|
* 支持通配符域名
|
||||||
|
* 支持多个域名打到一个证书上
|
||||||
|
* 邮件通知
|
||||||
|
* 证书自动更新
|
||||||
|
* 免费、免费、免费([阿里云单个通配符域名证书最便宜也要1800/年](https://yundun.console.aliyun.com/?p=cas#/certExtend/buy/cn-hangzhou))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 二、在线体验
|
||||||
|
|
||||||
|
官方Demo地址,自助注册后体验
|
||||||
|
|
||||||
|
https://certd.handsfree.work/
|
||||||
|
|
||||||
|
> 注意数据将不定期清理,生产使用请自行部署
|
||||||
|
> 包含敏感信息,务必自己本地部署进行生产使用
|
||||||
|
|
||||||
|
## 三、使用教程
|
||||||
|
本案例演示,如何配置自动申请证书,并部署到阿里云CDN,然后快要到期前自动更新证书并重新部署
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||||
|
-------> [点我查看详细使用步骤演示](./step.md) <--------
|
||||||
|
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||||
|
|
||||||
|
## 四、本地docker部署
|
||||||
|
|
||||||
|
### 1. 安装docker、docker-compose
|
||||||
|
|
||||||
|
1.1 准备一台云服务器
|
||||||
|
* 【阿里云】云服务器2核2G,新老用户同享,99元/年,续费同价!【 [立即购买](https://www.aliyun.com/benefit?scm=20140722.M_10244282._.V_1&source=5176.11533457&userCode=qya11txb )】
|
||||||
|
* 【腾讯云】云服务器2核2G,新老用户同享,99元/年,续费同价!【 [立即购买](https://cloud.tencent.com/act/cps/redirect?redirect=6094&cps_key=b3ef73330335d7a6efa4a4bbeeb6b2c9&from=console)】
|
||||||
|
|
||||||
|
|
||||||
|
1.2 安装docker
|
||||||
|
https://docs.docker.com/engine/install/
|
||||||
|
选择对应的操作系统,按照官方文档执行命令即可
|
||||||
|
|
||||||
|
|
||||||
|
### 2. 下载docker-compose.yaml文件
|
||||||
|
|
||||||
|
[docker-compose.yaml下载](https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir certd
|
||||||
|
cd certd
|
||||||
|
# wget下载docker-compose.yaml文件
|
||||||
|
wget https://raw.githubusercontent.com/certd/certd/v2/docker/run/docker-compose.yaml
|
||||||
|
# 或者使用gitee地址
|
||||||
|
wget https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml
|
||||||
|
|
||||||
|
# 根据需要修改里面的配置
|
||||||
|
# 1.修改镜像版本号【可选】
|
||||||
|
# 2.配置数据保存路径【可选】
|
||||||
|
# 3.配置certd_auth_jwt_secret【必须】
|
||||||
|
vi docker-compose.yaml
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
> 镜像版本号与release版本号同步:
|
||||||
|
https://github.com/certd/certd/releases
|
||||||
|
|
||||||
|
|
||||||
|
### 3. 运行
|
||||||
|
```bash
|
||||||
|
# 如果docker compose是插件化安装
|
||||||
|
export CERTD_VERSION=latest
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
如果提示 没有compose命令,请安装docker-compose
|
||||||
|
https://docs.docker.com/compose/install/linux/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 4. 访问
|
||||||
|
|
||||||
|
http://your_server_ip:7001
|
||||||
|
默认账号密码:admin/123456
|
||||||
|
记得修改密码
|
||||||
|
|
||||||
|
|
||||||
|
### 5. 升级
|
||||||
|
|
||||||
|
* 修改版本号,重新运行 `docker compose up -d` 即可
|
||||||
|
* 数据存在`/data/certd`目录下,不用担心数据丢失
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 五、一些说明
|
||||||
|
* 本项目ssl证书提供商为letencrypt
|
||||||
|
* 申请过程遵循acme协议
|
||||||
|
* 需要验证域名所有权,一般有两种方式(目前本项目仅支持dns-01)
|
||||||
|
* http-01: 在网站根目录下放置一份txt文件
|
||||||
|
* dns-01: 需要给域名添加txt解析记录,通配符域名只能用这种方式
|
||||||
|
* 证书续期:
|
||||||
|
* 实际上acme并没有续期概念。
|
||||||
|
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书。
|
||||||
|
* 免费证书过期时间90天,以后可能还会缩短,所以自动化部署必不可少
|
||||||
|
* 设置每天自动运行,当证书过期前20天,会自动重新申请证书并部署
|
||||||
|
|
||||||
|
## 六、不同平台的设置说明
|
||||||
|
|
||||||
|
* [Cloudflare](./doc/cf/cf.md)
|
||||||
|
* [腾讯云](./doc/tencent/tencent.md)
|
||||||
|
* [windows主机](./doc/host/host.md)
|
||||||
|
|
||||||
|
|
||||||
|
## 七、问题处理
|
||||||
|
### 7.1 忘记管理员密码
|
||||||
|
解决方法如下:
|
||||||
|
1. 修改docker-compose.yaml文件,将环境变量`certd_system_resetAdminPassword`改为`true`
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
certd:
|
||||||
|
environment: # 环境变量
|
||||||
|
- certd_system_resetAdminPassword=false
|
||||||
|
```
|
||||||
|
2. 重启容器
|
||||||
|
```shell
|
||||||
|
docker compose up -d
|
||||||
|
docker logs -f --tail 500 certd
|
||||||
|
# 观察日志,当日志中输出“重置1号管理员用户的密码完成”,即可操作下一步
|
||||||
|
```
|
||||||
|
3. 修改docker-compose.yaml,将`certd_system_resetAdminPassword`改回`false`
|
||||||
|
4. 再次重启容器
|
||||||
|
```shell
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
5. 使用admin/123456登录系统,请及时修改管理员密码
|
||||||
|
|
||||||
|
## 八、联系作者
|
||||||
|
如有疑问,欢迎加入群聊(请备注certd)
|
||||||
|
* QQ群:141236433
|
||||||
|
* 微信群:
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
加作者好友
|
||||||
|
<p align="center">
|
||||||
|
<img height="230" src="./doc/images/me.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 九、捐赠
|
||||||
|
媳妇儿说:“一天到晚搞开源,也不管管老婆孩子!😡😡😡”
|
||||||
|
拜托各位捐赠支持一下,让媳妇儿开心开心,我也能有更多时间进行开源项目,感谢🙏🙏🙏
|
||||||
|
<p align="center">
|
||||||
|
<img height="380" src="./doc/images/donate.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## 十、贡献代码
|
||||||
|
|
||||||
|
[贡献插件教程](./plugin.md)
|
||||||
|
|
||||||
|
|
||||||
|
## 十一、我的其他项目
|
||||||
|
* [袖手GPT](https://ai.handsfree.work/) ChatGPT,国内可用,无需FQ,每日免费额度
|
||||||
|
* [fast-crud](https://gitee.com/fast-crud/fast-crud/) 基于vue3的crud快速开发框架
|
||||||
|
* [dev-sidecar](https://github.com/docmirror/dev-sidecar/) 直连访问github工具,无需FQ,解决github无法访问的问题
|
||||||
106
deploy.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import http from 'axios'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
//读取 packages/core/pipline/package.json的版本号
|
||||||
|
import {default as packageJson} from './packages/core/pipeline/package.json' assert { type: "json" };
|
||||||
|
|
||||||
|
const certdVersion = packageJson.version
|
||||||
|
console.log("certdVersion", certdVersion)
|
||||||
|
|
||||||
|
// 同步npmmirror的包
|
||||||
|
async function getPackages(directoryPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 读取目录下的文件和目录列表
|
||||||
|
fs.readdir(directoryPath, {withFileTypes: true}, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.log('无法读取目录:', err);
|
||||||
|
reject(err)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤仅保留目录
|
||||||
|
const directories = files
|
||||||
|
.filter(file => file.isDirectory())
|
||||||
|
.map(directory => directory.name);
|
||||||
|
|
||||||
|
console.log('目录列表:', directories);
|
||||||
|
resolve(directories)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllPackages() {
|
||||||
|
const base = await getPackages("./packages/core")
|
||||||
|
const plugins = await getPackages("./packages/plugins")
|
||||||
|
|
||||||
|
return base.concat(plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sync() {
|
||||||
|
const packages = await getAllPackages()
|
||||||
|
for (const pkg of packages) {
|
||||||
|
await http({
|
||||||
|
url: `http://registry-direct.npmmirror.com/@certd/${pkg}/sync?sync_upstream=true`,
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
console.log(`sync success:${pkg}`)
|
||||||
|
await sleep(100*1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// curl -X PUT https://registry-direct.npmmirror.com/@certd/plugin-cert/sync?sync_upstream=true
|
||||||
|
|
||||||
|
const certdImageBuild = "http://flow-openapi.aliyun.com/pipeline/webhook/4zgFk3i4RZEMGuQzlOcI"
|
||||||
|
const certdImageRun = "http://flow-openapi.aliyun.com/pipeline/webhook/lzCzlGrLCOHQaTMMt0mG"
|
||||||
|
const webhooks = [certdImageBuild,certdImageRun]
|
||||||
|
|
||||||
|
async function sleep(time) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, time)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerBuild() {
|
||||||
|
await sleep(60000)
|
||||||
|
for (const webhook of webhooks) {
|
||||||
|
await http({
|
||||||
|
url: webhook,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
'CERTD_VERSION': certdVersion
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(`webhook success:${webhook}`)
|
||||||
|
await sleep(10*60*1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
// await build()
|
||||||
|
console.log("等待60秒")
|
||||||
|
await sleep(200 * 1000)
|
||||||
|
await sync()
|
||||||
|
await sleep(60 * 1000)
|
||||||
|
await triggerBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
start()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包前 修改 lerna
|
||||||
|
* nodemodules里面搜索如下
|
||||||
|
* return childProcess.exec("git", ["add", "--", ...files], execOpts);
|
||||||
|
*
|
||||||
|
* ('git', ['add', '--', ...files]
|
||||||
|
* ('git', ['add', '.']
|
||||||
|
*/
|
||||||
15
doc/cf/cf.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Cloudflare
|
||||||
|
|
||||||
|
|
||||||
|
## CF Token申请
|
||||||
|
|
||||||
|
### 申请地址:
|
||||||
|
https://dash.cloudflare.com/profile/api-tokens
|
||||||
|
|
||||||
|
### 权限设置:
|
||||||
|
需要设置权限和资源范围
|
||||||
|
权限包括:Zone.Zone.edit, Zone.DNS.edit
|
||||||
|
资源范围:要包含对应域名,推荐直接设置为All Zones
|
||||||
|
最终效果如下,可以切换语言为英文对比如下图检查
|
||||||
|
|
||||||
|

|
||||||
BIN
doc/cf/cf_token.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
24
doc/host/host.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 远程主机
|
||||||
|
|
||||||
|
远程主机基于ssh协议,通过ssh连接远程主机,执行命令。
|
||||||
|
|
||||||
|
## windows开启OpenSSH Server
|
||||||
|
1. 安装OpenSSH Server
|
||||||
|
请前往Microsoft官方文档查看如何开启openSSH
|
||||||
|
https://learn.microsoft.com/zh-cn/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui#install-openssh-for-windows
|
||||||
|
|
||||||
|
2. 启动OpenSSH Server服务
|
||||||
|
```
|
||||||
|
win+R 弹出运行对话框,输入 services.msc 打开服务管理器
|
||||||
|
找到 OpenSSH SSH Server
|
||||||
|
启动ssh server服务,并且设置为自动启动
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 测试ssh登录
|
||||||
|
使用你常用的ssh客户端,连接你的windows主机,进行测试
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
# 如何确定你用户名
|
||||||
|
C:\Users\xiaoj>
|
||||||
|
↑↑↑↑---------这个就是windows ssh的登录用户名
|
||||||
|
```
|
||||||
BIN
doc/images/1-add.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
doc/images/10-1-log.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
doc/images/11-1-error.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
doc/images/11-2-error.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
doc/images/12-1-log-success.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
doc/images/12-2-skip-log.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
doc/images/13-1-result.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
doc/images/13-2-result.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
doc/images/13-3-download.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
doc/images/14-timer.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
doc/images/15-1-email.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
doc/images/15-2-email.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
doc/images/2-access-provider.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
doc/images/3-add-access.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
doc/images/4-add-success.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
doc/images/5-view.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
doc/images/6-1-add-task.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
doc/images/6-2-add-task.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
doc/images/6-3-add-task.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
doc/images/7-1-add-host-task.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
doc/images/7-2-add-host-task.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
doc/images/7-3-add-host-task.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
doc/images/8-1-add-host-task.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
doc/images/8-2-add-host-task.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
doc/images/8-4-add-host-task.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
doc/images/8-5-add-host-task.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
doc/images/9-start.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
doc/images/donate.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
doc/images/me.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
doc/tencent/dnspod-token.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
doc/tencent/tencent-access.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
16
doc/tencent/tencent.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 腾讯云
|
||||||
|
|
||||||
|
|
||||||
|
## DNSPOD 授权设置
|
||||||
|
目前腾讯云管理的域名的dns暂时只支持从DNSPOD进行设置
|
||||||
|
打开 https://console.dnspod.cn/account/token/apikey
|
||||||
|
然后按如下方式获取DNSPOD的授权
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 腾讯云API密钥设置
|
||||||
|
|
||||||
|
腾讯云其他部署需要API密钥,需要在腾讯云控制台进行设置
|
||||||
|
打开 https://console.cloud.tencent.com/cam/capi
|
||||||
|
然后按如下方式获取腾讯云的API密钥
|
||||||
|

|
||||||
20
docker/image/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM registry.cn-shenzhen.aliyuncs.com/handsfree/node:18-alpine
|
||||||
|
EXPOSE 7001
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV MIDWAY_SERVER_ENV production
|
||||||
|
WORKDIR /app/
|
||||||
|
#RUN npm install -g pnpm
|
||||||
|
#RUN npm install cross-env -g --registry=https://registry.npmmirror.com
|
||||||
|
#RUN npm install pm2 -g --registry=https://registry.npmmirror.com
|
||||||
|
#RUN pm2 install pm2-logrotate
|
||||||
|
ADD ./workspace/certd-server/ /app/
|
||||||
|
RUN sed -i "s/workspace://g" "/app/package.json"
|
||||||
|
RUN yarn install --production --registry=https://registry.npmmirror.com
|
||||||
|
#RUN yarn install --production
|
||||||
|
RUN npm run build
|
||||||
|
#CMD ["pm2-runtime", "start", "./bootstrap.js","--name", "certd","-i","1"]
|
||||||
|
CMD ["npm", "run","start"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
37
docker/image/build.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
echo "请先输入一个版本号:"
|
||||||
|
read version
|
||||||
|
|
||||||
|
echo "您输入的版本号是: $version"
|
||||||
|
echo "登录aliyun镜像仓库"
|
||||||
|
sudo docker login --username=252959493@qq.com registry.cn-shenzhen.aliyuncs.com
|
||||||
|
|
||||||
|
build=$(pwd)
|
||||||
|
cd ../../
|
||||||
|
root=$(pwd)
|
||||||
|
echo "安装依赖"
|
||||||
|
#pnpm install --registry=https://registry.npmmirror.com
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
echo "client build"
|
||||||
|
cd $root/packages/ui/certd-client
|
||||||
|
pnpm run build
|
||||||
|
echo "client build success"
|
||||||
|
|
||||||
|
echo "server build"
|
||||||
|
cd $root/packages/ui/certd-server
|
||||||
|
pnpm run build
|
||||||
|
echo "server build success"
|
||||||
|
|
||||||
|
echo "rm node_modules"
|
||||||
|
rm ./node_modules -rf
|
||||||
|
|
||||||
|
echo "copy to workspace"
|
||||||
|
mkdir -p $build/workspace/certd-server
|
||||||
|
\cp ./* $build/workspace/certd-server -rf
|
||||||
|
\cp ../certd-client/dist/* $build/workspace/certd-server/public/ -rf
|
||||||
|
|
||||||
|
#export TAG=$version
|
||||||
|
#sudo -E docker compose build
|
||||||
|
#sudo -E docker compose push
|
||||||
15
docker/image/docker-compose.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
version: '3.3' # 指定docker-compose 版本
|
||||||
|
services: # 要拉起的服务们
|
||||||
|
certd:
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${TAG}
|
||||||
|
container_name: certd # 容器名
|
||||||
|
restart: unless-stopped # 重启
|
||||||
|
ports: # 端口映射
|
||||||
|
- "7001:7001"
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
- node_sqlite3_binary_host_mirror=https://registry.npmmirror.com/-/binary/sqlite3
|
||||||
|
|
||||||
26
docker/run/docker-compose.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
certd:
|
||||||
|
# 镜像 # ↓↓↓↓↓ --- 1、 修改镜像版本号,或者干脆写成latest, 如果设置了环境变量 export CERTD_VERSION=latest,这里可以不修改
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${CERTD_VERSION}
|
||||||
|
container_name: certd # 容器名
|
||||||
|
restart: unless-stopped # 自动重启
|
||||||
|
volumes:
|
||||||
|
# ↓↓↓↓↓ ------------------------------------------------------- 2、 修改数据库以及证书存储路径【可选】
|
||||||
|
- /data/certd:/app/data
|
||||||
|
ports: # 端口映射
|
||||||
|
# ↓↓↓↓ 如果端口有冲突,可以修改第一个7001为其他不冲突的端口号
|
||||||
|
- "7001:7001"
|
||||||
|
environment: # 环境变量
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
- certd_auth_jwt_secret=changeme
|
||||||
|
# ↑↑↑↑↑ ---------------------------------- 3、 修改成你的自定义密钥【必须,安全需要】
|
||||||
|
- certd_system_resetAdminPassword=false
|
||||||
|
# ↑↑↑↑↑ 如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false
|
||||||
|
# 设置环境变量即可自定义certd配置
|
||||||
|
# 服务端配置项见: packages/ui/certd-server/src/config/config.default.ts
|
||||||
|
# 服务端配置规则: certd_ + 配置项, 点号用_代替
|
||||||
|
# 如jwt密钥配置为: auth.jwt.secret,则设置环境变量 certd_auth_jwt_secret=changeme
|
||||||
|
|
||||||
|
# 客户端配置项见: packages/ui/certd-client/.env
|
||||||
|
# 按实际名称配置环境变量即可,如: VITE_APP_API=http://localhost:7001
|
||||||
13
docker/run/run.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
# 判断$CERTD_VERSION 是否存在
|
||||||
|
if [ -n "$CERTD_VERSION" ]; then
|
||||||
|
echo "CERTD_VERSION is set = $CERTD_VERSION"
|
||||||
|
else
|
||||||
|
echo "CERTD_VERSION is not set"
|
||||||
|
echo "请先输入一个版本号(如 1.0.6):"
|
||||||
|
read CERTD_VERSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "您输入的版本号是: $CERTD_VERSION"
|
||||||
|
sudo -E docker compose up -d
|
||||||
13
lerna.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
|
"useWorkspaces": true,
|
||||||
|
"command": {
|
||||||
|
"bootstrap": {
|
||||||
|
"npmClientArgs": [
|
||||||
|
"--no-package-lock"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"npmClient": "pnpm",
|
||||||
|
"version": "1.20.15"
|
||||||
|
}
|
||||||
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"version": "1.20.4",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@lerna-lite/cli": "^3.2.1",
|
||||||
|
"@lerna-lite/publish": "^3.2.1",
|
||||||
|
"@lerna-lite/run": "^3.2.1",
|
||||||
|
"@lerna-lite/version": "^3.2.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "lerna bootstrap --hoist",
|
||||||
|
"i-all": "lerna link && lerna exec npm install ",
|
||||||
|
"publish": "npm run prepublishOnly1 && lerna publish --conventional-commits && npm run afterpublishOnly && npm run deploy1",
|
||||||
|
"afterpublishOnly": "",
|
||||||
|
"prepublishOnly1": "npm run before-build && lerna run build ",
|
||||||
|
"before-build": "cd ./packages/core/acme-client && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
|
||||||
|
"deploy1": "node --experimental-json-modules deploy.js "
|
||||||
|
},
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"packages/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -9,8 +9,15 @@ env:
|
|||||||
rules:
|
rules:
|
||||||
indent: [2, 4, { SwitchCase: 1, VariableDeclarator: 1 }]
|
indent: [2, 4, { SwitchCase: 1, VariableDeclarator: 1 }]
|
||||||
brace-style: [2, 'stroustrup', { allowSingleLine: true }]
|
brace-style: [2, 'stroustrup', { allowSingleLine: true }]
|
||||||
|
space-before-function-paren: [2, { anonymous: 'never', named: 'never' }]
|
||||||
func-names: 0
|
func-names: 0
|
||||||
|
prefer-destructuring: 0
|
||||||
|
object-curly-newline: 0
|
||||||
class-methods-use-this: 0
|
class-methods-use-this: 0
|
||||||
|
wrap-iife: [2, 'inside']
|
||||||
no-param-reassign: 0
|
no-param-reassign: 0
|
||||||
|
comma-dangle: [2, 'never']
|
||||||
max-len: [1, 200, 2, { ignoreUrls: true, ignoreComments: false }]
|
max-len: [1, 200, 2, { ignoreUrls: true, ignoreComments: false }]
|
||||||
|
no-multiple-empty-lines: [2, { max: 2, maxBOF: 0, maxEOF: 0 }]
|
||||||
|
prefer-object-spread: 0
|
||||||
import/no-useless-path-segments: 0
|
import/no-useless-path-segments: 0
|
||||||
|
|||||||
@@ -5,10 +5,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Download and install
|
# Download and install
|
||||||
wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLECTS_VERSION}/pebble-challtestsrv-linux-amd64.tar.gz" -O /tmp/pebble-challtestsrv.tar.gz
|
wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLECTS_VERSION}/pebble-challtestsrv_linux-amd64" -O /usr/local/bin/pebble-challtestsrv
|
||||||
tar zxvf /tmp/pebble-challtestsrv.tar.gz -C /tmp
|
|
||||||
|
|
||||||
mv /tmp/pebble-challtestsrv-linux-amd64/linux/amd64/pebble-challtestsrv /usr/local/bin/pebble-challtestsrv
|
|
||||||
chown root:root /usr/local/bin/pebble-challtestsrv
|
chown root:root /usr/local/bin/pebble-challtestsrv
|
||||||
chmod 0755 /usr/local/bin/pebble-challtestsrv
|
chmod 0755 /usr/local/bin/pebble-challtestsrv
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,8 @@ wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION
|
|||||||
wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/config/${CONFIG_NAME}" -O /etc/pebble/pebble.json
|
wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/config/${CONFIG_NAME}" -O /etc/pebble/pebble.json
|
||||||
|
|
||||||
# Download and install Pebble
|
# Download and install Pebble
|
||||||
wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLE_VERSION}/pebble-linux-amd64.tar.gz" -O /tmp/pebble.tar.gz
|
wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLE_VERSION}/pebble_linux-amd64" -O /usr/local/bin/pebble
|
||||||
tar zxvf /tmp/pebble.tar.gz -C /tmp
|
|
||||||
|
|
||||||
mv /tmp/pebble-linux-amd64/linux/amd64/pebble /usr/local/bin/pebble
|
|
||||||
chown root:root /usr/local/bin/pebble
|
chown root:root /usr/local/bin/pebble
|
||||||
chmod 0755 /usr/local/bin/pebble
|
chmod 0755 /usr/local/bin/pebble
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
name: test
|
name: test
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
@@ -8,9 +9,10 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [16, 18, 20]
|
node: [16, 18, 20, 22]
|
||||||
eab: [0, 1]
|
eab: [0, 1]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Environment
|
# Environment
|
||||||
#
|
#
|
||||||
@@ -19,9 +21,9 @@ jobs:
|
|||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
NPM_CONFIG_COLOR: always
|
NPM_CONFIG_COLOR: always
|
||||||
|
|
||||||
PEBBLE_VERSION: 2.6.0
|
PEBBLE_VERSION: 2.3.1
|
||||||
PEBBLE_ALTERNATE_ROOTS: 2
|
PEBBLE_ALTERNATE_ROOTS: 2
|
||||||
PEBBLECTS_VERSION: 2.6.0
|
PEBBLECTS_VERSION: 2.3.1
|
||||||
PEBBLECTS_DNS_PORT: 8053
|
PEBBLECTS_DNS_PORT: 8053
|
||||||
COREDNS_VERSION: 1.11.1
|
COREDNS_VERSION: 1.11.1
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ jobs:
|
|||||||
ACME_HTTP_PORT: 5002
|
ACME_HTTP_PORT: 5002
|
||||||
ACME_HTTPS_PORT: 5003
|
ACME_HTTPS_PORT: 5003
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Pipeline
|
# Pipeline
|
||||||
#
|
#
|
||||||
|
|||||||
1
packages/core/acme-client/.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
/.idea/
|
||||||
|
|||||||
@@ -1,12 +1,61 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.20.15](https://github.com/publishlab/node-acme-client/compare/v1.20.14...v1.20.15) (2024-06-28)
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/publishlab/node-acme-client/commit/e0eb3a441384d474fe2923c69b25318264bdc9df))
|
||||||
|
|
||||||
|
## [1.20.14](https://github.com/publishlab/node-acme-client/compare/v1.20.13...v1.20.14) (2024-06-23)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|
||||||
|
## [1.20.13](https://github.com/publishlab/node-acme-client/compare/v1.20.12...v1.20.13) (2024-06-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|
||||||
|
## [1.20.12](https://github.com/publishlab/node-acme-client/compare/v1.20.10...v1.20.12) (2024-06-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复aliyun域名超过100个找不到域名的bug ([5b1494b](https://github.com/publishlab/node-acme-client/commit/5b1494b3ce93d1026dc56ee741342fbb8bf7be24))
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* 支持cloudflare域名 ([fbb9a47](https://github.com/publishlab/node-acme-client/commit/fbb9a47e8f7bb805289b9ee64bd46ffee0f01c06))
|
||||||
|
|
||||||
|
## [1.20.10](https://github.com/publishlab/node-acme-client/compare/v1.20.9...v1.20.10) (2024-05-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|
||||||
|
## [1.20.9](https://github.com/publishlab/node-acme-client/compare/v1.20.8...v1.20.9) (2024-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|
||||||
|
## [1.20.8](https://github.com/publishlab/node-acme-client/compare/v1.20.7...v1.20.8) (2024-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|
||||||
|
## [1.20.7](https://github.com/publishlab/node-acme-client/compare/v1.20.6...v1.20.7) (2024-03-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|
||||||
|
## [1.20.6](https://github.com/publishlab/node-acme-client/compare/v1.20.5...v1.20.6) (2024-03-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @certd/acme-client
|
||||||
|
|
||||||
|
## [1.20.5](https://github.com/publishlab/node-acme-client/compare/v1.20.2...v1.20.5) (2024-03-11)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 修复腾讯云cdn部署无法选择端点的bug ([154409b](https://github.com/publishlab/node-acme-client/commit/154409b1dfee3ea1caae740ad9c1f99a6e7a9814))
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v5.4.0 (2024-07-16)
|
## v5.3.1
|
||||||
|
|
||||||
* `added` Directory URLs for [Google](https://cloud.google.com/certificate-manager/docs/overview) ACME provider
|
|
||||||
* `fixed` Invalidate ACME provider directory cache after 24 hours
|
|
||||||
* `fixed` Retry HTTP requests on server errors or when rate limited - [#89](https://github.com/publishlab/node-acme-client/issues/89)
|
|
||||||
|
|
||||||
## v5.3.1 (2024-05-22)
|
|
||||||
|
|
||||||
* `fixed` Allow `client.auto()` being called with an empty CSR common name
|
* `fixed` Allow `client.auto()` being called with an empty CSR common name
|
||||||
* `fixed` Bug when calling `updateAccountKey()` with external account binding
|
* `fixed` Bug when calling `updateAccountKey()` with external account binding
|
||||||
@@ -14,7 +63,7 @@
|
|||||||
## v5.3.0 (2024-02-05)
|
## v5.3.0 (2024-02-05)
|
||||||
|
|
||||||
* `added` Support and tests for satisfying `tls-alpn-01` challenges
|
* `added` Support and tests for satisfying `tls-alpn-01` challenges
|
||||||
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR handling
|
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing
|
||||||
* `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge
|
* `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge
|
||||||
* Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
|
* Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
|
||||||
* This change is not considered breaking since the previous behavior was incorrect
|
* This change is not considered breaking since the previous behavior was incorrect
|
||||||
@@ -43,7 +92,7 @@
|
|||||||
* `fixed` Upgrade `axios@0.26.1`
|
* `fixed` Upgrade `axios@0.26.1`
|
||||||
* `fixed` Upgrade `node-forge@1.3.0` - [CVE-2022-24771](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24771), [CVE-2022-24772](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24772), [CVE-2022-24773](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24773)
|
* `fixed` Upgrade `node-forge@1.3.0` - [CVE-2022-24771](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24771), [CVE-2022-24772](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24772), [CVE-2022-24773](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24773)
|
||||||
|
|
||||||
## v4.2.4 (2022-03-19)
|
## 4.2.4 (2022-03-19)
|
||||||
|
|
||||||
* `fixed` Use SHA-256 when signing CSRs
|
* `fixed` Use SHA-256 when signing CSRs
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ This module is written to handle communication with a Boulder/Let's Encrypt-styl
|
|||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
| acme-client | Node.js | |
|
| acme-client | Node.js | |
|
||||||
| ----------- | ------- | ----------------------------------------- |
|
| ------------- | --------- | ----------------------------------------- |
|
||||||
| v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) |
|
| v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) |
|
||||||
| v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) |
|
| v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) |
|
||||||
| v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) |
|
| v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) |
|
||||||
| v2.x | >= v4 | [Changelog](CHANGELOG.md#v200-2018-04-02) |
|
| v2.x | >= v4 | [Changelog](CHANGELOG.md#v200-2018-04-02) |
|
||||||
| v1.x | >= v4 | [Changelog](CHANGELOG.md#v100-2017-10-20) |
|
| v1.x | >= v4 | [Changelog](CHANGELOG.md#v100-2017-10-20) |
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ const accountPrivateKey = '<PEM encoded private key>';
|
|||||||
|
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: accountPrivateKey,
|
accountKey: accountPrivateKey
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -59,9 +59,6 @@ const client = new acme.Client({
|
|||||||
acme.directory.buypass.staging;
|
acme.directory.buypass.staging;
|
||||||
acme.directory.buypass.production;
|
acme.directory.buypass.production;
|
||||||
|
|
||||||
acme.directory.google.staging;
|
|
||||||
acme.directory.google.production;
|
|
||||||
|
|
||||||
acme.directory.letsencrypt.staging;
|
acme.directory.letsencrypt.staging;
|
||||||
acme.directory.letsencrypt.production;
|
acme.directory.letsencrypt.production;
|
||||||
|
|
||||||
@@ -78,8 +75,8 @@ const client = new acme.Client({
|
|||||||
accountKey: accountPrivateKey,
|
accountKey: accountPrivateKey,
|
||||||
externalAccountBinding: {
|
externalAccountBinding: {
|
||||||
kid: 'YOUR-EAB-KID',
|
kid: 'YOUR-EAB-KID',
|
||||||
hmacKey: 'YOUR-EAB-HMAC-KEY',
|
hmacKey: 'YOUR-EAB-HMAC-KEY'
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -93,7 +90,7 @@ In some cases, for example with some EAB providers, this account creation step m
|
|||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: accountPrivateKey,
|
accountKey: accountPrivateKey,
|
||||||
accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678',
|
accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -116,7 +113,8 @@ const privateRsaKey = await acme.crypto.createPrivateRsaKey();
|
|||||||
const privateEcdsaKey = await acme.crypto.createPrivateEcdsaKey();
|
const privateEcdsaKey = await acme.crypto.createPrivateEcdsaKey();
|
||||||
|
|
||||||
const [certificateKey, certificateCsr] = await acme.crypto.createCsr({
|
const [certificateKey, certificateCsr] = await acme.crypto.createCsr({
|
||||||
altNames: ['example.com', '*.example.com'],
|
commonName: '*.example.com',
|
||||||
|
altNames: ['example.com']
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -141,7 +139,7 @@ const autoOpts = {
|
|||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
|
challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
|
||||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {},
|
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const certificate = await client.auto(autoOpts);
|
const certificate = await client.auto(autoOpts);
|
||||||
@@ -158,7 +156,7 @@ To modify challenge priority, provide a list of challenge types in `challengePri
|
|||||||
```js
|
```js
|
||||||
await client.auto({
|
await client.auto({
|
||||||
...,
|
...,
|
||||||
challengePriority: ['http-01', 'dns-01'],
|
challengePriority: ['http-01', 'dns-01']
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -173,7 +171,7 @@ To completely disable `acme-client`s internal challenge verification, enable `sk
|
|||||||
```js
|
```js
|
||||||
await client.auto({
|
await client.auto({
|
||||||
...,
|
...,
|
||||||
skipChallengeVerification: true,
|
skipChallengeVerification: true
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -187,14 +185,14 @@ For more fine-grained control you can interact with the ACME API using the metho
|
|||||||
```js
|
```js
|
||||||
const account = await client.createAccount({
|
const account = await client.createAccount({
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
contact: ['mailto:test@example.com'],
|
contact: ['mailto:test@example.com']
|
||||||
});
|
});
|
||||||
|
|
||||||
const order = await client.createOrder({
|
const order = await client.createOrder({
|
||||||
identifiers: [
|
identifiers: [
|
||||||
{ type: 'dns', value: 'example.com' },
|
{ type: 'dns', value: 'example.com' },
|
||||||
{ type: 'dns', value: '*.example.com' },
|
{ type: 'dns', value: '*.example.com' }
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -209,7 +207,7 @@ const acme = require('acme-client');
|
|||||||
|
|
||||||
acme.axios.defaults.proxy = {
|
acme.axios.defaults.proxy = {
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 9000,
|
port: 9000
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
1
packages/core/acme-client/build.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
14:27
|
||||||
@@ -63,7 +63,7 @@ Create ACME client instance
|
|||||||
```js
|
```js
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: 'Private key goes here',
|
accountKey: 'Private key goes here'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
@@ -75,7 +75,7 @@ const client = new acme.Client({
|
|||||||
accountUrl: 'Optional account URL goes here',
|
accountUrl: 'Optional account URL goes here',
|
||||||
backoffAttempts: 10,
|
backoffAttempts: 10,
|
||||||
backoffMin: 5000,
|
backoffMin: 5000,
|
||||||
backoffMax: 30000,
|
backoffMax: 30000
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
@@ -86,8 +86,8 @@ const client = new acme.Client({
|
|||||||
accountKey: 'Private key goes here',
|
accountKey: 'Private key goes here',
|
||||||
externalAccountBinding: {
|
externalAccountBinding: {
|
||||||
kid: 'YOUR-EAB-KID',
|
kid: 'YOUR-EAB-KID',
|
||||||
hmacKey: 'YOUR-EAB-HMAC-KEY',
|
hmacKey: 'YOUR-EAB-HMAC-KEY'
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
<a name="AcmeClient+getTermsOfServiceUrl"></a>
|
<a name="AcmeClient+getTermsOfServiceUrl"></a>
|
||||||
@@ -145,7 +145,7 @@ https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
|
|||||||
Create a new account
|
Create a new account
|
||||||
```js
|
```js
|
||||||
const account = await client.createAccount({
|
const account = await client.createAccount({
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
@@ -153,7 +153,7 @@ Create a new account with contact info
|
|||||||
```js
|
```js
|
||||||
const account = await client.createAccount({
|
const account = await client.createAccount({
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
contact: ['mailto:test@example.com'],
|
contact: ['mailto:test@example.com']
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
<a name="AcmeClient+updateAccount"></a>
|
<a name="AcmeClient+updateAccount"></a>
|
||||||
@@ -174,7 +174,7 @@ https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
|
|||||||
Update existing account
|
Update existing account
|
||||||
```js
|
```js
|
||||||
const account = await client.updateAccount({
|
const account = await client.updateAccount({
|
||||||
contact: ['mailto:foo@example.com'],
|
contact: ['mailto:foo@example.com']
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
<a name="AcmeClient+updateAccountKey"></a>
|
<a name="AcmeClient+updateAccountKey"></a>
|
||||||
@@ -218,8 +218,8 @@ Create a new order
|
|||||||
const order = await client.createOrder({
|
const order = await client.createOrder({
|
||||||
identifiers: [
|
identifiers: [
|
||||||
{ type: 'dns', value: 'example.com' },
|
{ type: 'dns', value: 'example.com' },
|
||||||
{ type: 'dns', value: 'test.example.com' },
|
{ type: 'dns', value: 'test.example.com' }
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
<a name="AcmeClient+getOrder"></a>
|
<a name="AcmeClient+getOrder"></a>
|
||||||
@@ -452,7 +452,7 @@ Revoke certificate with reason
|
|||||||
```js
|
```js
|
||||||
const certificate = { ... }; // Previously created certificate
|
const certificate = { ... }; // Previously created certificate
|
||||||
const result = await client.revokeCertificate(certificate, {
|
const result = await client.revokeCertificate(certificate, {
|
||||||
reason: 4,
|
reason: 4
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
<a name="AcmeClient+auto"></a>
|
<a name="AcmeClient+auto"></a>
|
||||||
@@ -479,7 +479,7 @@ Auto mode
|
|||||||
Order a certificate using auto mode
|
Order a certificate using auto mode
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com'
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificate = await client.auto({
|
const certificate = await client.auto({
|
||||||
@@ -491,14 +491,14 @@ const certificate = await client.auto({
|
|||||||
},
|
},
|
||||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||||
// Clean up challenge here
|
// Clean up challenge here
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
Order a certificate using auto mode with preferred chain
|
Order a certificate using auto mode with preferred chain
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com'
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificate = await client.auto({
|
const certificate = await client.auto({
|
||||||
@@ -507,7 +507,7 @@ const certificate = await client.auto({
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
preferredChain: 'DST Root CA X3',
|
preferredChain: 'DST Root CA X3',
|
||||||
challengeCreateFn: async () => {},
|
challengeCreateFn: async () => {},
|
||||||
challengeRemoveFn: async () => {},
|
challengeRemoveFn: async () => {}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
<a name="Client"></a>
|
<a name="Client"></a>
|
||||||
|
|||||||
@@ -239,30 +239,29 @@ Create a Certificate Signing Request
|
|||||||
Create a Certificate Signing Request
|
Create a Certificate Signing Request
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
Certificate Signing Request with both common and alternative names
|
Certificate Signing Request with both common and alternative names
|
||||||
> *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
|
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
keySize: 4096,
|
keySize: 4096,
|
||||||
commonName: 'test.example.com',
|
commonName: 'test.example.com',
|
||||||
altNames: ['foo.example.com', 'bar.example.com'],
|
altNames: ['foo.example.com', 'bar.example.com']
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
Certificate Signing Request with additional information
|
Certificate Signing Request with additional information
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com',
|
||||||
country: 'US',
|
country: 'US',
|
||||||
state: 'California',
|
state: 'California',
|
||||||
locality: 'Los Angeles',
|
locality: 'Los Angeles',
|
||||||
organization: 'The Company Inc.',
|
organization: 'The Company Inc.',
|
||||||
organizationUnit: 'IT Department',
|
organizationUnit: 'IT Department',
|
||||||
emailAddress: 'contact@example.com',
|
emailAddress: 'contact@example.com'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
@@ -271,9 +270,8 @@ Certificate Signing Request with ECDSA private key
|
|||||||
const certificateKey = await acme.crypto.createPrivateEcdsaKey();
|
const certificateKey = await acme.crypto.createPrivateEcdsaKey();
|
||||||
|
|
||||||
const [, certificateRequest] = await acme.crypto.createCsr({
|
const [, certificateRequest] = await acme.crypto.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com'
|
||||||
}, certificateKey);
|
}, certificateKey);
|
||||||
```
|
|
||||||
<a name="createAlpnCertificate"></a>
|
<a name="createAlpnCertificate"></a>
|
||||||
|
|
||||||
## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ <code>Promise.<Array.<buffer>></code>
|
## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ <code>Promise.<Array.<buffer>></code>
|
||||||
@@ -300,7 +298,6 @@ Create a ALPN certificate with ECDSA private key
|
|||||||
```js
|
```js
|
||||||
const alpnKey = await acme.crypto.createPrivateEcdsaKey();
|
const alpnKey = await acme.crypto.createPrivateEcdsaKey();
|
||||||
const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
|
const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
|
||||||
```
|
|
||||||
<a name="isAlpnCertificateAuthorizationValid"></a>
|
<a name="isAlpnCertificateAuthorizationValid"></a>
|
||||||
|
|
||||||
## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ <code>boolean</code>
|
## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ <code>boolean</code>
|
||||||
|
|||||||
@@ -222,30 +222,29 @@ Create a Certificate Signing Request
|
|||||||
Create a Certificate Signing Request
|
Create a Certificate Signing Request
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
Certificate Signing Request with both common and alternative names
|
Certificate Signing Request with both common and alternative names
|
||||||
> *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
|
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
||||||
keySize: 4096,
|
keySize: 4096,
|
||||||
commonName: 'test.example.com',
|
commonName: 'test.example.com',
|
||||||
altNames: ['foo.example.com', 'bar.example.com'],
|
altNames: ['foo.example.com', 'bar.example.com']
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
Certificate Signing Request with additional information
|
Certificate Signing Request with additional information
|
||||||
```js
|
```js
|
||||||
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com',
|
||||||
country: 'US',
|
country: 'US',
|
||||||
state: 'California',
|
state: 'California',
|
||||||
locality: 'Los Angeles',
|
locality: 'Los Angeles',
|
||||||
organization: 'The Company Inc.',
|
organization: 'The Company Inc.',
|
||||||
organizationUnit: 'IT Department',
|
organizationUnit: 'IT Department',
|
||||||
emailAddress: 'contact@example.com',
|
emailAddress: 'contact@example.com'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
**Example**
|
**Example**
|
||||||
@@ -254,5 +253,5 @@ Certificate Signing Request with predefined private key
|
|||||||
const certificateKey = await acme.forge.createPrivateKey();
|
const certificateKey = await acme.forge.createPrivateKey();
|
||||||
|
|
||||||
const [, certificateRequest] = await acme.forge.createCsr({
|
const [, certificateRequest] = await acme.forge.createCsr({
|
||||||
altNames: ['test.example.com'],
|
commonName: 'test.example.com'
|
||||||
}, certificateKey);
|
}, certificateKey);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ function log(m) {
|
|||||||
process.stdout.write(`${m}\n`);
|
process.stdout.write(`${m}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function used to satisfy an ACME challenge
|
* Function used to satisfy an ACME challenge
|
||||||
*
|
*
|
||||||
@@ -24,6 +25,7 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
|
|||||||
log(keyAuthorization);
|
log(keyAuthorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function used to remove an ACME challenge response
|
* Function used to remove an ACME challenge response
|
||||||
*
|
*
|
||||||
@@ -39,29 +41,30 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
|
|||||||
log(keyAuthorization);
|
log(keyAuthorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main
|
* Main
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async function() {
|
||||||
/* Init client */
|
/* Init client */
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: await acme.crypto.createPrivateKey(),
|
accountKey: await acme.crypto.createPrivateKey()
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Register account */
|
/* Register account */
|
||||||
await client.createAccount({
|
await client.createAccount({
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
contact: ['mailto:test@example.com'],
|
contact: ['mailto:test@example.com']
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Place new order */
|
/* Place new order */
|
||||||
const order = await client.createOrder({
|
const order = await client.createOrder({
|
||||||
identifiers: [
|
identifiers: [
|
||||||
{ type: 'dns', value: 'example.com' },
|
{ type: 'dns', value: 'example.com' },
|
||||||
{ type: 'dns', value: '*.example.com' },
|
{ type: 'dns', value: '*.example.com' }
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,7 +138,8 @@ module.exports = async () => {
|
|||||||
|
|
||||||
/* Finalize order */
|
/* Finalize order */
|
||||||
const [key, csr] = await acme.crypto.createCsr({
|
const [key, csr] = await acme.crypto.createCsr({
|
||||||
altNames: ['example.com', '*.example.com'],
|
commonName: '*.example.com',
|
||||||
|
altNames: ['example.com']
|
||||||
});
|
});
|
||||||
|
|
||||||
const finalized = await client.finalizeOrder(order, csr);
|
const finalized = await client.finalizeOrder(order, csr);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ function log(m) {
|
|||||||
process.stdout.write(`${m}\n`);
|
process.stdout.write(`${m}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function used to satisfy an ACME challenge
|
* Function used to satisfy an ACME challenge
|
||||||
*
|
*
|
||||||
@@ -46,6 +47,7 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function used to remove an ACME challenge response
|
* Function used to remove an ACME challenge response
|
||||||
*
|
*
|
||||||
@@ -78,24 +80,25 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
|
|||||||
|
|
||||||
/* Replace this */
|
/* Replace this */
|
||||||
log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
|
log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
|
||||||
// await dnsProvider.removeRecord(dnsRecord, 'TXT', recordValue);
|
// await dnsProvider.removeRecord(dnsRecord, 'TXT');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main
|
* Main
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async function() {
|
||||||
/* Init client */
|
/* Init client */
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: await acme.crypto.createPrivateKey(),
|
accountKey: await acme.crypto.createPrivateKey()
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Create CSR */
|
/* Create CSR */
|
||||||
const [key, csr] = await acme.crypto.createCsr({
|
const [key, csr] = await acme.crypto.createCsr({
|
||||||
altNames: ['example.com'],
|
commonName: 'example.com'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Certificate */
|
/* Certificate */
|
||||||
@@ -104,7 +107,7 @@ module.exports = async () => {
|
|||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn,
|
challengeCreateFn,
|
||||||
challengeRemoveFn,
|
challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Done */
|
/* Done */
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function log(m) {
|
|||||||
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main
|
* Main
|
||||||
*/
|
*/
|
||||||
@@ -32,16 +33,18 @@ function log(m) {
|
|||||||
log('Initializing ACME client');
|
log('Initializing ACME client');
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: await acme.crypto.createPrivateKey(),
|
accountKey: await acme.crypto.createPrivateKey()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Order wildcard certificate
|
* Order wildcard certificate
|
||||||
*/
|
*/
|
||||||
|
|
||||||
log(`Creating CSR for ${WILDCARD_DOMAIN}`);
|
log(`Creating CSR for ${WILDCARD_DOMAIN}`);
|
||||||
const [key, csr] = await acme.crypto.createCsr({
|
const [key, csr] = await acme.crypto.createCsr({
|
||||||
altNames: [WILDCARD_DOMAIN, `*.${WILDCARD_DOMAIN}`],
|
commonName: WILDCARD_DOMAIN,
|
||||||
|
altNames: [`*.${WILDCARD_DOMAIN}`]
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Ordering certificate for ${WILDCARD_DOMAIN}`);
|
log(`Ordering certificate for ${WILDCARD_DOMAIN}`);
|
||||||
@@ -57,11 +60,12 @@ function log(m) {
|
|||||||
challengeRemoveFn: (authz, challenge, keyAuthorization) => {
|
challengeRemoveFn: (authz, challenge, keyAuthorization) => {
|
||||||
/* TODO: Implement this */
|
/* TODO: Implement this */
|
||||||
log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
|
log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Certificate for ${WILDCARD_DOMAIN} created successfully`);
|
log(`Certificate for ${WILDCARD_DOMAIN} created successfully`);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTPS server
|
* HTTPS server
|
||||||
*/
|
*/
|
||||||
@@ -74,7 +78,7 @@ function log(m) {
|
|||||||
|
|
||||||
const httpsServer = https.createServer({
|
const httpsServer = https.createServer({
|
||||||
key,
|
key,
|
||||||
cert,
|
cert
|
||||||
}, requestListener);
|
}, requestListener);
|
||||||
|
|
||||||
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function log(m) {
|
|||||||
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On-demand certificate generation using http-01
|
* On-demand certificate generation using http-01
|
||||||
*/
|
*/
|
||||||
@@ -51,7 +52,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
/* Create CSR */
|
/* Create CSR */
|
||||||
log(`Creating CSR for ${servername}`);
|
log(`Creating CSR for ${servername}`);
|
||||||
const [key, csr] = await acme.crypto.createCsr({
|
const [key, csr] = await acme.crypto.createCsr({
|
||||||
altNames: [servername],
|
commonName: servername
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Order certificate */
|
/* Order certificate */
|
||||||
@@ -66,7 +67,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
},
|
},
|
||||||
challengeRemoveFn: (authz, challenge) => {
|
challengeRemoveFn: (authz, challenge) => {
|
||||||
delete challengeResponses[challenge.token];
|
delete challengeResponses[challenge.token];
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Done, store certificate */
|
/* Done, store certificate */
|
||||||
@@ -76,6 +77,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
return certificateStore[servername];
|
return certificateStore[servername];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main
|
* Main
|
||||||
*/
|
*/
|
||||||
@@ -89,9 +91,10 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
log('Initializing ACME client');
|
log('Initializing ACME client');
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: await acme.crypto.createPrivateKey(),
|
accountKey: await acme.crypto.createPrivateKey()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP server
|
* HTTP server
|
||||||
*/
|
*/
|
||||||
@@ -126,6 +129,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
log(`HTTP server listening on port ${HTTP_SERVER_PORT}`);
|
log(`HTTP server listening on port ${HTTP_SERVER_PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTPS server
|
* HTTPS server
|
||||||
*/
|
*/
|
||||||
@@ -154,7 +158,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
log(`[ERROR] ${e.message}`);
|
log(`[ERROR] ${e.message}`);
|
||||||
cb(e.message);
|
cb(e.message);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}, requestListener);
|
}, requestListener);
|
||||||
|
|
||||||
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function log(m) {
|
|||||||
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On-demand certificate generation using tls-alpn-01
|
* On-demand certificate generation using tls-alpn-01
|
||||||
*/
|
*/
|
||||||
@@ -50,7 +51,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
/* Create CSR */
|
/* Create CSR */
|
||||||
log(`Creating CSR for ${servername}`);
|
log(`Creating CSR for ${servername}`);
|
||||||
const [key, csr] = await acme.crypto.createCsr({
|
const [key, csr] = await acme.crypto.createCsr({
|
||||||
altNames: [servername],
|
commonName: servername
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Order certificate */
|
/* Order certificate */
|
||||||
@@ -65,7 +66,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
},
|
},
|
||||||
challengeRemoveFn: (authz) => {
|
challengeRemoveFn: (authz) => {
|
||||||
delete alpnResponses[authz.identifier.value];
|
delete alpnResponses[authz.identifier.value];
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Done, store certificate */
|
/* Done, store certificate */
|
||||||
@@ -75,6 +76,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
return certificateStore[servername];
|
return certificateStore[servername];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main
|
* Main
|
||||||
*/
|
*/
|
||||||
@@ -88,9 +90,10 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
log('Initializing ACME client');
|
log('Initializing ACME client');
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
directoryUrl: acme.directory.letsencrypt.staging,
|
directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
accountKey: await acme.crypto.createPrivateKey(),
|
accountKey: await acme.crypto.createPrivateKey()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ALPN responder
|
* ALPN responder
|
||||||
*/
|
*/
|
||||||
@@ -115,14 +118,14 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
log(`Found ALPN certificate for ${servername}, serving secure context`);
|
log(`Found ALPN certificate for ${servername}, serving secure context`);
|
||||||
cb(null, tls.createSecureContext({
|
cb(null, tls.createSecureContext({
|
||||||
key: alpnResponses[servername][0],
|
key: alpnResponses[servername][0],
|
||||||
cert: alpnResponses[servername][1],
|
cert: alpnResponses[servername][1]
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
log(`[ERROR] ${e.message}`);
|
log(`[ERROR] ${e.message}`);
|
||||||
cb(e.message);
|
cb(e.message);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Terminate once TLS handshake has been established */
|
/* Terminate once TLS handshake has been established */
|
||||||
@@ -134,6 +137,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`);
|
log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTPS server
|
* HTTPS server
|
||||||
*/
|
*/
|
||||||
@@ -162,7 +166,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
|||||||
log(`[ERROR] ${e.message}`);
|
log(`[ERROR] ${e.message}`);
|
||||||
cb(e.message);
|
cb(e.message);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}, requestListener);
|
}, requestListener);
|
||||||
|
|
||||||
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "acme-client",
|
"name": "@certd/acme-client",
|
||||||
"description": "Simple and unopinionated ACME client",
|
"description": "Simple and unopinionated ACME client",
|
||||||
|
"private": false,
|
||||||
"author": "nmorsman",
|
"author": "nmorsman",
|
||||||
"version": "5.4.0",
|
"version": "1.20.15",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"types": "types/index.d.ts",
|
"types": "types/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -15,23 +16,26 @@
|
|||||||
"types"
|
"types"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/x509": "^1.11.0",
|
"@peculiar/x509": "^1.9.7",
|
||||||
"asn1js": "^3.0.5",
|
"asn1js": "^3.0.5",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.6.5",
|
||||||
"debug": "^4.3.5",
|
"debug": "^4.1.1",
|
||||||
|
"https-proxy-agent": "^7.0.4",
|
||||||
"node-forge": "^1.3.1"
|
"node-forge": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.11.5",
|
||||||
"chai": "^4.4.1",
|
"chai": "^4.4.1",
|
||||||
"chai-as-promised": "^7.1.2",
|
"chai-as-promised": "^7.1.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"jsdoc-to-markdown": "^8.0.1",
|
"jsdoc-to-markdown": "^8.0.0",
|
||||||
"mocha": "^10.6.0",
|
"mocha": "^10.2.0",
|
||||||
"nock": "^13.5.4",
|
"nock": "^13.5.0",
|
||||||
"tsd": "^0.31.1"
|
"tsd": "^0.30.4",
|
||||||
|
"typescript": "^4.8.4",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-docs": "jsdoc2md src/client.js > docs/client.md && jsdoc2md src/crypto/index.js > docs/crypto.md && jsdoc2md src/crypto/forge.js > docs/forge.md",
|
"build-docs": "jsdoc2md src/client.js > docs/client.md && jsdoc2md src/crypto/index.js > docs/crypto.md && jsdoc2md src/crypto/forge.js > docs/forge.md",
|
||||||
@@ -54,5 +58,6 @@
|
|||||||
],
|
],
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||||
}
|
},
|
||||||
|
"gitHead": "a31f1c7f5e71fa946de9bf0283e11d6ce049b3e9"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
const util = require('./util');
|
const util = require('./util');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AcmeApi
|
* AcmeApi
|
||||||
*
|
*
|
||||||
@@ -17,6 +18,7 @@ class AcmeApi {
|
|||||||
this.accountUrl = accountUrl;
|
this.accountUrl = accountUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get account URL
|
* Get account URL
|
||||||
*
|
*
|
||||||
@@ -32,6 +34,7 @@ class AcmeApi {
|
|||||||
return this.accountUrl;
|
return this.accountUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACME API request
|
* ACME API request
|
||||||
*
|
*
|
||||||
@@ -56,6 +59,7 @@ class AcmeApi {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACME API request by resource name helper
|
* ACME API request by resource name helper
|
||||||
*
|
*
|
||||||
@@ -74,6 +78,7 @@ class AcmeApi {
|
|||||||
return this.apiRequest(resourceUrl, payload, validStatusCodes, { includeJwsKid, includeExternalAccountBinding });
|
return this.apiRequest(resourceUrl, payload, validStatusCodes, { includeJwsKid, includeExternalAccountBinding });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Terms of Service URL if available
|
* Get Terms of Service URL if available
|
||||||
*
|
*
|
||||||
@@ -86,6 +91,7 @@ class AcmeApi {
|
|||||||
return this.http.getMetaField('termsOfService');
|
return this.http.getMetaField('termsOfService');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new account
|
* Create new account
|
||||||
*
|
*
|
||||||
@@ -98,7 +104,7 @@ class AcmeApi {
|
|||||||
async createAccount(data) {
|
async createAccount(data) {
|
||||||
const resp = await this.apiResourceRequest('newAccount', data, [200, 201], {
|
const resp = await this.apiResourceRequest('newAccount', data, [200, 201], {
|
||||||
includeJwsKid: false,
|
includeJwsKid: false,
|
||||||
includeExternalAccountBinding: (data.onlyReturnExisting !== true),
|
includeExternalAccountBinding: (data.onlyReturnExisting !== true)
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Set account URL */
|
/* Set account URL */
|
||||||
@@ -109,6 +115,7 @@ class AcmeApi {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update account
|
* Update account
|
||||||
*
|
*
|
||||||
@@ -122,6 +129,7 @@ class AcmeApi {
|
|||||||
return this.apiRequest(this.getAccountUrl(), data, [200, 202]);
|
return this.apiRequest(this.getAccountUrl(), data, [200, 202]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update account key
|
* Update account key
|
||||||
*
|
*
|
||||||
@@ -135,6 +143,7 @@ class AcmeApi {
|
|||||||
return this.apiResourceRequest('keyChange', data, [200]);
|
return this.apiResourceRequest('keyChange', data, [200]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new order
|
* Create new order
|
||||||
*
|
*
|
||||||
@@ -148,6 +157,7 @@ class AcmeApi {
|
|||||||
return this.apiResourceRequest('newOrder', data, [201]);
|
return this.apiResourceRequest('newOrder', data, [201]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get order
|
* Get order
|
||||||
*
|
*
|
||||||
@@ -161,6 +171,7 @@ class AcmeApi {
|
|||||||
return this.apiRequest(url, null, [200]);
|
return this.apiRequest(url, null, [200]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalize order
|
* Finalize order
|
||||||
*
|
*
|
||||||
@@ -175,6 +186,7 @@ class AcmeApi {
|
|||||||
return this.apiRequest(url, data, [200]);
|
return this.apiRequest(url, data, [200]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get identifier authorization
|
* Get identifier authorization
|
||||||
*
|
*
|
||||||
@@ -188,6 +200,7 @@ class AcmeApi {
|
|||||||
return this.apiRequest(url, null, [200]);
|
return this.apiRequest(url, null, [200]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update identifier authorization
|
* Update identifier authorization
|
||||||
*
|
*
|
||||||
@@ -202,6 +215,7 @@ class AcmeApi {
|
|||||||
return this.apiRequest(url, data, [200]);
|
return this.apiRequest(url, data, [200]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete challenge
|
* Complete challenge
|
||||||
*
|
*
|
||||||
@@ -216,6 +230,7 @@ class AcmeApi {
|
|||||||
return this.apiRequest(url, data, [200]);
|
return this.apiRequest(url, data, [200]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke certificate
|
* Revoke certificate
|
||||||
*
|
*
|
||||||
@@ -230,5 +245,6 @@ class AcmeApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Export API */
|
/* Export API */
|
||||||
module.exports = AcmeApi;
|
module.exports = AcmeApi;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
const { readCsrDomains } = require('./crypto');
|
const { readCsrDomains } = require('./crypto');
|
||||||
const { log } = require('./logger');
|
const { log } = require('./logger');
|
||||||
|
const { wait } = require('./wait');
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
csr: null,
|
csr: null,
|
||||||
@@ -13,9 +14,10 @@ const defaultOpts = {
|
|||||||
skipChallengeVerification: false,
|
skipChallengeVerification: false,
|
||||||
challengePriority: ['http-01', 'dns-01'],
|
challengePriority: ['http-01', 'dns-01'],
|
||||||
challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()'); },
|
challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()'); },
|
||||||
challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); },
|
challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACME client auto mode
|
* ACME client auto mode
|
||||||
*
|
*
|
||||||
@@ -24,8 +26,8 @@ const defaultOpts = {
|
|||||||
* @returns {Promise<buffer>} Certificate
|
* @returns {Promise<buffer>} Certificate
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = async (client, userOpts) => {
|
module.exports = async function(client, userOpts) {
|
||||||
const opts = { ...defaultOpts, ...userOpts };
|
const opts = Object.assign({}, defaultOpts, userOpts);
|
||||||
const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed };
|
const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed };
|
||||||
|
|
||||||
if (!Buffer.isBuffer(opts.csr)) {
|
if (!Buffer.isBuffer(opts.csr)) {
|
||||||
@@ -36,6 +38,7 @@ module.exports = async (client, userOpts) => {
|
|||||||
accountPayload.contact = [`mailto:${opts.email}`];
|
accountPayload.contact = [`mailto:${opts.email}`];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register account
|
* Register account
|
||||||
*/
|
*/
|
||||||
@@ -51,6 +54,7 @@ module.exports = async (client, userOpts) => {
|
|||||||
await client.createAccount(accountPayload);
|
await client.createAccount(accountPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse domains from CSR
|
* Parse domains from CSR
|
||||||
*/
|
*/
|
||||||
@@ -61,6 +65,7 @@ module.exports = async (client, userOpts) => {
|
|||||||
|
|
||||||
log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`);
|
log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Place order
|
* Place order
|
||||||
*/
|
*/
|
||||||
@@ -72,13 +77,16 @@ module.exports = async (client, userOpts) => {
|
|||||||
|
|
||||||
log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`);
|
log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve and satisfy challenges
|
* Resolve and satisfy challenges
|
||||||
*/
|
*/
|
||||||
|
|
||||||
log('[auto] Resolving and satisfying authorization challenges');
|
log('[auto] Resolving and satisfying authorization challenges');
|
||||||
|
|
||||||
const challengePromises = authorizations.map(async (authz) => {
|
const clearTasks = [];
|
||||||
|
|
||||||
|
const challengeFunc = async (authz) => {
|
||||||
const d = authz.identifier.value;
|
const d = authz.identifier.value;
|
||||||
let challengeCompleted = false;
|
let challengeCompleted = false;
|
||||||
|
|
||||||
@@ -108,10 +116,22 @@ module.exports = async (client, userOpts) => {
|
|||||||
/* Trigger challengeCreateFn() */
|
/* Trigger challengeCreateFn() */
|
||||||
log(`[auto] [${d}] Trigger challengeCreateFn()`);
|
log(`[auto] [${d}] Trigger challengeCreateFn()`);
|
||||||
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
||||||
|
let recordItem = null;
|
||||||
try {
|
try {
|
||||||
await opts.challengeCreateFn(authz, challenge, keyAuthorization);
|
recordItem = await opts.challengeCreateFn(authz, challenge, keyAuthorization);
|
||||||
|
log(`[auto] [${d}] challengeCreateFn success`);
|
||||||
|
log(`[auto] [${d}] add challengeRemoveFn()`);
|
||||||
|
clearTasks.push(async () => {
|
||||||
|
/* Trigger challengeRemoveFn(), suppress errors */
|
||||||
|
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
|
||||||
|
try {
|
||||||
|
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// throw new Error('测试异常');
|
||||||
/* Challenge verification */
|
/* Challenge verification */
|
||||||
if (opts.skipChallengeVerification === true) {
|
if (opts.skipChallengeVerification === true) {
|
||||||
log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`);
|
log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`);
|
||||||
@@ -128,16 +148,9 @@ module.exports = async (client, userOpts) => {
|
|||||||
|
|
||||||
await client.waitForValidStatus(challenge);
|
await client.waitForValidStatus(challenge);
|
||||||
}
|
}
|
||||||
finally {
|
catch (e) {
|
||||||
/* Trigger challengeRemoveFn(), suppress errors */
|
log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`);
|
||||||
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
|
throw e;
|
||||||
|
|
||||||
try {
|
|
||||||
await opts.challengeRemoveFn(authz, challenge, keyAuthorization);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@@ -157,26 +170,64 @@ module.exports = async (client, userOpts) => {
|
|||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const challengePromises = authorizations.map((authz) => async () => {
|
||||||
|
await challengeFunc(authz);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for all challenge promises to settle
|
function runAllPromise(tasks) {
|
||||||
*/
|
let promise = Promise.resolve();
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
promise = promise.then(task);
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPromisePa(tasks) {
|
||||||
|
const results = [];
|
||||||
|
// eslint-disable-next-line no-await-in-loop,no-restricted-syntax
|
||||||
|
for (const task of tasks) {
|
||||||
|
results.push(task());
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await wait(10000);
|
||||||
|
}
|
||||||
|
return Promise.all(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log('[auto] Waiting for challenge valid status');
|
log('开始challenge');
|
||||||
await Promise.all(challengePromises);
|
await runPromisePa(challengePromises);
|
||||||
|
|
||||||
|
log('challenge结束');
|
||||||
|
|
||||||
|
// log('[auto] Waiting for challenge valid status');
|
||||||
|
// await Promise.all(challengePromises);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalize order and download certificate
|
||||||
|
*/
|
||||||
|
|
||||||
|
log('[auto] Finalizing order and downloading certificate');
|
||||||
|
const finalized = await client.finalizeOrder(order, opts.csr);
|
||||||
|
return await client.getCertificate(finalized, opts.preferredChain);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
await Promise.allSettled(challengePromises);
|
log('证书申请失败');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
log(`清理challenge痕迹,length:${clearTasks.length}`);
|
||||||
|
await runAllPromise(clearTasks);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
// try {
|
||||||
* Finalize order and download certificate
|
// await Promise.allSettled(challengePromises);
|
||||||
*/
|
// }
|
||||||
|
// finally {
|
||||||
log('[auto] Finalizing order and downloading certificate');
|
// log('清理challenge');
|
||||||
const finalized = await client.finalizeOrder(order, opts.csr);
|
// await Promise.allSettled(clearTasks);
|
||||||
return client.getCertificate(finalized, opts.preferredChain);
|
// }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,14 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { parseRetryAfterHeader } = require('./util');
|
|
||||||
const { log } = require('./logger');
|
|
||||||
const pkg = require('./../package.json');
|
const pkg = require('./../package.json');
|
||||||
|
|
||||||
const { AxiosError } = axios;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults
|
* Instance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const instance = axios.create();
|
const instance = axios.create();
|
||||||
@@ -22,12 +19,13 @@ instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version
|
|||||||
instance.defaults.acmeSettings = {
|
instance.defaults.acmeSettings = {
|
||||||
httpChallengePort: 80,
|
httpChallengePort: 80,
|
||||||
httpsChallengePort: 443,
|
httpsChallengePort: 443,
|
||||||
tlsAlpnChallengePort: 443,
|
tlsAlpnChallengePort: 443
|
||||||
|
|
||||||
retryMaxAttempts: 5,
|
|
||||||
retryDefaultDelay: 5,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// instance.defaults.proxy = {
|
||||||
|
// host: '192.168.34.139',
|
||||||
|
// port: 10811
|
||||||
|
// };
|
||||||
/**
|
/**
|
||||||
* Explicitly set Node as default HTTP adapter
|
* Explicitly set Node as default HTTP adapter
|
||||||
*
|
*
|
||||||
@@ -37,84 +35,6 @@ instance.defaults.acmeSettings = {
|
|||||||
|
|
||||||
instance.defaults.adapter = 'http';
|
instance.defaults.adapter = 'http';
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry requests on server errors or when rate limited
|
|
||||||
*
|
|
||||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.6
|
|
||||||
*/
|
|
||||||
|
|
||||||
function isRetryableError(error) {
|
|
||||||
return (error.code !== 'ECONNABORTED')
|
|
||||||
&& (error.code !== 'ERR_NOCK_NO_MATCH')
|
|
||||||
&& (!error.response
|
|
||||||
|| (error.response.status === 429)
|
|
||||||
|| ((error.response.status >= 500) && (error.response.status <= 599)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* https://github.com/axios/axios/blob/main/lib/core/settle.js */
|
|
||||||
function validateStatus(response) {
|
|
||||||
const validator = response.config.retryValidateStatus;
|
|
||||||
|
|
||||||
if (!response.status || !validator || validator(response.status)) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AxiosError(
|
|
||||||
`Request failed with status code ${response.status}`,
|
|
||||||
(Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
|
|
||||||
response.config,
|
|
||||||
response.request,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pass all responses through the error interceptor */
|
|
||||||
instance.interceptors.request.use((config) => {
|
|
||||||
if (!('retryValidateStatus' in config)) {
|
|
||||||
config.retryValidateStatus = config.validateStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.validateStatus = () => false;
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Handle request retries if applicable */
|
|
||||||
instance.interceptors.response.use(null, async (error) => {
|
|
||||||
const { config, response } = error;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pick up errors we want to retry */
|
|
||||||
if (isRetryableError(error)) {
|
|
||||||
const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings;
|
|
||||||
config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1;
|
|
||||||
|
|
||||||
if (config.retryAttempt <= retryMaxAttempts) {
|
|
||||||
const code = response ? `HTTP ${response.status}` : error.code;
|
|
||||||
log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`);
|
|
||||||
|
|
||||||
/* Attempt to parse Retry-After header, fallback to default delay */
|
|
||||||
let retryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0;
|
|
||||||
|
|
||||||
if (retryAfter > 0) {
|
|
||||||
log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${retryAfter} seconds`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
retryAfter = (retryDefaultDelay * config.retryAttempt);
|
|
||||||
log(`Unable to locate or parse retry-after response header, waiting ${retryAfter} seconds`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wait and retry the request */
|
|
||||||
await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); });
|
|
||||||
return instance(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Validate and return response */
|
|
||||||
return validateStatus(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export instance
|
* Export instance
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const verify = require('./verify');
|
|||||||
const util = require('./util');
|
const util = require('./util');
|
||||||
const auto = require('./auto');
|
const auto = require('./auto');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACME states
|
* ACME states
|
||||||
*
|
*
|
||||||
@@ -23,6 +24,7 @@ const validStates = ['ready', 'valid'];
|
|||||||
const pendingStates = ['pending', 'processing'];
|
const pendingStates = ['pending', 'processing'];
|
||||||
const invalidStates = ['invalid'];
|
const invalidStates = ['invalid'];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default options
|
* Default options
|
||||||
*
|
*
|
||||||
@@ -36,9 +38,10 @@ const defaultOpts = {
|
|||||||
externalAccountBinding: {},
|
externalAccountBinding: {},
|
||||||
backoffAttempts: 10,
|
backoffAttempts: 10,
|
||||||
backoffMin: 5000,
|
backoffMin: 5000,
|
||||||
backoffMax: 30000,
|
backoffMax: 30000
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AcmeClient
|
* AcmeClient
|
||||||
*
|
*
|
||||||
@@ -58,7 +61,7 @@ const defaultOpts = {
|
|||||||
* ```js
|
* ```js
|
||||||
* const client = new acme.Client({
|
* const client = new acme.Client({
|
||||||
* directoryUrl: acme.directory.letsencrypt.staging,
|
* directoryUrl: acme.directory.letsencrypt.staging,
|
||||||
* accountKey: 'Private key goes here',
|
* accountKey: 'Private key goes here'
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -70,7 +73,7 @@ const defaultOpts = {
|
|||||||
* accountUrl: 'Optional account URL goes here',
|
* accountUrl: 'Optional account URL goes here',
|
||||||
* backoffAttempts: 10,
|
* backoffAttempts: 10,
|
||||||
* backoffMin: 5000,
|
* backoffMin: 5000,
|
||||||
* backoffMax: 30000,
|
* backoffMax: 30000
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -81,8 +84,8 @@ const defaultOpts = {
|
|||||||
* accountKey: 'Private key goes here',
|
* accountKey: 'Private key goes here',
|
||||||
* externalAccountBinding: {
|
* externalAccountBinding: {
|
||||||
* kid: 'YOUR-EAB-KID',
|
* kid: 'YOUR-EAB-KID',
|
||||||
* hmacKey: 'YOUR-EAB-HMAC-KEY',
|
* hmacKey: 'YOUR-EAB-HMAC-KEY'
|
||||||
* },
|
* }
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -93,17 +96,19 @@ class AcmeClient {
|
|||||||
opts.accountKey = Buffer.from(opts.accountKey);
|
opts.accountKey = Buffer.from(opts.accountKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.opts = { ...defaultOpts, ...opts };
|
this.opts = Object.assign({}, defaultOpts, opts);
|
||||||
|
|
||||||
this.backoffOpts = {
|
this.backoffOpts = {
|
||||||
attempts: this.opts.backoffAttempts,
|
attempts: this.opts.backoffAttempts,
|
||||||
min: this.opts.backoffMin,
|
min: this.opts.backoffMin,
|
||||||
max: this.opts.backoffMax,
|
max: this.opts.backoffMax
|
||||||
};
|
};
|
||||||
|
|
||||||
this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey, this.opts.externalAccountBinding);
|
this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey, this.opts.externalAccountBinding);
|
||||||
this.api = new AcmeApi(this.http, this.opts.accountUrl);
|
this.api = new AcmeApi(this.http, this.opts.accountUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Terms of Service URL if available
|
* Get Terms of Service URL if available
|
||||||
*
|
*
|
||||||
@@ -123,6 +128,7 @@ class AcmeClient {
|
|||||||
return this.api.getTermsOfServiceUrl();
|
return this.api.getTermsOfServiceUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current account URL
|
* Get current account URL
|
||||||
*
|
*
|
||||||
@@ -144,6 +150,7 @@ class AcmeClient {
|
|||||||
return this.api.getAccountUrl();
|
return this.api.getAccountUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new account
|
* Create a new account
|
||||||
*
|
*
|
||||||
@@ -155,7 +162,7 @@ class AcmeClient {
|
|||||||
* @example Create a new account
|
* @example Create a new account
|
||||||
* ```js
|
* ```js
|
||||||
* const account = await client.createAccount({
|
* const account = await client.createAccount({
|
||||||
* termsOfServiceAgreed: true,
|
* termsOfServiceAgreed: true
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -163,7 +170,7 @@ class AcmeClient {
|
|||||||
* ```js
|
* ```js
|
||||||
* const account = await client.createAccount({
|
* const account = await client.createAccount({
|
||||||
* termsOfServiceAgreed: true,
|
* termsOfServiceAgreed: true,
|
||||||
* contact: ['mailto:test@example.com'],
|
* contact: ['mailto:test@example.com']
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -189,6 +196,7 @@ class AcmeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update existing account
|
* Update existing account
|
||||||
*
|
*
|
||||||
@@ -200,7 +208,7 @@ class AcmeClient {
|
|||||||
* @example Update existing account
|
* @example Update existing account
|
||||||
* ```js
|
* ```js
|
||||||
* const account = await client.updateAccount({
|
* const account = await client.updateAccount({
|
||||||
* contact: ['mailto:foo@example.com'],
|
* contact: ['mailto:foo@example.com']
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -228,6 +236,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update account private key
|
* Update account private key
|
||||||
*
|
*
|
||||||
@@ -273,6 +282,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new order
|
* Create a new order
|
||||||
*
|
*
|
||||||
@@ -286,8 +296,8 @@ class AcmeClient {
|
|||||||
* const order = await client.createOrder({
|
* const order = await client.createOrder({
|
||||||
* identifiers: [
|
* identifiers: [
|
||||||
* { type: 'dns', value: 'example.com' },
|
* { type: 'dns', value: 'example.com' },
|
||||||
* { type: 'dns', value: 'test.example.com' },
|
* { type: 'dns', value: 'test.example.com' }
|
||||||
* ],
|
* ]
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -304,6 +314,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh order object from CA
|
* Refresh order object from CA
|
||||||
*
|
*
|
||||||
@@ -365,6 +376,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get identifier authorizations from order
|
* Get identifier authorizations from order
|
||||||
*
|
*
|
||||||
@@ -394,6 +406,7 @@ class AcmeClient {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deactivate identifier authorization
|
* Deactivate identifier authorization
|
||||||
*
|
*
|
||||||
@@ -414,7 +427,10 @@ class AcmeClient {
|
|||||||
throw new Error('Unable to deactivate identifier authorization, URL not found');
|
throw new Error('Unable to deactivate identifier authorization, URL not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = { status: 'deactivated' };
|
const data = {
|
||||||
|
status: 'deactivated'
|
||||||
|
};
|
||||||
|
|
||||||
const resp = await this.api.updateAuthorization(authz.url, data);
|
const resp = await this.api.updateAuthorization(authz.url, data);
|
||||||
|
|
||||||
/* Add URL to response */
|
/* Add URL to response */
|
||||||
@@ -422,6 +438,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get key authorization for ACME challenge
|
* Get key authorization for ACME challenge
|
||||||
*
|
*
|
||||||
@@ -463,6 +480,7 @@ class AcmeClient {
|
|||||||
throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
|
throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that ACME challenge is satisfied
|
* Verify that ACME challenge is satisfied
|
||||||
*
|
*
|
||||||
@@ -497,6 +515,7 @@ class AcmeClient {
|
|||||||
return util.retry(verifyFn, this.backoffOpts);
|
return util.retry(verifyFn, this.backoffOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify CA that challenge has been completed
|
* Notify CA that challenge has been completed
|
||||||
*
|
*
|
||||||
@@ -517,6 +536,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for ACME provider to verify status on a order, authorization or challenge
|
* Wait for ACME provider to verify status on a order, authorization or challenge
|
||||||
*
|
*
|
||||||
@@ -573,6 +593,7 @@ class AcmeClient {
|
|||||||
return util.retry(verifyFn, this.backoffOpts);
|
return util.retry(verifyFn, this.backoffOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get certificate from ACME order
|
* Get certificate from ACME order
|
||||||
*
|
*
|
||||||
@@ -619,6 +640,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke certificate
|
* Revoke certificate
|
||||||
*
|
*
|
||||||
@@ -638,7 +660,7 @@ class AcmeClient {
|
|||||||
* ```js
|
* ```js
|
||||||
* const certificate = { ... }; // Previously created certificate
|
* const certificate = { ... }; // Previously created certificate
|
||||||
* const result = await client.revokeCertificate(certificate, {
|
* const result = await client.revokeCertificate(certificate, {
|
||||||
* reason: 4,
|
* reason: 4
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -649,6 +671,7 @@ class AcmeClient {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto mode
|
* Auto mode
|
||||||
*
|
*
|
||||||
@@ -666,7 +689,7 @@ class AcmeClient {
|
|||||||
* @example Order a certificate using auto mode
|
* @example Order a certificate using auto mode
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com'
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* const certificate = await client.auto({
|
* const certificate = await client.auto({
|
||||||
@@ -678,14 +701,14 @@ class AcmeClient {
|
|||||||
* },
|
* },
|
||||||
* challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
* challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||||
* // Clean up challenge here
|
* // Clean up challenge here
|
||||||
* },
|
* }
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @example Order a certificate using auto mode with preferred chain
|
* @example Order a certificate using auto mode with preferred chain
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com'
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* const certificate = await client.auto({
|
* const certificate = await client.auto({
|
||||||
@@ -694,7 +717,7 @@ class AcmeClient {
|
|||||||
* termsOfServiceAgreed: true,
|
* termsOfServiceAgreed: true,
|
||||||
* preferredChain: 'DST Root CA X3',
|
* preferredChain: 'DST Root CA X3',
|
||||||
* challengeCreateFn: async () => {},
|
* challengeCreateFn: async () => {},
|
||||||
* challengeRemoveFn: async () => {},
|
* challengeRemoveFn: async () => {}
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -704,5 +727,6 @@ class AcmeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Export client */
|
/* Export client */
|
||||||
module.exports = AcmeClient;
|
module.exports = AcmeClient;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const forge = require('node-forge');
|
|||||||
|
|
||||||
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
|
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to parse forge object from PEM encoded string
|
* Attempt to parse forge object from PEM encoded string
|
||||||
*
|
*
|
||||||
@@ -53,6 +54,7 @@ function forgeObjectFromPem(input) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse domain names from a certificate or CSR
|
* Parse domain names from a certificate or CSR
|
||||||
*
|
*
|
||||||
@@ -91,10 +93,11 @@ function parseDomains(obj) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a private RSA key
|
* Generate a private RSA key
|
||||||
*
|
*
|
||||||
@@ -120,6 +123,7 @@ async function createPrivateKey(size = 2048) {
|
|||||||
|
|
||||||
exports.createPrivateKey = createPrivateKey;
|
exports.createPrivateKey = createPrivateKey;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create public key from a private RSA key
|
* Create public key from a private RSA key
|
||||||
*
|
*
|
||||||
@@ -132,13 +136,14 @@ exports.createPrivateKey = createPrivateKey;
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.createPublicKey = async (key) => {
|
exports.createPublicKey = async function(key) {
|
||||||
const privateKey = forge.pki.privateKeyFromPem(key);
|
const privateKey = forge.pki.privateKeyFromPem(key);
|
||||||
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
|
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
|
||||||
const pemKey = forge.pki.publicKeyToPem(publicKey);
|
const pemKey = forge.pki.publicKeyToPem(publicKey);
|
||||||
return Buffer.from(pemKey);
|
return Buffer.from(pemKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse body of PEM encoded object from buffer or string
|
* Parse body of PEM encoded object from buffer or string
|
||||||
* If multiple objects are chained, the first body will be returned
|
* If multiple objects are chained, the first body will be returned
|
||||||
@@ -152,6 +157,7 @@ exports.getPemBody = (str) => {
|
|||||||
return forge.util.encode64(msg.body);
|
return forge.util.encode64(msg.body);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split chain of PEM encoded objects from buffer or string into array
|
* Split chain of PEM encoded objects from buffer or string into array
|
||||||
*
|
*
|
||||||
@@ -161,6 +167,7 @@ exports.getPemBody = (str) => {
|
|||||||
|
|
||||||
exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
|
exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get modulus
|
* Get modulus
|
||||||
*
|
*
|
||||||
@@ -175,7 +182,7 @@ exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.getModulus = async (input) => {
|
exports.getModulus = async function(input) {
|
||||||
if (!Buffer.isBuffer(input)) {
|
if (!Buffer.isBuffer(input)) {
|
||||||
input = Buffer.from(input);
|
input = Buffer.from(input);
|
||||||
}
|
}
|
||||||
@@ -184,6 +191,7 @@ exports.getModulus = async (input) => {
|
|||||||
return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary');
|
return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get public exponent
|
* Get public exponent
|
||||||
*
|
*
|
||||||
@@ -198,7 +206,7 @@ exports.getModulus = async (input) => {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.getPublicExponent = async (input) => {
|
exports.getPublicExponent = async function(input) {
|
||||||
if (!Buffer.isBuffer(input)) {
|
if (!Buffer.isBuffer(input)) {
|
||||||
input = Buffer.from(input);
|
input = Buffer.from(input);
|
||||||
}
|
}
|
||||||
@@ -207,6 +215,7 @@ exports.getPublicExponent = async (input) => {
|
|||||||
return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary');
|
return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read domains from a Certificate Signing Request
|
* Read domains from a Certificate Signing Request
|
||||||
*
|
*
|
||||||
@@ -222,7 +231,7 @@ exports.getPublicExponent = async (input) => {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.readCsrDomains = async (csr) => {
|
exports.readCsrDomains = async function(csr) {
|
||||||
if (!Buffer.isBuffer(csr)) {
|
if (!Buffer.isBuffer(csr)) {
|
||||||
csr = Buffer.from(csr);
|
csr = Buffer.from(csr);
|
||||||
}
|
}
|
||||||
@@ -231,6 +240,7 @@ exports.readCsrDomains = async (csr) => {
|
|||||||
return parseDomains(obj);
|
return parseDomains(obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read information from a certificate
|
* Read information from a certificate
|
||||||
*
|
*
|
||||||
@@ -250,7 +260,7 @@ exports.readCsrDomains = async (csr) => {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.readCertificateInfo = async (cert) => {
|
exports.readCertificateInfo = async function(cert) {
|
||||||
if (!Buffer.isBuffer(cert)) {
|
if (!Buffer.isBuffer(cert)) {
|
||||||
cert = Buffer.from(cert);
|
cert = Buffer.from(cert);
|
||||||
}
|
}
|
||||||
@@ -260,14 +270,15 @@ exports.readCertificateInfo = async (cert) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
issuer: {
|
issuer: {
|
||||||
commonName: issuerCn ? issuerCn.value : null,
|
commonName: issuerCn ? issuerCn.value : null
|
||||||
},
|
},
|
||||||
domains: parseDomains(obj),
|
domains: parseDomains(obj),
|
||||||
notAfter: obj.validity.notAfter,
|
notAfter: obj.validity.notAfter,
|
||||||
notBefore: obj.validity.notBefore,
|
notBefore: obj.validity.notBefore
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine ASN.1 type for CSR subject short name
|
* Determine ASN.1 type for CSR subject short name
|
||||||
* Note: https://datatracker.ietf.org/doc/html/rfc5280
|
* Note: https://datatracker.ietf.org/doc/html/rfc5280
|
||||||
@@ -288,6 +299,7 @@ function getCsrValueTagClass(shortName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create array of short names and values for Certificate Signing Request subjects
|
* Create array of short names and values for Certificate Signing Request subjects
|
||||||
*
|
*
|
||||||
@@ -307,6 +319,7 @@ function createCsrSubject(subjectObj) {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create array of alt names for Certificate Signing Requests
|
* Create array of alt names for Certificate Signing Requests
|
||||||
* Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454
|
* Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454
|
||||||
@@ -323,6 +336,7 @@ function formatCsrAltNames(altNames) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Certificate Signing Request
|
* Create a Certificate Signing Request
|
||||||
*
|
*
|
||||||
@@ -342,30 +356,29 @@ function formatCsrAltNames(altNames) {
|
|||||||
* @example Create a Certificate Signing Request
|
* @example Create a Certificate Signing Request
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com'
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @example Certificate Signing Request with both common and alternative names
|
* @example Certificate Signing Request with both common and alternative names
|
||||||
* > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
|
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
||||||
* keySize: 4096,
|
* keySize: 4096,
|
||||||
* commonName: 'test.example.com',
|
* commonName: 'test.example.com',
|
||||||
* altNames: ['foo.example.com', 'bar.example.com'],
|
* altNames: ['foo.example.com', 'bar.example.com']
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @example Certificate Signing Request with additional information
|
* @example Certificate Signing Request with additional information
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com',
|
||||||
* country: 'US',
|
* country: 'US',
|
||||||
* state: 'California',
|
* state: 'California',
|
||||||
* locality: 'Los Angeles',
|
* locality: 'Los Angeles',
|
||||||
* organization: 'The Company Inc.',
|
* organization: 'The Company Inc.',
|
||||||
* organizationUnit: 'IT Department',
|
* organizationUnit: 'IT Department',
|
||||||
* emailAddress: 'contact@example.com',
|
* emailAddress: 'contact@example.com'
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -374,11 +387,11 @@ function formatCsrAltNames(altNames) {
|
|||||||
* const certificateKey = await acme.forge.createPrivateKey();
|
* const certificateKey = await acme.forge.createPrivateKey();
|
||||||
*
|
*
|
||||||
* const [, certificateRequest] = await acme.forge.createCsr({
|
* const [, certificateRequest] = await acme.forge.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com'
|
||||||
* }, certificateKey);
|
* }, certificateKey);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.createCsr = async (data, key = null) => {
|
exports.createCsr = async function(data, key = null) {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
key = await createPrivateKey(data.keySize);
|
key = await createPrivateKey(data.keySize);
|
||||||
}
|
}
|
||||||
@@ -410,7 +423,7 @@ exports.createCsr = async (data, key = null) => {
|
|||||||
L: data.locality,
|
L: data.locality,
|
||||||
O: data.organization,
|
O: data.organization,
|
||||||
OU: data.organizationUnit,
|
OU: data.organizationUnit,
|
||||||
E: data.emailAddress,
|
E: data.emailAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
csr.setSubject(subject);
|
csr.setSubject(subject);
|
||||||
@@ -421,8 +434,8 @@ exports.createCsr = async (data, key = null) => {
|
|||||||
name: 'extensionRequest',
|
name: 'extensionRequest',
|
||||||
extensions: [{
|
extensions: [{
|
||||||
name: 'subjectAltName',
|
name: 'subjectAltName',
|
||||||
altNames: formatCsrAltNames(data.altNames),
|
altNames: formatCsrAltNames(data.altNames)
|
||||||
}],
|
}]
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const subjectAltNameOID = '2.5.29.17';
|
|||||||
/* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
|
/* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
|
||||||
const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
|
const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine key type and info by attempting to derive public key
|
* Determine key type and info by attempting to derive public key
|
||||||
*
|
*
|
||||||
@@ -34,7 +35,7 @@ function getKeyInfo(keyPem) {
|
|||||||
const result = {
|
const result = {
|
||||||
isRSA: false,
|
isRSA: false,
|
||||||
isECDSA: false,
|
isECDSA: false,
|
||||||
publicKey: crypto.createPublicKey(keyPem),
|
publicKey: crypto.createPublicKey(keyPem)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.publicKey.asymmetricKeyType === 'rsa') {
|
if (result.publicKey.asymmetricKeyType === 'rsa') {
|
||||||
@@ -50,6 +51,7 @@ function getKeyInfo(keyPem) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a private RSA key
|
* Generate a private RSA key
|
||||||
*
|
*
|
||||||
@@ -72,8 +74,8 @@ async function createPrivateRsaKey(modulusLength = 2048) {
|
|||||||
modulusLength,
|
modulusLength,
|
||||||
privateKeyEncoding: {
|
privateKeyEncoding: {
|
||||||
type: 'pkcs8',
|
type: 'pkcs8',
|
||||||
format: 'pem',
|
format: 'pem'
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Buffer.from(pair.privateKey);
|
return Buffer.from(pair.privateKey);
|
||||||
@@ -81,6 +83,7 @@ async function createPrivateRsaKey(modulusLength = 2048) {
|
|||||||
|
|
||||||
exports.createPrivateRsaKey = createPrivateRsaKey;
|
exports.createPrivateRsaKey = createPrivateRsaKey;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alias of `createPrivateRsaKey()`
|
* Alias of `createPrivateRsaKey()`
|
||||||
*
|
*
|
||||||
@@ -89,6 +92,7 @@ exports.createPrivateRsaKey = createPrivateRsaKey;
|
|||||||
|
|
||||||
exports.createPrivateKey = createPrivateRsaKey;
|
exports.createPrivateKey = createPrivateRsaKey;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a private ECDSA key
|
* Generate a private ECDSA key
|
||||||
*
|
*
|
||||||
@@ -111,13 +115,14 @@ exports.createPrivateEcdsaKey = async (namedCurve = 'P-256') => {
|
|||||||
namedCurve,
|
namedCurve,
|
||||||
privateKeyEncoding: {
|
privateKeyEncoding: {
|
||||||
type: 'pkcs8',
|
type: 'pkcs8',
|
||||||
format: 'pem',
|
format: 'pem'
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Buffer.from(pair.privateKey);
|
return Buffer.from(pair.privateKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a public key derived from a RSA or ECDSA key
|
* Get a public key derived from a RSA or ECDSA key
|
||||||
*
|
*
|
||||||
@@ -135,12 +140,13 @@ exports.getPublicKey = (keyPem) => {
|
|||||||
|
|
||||||
const publicKey = info.publicKey.export({
|
const publicKey = info.publicKey.export({
|
||||||
type: info.isECDSA ? 'spki' : 'pkcs1',
|
type: info.isECDSA ? 'spki' : 'pkcs1',
|
||||||
format: 'pem',
|
format: 'pem'
|
||||||
});
|
});
|
||||||
|
|
||||||
return Buffer.from(publicKey);
|
return Buffer.from(publicKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a JSON Web Key derived from a RSA or ECDSA key
|
* Get a JSON Web Key derived from a RSA or ECDSA key
|
||||||
*
|
*
|
||||||
@@ -157,7 +163,7 @@ exports.getPublicKey = (keyPem) => {
|
|||||||
|
|
||||||
function getJwk(keyPem) {
|
function getJwk(keyPem) {
|
||||||
const jwk = crypto.createPublicKey(keyPem).export({
|
const jwk = crypto.createPublicKey(keyPem).export({
|
||||||
format: 'jwk',
|
format: 'jwk'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Sort keys */
|
/* Sort keys */
|
||||||
@@ -169,6 +175,7 @@ function getJwk(keyPem) {
|
|||||||
|
|
||||||
exports.getJwk = getJwk;
|
exports.getJwk = getJwk;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
|
* Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
|
||||||
*
|
*
|
||||||
@@ -184,7 +191,7 @@ async function getWebCryptoKeyPair(keyPem) {
|
|||||||
/* Signing algorithm */
|
/* Signing algorithm */
|
||||||
const sigalg = {
|
const sigalg = {
|
||||||
name: 'RSASSA-PKCS1-v1_5',
|
name: 'RSASSA-PKCS1-v1_5',
|
||||||
hash: { name: 'SHA-256' },
|
hash: { name: 'SHA-256' }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (info.isECDSA) {
|
if (info.isECDSA) {
|
||||||
@@ -208,6 +215,7 @@ async function getWebCryptoKeyPair(keyPem) {
|
|||||||
return [{ privateKey, publicKey }, sigalg];
|
return [{ privateKey, publicKey }, sigalg];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split chain of PEM encoded objects from string into array
|
* Split chain of PEM encoded objects from string into array
|
||||||
*
|
*
|
||||||
@@ -227,6 +235,7 @@ function splitPemChain(chainPem) {
|
|||||||
|
|
||||||
exports.splitPemChain = splitPemChain;
|
exports.splitPemChain = splitPemChain;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse body of PEM encoded object and return a Base64URL string
|
* Parse body of PEM encoded object and return a Base64URL string
|
||||||
* If multiple objects are chained, the first body will be returned
|
* If multiple objects are chained, the first body will be returned
|
||||||
@@ -247,6 +256,7 @@ exports.getPemBodyAsB64u = (pem) => {
|
|||||||
return Buffer.from(dec).toString('base64url');
|
return Buffer.from(dec).toString('base64url');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse domains from a certificate or CSR
|
* Parse domains from a certificate or CSR
|
||||||
*
|
*
|
||||||
@@ -267,10 +277,11 @@ function parseDomains(input) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read domains from a Certificate Signing Request
|
* Read domains from a Certificate Signing Request
|
||||||
*
|
*
|
||||||
@@ -296,6 +307,7 @@ exports.readCsrDomains = (csrPem) => {
|
|||||||
return parseDomains(csr);
|
return parseDomains(csr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read information from a certificate
|
* Read information from a certificate
|
||||||
* If multiple certificates are chained, the first will be read
|
* If multiple certificates are chained, the first will be read
|
||||||
@@ -326,14 +338,15 @@ exports.readCertificateInfo = (certPem) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
issuer: {
|
issuer: {
|
||||||
commonName: cert.issuerName.getField('CN').pop() || null,
|
commonName: cert.issuerName.getField('CN').pop() || null
|
||||||
},
|
},
|
||||||
domains: parseDomains(cert),
|
domains: parseDomains(cert),
|
||||||
notBefore: cert.notBefore,
|
notBefore: cert.notBefore,
|
||||||
notAfter: cert.notAfter,
|
notAfter: cert.notAfter
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine ASN.1 character string type for CSR subject field name
|
* Determine ASN.1 character string type for CSR subject field name
|
||||||
*
|
*
|
||||||
@@ -356,6 +369,7 @@ function getCsrAsn1CharStringType(field) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create array of subject fields for a Certificate Signing Request
|
* Create array of subject fields for a Certificate Signing Request
|
||||||
*
|
*
|
||||||
@@ -377,6 +391,7 @@ function createCsrSubject(input) {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create x509 subject alternate name extension
|
* Create x509 subject alternate name extension
|
||||||
*
|
*
|
||||||
@@ -394,6 +409,7 @@ function createSubjectAltNameExtension(altNames) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Certificate Signing Request
|
* Create a Certificate Signing Request
|
||||||
*
|
*
|
||||||
@@ -413,30 +429,29 @@ function createSubjectAltNameExtension(altNames) {
|
|||||||
* @example Create a Certificate Signing Request
|
* @example Create a Certificate Signing Request
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com'
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @example Certificate Signing Request with both common and alternative names
|
* @example Certificate Signing Request with both common and alternative names
|
||||||
* > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
|
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
* keySize: 4096,
|
* keySize: 4096,
|
||||||
* commonName: 'test.example.com',
|
* commonName: 'test.example.com',
|
||||||
* altNames: ['foo.example.com', 'bar.example.com'],
|
* altNames: ['foo.example.com', 'bar.example.com']
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @example Certificate Signing Request with additional information
|
* @example Certificate Signing Request with additional information
|
||||||
* ```js
|
* ```js
|
||||||
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com',
|
||||||
* country: 'US',
|
* country: 'US',
|
||||||
* state: 'California',
|
* state: 'California',
|
||||||
* locality: 'Los Angeles',
|
* locality: 'Los Angeles',
|
||||||
* organization: 'The Company Inc.',
|
* organization: 'The Company Inc.',
|
||||||
* organizationUnit: 'IT Department',
|
* organizationUnit: 'IT Department',
|
||||||
* emailAddress: 'contact@example.com',
|
* emailAddress: 'contact@example.com'
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -445,9 +460,8 @@ function createSubjectAltNameExtension(altNames) {
|
|||||||
* const certificateKey = await acme.crypto.createPrivateEcdsaKey();
|
* const certificateKey = await acme.crypto.createPrivateEcdsaKey();
|
||||||
*
|
*
|
||||||
* const [, certificateRequest] = await acme.crypto.createCsr({
|
* const [, certificateRequest] = await acme.crypto.createCsr({
|
||||||
* altNames: ['test.example.com'],
|
* commonName: 'test.example.com'
|
||||||
* }, certificateKey);
|
* }, certificateKey);
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.createCsr = async (data, keyPem = null) => {
|
exports.createCsr = async (data, keyPem = null) => {
|
||||||
@@ -475,7 +489,7 @@ exports.createCsr = async (data, keyPem = null) => {
|
|||||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise
|
||||||
|
|
||||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
|
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
|
||||||
createSubjectAltNameExtension(data.altNames),
|
createSubjectAltNameExtension(data.altNames)
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Create CSR */
|
/* Create CSR */
|
||||||
@@ -490,8 +504,8 @@ exports.createCsr = async (data, keyPem = null) => {
|
|||||||
L: data.locality,
|
L: data.locality,
|
||||||
O: data.organization,
|
O: data.organization,
|
||||||
OU: data.organizationUnit,
|
OU: data.organizationUnit,
|
||||||
E: data.emailAddress,
|
E: data.emailAddress
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Done */
|
/* Done */
|
||||||
@@ -499,6 +513,7 @@ exports.createCsr = async (data, keyPem = null) => {
|
|||||||
return [keyPem, Buffer.from(pem)];
|
return [keyPem, Buffer.from(pem)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
|
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
|
||||||
*
|
*
|
||||||
@@ -518,7 +533,6 @@ exports.createCsr = async (data, keyPem = null) => {
|
|||||||
* ```js
|
* ```js
|
||||||
* const alpnKey = await acme.crypto.createPrivateEcdsaKey();
|
* const alpnKey = await acme.crypto.createPrivateEcdsaKey();
|
||||||
* const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
|
* const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
|
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
|
||||||
@@ -550,7 +564,7 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
|
|||||||
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
|
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
|
||||||
|
|
||||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
|
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
|
||||||
createSubjectAltNameExtension([commonName]),
|
createSubjectAltNameExtension([commonName])
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ALPN extension */
|
/* ALPN extension */
|
||||||
@@ -567,8 +581,8 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
|
|||||||
notBefore: now,
|
notBefore: now,
|
||||||
notAfter: now,
|
notAfter: now,
|
||||||
name: createCsrSubject({
|
name: createCsrSubject({
|
||||||
CN: commonName,
|
CN: commonName
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Done */
|
/* Done */
|
||||||
@@ -576,6 +590,7 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
|
|||||||
return [keyPem, Buffer.from(pem)];
|
return [keyPem, Buffer.from(pem)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that a ALPN certificate contains the expected key authorization
|
* Validate that a ALPN certificate contains the expected key authorization
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,9 +3,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { createHmac, createSign, constants: { RSA_PKCS1_PADDING } } = require('crypto');
|
const { createHmac, createSign, constants: { RSA_PKCS1_PADDING } } = require('crypto');
|
||||||
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
const { getJwk } = require('./crypto');
|
const { getJwk } = require('./crypto');
|
||||||
const { log } = require('./logger');
|
const { log } = require('./logger');
|
||||||
const axios = require('./axios');
|
const axios1 = require('./axios');
|
||||||
|
|
||||||
|
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||||
|
let httpsAgent = null;
|
||||||
|
if (httpsProxy) {
|
||||||
|
httpsAgent = new HttpsProxyAgent(httpsProxy);
|
||||||
|
}
|
||||||
|
const axios = axios1.create({
|
||||||
|
proxy: false,
|
||||||
|
httpsAgent
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACME HTTP client
|
* ACME HTTP client
|
||||||
@@ -25,13 +36,11 @@ class HttpClient {
|
|||||||
this.externalAccountBinding = externalAccountBinding;
|
this.externalAccountBinding = externalAccountBinding;
|
||||||
|
|
||||||
this.maxBadNonceRetries = 5;
|
this.maxBadNonceRetries = 5;
|
||||||
|
this.directory = null;
|
||||||
this.jwk = null;
|
this.jwk = null;
|
||||||
|
|
||||||
this.directoryCache = null;
|
|
||||||
this.directoryMaxAge = 86400;
|
|
||||||
this.directoryTimestamp = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request
|
* HTTP request
|
||||||
*
|
*
|
||||||
@@ -61,20 +70,17 @@ class HttpClient {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ACME provider directory
|
* Ensure provider directory exists
|
||||||
*
|
*
|
||||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
|
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
|
||||||
*
|
*
|
||||||
* @returns {Promise<object>} ACME directory contents
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async getDirectory() {
|
async getDirectory() {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
if (!this.directory) {
|
||||||
const age = (now - this.directoryTimestamp);
|
|
||||||
|
|
||||||
if (!this.directoryCache || (age > this.directoryMaxAge)) {
|
|
||||||
log(`Refreshing ACME directory, age: ${age}`);
|
|
||||||
const resp = await this.request(this.directoryUrl, 'get');
|
const resp = await this.request(this.directoryUrl, 'get');
|
||||||
|
|
||||||
if (resp.status >= 400) {
|
if (resp.status >= 400) {
|
||||||
@@ -85,13 +91,11 @@ class HttpClient {
|
|||||||
throw new Error('Attempting to read ACME directory returned no data');
|
throw new Error('Attempting to read ACME directory returned no data');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.directoryCache = resp.data;
|
this.directory = resp.data;
|
||||||
this.directoryTimestamp = now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.directoryCache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get JSON Web Key
|
* Get JSON Web Key
|
||||||
*
|
*
|
||||||
@@ -106,12 +110,13 @@ class HttpClient {
|
|||||||
return this.jwk;
|
return this.jwk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get nonce from directory API endpoint
|
* Get nonce from directory API endpoint
|
||||||
*
|
*
|
||||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
|
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
|
||||||
*
|
*
|
||||||
* @returns {Promise<string>} Nonce
|
* @returns {Promise<string>} nonce
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async getNonce() {
|
async getNonce() {
|
||||||
@@ -125,6 +130,7 @@ class HttpClient {
|
|||||||
return resp.headers['replay-nonce'];
|
return resp.headers['replay-nonce'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get URL for a directory resource
|
* Get URL for a directory resource
|
||||||
*
|
*
|
||||||
@@ -133,15 +139,16 @@ class HttpClient {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async getResourceUrl(resource) {
|
async getResourceUrl(resource) {
|
||||||
const dir = await this.getDirectory();
|
await this.getDirectory();
|
||||||
|
|
||||||
if (!dir[resource]) {
|
if (!this.directory[resource]) {
|
||||||
throw new Error(`Unable to locate API resource URL in ACME directory: "${resource}"`);
|
throw new Error(`Unable to locate API resource URL in ACME directory: "${resource}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dir[resource];
|
return this.directory[resource];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get directory meta field
|
* Get directory meta field
|
||||||
*
|
*
|
||||||
@@ -150,15 +157,16 @@ class HttpClient {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async getMetaField(field) {
|
async getMetaField(field) {
|
||||||
const dir = await this.getDirectory();
|
await this.getDirectory();
|
||||||
|
|
||||||
if (('meta' in dir) && (field in dir.meta)) {
|
if (('meta' in this.directory) && (field in this.directory.meta)) {
|
||||||
return dir.meta[field];
|
return this.directory.meta[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare HTTP request body for signature
|
* Prepare HTTP request body for signature
|
||||||
*
|
*
|
||||||
@@ -191,10 +199,11 @@ class HttpClient {
|
|||||||
/* Body */
|
/* Body */
|
||||||
return {
|
return {
|
||||||
payload: payload ? Buffer.from(JSON.stringify(payload)).toString('base64url') : '',
|
payload: payload ? Buffer.from(JSON.stringify(payload)).toString('base64url') : '',
|
||||||
protected: Buffer.from(JSON.stringify(header)).toString('base64url'),
|
protected: Buffer.from(JSON.stringify(header)).toString('base64url')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create JWS HTTP request body using HMAC
|
* Create JWS HTTP request body using HMAC
|
||||||
*
|
*
|
||||||
@@ -217,6 +226,7 @@ class HttpClient {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create JWS HTTP request body using RSA or ECC
|
* Create JWS HTTP request body using RSA or ECC
|
||||||
*
|
*
|
||||||
@@ -257,12 +267,13 @@ class HttpClient {
|
|||||||
result.signature = signer.sign({
|
result.signature = signer.sign({
|
||||||
key: this.accountKey,
|
key: this.accountKey,
|
||||||
padding: RSA_PKCS1_PADDING,
|
padding: RSA_PKCS1_PADDING,
|
||||||
dsaEncoding: 'ieee-p1363',
|
dsaEncoding: 'ieee-p1363'
|
||||||
}, 'base64url');
|
}, 'base64url');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signed HTTP request
|
* Signed HTTP request
|
||||||
*
|
*
|
||||||
@@ -298,7 +309,7 @@ class HttpClient {
|
|||||||
const data = this.createSignedBody(url, payload, { nonce, kid });
|
const data = this.createSignedBody(url, payload, { nonce, kid });
|
||||||
const resp = await this.request(url, 'post', { data });
|
const resp = await this.request(url, 'post', { data });
|
||||||
|
|
||||||
/* Retry on bad nonce - https://datatracker.ietf.org/doc/html/rfc8555#section-6.5 */
|
/* Retry on bad nonce - https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-10#section-6.4 */
|
||||||
if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
|
if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
|
||||||
nonce = resp.headers['replay-nonce'] || null;
|
nonce = resp.headers['replay-nonce'] || null;
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
@@ -312,5 +323,6 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Export client */
|
/* Export client */
|
||||||
module.exports = HttpClient;
|
module.exports = HttpClient;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
exports.Client = require('./client');
|
exports.Client = require('./client');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directory URLs
|
* Directory URLs
|
||||||
*/
|
*/
|
||||||
@@ -11,21 +12,18 @@ exports.Client = require('./client');
|
|||||||
exports.directory = {
|
exports.directory = {
|
||||||
buypass: {
|
buypass: {
|
||||||
staging: 'https://api.test4.buypass.no/acme/directory',
|
staging: 'https://api.test4.buypass.no/acme/directory',
|
||||||
production: 'https://api.buypass.com/acme/directory',
|
production: 'https://api.buypass.com/acme/directory'
|
||||||
},
|
|
||||||
google: {
|
|
||||||
staging: 'https://dv.acme-v02.test-api.pki.goog/directory',
|
|
||||||
production: 'https://dv.acme-v02.api.pki.goog/directory',
|
|
||||||
},
|
},
|
||||||
letsencrypt: {
|
letsencrypt: {
|
||||||
staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
||||||
production: 'https://acme-v02.api.letsencrypt.org/directory',
|
production: 'https://acme-v02.api.letsencrypt.org/directory'
|
||||||
},
|
},
|
||||||
zerossl: {
|
zerossl: {
|
||||||
production: 'https://acme.zerossl.com/v2/DV90',
|
production: 'https://acme.zerossl.com/v2/DV90'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crypto
|
* Crypto
|
||||||
*/
|
*/
|
||||||
@@ -33,12 +31,14 @@ exports.directory = {
|
|||||||
exports.crypto = require('./crypto');
|
exports.crypto = require('./crypto');
|
||||||
exports.forge = require('./crypto/forge');
|
exports.forge = require('./crypto/forge');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Axios
|
* Axios
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.axios = require('./axios');
|
exports.axios = require('./axios');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger
|
* Logger
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const debug = require('debug')('acme-client');
|
|||||||
|
|
||||||
let logger = () => {};
|
let logger = () => {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set logger function
|
* Set logger function
|
||||||
*
|
*
|
||||||
@@ -16,10 +17,11 @@ exports.setLogger = (fn) => {
|
|||||||
logger = fn;
|
logger = fn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log message
|
* Log message
|
||||||
*
|
*
|
||||||
* @param {string} msg Message
|
* @param {string} Message
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.log = (msg) => {
|
exports.log = (msg) => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const dns = require('dns').promises;
|
|||||||
const { readCertificateInfo, splitPemChain } = require('./crypto');
|
const { readCertificateInfo, splitPemChain } = require('./crypto');
|
||||||
const { log } = require('./logger');
|
const { log } = require('./logger');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exponential backoff
|
* Exponential backoff
|
||||||
*
|
*
|
||||||
@@ -25,6 +26,7 @@ class Backoff {
|
|||||||
this.attempts = 0;
|
this.attempts = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get backoff duration
|
* Get backoff duration
|
||||||
*
|
*
|
||||||
@@ -38,6 +40,7 @@ class Backoff {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry promise
|
* Retry promise
|
||||||
*
|
*
|
||||||
@@ -67,6 +70,7 @@ async function retryPromise(fn, attempts, backoff) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry promise
|
* Retry promise
|
||||||
*
|
*
|
||||||
@@ -83,13 +87,11 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
|
|||||||
return retryPromise(fn, attempts, backoff);
|
return retryPromise(fn, attempts, backoff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse URLs from Link header
|
* Parse URLs from link header
|
||||||
*
|
*
|
||||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
|
* @param {string} header Link header contents
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
|
|
||||||
*
|
|
||||||
* @param {string} header Header contents
|
|
||||||
* @param {string} rel Link relation, default: `alternate`
|
* @param {string} rel Link relation, default: `alternate`
|
||||||
* @returns {string[]} Array of URLs
|
* @returns {string[]} Array of URLs
|
||||||
*/
|
*/
|
||||||
@@ -105,36 +107,6 @@ function parseLinkHeader(header, rel = 'alternate') {
|
|||||||
return results.filter((r) => r);
|
return results.filter((r) => r);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse date or duration from Retry-After header
|
|
||||||
*
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
|
||||||
*
|
|
||||||
* @param {string} header Header contents
|
|
||||||
* @returns {number} Retry duration in seconds
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parseRetryAfterHeader(header) {
|
|
||||||
const sec = parseInt(header, 10);
|
|
||||||
const date = new Date(header);
|
|
||||||
|
|
||||||
/* Seconds into the future */
|
|
||||||
if (Number.isSafeInteger(sec) && (sec > 0)) {
|
|
||||||
return sec;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Future date string */
|
|
||||||
if (date instanceof Date && !Number.isNaN(date)) {
|
|
||||||
const now = new Date();
|
|
||||||
const diff = Math.ceil((date.getTime() - now.getTime()) / 1000);
|
|
||||||
|
|
||||||
if (diff > 0) {
|
|
||||||
return diff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find certificate chain with preferred issuer common name
|
* Find certificate chain with preferred issuer common name
|
||||||
@@ -185,6 +157,7 @@ function findCertificateChainForIssuer(chains, issuer) {
|
|||||||
return chains[0];
|
return chains[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find and format error in response object
|
* Find and format error in response object
|
||||||
*
|
*
|
||||||
@@ -195,18 +168,17 @@ function findCertificateChainForIssuer(chains, issuer) {
|
|||||||
function formatResponseError(resp) {
|
function formatResponseError(resp) {
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
if (resp.data) {
|
if (resp.data.error) {
|
||||||
if (resp.data.error) {
|
result = resp.data.error.detail || resp.data.error;
|
||||||
result = resp.data.error.detail || resp.data.error;
|
}
|
||||||
}
|
else {
|
||||||
else {
|
result = resp.data.detail || JSON.stringify(resp.data);
|
||||||
result = resp.data.detail || JSON.stringify(resp.data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (result || '').replace(/\n/g, '');
|
return result.replace(/\n/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve root domain name by looking for SOA record
|
* Resolve root domain name by looking for SOA record
|
||||||
*
|
*
|
||||||
@@ -232,6 +204,7 @@ async function resolveDomainBySoaRecord(recordName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get DNS resolver using domains authoritative NS records
|
* Get DNS resolver using domains authoritative NS records
|
||||||
*
|
*
|
||||||
@@ -272,6 +245,7 @@ async function getAuthoritativeDnsResolver(recordName) {
|
|||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to retrieve TLS ALPN certificate from peer
|
* Attempt to retrieve TLS ALPN certificate from peer
|
||||||
*
|
*
|
||||||
@@ -293,7 +267,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
|
|||||||
port,
|
port,
|
||||||
servername: host,
|
servername: host,
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
ALPNProtocols: ['acme-tls/1'],
|
ALPNProtocols: ['acme-tls/1']
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.setTimeout(timeout);
|
socket.setTimeout(timeout);
|
||||||
@@ -325,6 +299,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export utils
|
* Export utils
|
||||||
*/
|
*/
|
||||||
@@ -332,9 +307,8 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
retry,
|
retry,
|
||||||
parseLinkHeader,
|
parseLinkHeader,
|
||||||
parseRetryAfterHeader,
|
|
||||||
findCertificateChainForIssuer,
|
findCertificateChainForIssuer,
|
||||||
formatResponseError,
|
formatResponseError,
|
||||||
getAuthoritativeDnsResolver,
|
getAuthoritativeDnsResolver,
|
||||||
retrieveTlsAlpnCertificate,
|
retrieveTlsAlpnCertificate
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const axios = require('./axios');
|
|||||||
const util = require('./util');
|
const util = require('./util');
|
||||||
const { isAlpnCertificateAuthorizationValid } = require('./crypto');
|
const { isAlpnCertificateAuthorizationValid } = require('./crypto');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify ACME HTTP challenge
|
* Verify ACME HTTP challenge
|
||||||
*
|
*
|
||||||
@@ -42,6 +43,7 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk DNS until TXT records are found
|
* Walk DNS until TXT records are found
|
||||||
*/
|
*/
|
||||||
@@ -79,6 +81,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) {
|
|||||||
throw new Error(`No TXT records found for name: ${recordName}`);
|
throw new Error(`No TXT records found for name: ${recordName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify ACME DNS challenge
|
* Verify ACME DNS challenge
|
||||||
*
|
*
|
||||||
@@ -118,6 +121,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify ACME TLS ALPN challenge
|
* Verify ACME TLS ALPN challenge
|
||||||
*
|
*
|
||||||
@@ -145,6 +149,7 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export API
|
* Export API
|
||||||
*/
|
*/
|
||||||
@@ -152,5 +157,5 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
'http-01': verifyHttpChallenge,
|
'http-01': verifyHttpChallenge,
|
||||||
'dns-01': verifyDnsChallenge,
|
'dns-01': verifyDnsChallenge,
|
||||||
'tls-alpn-01': verifyTlsAlpnChallenge,
|
'tls-alpn-01': verifyTlsAlpnChallenge
|
||||||
};
|
};
|
||||||
|
|||||||
9
packages/core/acme-client/src/wait.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
async function wait(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
wait
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
|
|||||||
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
||||||
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
|
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
|
||||||
|
|
||||||
|
|
||||||
describe('pebble', () => {
|
describe('pebble', () => {
|
||||||
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
|
||||||
@@ -38,16 +39,18 @@ describe('pebble', () => {
|
|||||||
const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`;
|
const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`;
|
||||||
const testTlsAlpn01ChallengeValue = uuid();
|
const testTlsAlpn01ChallengeValue = uuid();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pebble CTS required
|
* Pebble CTS required
|
||||||
*/
|
*/
|
||||||
|
|
||||||
before(function () {
|
before(function() {
|
||||||
if (!cts.isEnabled()) {
|
if (!cts.isEnabled()) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DNS mocking
|
* DNS mocking
|
||||||
*/
|
*/
|
||||||
@@ -89,6 +92,7 @@ describe('pebble', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP-01 challenge response
|
* HTTP-01 challenge response
|
||||||
*/
|
*/
|
||||||
@@ -114,6 +118,7 @@ describe('pebble', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTPS-01 challenge response
|
* HTTPS-01 challenge response
|
||||||
*/
|
*/
|
||||||
@@ -138,7 +143,7 @@ describe('pebble', () => {
|
|||||||
/* Assert HTTP 302 */
|
/* Assert HTTP 302 */
|
||||||
const resp = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, {
|
const resp = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, {
|
||||||
maxRedirects: 0,
|
maxRedirects: 0,
|
||||||
validateStatus: null,
|
validateStatus: null
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(resp.status, 302);
|
assert.strictEqual(resp.status, 302);
|
||||||
@@ -160,6 +165,7 @@ describe('pebble', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DNS-01 challenge response
|
* DNS-01 challenge response
|
||||||
*/
|
*/
|
||||||
@@ -182,6 +188,7 @@ describe('pebble', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TLS-ALPN-01 challenge response
|
* TLS-ALPN-01 challenge response
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,17 +9,41 @@ const axios = require('./../src/axios');
|
|||||||
const HttpClient = require('./../src/http');
|
const HttpClient = require('./../src/http');
|
||||||
const pkg = require('./../package.json');
|
const pkg = require('./../package.json');
|
||||||
|
|
||||||
|
|
||||||
describe('http', () => {
|
describe('http', () => {
|
||||||
let testClient;
|
let testClient;
|
||||||
|
|
||||||
const endpoint = `http://${uuid()}.example.com`;
|
|
||||||
const defaultUserAgent = `node-${pkg.name}/${pkg.version}`;
|
const defaultUserAgent = `node-${pkg.name}/${pkg.version}`;
|
||||||
const customUserAgent = 'custom-ua-123';
|
const customUserAgent = 'custom-ua-123';
|
||||||
|
|
||||||
afterEach(() => {
|
const primaryEndpoint = `http://${uuid()}.example.com`;
|
||||||
nock.cleanAll();
|
const defaultUaEndpoint = `http://${uuid()}.example.com`;
|
||||||
|
const customUaEndpoint = `http://${uuid()}.example.com`;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP mocking
|
||||||
|
*/
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
const defaultUaOpts = { reqheaders: { 'User-Agent': defaultUserAgent } };
|
||||||
|
const customUaOpts = { reqheaders: { 'User-Agent': customUserAgent } };
|
||||||
|
|
||||||
|
nock(primaryEndpoint)
|
||||||
|
.persist().get('/').reply(200, 'ok');
|
||||||
|
|
||||||
|
nock(defaultUaEndpoint, defaultUaOpts)
|
||||||
|
.persist().get('/').reply(200, 'ok');
|
||||||
|
|
||||||
|
nock(customUaEndpoint, customUaOpts)
|
||||||
|
.persist().get('/').reply(200, 'ok');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize
|
* Initialize
|
||||||
*/
|
*/
|
||||||
@@ -28,94 +52,47 @@ describe('http', () => {
|
|||||||
testClient = new HttpClient();
|
testClient = new HttpClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP verbs
|
* HTTP verbs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should http get', async () => {
|
it('should http get', async () => {
|
||||||
nock(endpoint).get('/').reply(200, 'ok');
|
const resp = await testClient.request(primaryEndpoint, 'get');
|
||||||
const resp = await testClient.request(endpoint, 'get');
|
|
||||||
|
|
||||||
assert.isObject(resp);
|
assert.isObject(resp);
|
||||||
assert.strictEqual(resp.status, 200);
|
assert.strictEqual(resp.status, 200);
|
||||||
assert.strictEqual(resp.data, 'ok');
|
assert.strictEqual(resp.data, 'ok');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User-Agent
|
* User-Agent
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should request using default user-agent', async () => {
|
it('should request using default user-agent', async () => {
|
||||||
nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok');
|
const resp = await testClient.request(defaultUaEndpoint, 'get');
|
||||||
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
|
|
||||||
const resp = await testClient.request(endpoint, 'get');
|
|
||||||
|
|
||||||
assert.isObject(resp);
|
assert.isObject(resp);
|
||||||
assert.strictEqual(resp.status, 200);
|
assert.strictEqual(resp.status, 200);
|
||||||
assert.strictEqual(resp.data, 'ok');
|
assert.strictEqual(resp.data, 'ok');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject using custom user-agent', async () => {
|
it('should not request using custom user-agent', async () => {
|
||||||
nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok');
|
await assert.isRejected(testClient.request(customUaEndpoint, 'get'));
|
||||||
axios.defaults.headers.common['User-Agent'] = customUserAgent;
|
|
||||||
await assert.isRejected(testClient.request(endpoint, 'get'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should request using custom user-agent', async () => {
|
it('should request using custom user-agent', async () => {
|
||||||
nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok');
|
|
||||||
axios.defaults.headers.common['User-Agent'] = customUserAgent;
|
axios.defaults.headers.common['User-Agent'] = customUserAgent;
|
||||||
const resp = await testClient.request(endpoint, 'get');
|
const resp = await testClient.request(customUaEndpoint, 'get');
|
||||||
|
|
||||||
assert.isObject(resp);
|
assert.isObject(resp);
|
||||||
assert.strictEqual(resp.status, 200);
|
assert.strictEqual(resp.status, 200);
|
||||||
assert.strictEqual(resp.data, 'ok');
|
assert.strictEqual(resp.data, 'ok');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject using default user-agent', async () => {
|
it('should not request using default user-agent', async () => {
|
||||||
nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok');
|
axios.defaults.headers.common['User-Agent'] = customUserAgent;
|
||||||
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
|
await assert.isRejected(testClient.request(defaultUaEndpoint, 'get'));
|
||||||
await assert.isRejected(testClient.request(endpoint, 'get'));
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry on HTTP errors
|
|
||||||
*/
|
|
||||||
|
|
||||||
it('should retry on 429 rate limit', async () => {
|
|
||||||
let rateLimitCount = 0;
|
|
||||||
|
|
||||||
nock(endpoint).persist().get('/').reply(() => {
|
|
||||||
rateLimitCount += 1;
|
|
||||||
|
|
||||||
if (rateLimitCount < 3) {
|
|
||||||
return [429, 'Rate Limit Exceeded', { 'Retry-After': 1 }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [200, 'ok'];
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.strictEqual(rateLimitCount, 0);
|
|
||||||
const resp = await testClient.request(endpoint, 'get');
|
|
||||||
|
|
||||||
assert.isObject(resp);
|
|
||||||
assert.strictEqual(resp.status, 200);
|
|
||||||
assert.strictEqual(resp.data, 'ok');
|
|
||||||
assert.strictEqual(rateLimitCount, 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should retry on 5xx server error', async () => {
|
|
||||||
let serverErrorCount = 0;
|
|
||||||
|
|
||||||
nock(endpoint).persist().get('/').reply(() => {
|
|
||||||
serverErrorCount += 1;
|
|
||||||
return [500, 'Internal Server Error', { 'Retry-After': 1 }];
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.strictEqual(serverErrorCount, 0);
|
|
||||||
const resp = await testClient.request(endpoint, 'get');
|
|
||||||
|
|
||||||
assert.isObject(resp);
|
|
||||||
assert.strictEqual(resp.status, 500);
|
|
||||||
assert.strictEqual(serverErrorCount, 4);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
const { assert } = require('chai');
|
const { assert } = require('chai');
|
||||||
const logger = require('./../src/logger');
|
const logger = require('./../src/logger');
|
||||||
|
|
||||||
|
|
||||||
describe('logger', () => {
|
describe('logger', () => {
|
||||||
let lastLogMessage = null;
|
let lastLogMessage = null;
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ describe('logger', () => {
|
|||||||
lastLogMessage = msg;
|
lastLogMessage = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger
|
* Logger
|
||||||
*/
|
*/
|
||||||
@@ -21,6 +23,7 @@ describe('logger', () => {
|
|||||||
assert.isNull(lastLogMessage);
|
assert.isNull(lastLogMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should log with custom logger', () => {
|
it('should log with custom logger', () => {
|
||||||
logger.setLogger(customLoggerFn);
|
logger.setLogger(customLoggerFn);
|
||||||
|
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* Utility method tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
const dns = require('dns').promises;
|
|
||||||
const fs = require('fs').promises;
|
|
||||||
const path = require('path');
|
|
||||||
const { assert } = require('chai');
|
|
||||||
const util = require('./../src/util');
|
|
||||||
const { readCertificateInfo } = require('./../src/crypto');
|
|
||||||
|
|
||||||
describe('util', () => {
|
|
||||||
const testCertPath1 = path.join(__dirname, 'fixtures', 'certificate.crt');
|
|
||||||
const testCertPath2 = path.join(__dirname, 'fixtures', 'letsencrypt.crt');
|
|
||||||
|
|
||||||
it('retry()', async () => {
|
|
||||||
let attempts = 0;
|
|
||||||
const backoffOpts = {
|
|
||||||
min: 100,
|
|
||||||
max: 500,
|
|
||||||
};
|
|
||||||
|
|
||||||
await assert.isRejected(util.retry(() => {
|
|
||||||
throw new Error('oops');
|
|
||||||
}, backoffOpts));
|
|
||||||
|
|
||||||
const r = await util.retry(() => {
|
|
||||||
attempts += 1;
|
|
||||||
|
|
||||||
if (attempts < 3) {
|
|
||||||
throw new Error('oops');
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'abc';
|
|
||||||
}, backoffOpts);
|
|
||||||
|
|
||||||
assert.strictEqual(r, 'abc');
|
|
||||||
assert.strictEqual(attempts, 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parseLinkHeader()', () => {
|
|
||||||
const r1 = util.parseLinkHeader('<https://example.com/a>;rel="alternate"');
|
|
||||||
assert.isArray(r1);
|
|
||||||
assert.strictEqual(r1.length, 1);
|
|
||||||
assert.strictEqual(r1[0], 'https://example.com/a');
|
|
||||||
|
|
||||||
const r2 = util.parseLinkHeader('<https://example.com/b>;rel="test"');
|
|
||||||
assert.isArray(r2);
|
|
||||||
assert.strictEqual(r2.length, 0);
|
|
||||||
|
|
||||||
const r3 = util.parseLinkHeader('<http://example.com/c>; rel="test"', 'test');
|
|
||||||
assert.isArray(r3);
|
|
||||||
assert.strictEqual(r3.length, 1);
|
|
||||||
assert.strictEqual(r3[0], 'http://example.com/c');
|
|
||||||
|
|
||||||
const r4 = util.parseLinkHeader(`<https://example.com/a>; rel="alternate",
|
|
||||||
<https://example.com/x>; rel="nope",
|
|
||||||
<https://example.com/b>;rel="alternate",
|
|
||||||
<https://example.com/c>; rel="alternate"`);
|
|
||||||
assert.isArray(r4);
|
|
||||||
assert.strictEqual(r4.length, 3);
|
|
||||||
assert.strictEqual(r4[0], 'https://example.com/a');
|
|
||||||
assert.strictEqual(r4[1], 'https://example.com/b');
|
|
||||||
assert.strictEqual(r4[2], 'https://example.com/c');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parseRetryAfterHeader()', () => {
|
|
||||||
const r1 = util.parseRetryAfterHeader('');
|
|
||||||
assert.strictEqual(r1, 0);
|
|
||||||
|
|
||||||
const r2 = util.parseRetryAfterHeader('abcdef');
|
|
||||||
assert.strictEqual(r2, 0);
|
|
||||||
|
|
||||||
const r3 = util.parseRetryAfterHeader('123');
|
|
||||||
assert.strictEqual(r3, 123);
|
|
||||||
|
|
||||||
const r4 = util.parseRetryAfterHeader('123.456');
|
|
||||||
assert.strictEqual(r4, 123);
|
|
||||||
|
|
||||||
const r5 = util.parseRetryAfterHeader('-555');
|
|
||||||
assert.strictEqual(r5, 0);
|
|
||||||
|
|
||||||
const r6 = util.parseRetryAfterHeader('Wed, 21 Oct 2015 07:28:00 GMT');
|
|
||||||
assert.strictEqual(r6, 0);
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const future = new Date(now.getTime() + 123000);
|
|
||||||
const r7 = util.parseRetryAfterHeader(future.toUTCString());
|
|
||||||
assert.isTrue(r7 > 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('findCertificateChainForIssuer()', async () => {
|
|
||||||
const certs = [
|
|
||||||
(await fs.readFile(testCertPath1)).toString(),
|
|
||||||
(await fs.readFile(testCertPath2)).toString(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const r1 = util.findCertificateChainForIssuer(certs, 'abc123');
|
|
||||||
const r2 = util.findCertificateChainForIssuer(certs, 'example.com');
|
|
||||||
const r3 = util.findCertificateChainForIssuer(certs, 'E6');
|
|
||||||
|
|
||||||
[r1, r2, r3].forEach((r) => {
|
|
||||||
assert.isString(r);
|
|
||||||
assert.isNotEmpty(r);
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.strictEqual(readCertificateInfo(r1).issuer.commonName, 'example.com');
|
|
||||||
assert.strictEqual(readCertificateInfo(r2).issuer.commonName, 'example.com');
|
|
||||||
assert.strictEqual(readCertificateInfo(r3).issuer.commonName, 'E6');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formatResponseError()', () => {
|
|
||||||
const e1 = util.formatResponseError({ data: { error: 'aaa' } });
|
|
||||||
assert.strictEqual(e1, 'aaa');
|
|
||||||
|
|
||||||
const e2 = util.formatResponseError({ data: { error: { detail: 'bbb' } } });
|
|
||||||
assert.strictEqual(e2, 'bbb');
|
|
||||||
|
|
||||||
const e3 = util.formatResponseError({ data: { detail: 'ccc' } });
|
|
||||||
assert.strictEqual(e3, 'ccc');
|
|
||||||
|
|
||||||
const e4 = util.formatResponseError({ data: { a: 123 } });
|
|
||||||
assert.strictEqual(e4, '{"a":123}');
|
|
||||||
|
|
||||||
const e5 = util.formatResponseError({});
|
|
||||||
assert.isString(e5);
|
|
||||||
assert.isEmpty(e5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getAuthoritativeDnsResolver()', async () => {
|
|
||||||
/* valid domain - should not use global default */
|
|
||||||
const r1 = await util.getAuthoritativeDnsResolver('example.com');
|
|
||||||
assert.instanceOf(r1, dns.Resolver);
|
|
||||||
assert.isNotEmpty(r1.getServers());
|
|
||||||
assert.notDeepEqual(r1.getServers(), dns.getServers());
|
|
||||||
|
|
||||||
/* invalid domain - fallback to global default */
|
|
||||||
const r2 = await util.getAuthoritativeDnsResolver('invalid.xtldx');
|
|
||||||
assert.instanceOf(r2, dns.Resolver);
|
|
||||||
assert.deepStrictEqual(r2.getServers(), dns.getServers());
|
|
||||||
});
|
|
||||||
|
|
||||||
/* TODO: Figure out how to test this */
|
|
||||||
it('retrieveTlsAlpnCertificate()');
|
|
||||||
});
|
|
||||||
@@ -9,6 +9,7 @@ const verify = require('./../src/verify');
|
|||||||
|
|
||||||
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
|
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
|
||||||
|
|
||||||
|
|
||||||
describe('verify', () => {
|
describe('verify', () => {
|
||||||
const challengeTypes = ['http-01', 'dns-01'];
|
const challengeTypes = ['http-01', 'dns-01'];
|
||||||
|
|
||||||
@@ -29,16 +30,18 @@ describe('verify', () => {
|
|||||||
const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
|
const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
|
||||||
const testTlsAlpn01Key = uuid();
|
const testTlsAlpn01Key = uuid();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pebble CTS required
|
* Pebble CTS required
|
||||||
*/
|
*/
|
||||||
|
|
||||||
before(function () {
|
before(function() {
|
||||||
if (!cts.isEnabled()) {
|
if (!cts.isEnabled()) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API
|
* API
|
||||||
*/
|
*/
|
||||||
@@ -47,6 +50,7 @@ describe('verify', () => {
|
|||||||
assert.containsAllKeys(verify, challengeTypes);
|
assert.containsAllKeys(verify, challengeTypes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* http-01
|
* http-01
|
||||||
*/
|
*/
|
||||||
@@ -77,6 +81,7 @@ describe('verify', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https-01
|
* https-01
|
||||||
*/
|
*/
|
||||||
@@ -97,6 +102,7 @@ describe('verify', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* dns-01
|
* dns-01
|
||||||
*/
|
*/
|
||||||
@@ -127,6 +133,7 @@ describe('verify', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* tls-alpn-01
|
* tls-alpn-01
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ const spec = require('./spec');
|
|||||||
const forge = require('./../src/crypto/forge');
|
const forge = require('./../src/crypto/forge');
|
||||||
|
|
||||||
const cryptoEngines = {
|
const cryptoEngines = {
|
||||||
forge,
|
forge
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
describe('crypto-legacy', () => {
|
describe('crypto-legacy', () => {
|
||||||
let testPemKey;
|
let testPemKey;
|
||||||
let testCert;
|
let testCert;
|
||||||
@@ -27,6 +28,7 @@ describe('crypto-legacy', () => {
|
|||||||
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
|
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
|
||||||
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
|
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixtures
|
* Fixtures
|
||||||
*/
|
*/
|
||||||
@@ -48,6 +50,7 @@ describe('crypto-legacy', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Engines
|
* Engines
|
||||||
*/
|
*/
|
||||||
@@ -59,6 +62,7 @@ describe('crypto-legacy', () => {
|
|||||||
let testNonCnCsr;
|
let testNonCnCsr;
|
||||||
let testNonAsciiCsr;
|
let testNonAsciiCsr;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key generation
|
* Key generation
|
||||||
*/
|
*/
|
||||||
@@ -79,13 +83,14 @@ describe('crypto-legacy', () => {
|
|||||||
publicKeyStore.push(key.toString().replace(/[\r\n]/gm, ''));
|
publicKeyStore.push(key.toString().replace(/[\r\n]/gm, ''));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Certificate Signing Request
|
* Certificate Signing Request
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should generate a csr', async () => {
|
it('should generate a csr', async () => {
|
||||||
const [key, csr] = await engine.createCsr({
|
const [key, csr] = await engine.createCsr({
|
||||||
commonName: testCsrDomain,
|
commonName: testCsrDomain
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -97,7 +102,7 @@ describe('crypto-legacy', () => {
|
|||||||
it('should generate a san csr', async () => {
|
it('should generate a san csr', async () => {
|
||||||
const [key, csr] = await engine.createCsr({
|
const [key, csr] = await engine.createCsr({
|
||||||
commonName: testSanCsrDomains[0],
|
commonName: testSanCsrDomains[0],
|
||||||
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length),
|
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length)
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -108,7 +113,7 @@ describe('crypto-legacy', () => {
|
|||||||
|
|
||||||
it('should generate a csr without common name', async () => {
|
it('should generate a csr without common name', async () => {
|
||||||
const [key, csr] = await engine.createCsr({
|
const [key, csr] = await engine.createCsr({
|
||||||
altNames: testSanCsrDomains,
|
altNames: testSanCsrDomains
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -121,7 +126,7 @@ describe('crypto-legacy', () => {
|
|||||||
const [key, csr] = await engine.createCsr({
|
const [key, csr] = await engine.createCsr({
|
||||||
commonName: testCsrDomain,
|
commonName: testCsrDomain,
|
||||||
organization: '大安區',
|
organization: '大安區',
|
||||||
organizationUnit: '中文部門',
|
organizationUnit: '中文部門'
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -162,6 +167,7 @@ describe('crypto-legacy', () => {
|
|||||||
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
|
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Certificate
|
* Certificate
|
||||||
*/
|
*/
|
||||||
@@ -182,6 +188,7 @@ describe('crypto-legacy', () => {
|
|||||||
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
|
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PEM utils
|
* PEM utils
|
||||||
*/
|
*/
|
||||||
@@ -207,6 +214,7 @@ describe('crypto-legacy', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modulus and exponent
|
* Modulus and exponent
|
||||||
*/
|
*/
|
||||||
@@ -238,6 +246,7 @@ describe('crypto-legacy', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify identical results
|
* Verify identical results
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0
|
|||||||
-----END TEST-----
|
-----END TEST-----
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
describe('crypto', () => {
|
describe('crypto', () => {
|
||||||
const testCsrDomain = 'example.com';
|
const testCsrDomain = 'example.com';
|
||||||
const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com'];
|
const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com'];
|
||||||
@@ -57,6 +58,7 @@ describe('crypto', () => {
|
|||||||
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
|
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
|
||||||
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
|
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key types
|
* Key types
|
||||||
*/
|
*/
|
||||||
@@ -66,23 +68,24 @@ describe('crypto', () => {
|
|||||||
createKeyFns: {
|
createKeyFns: {
|
||||||
s1024: () => crypto.createPrivateRsaKey(1024),
|
s1024: () => crypto.createPrivateRsaKey(1024),
|
||||||
s2048: () => crypto.createPrivateRsaKey(),
|
s2048: () => crypto.createPrivateRsaKey(),
|
||||||
s4096: () => crypto.createPrivateRsaKey(4096),
|
s4096: () => crypto.createPrivateRsaKey(4096)
|
||||||
},
|
},
|
||||||
jwkSpecFn: spec.jwk.rsa,
|
jwkSpecFn: spec.jwk.rsa
|
||||||
},
|
},
|
||||||
ecdsa: {
|
ecdsa: {
|
||||||
createKeyFns: {
|
createKeyFns: {
|
||||||
p256: () => crypto.createPrivateEcdsaKey(),
|
p256: () => crypto.createPrivateEcdsaKey(),
|
||||||
p384: () => crypto.createPrivateEcdsaKey('P-384'),
|
p384: () => crypto.createPrivateEcdsaKey('P-384'),
|
||||||
p521: () => crypto.createPrivateEcdsaKey('P-521'),
|
p521: () => crypto.createPrivateEcdsaKey('P-521')
|
||||||
},
|
},
|
||||||
jwkSpecFn: spec.jwk.ecdsa,
|
jwkSpecFn: spec.jwk.ecdsa
|
||||||
},
|
}
|
||||||
}).forEach(([name, { createKeyFns, jwkSpecFn }]) => {
|
}).forEach(([name, { createKeyFns, jwkSpecFn }]) => {
|
||||||
describe(name, () => {
|
describe(name, () => {
|
||||||
const testPrivateKeys = {};
|
const testPrivateKeys = {};
|
||||||
const testPublicKeys = {};
|
const testPublicKeys = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterate through all generator variations
|
* Iterate through all generator variations
|
||||||
*/
|
*/
|
||||||
@@ -94,6 +97,7 @@ describe('crypto', () => {
|
|||||||
let testNonAsciiCsr;
|
let testNonAsciiCsr;
|
||||||
let testAlpnCertificate;
|
let testAlpnCertificate;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keys and JWK
|
* Keys and JWK
|
||||||
*/
|
*/
|
||||||
@@ -128,13 +132,14 @@ describe('crypto', () => {
|
|||||||
jwkSpecFn(jwk);
|
jwkSpecFn(jwk);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Certificate Signing Request
|
* Certificate Signing Request
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it(`${n}/should generate a csr`, async () => {
|
it(`${n}/should generate a csr`, async () => {
|
||||||
const [key, csr] = await crypto.createCsr({
|
const [key, csr] = await crypto.createCsr({
|
||||||
commonName: testCsrDomain,
|
commonName: testCsrDomain
|
||||||
}, testPrivateKeys[n]);
|
}, testPrivateKeys[n]);
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -146,7 +151,7 @@ describe('crypto', () => {
|
|||||||
it(`${n}/should generate a san csr`, async () => {
|
it(`${n}/should generate a san csr`, async () => {
|
||||||
const [key, csr] = await crypto.createCsr({
|
const [key, csr] = await crypto.createCsr({
|
||||||
commonName: testSanCsrDomains[0],
|
commonName: testSanCsrDomains[0],
|
||||||
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length),
|
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length)
|
||||||
}, testPrivateKeys[n]);
|
}, testPrivateKeys[n]);
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -157,7 +162,7 @@ describe('crypto', () => {
|
|||||||
|
|
||||||
it(`${n}/should generate a csr without common name`, async () => {
|
it(`${n}/should generate a csr without common name`, async () => {
|
||||||
const [key, csr] = await crypto.createCsr({
|
const [key, csr] = await crypto.createCsr({
|
||||||
altNames: testSanCsrDomains,
|
altNames: testSanCsrDomains
|
||||||
}, testPrivateKeys[n]);
|
}, testPrivateKeys[n]);
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -170,7 +175,7 @@ describe('crypto', () => {
|
|||||||
const [key, csr] = await crypto.createCsr({
|
const [key, csr] = await crypto.createCsr({
|
||||||
commonName: testCsrDomain,
|
commonName: testCsrDomain,
|
||||||
organization: '大安區',
|
organization: '大安區',
|
||||||
organizationUnit: '中文部門',
|
organizationUnit: '中文部門'
|
||||||
}, testPrivateKeys[n]);
|
}, testPrivateKeys[n]);
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -181,7 +186,7 @@ describe('crypto', () => {
|
|||||||
|
|
||||||
it(`${n}/should generate a csr with key as string`, async () => {
|
it(`${n}/should generate a csr with key as string`, async () => {
|
||||||
const [key, csr] = await crypto.createCsr({
|
const [key, csr] = await crypto.createCsr({
|
||||||
commonName: testCsrDomain,
|
commonName: testCsrDomain
|
||||||
}, testPrivateKeys[n].toString());
|
}, testPrivateKeys[n].toString());
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
@@ -190,10 +195,11 @@ describe('crypto', () => {
|
|||||||
|
|
||||||
it(`${n}/should throw with invalid key`, async () => {
|
it(`${n}/should throw with invalid key`, async () => {
|
||||||
await assert.isRejected(crypto.createCsr({
|
await assert.isRejected(crypto.createCsr({
|
||||||
commonName: testCsrDomain,
|
commonName: testCsrDomain
|
||||||
}, testPublicKeys[n]));
|
}, testPublicKeys[n]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain and info resolver
|
* Domain and info resolver
|
||||||
*/
|
*/
|
||||||
@@ -237,6 +243,7 @@ describe('crypto', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ALPN
|
* ALPN
|
||||||
*/
|
*/
|
||||||
@@ -277,6 +284,7 @@ describe('crypto', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common functionality
|
* Common functionality
|
||||||
*/
|
*/
|
||||||
@@ -286,6 +294,7 @@ describe('crypto', () => {
|
|||||||
let testCert;
|
let testCert;
|
||||||
let testSanCert;
|
let testSanCert;
|
||||||
|
|
||||||
|
|
||||||
it('should read private key fixture', async () => {
|
it('should read private key fixture', async () => {
|
||||||
testPemKey = await fs.readFile(testKeyPath);
|
testPemKey = await fs.readFile(testKeyPath);
|
||||||
assert.isTrue(Buffer.isBuffer(testPemKey));
|
assert.isTrue(Buffer.isBuffer(testPemKey));
|
||||||
@@ -301,19 +310,21 @@ describe('crypto', () => {
|
|||||||
assert.isTrue(Buffer.isBuffer(testSanCert));
|
assert.isTrue(Buffer.isBuffer(testSanCert));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSR with auto-generated key
|
* CSR with auto-generated key
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should generate a csr with default key', async () => {
|
it('should generate a csr with default key', async () => {
|
||||||
const [key, csr] = await crypto.createCsr({
|
const [key, csr] = await crypto.createCsr({
|
||||||
commonName: testCsrDomain,
|
commonName: testCsrDomain
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(Buffer.isBuffer(key));
|
assert.isTrue(Buffer.isBuffer(key));
|
||||||
assert.isTrue(Buffer.isBuffer(csr));
|
assert.isTrue(Buffer.isBuffer(csr));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Certificate
|
* Certificate
|
||||||
*/
|
*/
|
||||||
@@ -341,6 +352,7 @@ describe('crypto', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ALPN
|
* ALPN
|
||||||
*/
|
*/
|
||||||
@@ -353,6 +365,7 @@ describe('crypto', () => {
|
|||||||
assert.isTrue(Buffer.isBuffer(cert));
|
assert.isTrue(Buffer.isBuffer(cert));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PEM utils
|
* PEM utils
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,32 +20,35 @@ const clientOpts = {
|
|||||||
directoryUrl,
|
directoryUrl,
|
||||||
backoffAttempts: 5,
|
backoffAttempts: 5,
|
||||||
backoffMin: 1000,
|
backoffMin: 1000,
|
||||||
backoffMax: 5000,
|
backoffMax: 5000
|
||||||
};
|
};
|
||||||
|
|
||||||
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
|
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
|
||||||
clientOpts.externalAccountBinding = {
|
clientOpts.externalAccountBinding = {
|
||||||
kid: process.env.ACME_EAB_KID,
|
kid: process.env.ACME_EAB_KID,
|
||||||
hmacKey: process.env.ACME_EAB_HMAC_KEY,
|
hmacKey: process.env.ACME_EAB_HMAC_KEY
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe('client', () => {
|
describe('client', () => {
|
||||||
const testDomain = `${uuid()}.${domainName}`;
|
const testDomain = `${uuid()}.${domainName}`;
|
||||||
const testDomainAlpn = `${uuid()}.${domainName}`;
|
const testDomainAlpn = `${uuid()}.${domainName}`;
|
||||||
const testDomainWildcard = `*.${testDomain}`;
|
const testDomainWildcard = `*.${testDomain}`;
|
||||||
const testContact = `mailto:test-${uuid()}@nope.com`;
|
const testContact = `mailto:test-${uuid()}@nope.com`;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pebble CTS required
|
* Pebble CTS required
|
||||||
*/
|
*/
|
||||||
|
|
||||||
before(function () {
|
before(function() {
|
||||||
if (!cts.isEnabled()) {
|
if (!cts.isEnabled()) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key types
|
* Key types
|
||||||
*/
|
*/
|
||||||
@@ -55,18 +58,18 @@ describe('client', () => {
|
|||||||
createKeyFn: () => acme.crypto.createPrivateRsaKey(),
|
createKeyFn: () => acme.crypto.createPrivateRsaKey(),
|
||||||
createKeyAltFns: {
|
createKeyAltFns: {
|
||||||
s1024: () => acme.crypto.createPrivateRsaKey(1024),
|
s1024: () => acme.crypto.createPrivateRsaKey(1024),
|
||||||
s4096: () => acme.crypto.createPrivateRsaKey(4096),
|
s4096: () => acme.crypto.createPrivateRsaKey(4096)
|
||||||
},
|
},
|
||||||
jwkSpecFn: spec.jwk.rsa,
|
jwkSpecFn: spec.jwk.rsa
|
||||||
},
|
},
|
||||||
ecdsa: {
|
ecdsa: {
|
||||||
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
|
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
|
||||||
createKeyAltFns: {
|
createKeyAltFns: {
|
||||||
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
|
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
|
||||||
p521: () => acme.crypto.createPrivateEcdsaKey('P-521'),
|
p521: () => acme.crypto.createPrivateEcdsaKey('P-521')
|
||||||
},
|
},
|
||||||
jwkSpecFn: spec.jwk.ecdsa,
|
jwkSpecFn: spec.jwk.ecdsa
|
||||||
},
|
}
|
||||||
}).forEach(([name, { createKeyFn, createKeyAltFns, jwkSpecFn }]) => {
|
}).forEach(([name, { createKeyFn, createKeyAltFns, jwkSpecFn }]) => {
|
||||||
describe(name, () => {
|
describe(name, () => {
|
||||||
let testIssuers;
|
let testIssuers;
|
||||||
@@ -94,6 +97,7 @@ describe('client', () => {
|
|||||||
let testCertificateAlpn;
|
let testCertificateAlpn;
|
||||||
let testCertificateWildcard;
|
let testCertificateWildcard;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixtures
|
* Fixtures
|
||||||
*/
|
*/
|
||||||
@@ -110,11 +114,11 @@ describe('client', () => {
|
|||||||
|
|
||||||
it('should generate certificate signing request', async () => {
|
it('should generate certificate signing request', async () => {
|
||||||
[, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn());
|
[, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn());
|
||||||
[, testCsrAlpn] = await acme.crypto.createCsr({ altNames: [testDomainAlpn] }, await createKeyFn());
|
[, testCsrAlpn] = await acme.crypto.createCsr({ commonName: testDomainAlpn }, await createKeyFn());
|
||||||
[, testCsrWildcard] = await acme.crypto.createCsr({ altNames: [testDomainWildcard] }, await createKeyFn());
|
[, testCsrWildcard] = await acme.crypto.createCsr({ commonName: testDomainWildcard }, await createKeyFn());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
|
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
|
||||||
if (!capAlternateCertRoots) {
|
if (!capAlternateCertRoots) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -130,6 +134,7 @@ describe('client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize clients
|
* Initialize clients
|
||||||
*/
|
*/
|
||||||
@@ -137,7 +142,7 @@ describe('client', () => {
|
|||||||
it('should initialize client', () => {
|
it('should initialize client', () => {
|
||||||
testClient = new acme.Client({
|
testClient = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: testAccountKey,
|
accountKey: testAccountKey
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,11 +151,12 @@ describe('client', () => {
|
|||||||
jwkSpecFn(jwk);
|
jwkSpecFn(jwk);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Terms of Service
|
* Terms of Service
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should produce tos url [ACME_CAP_META_TOS_FIELD]', async function () {
|
it('should produce tos url [ACME_CAP_META_TOS_FIELD]', async function() {
|
||||||
if (!capMetaTosField) {
|
if (!capMetaTosField) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -159,7 +165,7 @@ describe('client', () => {
|
|||||||
assert.isString(tos);
|
assert.isString(tos);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not produce tos url [!ACME_CAP_META_TOS_FIELD]', async function () {
|
it('should not produce tos url [!ACME_CAP_META_TOS_FIELD]', async function() {
|
||||||
if (capMetaTosField) {
|
if (capMetaTosField) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -168,11 +174,12 @@ describe('client', () => {
|
|||||||
assert.isNull(tos);
|
assert.isNull(tos);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create account
|
* Create account
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should refuse account creation without tos [ACME_CAP_META_TOS_FIELD]', async function () {
|
it('should refuse account creation without tos [ACME_CAP_META_TOS_FIELD]', async function() {
|
||||||
if (!capMetaTosField) {
|
if (!capMetaTosField) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -180,7 +187,7 @@ describe('client', () => {
|
|||||||
await assert.isRejected(testClient.createAccount());
|
await assert.isRejected(testClient.createAccount());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should refuse account creation without eab [ACME_CAP_EAB_ENABLED]', async function () {
|
it('should refuse account creation without eab [ACME_CAP_EAB_ENABLED]', async function() {
|
||||||
if (!capEabEnabled) {
|
if (!capEabEnabled) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -188,17 +195,17 @@ describe('client', () => {
|
|||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: testAccountKey,
|
accountKey: testAccountKey,
|
||||||
externalAccountBinding: null,
|
externalAccountBinding: null
|
||||||
});
|
});
|
||||||
|
|
||||||
await assert.isRejected(client.createAccount({
|
await assert.isRejected(client.createAccount({
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create an account', async () => {
|
it('should create an account', async () => {
|
||||||
testAccount = await testClient.createAccount({
|
testAccount = await testClient.createAccount({
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true
|
||||||
});
|
});
|
||||||
|
|
||||||
spec.rfc8555.account(testAccount);
|
spec.rfc8555.account(testAccount);
|
||||||
@@ -210,6 +217,7 @@ describe('client', () => {
|
|||||||
assert.isString(testAccountUrl);
|
assert.isString(testAccountUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create account with alternate key sizes
|
* Create account with alternate key sizes
|
||||||
*/
|
*/
|
||||||
@@ -218,11 +226,11 @@ describe('client', () => {
|
|||||||
it(`should create account with key=${k}`, async () => {
|
it(`should create account with key=${k}`, async () => {
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: await altKeyFn(),
|
accountKey: await altKeyFn()
|
||||||
});
|
});
|
||||||
|
|
||||||
const account = await client.createAccount({
|
const account = await client.createAccount({
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true
|
||||||
});
|
});
|
||||||
|
|
||||||
spec.rfc8555.account(account);
|
spec.rfc8555.account(account);
|
||||||
@@ -230,6 +238,7 @@ describe('client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find existing account using secondary client
|
* Find existing account using secondary client
|
||||||
*/
|
*/
|
||||||
@@ -237,22 +246,22 @@ describe('client', () => {
|
|||||||
it('should throw when trying to find account using invalid account key', async () => {
|
it('should throw when trying to find account using invalid account key', async () => {
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: testAccountSecondaryKey,
|
accountKey: testAccountSecondaryKey
|
||||||
});
|
});
|
||||||
|
|
||||||
await assert.isRejected(client.createAccount({
|
await assert.isRejected(client.createAccount({
|
||||||
onlyReturnExisting: true,
|
onlyReturnExisting: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find existing account using account key', async () => {
|
it('should find existing account using account key', async () => {
|
||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: testAccountKey,
|
accountKey: testAccountKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const account = await client.createAccount({
|
const account = await client.createAccount({
|
||||||
onlyReturnExisting: true,
|
onlyReturnExisting: true
|
||||||
});
|
});
|
||||||
|
|
||||||
spec.rfc8555.account(account);
|
spec.rfc8555.account(account);
|
||||||
@@ -260,6 +269,7 @@ describe('client', () => {
|
|||||||
assert.deepStrictEqual(account.key, testAccount.key);
|
assert.deepStrictEqual(account.key, testAccount.key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account URL
|
* Account URL
|
||||||
*/
|
*/
|
||||||
@@ -268,7 +278,7 @@ describe('client', () => {
|
|||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: testAccountKey,
|
accountKey: testAccountKey,
|
||||||
accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1',
|
accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1'
|
||||||
});
|
});
|
||||||
|
|
||||||
await assert.isRejected(client.updateAccount());
|
await assert.isRejected(client.updateAccount());
|
||||||
@@ -278,11 +288,11 @@ describe('client', () => {
|
|||||||
const client = new acme.Client({
|
const client = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: testAccountKey,
|
accountKey: testAccountKey,
|
||||||
accountUrl: testAccountUrl,
|
accountUrl: testAccountUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
const account = await client.createAccount({
|
const account = await client.createAccount({
|
||||||
onlyReturnExisting: true,
|
onlyReturnExisting: true
|
||||||
});
|
});
|
||||||
|
|
||||||
spec.rfc8555.account(account);
|
spec.rfc8555.account(account);
|
||||||
@@ -290,6 +300,7 @@ describe('client', () => {
|
|||||||
assert.deepStrictEqual(account.key, testAccount.key);
|
assert.deepStrictEqual(account.key, testAccount.key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update account contact info
|
* Update account contact info
|
||||||
*/
|
*/
|
||||||
@@ -305,11 +316,12 @@ describe('client', () => {
|
|||||||
assert.include(account.contact, testContact);
|
assert.include(account.contact, testContact);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change account private key
|
* Change account private key
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should change account private key [ACME_CAP_UPDATE_ACCOUNT_KEY]', async function () {
|
it('should change account private key [ACME_CAP_UPDATE_ACCOUNT_KEY]', async function() {
|
||||||
if (!capUpdateAccountKey) {
|
if (!capUpdateAccountKey) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -317,7 +329,7 @@ describe('client', () => {
|
|||||||
await testClient.updateAccountKey(testAccountSecondaryKey);
|
await testClient.updateAccountKey(testAccountSecondaryKey);
|
||||||
|
|
||||||
const account = await testClient.createAccount({
|
const account = await testClient.createAccount({
|
||||||
onlyReturnExisting: true,
|
onlyReturnExisting: true
|
||||||
});
|
});
|
||||||
|
|
||||||
spec.rfc8555.account(account);
|
spec.rfc8555.account(account);
|
||||||
@@ -325,6 +337,7 @@ describe('client', () => {
|
|||||||
assert.notDeepEqual(account.key, testAccount.key);
|
assert.notDeepEqual(account.key, testAccount.key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new certificate order
|
* Create new certificate order
|
||||||
*/
|
*/
|
||||||
@@ -344,6 +357,7 @@ describe('client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get status of existing certificate order
|
* Get status of existing certificate order
|
||||||
*/
|
*/
|
||||||
@@ -357,6 +371,7 @@ describe('client', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get identifier authorization
|
* Get identifier authorization
|
||||||
*/
|
*/
|
||||||
@@ -386,6 +401,7 @@ describe('client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate challenge key authorization
|
* Generate challenge key authorization
|
||||||
*/
|
*/
|
||||||
@@ -402,6 +418,7 @@ describe('client', () => {
|
|||||||
[testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
|
[testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deactivate identifier authorization
|
* Deactivate identifier authorization
|
||||||
*/
|
*/
|
||||||
@@ -410,8 +427,8 @@ describe('client', () => {
|
|||||||
const order = await testClient.createOrder({
|
const order = await testClient.createOrder({
|
||||||
identifiers: [
|
identifiers: [
|
||||||
{ type: 'dns', value: `${uuid()}.${domainName}` },
|
{ type: 'dns', value: `${uuid()}.${domainName}` },
|
||||||
{ type: 'dns', value: `${uuid()}.${domainName}` },
|
{ type: 'dns', value: `${uuid()}.${domainName}` }
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const authzCollection = await testClient.getAuthorizations(order);
|
const authzCollection = await testClient.getAuthorizations(order);
|
||||||
@@ -428,6 +445,7 @@ describe('client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify satisfied challenge
|
* Verify satisfied challenge
|
||||||
*/
|
*/
|
||||||
@@ -442,6 +460,7 @@ describe('client', () => {
|
|||||||
await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard);
|
await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete challenge
|
* Complete challenge
|
||||||
*/
|
*/
|
||||||
@@ -455,6 +474,7 @@ describe('client', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for valid challenge
|
* Wait for valid challenge
|
||||||
*/
|
*/
|
||||||
@@ -463,6 +483,7 @@ describe('client', () => {
|
|||||||
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
|
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalize order
|
* Finalize order
|
||||||
*/
|
*/
|
||||||
@@ -479,6 +500,7 @@ describe('client', () => {
|
|||||||
assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url);
|
assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for valid order
|
* Wait for valid order
|
||||||
*/
|
*/
|
||||||
@@ -487,6 +509,7 @@ describe('client', () => {
|
|||||||
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
|
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get certificate
|
* Get certificate
|
||||||
*/
|
*/
|
||||||
@@ -502,7 +525,7 @@ describe('client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
|
it('should get alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
|
||||||
if (!capAlternateCertRoots) {
|
if (!capAlternateCertRoots) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -516,7 +539,7 @@ describe('client', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
|
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
|
||||||
if (!capAlternateCertRoots) {
|
if (!capAlternateCertRoots) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -528,6 +551,7 @@ describe('client', () => {
|
|||||||
assert.strictEqual(testIssuers[0], info.issuer.commonName);
|
assert.strictEqual(testIssuers[0], info.issuer.commonName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke certificate
|
* Revoke certificate
|
||||||
*/
|
*/
|
||||||
@@ -544,6 +568,7 @@ describe('client', () => {
|
|||||||
await assert.isRejected(testClient.getCertificate(testOrderWildcard));
|
await assert.isRejected(testClient.getCertificate(testOrderWildcard));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deactivate account
|
* Deactivate account
|
||||||
*/
|
*/
|
||||||
@@ -556,6 +581,7 @@ describe('client', () => {
|
|||||||
assert.strictEqual(account.status, 'deactivated');
|
assert.strictEqual(account.status, 'deactivated');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that no new orders can be made
|
* Verify that no new orders can be made
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,16 +18,17 @@ const clientOpts = {
|
|||||||
directoryUrl,
|
directoryUrl,
|
||||||
backoffAttempts: 5,
|
backoffAttempts: 5,
|
||||||
backoffMin: 1000,
|
backoffMin: 1000,
|
||||||
backoffMax: 5000,
|
backoffMax: 5000
|
||||||
};
|
};
|
||||||
|
|
||||||
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
|
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
|
||||||
clientOpts.externalAccountBinding = {
|
clientOpts.externalAccountBinding = {
|
||||||
kid: process.env.ACME_EAB_KID,
|
kid: process.env.ACME_EAB_KID,
|
||||||
hmacKey: process.env.ACME_EAB_HMAC_KEY,
|
hmacKey: process.env.ACME_EAB_HMAC_KEY
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe('client.auto', () => {
|
describe('client.auto', () => {
|
||||||
const testDomain = `${uuid()}.${domainName}`;
|
const testDomain = `${uuid()}.${domainName}`;
|
||||||
const testHttpDomain = `${uuid()}.${domainName}`;
|
const testHttpDomain = `${uuid()}.${domainName}`;
|
||||||
@@ -39,19 +40,21 @@ describe('client.auto', () => {
|
|||||||
const testSanDomains = [
|
const testSanDomains = [
|
||||||
`${uuid()}.${domainName}`,
|
`${uuid()}.${domainName}`,
|
||||||
`${uuid()}.${domainName}`,
|
`${uuid()}.${domainName}`,
|
||||||
`${uuid()}.${domainName}`,
|
`${uuid()}.${domainName}`
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pebble CTS required
|
* Pebble CTS required
|
||||||
*/
|
*/
|
||||||
|
|
||||||
before(function () {
|
before(function() {
|
||||||
if (!cts.isEnabled()) {
|
if (!cts.isEnabled()) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key types
|
* Key types
|
||||||
*/
|
*/
|
||||||
@@ -61,16 +64,16 @@ describe('client.auto', () => {
|
|||||||
createKeyFn: () => acme.crypto.createPrivateRsaKey(),
|
createKeyFn: () => acme.crypto.createPrivateRsaKey(),
|
||||||
createKeyAltFns: {
|
createKeyAltFns: {
|
||||||
s1024: () => acme.crypto.createPrivateRsaKey(1024),
|
s1024: () => acme.crypto.createPrivateRsaKey(1024),
|
||||||
s4096: () => acme.crypto.createPrivateRsaKey(4096),
|
s4096: () => acme.crypto.createPrivateRsaKey(4096)
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
ecdsa: {
|
ecdsa: {
|
||||||
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
|
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
|
||||||
createKeyAltFns: {
|
createKeyAltFns: {
|
||||||
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
|
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
|
||||||
p521: () => acme.crypto.createPrivateEcdsaKey('P-521'),
|
p521: () => acme.crypto.createPrivateEcdsaKey('P-521')
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}).forEach(([name, { createKeyFn, createKeyAltFns }]) => {
|
}).forEach(([name, { createKeyFn, createKeyAltFns }]) => {
|
||||||
describe(name, () => {
|
describe(name, () => {
|
||||||
let testIssuers;
|
let testIssuers;
|
||||||
@@ -79,11 +82,12 @@ describe('client.auto', () => {
|
|||||||
let testSanCertificate;
|
let testSanCertificate;
|
||||||
let testWildcardCertificate;
|
let testWildcardCertificate;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixtures
|
* Fixtures
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
|
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
|
||||||
if (!capAlternateCertRoots) {
|
if (!capAlternateCertRoots) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -99,6 +103,7 @@ describe('client.auto', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize client
|
* Initialize client
|
||||||
*/
|
*/
|
||||||
@@ -106,30 +111,31 @@ describe('client.auto', () => {
|
|||||||
it('should initialize client', async () => {
|
it('should initialize client', async () => {
|
||||||
testClient = new acme.Client({
|
testClient = new acme.Client({
|
||||||
...clientOpts,
|
...clientOpts,
|
||||||
accountKey: await createKeyFn(),
|
accountKey: await createKeyFn()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalid challenge response
|
* Invalid challenge response
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should throw on invalid challenge response', async () => {
|
it('should throw on invalid challenge response', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: `${uuid()}.${domainName}`,
|
commonName: `${uuid()}.${domainName}`
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
await assert.isRejected(testClient.auto({
|
await assert.isRejected(testClient.auto({
|
||||||
csr,
|
csr,
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.challengeNoopFn,
|
challengeCreateFn: cts.challengeNoopFn,
|
||||||
challengeRemoveFn: cts.challengeNoopFn,
|
challengeRemoveFn: cts.challengeNoopFn
|
||||||
}), /^authorization not found/i);
|
}), /^authorization not found/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on invalid challenge response with opts.skipChallengeVerification=true', async () => {
|
it('should throw on invalid challenge response with opts.skipChallengeVerification=true', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: `${uuid()}.${domainName}`,
|
commonName: `${uuid()}.${domainName}`
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
await assert.isRejected(testClient.auto({
|
await assert.isRejected(testClient.auto({
|
||||||
@@ -137,37 +143,38 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
skipChallengeVerification: true,
|
skipChallengeVerification: true,
|
||||||
challengeCreateFn: cts.challengeNoopFn,
|
challengeCreateFn: cts.challengeNoopFn,
|
||||||
challengeRemoveFn: cts.challengeNoopFn,
|
challengeRemoveFn: cts.challengeNoopFn
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Challenge function exceptions
|
* Challenge function exceptions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should throw on challengeCreate exception', async () => {
|
it('should throw on challengeCreate exception', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: `${uuid()}.${domainName}`,
|
commonName: `${uuid()}.${domainName}`
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
await assert.isRejected(testClient.auto({
|
await assert.isRejected(testClient.auto({
|
||||||
csr,
|
csr,
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.challengeThrowFn,
|
challengeCreateFn: cts.challengeThrowFn,
|
||||||
challengeRemoveFn: cts.challengeNoopFn,
|
challengeRemoveFn: cts.challengeNoopFn
|
||||||
}), /^oops$/);
|
}), /^oops$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw on challengeRemove exception', async () => {
|
it('should not throw on challengeRemove exception', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: `${uuid()}.${domainName}`,
|
commonName: `${uuid()}.${domainName}`
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
csr,
|
csr,
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeThrowFn,
|
challengeRemoveFn: cts.challengeThrowFn
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -181,8 +188,8 @@ describe('client.auto', () => {
|
|||||||
`${uuid()}.${domainName}`,
|
`${uuid()}.${domainName}`,
|
||||||
`${uuid()}.${domainName}`,
|
`${uuid()}.${domainName}`,
|
||||||
`${uuid()}.${domainName}`,
|
`${uuid()}.${domainName}`,
|
||||||
`${uuid()}.${domainName}`,
|
`${uuid()}.${domainName}`
|
||||||
],
|
]
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
await assert.isRejected(testClient.auto({
|
await assert.isRejected(testClient.auto({
|
||||||
@@ -198,27 +205,28 @@ describe('client.auto', () => {
|
|||||||
results.push(true);
|
results.push(true);
|
||||||
return cts.challengeCreateFn(...args);
|
return cts.challengeCreateFn(...args);
|
||||||
},
|
},
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
}));
|
}));
|
||||||
|
|
||||||
assert.strictEqual(results.length, 5);
|
assert.strictEqual(results.length, 5);
|
||||||
assert.deepStrictEqual(results, [false, false, false, true, true]);
|
assert.deepStrictEqual(results, [false, false, false, true, true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Order certificates
|
* Order certificates
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should order certificate', async () => {
|
it('should order certificate', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: testDomain,
|
commonName: testDomain
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
csr,
|
csr,
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -227,7 +235,7 @@ describe('client.auto', () => {
|
|||||||
|
|
||||||
it('should order certificate using http-01', async () => {
|
it('should order certificate using http-01', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: testHttpDomain,
|
commonName: testHttpDomain
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
@@ -235,7 +243,7 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.assertHttpChallengeCreateFn,
|
challengeCreateFn: cts.assertHttpChallengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn,
|
||||||
challengePriority: ['http-01'],
|
challengePriority: ['http-01']
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -243,7 +251,7 @@ describe('client.auto', () => {
|
|||||||
|
|
||||||
it('should order certificate using https-01', async () => {
|
it('should order certificate using https-01', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: testHttpsDomain,
|
commonName: testHttpsDomain
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
@@ -251,7 +259,7 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.assertHttpsChallengeCreateFn,
|
challengeCreateFn: cts.assertHttpsChallengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn,
|
||||||
challengePriority: ['http-01'],
|
challengePriority: ['http-01']
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -259,7 +267,7 @@ describe('client.auto', () => {
|
|||||||
|
|
||||||
it('should order certificate using dns-01', async () => {
|
it('should order certificate using dns-01', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: testDnsDomain,
|
commonName: testDnsDomain
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
@@ -267,7 +275,7 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.assertDnsChallengeCreateFn,
|
challengeCreateFn: cts.assertDnsChallengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn,
|
||||||
challengePriority: ['dns-01'],
|
challengePriority: ['dns-01']
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -275,7 +283,7 @@ describe('client.auto', () => {
|
|||||||
|
|
||||||
it('should order certificate using tls-alpn-01', async () => {
|
it('should order certificate using tls-alpn-01', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: testAlpnDomain,
|
commonName: testAlpnDomain
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
@@ -283,7 +291,7 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn,
|
challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn,
|
||||||
challengePriority: ['tls-alpn-01'],
|
challengePriority: ['tls-alpn-01']
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -291,14 +299,14 @@ describe('client.auto', () => {
|
|||||||
|
|
||||||
it('should order san certificate', async () => {
|
it('should order san certificate', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
altNames: testSanDomains,
|
altNames: testSanDomains
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
csr,
|
csr,
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -307,14 +315,15 @@ describe('client.auto', () => {
|
|||||||
|
|
||||||
it('should order wildcard certificate', async () => {
|
it('should order wildcard certificate', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
altNames: [testWildcardDomain, `*.${testWildcardDomain}`],
|
commonName: testWildcardDomain,
|
||||||
|
altNames: [`*.${testWildcardDomain}`]
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
csr,
|
csr,
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
@@ -323,7 +332,7 @@ describe('client.auto', () => {
|
|||||||
|
|
||||||
it('should order certificate with opts.skipChallengeVerification=true', async () => {
|
it('should order certificate with opts.skipChallengeVerification=true', async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: `${uuid()}.${domainName}`,
|
commonName: `${uuid()}.${domainName}`
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
@@ -331,20 +340,20 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
skipChallengeVerification: true,
|
skipChallengeVerification: true,
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should order alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
|
it('should order alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
|
||||||
if (!capAlternateCertRoots) {
|
if (!capAlternateCertRoots) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(testIssuers.map(async (issuer) => {
|
await Promise.all(testIssuers.map(async (issuer) => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: `${uuid()}.${domainName}`,
|
commonName: `${uuid()}.${domainName}`
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
@@ -352,7 +361,7 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
preferredChain: issuer,
|
preferredChain: issuer,
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootCert = acme.crypto.splitPemChain(cert).pop();
|
const rootCert = acme.crypto.splitPemChain(cert).pop();
|
||||||
@@ -362,13 +371,13 @@ describe('client.auto', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
|
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
|
||||||
if (!capAlternateCertRoots) {
|
if (!capAlternateCertRoots) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: `${uuid()}.${domainName}`,
|
commonName: `${uuid()}.${domainName}`
|
||||||
}, await createKeyFn());
|
}, await createKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
@@ -376,7 +385,7 @@ describe('client.auto', () => {
|
|||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
preferredChain: uuid(),
|
preferredChain: uuid(),
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootCert = acme.crypto.splitPemChain(cert).pop();
|
const rootCert = acme.crypto.splitPemChain(cert).pop();
|
||||||
@@ -385,6 +394,7 @@ describe('client.auto', () => {
|
|||||||
assert.strictEqual(testIssuers[0], info.issuer.commonName);
|
assert.strictEqual(testIssuers[0], info.issuer.commonName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Order certificate with alternate key sizes
|
* Order certificate with alternate key sizes
|
||||||
*/
|
*/
|
||||||
@@ -392,20 +402,21 @@ describe('client.auto', () => {
|
|||||||
Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => {
|
Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => {
|
||||||
it(`should order certificate with key=${k}`, async () => {
|
it(`should order certificate with key=${k}`, async () => {
|
||||||
const [, csr] = await acme.crypto.createCsr({
|
const [, csr] = await acme.crypto.createCsr({
|
||||||
commonName: testDomain,
|
commonName: testDomain
|
||||||
}, await altKeyFn());
|
}, await altKeyFn());
|
||||||
|
|
||||||
const cert = await testClient.auto({
|
const cert = await testClient.auto({
|
||||||
csr,
|
csr,
|
||||||
termsOfServiceAgreed: true,
|
termsOfServiceAgreed: true,
|
||||||
challengeCreateFn: cts.challengeCreateFn,
|
challengeCreateFn: cts.challengeCreateFn,
|
||||||
challengeRemoveFn: cts.challengeRemoveFn,
|
challengeRemoveFn: cts.challengeRemoveFn
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isString(cert);
|
assert.isString(cert);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read certificates
|
* Read certificates
|
||||||
*/
|
*/
|
||||||
@@ -414,7 +425,7 @@ describe('client.auto', () => {
|
|||||||
const info = acme.crypto.readCertificateInfo(testCertificate);
|
const info = acme.crypto.readCertificateInfo(testCertificate);
|
||||||
|
|
||||||
spec.crypto.certificateInfo(info);
|
spec.crypto.certificateInfo(info);
|
||||||
assert.isNull(info.domains.commonName);
|
assert.strictEqual(info.domains.commonName, testDomain);
|
||||||
assert.deepStrictEqual(info.domains.altNames, [testDomain]);
|
assert.deepStrictEqual(info.domains.altNames, [testDomain]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -422,7 +433,7 @@ describe('client.auto', () => {
|
|||||||
const info = acme.crypto.readCertificateInfo(testSanCertificate);
|
const info = acme.crypto.readCertificateInfo(testSanCertificate);
|
||||||
|
|
||||||
spec.crypto.certificateInfo(info);
|
spec.crypto.certificateInfo(info);
|
||||||
assert.isNull(info.domains.commonName);
|
assert.strictEqual(info.domains.commonName, testSanDomains[0]);
|
||||||
assert.deepStrictEqual(info.domains.altNames, testSanDomains);
|
assert.deepStrictEqual(info.domains.altNames, testSanDomains);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -430,7 +441,7 @@ describe('client.auto', () => {
|
|||||||
const info = acme.crypto.readCertificateInfo(testWildcardCertificate);
|
const info = acme.crypto.readCertificateInfo(testWildcardCertificate);
|
||||||
|
|
||||||
spec.crypto.certificateInfo(info);
|
spec.crypto.certificateInfo(info);
|
||||||
assert.isNull(info.domains.commonName);
|
assert.strictEqual(info.domains.commonName, testWildcardDomain);
|
||||||
assert.deepStrictEqual(info.domains.altNames, [testWildcardDomain, `*.${testWildcardDomain}`]);
|
assert.deepStrictEqual(info.domains.altNames, [testWildcardDomain, `*.${testWildcardDomain}`]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const axios = require('./../src/axios');
|
|||||||
const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null;
|
const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null;
|
||||||
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send request
|
* Send request
|
||||||
*/
|
*/
|
||||||
@@ -20,18 +21,20 @@ async function request(apiPath, data = {}) {
|
|||||||
await axios.request({
|
await axios.request({
|
||||||
url: `${apiBaseUrl}/${apiPath}`,
|
url: `${apiBaseUrl}/${apiPath}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State
|
* State
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.isEnabled = () => !!apiBaseUrl;
|
exports.isEnabled = () => !!apiBaseUrl;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DNS
|
* DNS
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +42,7 @@ exports.isEnabled = () => !!apiBaseUrl;
|
|||||||
exports.addDnsARecord = async (host, addresses) => request('add-a', { host, addresses });
|
exports.addDnsARecord = async (host, addresses) => request('add-a', { host, addresses });
|
||||||
exports.setDnsCnameRecord = async (host, target) => request('set-cname', { host, target });
|
exports.setDnsCnameRecord = async (host, target) => request('set-cname', { host, target });
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Challenge response
|
* Challenge response
|
||||||
*/
|
*/
|
||||||
@@ -51,7 +55,7 @@ async function addHttps01ChallengeResponse(token, content, targetHostname) {
|
|||||||
await addHttp01ChallengeResponse(token, content);
|
await addHttp01ChallengeResponse(token, content);
|
||||||
return request('add-redirect', {
|
return request('add-redirect', {
|
||||||
path: `/.well-known/acme-challenge/${token}`,
|
path: `/.well-known/acme-challenge/${token}`,
|
||||||
targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`,
|
targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +72,7 @@ exports.addHttps01ChallengeResponse = addHttps01ChallengeResponse;
|
|||||||
exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
|
exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
|
||||||
exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse;
|
exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Challenge response mock functions
|
* Challenge response mock functions
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDzzCCA1WgAwIBAgISA0ghDoSv5DpT3Pd3lqwjbVDDMAoGCCqGSM49BAMDMDIx
|
|
||||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
|
||||||
NjAeFw0yNDA2MTAxNzEyMjZaFw0yNDA5MDgxNzEyMjVaMBQxEjAQBgNVBAMTCWxl
|
|
||||||
bmNyLm9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHJ3DjN7pYV3mftHzaP
|
|
||||||
V/WI0RhOJnSI5AIFEPFHDi8UowOINRGIfm9FHGIDqrb4Rmyvr9JrrqBdFGDen8BW
|
|
||||||
6OGjggJnMIICYzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
|
|
||||||
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIdCTnxqmpOELDyzPaEM
|
|
||||||
seB36lUOMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUGCCsGAQUF
|
|
||||||
BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3JnMCIGCCsG
|
|
||||||
AQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMG8GA1UdEQRoMGaCCWxlbmNy
|
|
||||||
Lm9yZ4IPbGV0c2VuY3J5cHQuY29tgg9sZXRzZW5jcnlwdC5vcmeCDXd3dy5sZW5j
|
|
||||||
ci5vcmeCE3d3dy5sZXRzZW5jcnlwdC5jb22CE3d3dy5sZXRzZW5jcnlwdC5vcmcw
|
|
||||||
EwYDVR0gBAwwCjAIBgZngQwBAgEwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEAdgA/
|
|
||||||
F0tP1yJHWJQdZRyEvg0S7ZA3fx+FauvBvyiF7PhkbgAAAZADWfneAAAEAwBHMEUC
|
|
||||||
IGlp+dPU2hLT2suTMYkYMlt/xbzSnKLZDA/wYSsPACP7AiEAxbAzx6mkzn0cs0hh
|
|
||||||
ti6sLf0pcbmDhxHdlJRjuo6SQZEAdwDf4VbrqgWvtZwPhnGNqMAyTq5W2W6n9aVq
|
|
||||||
AdHBO75SXAAAAZADWfqrAAAEAwBIMEYCIQCrAmDUrlX3oGhri1qCIb65Cuf8h2GR
|
|
||||||
LC1VfXBenX7dCAIhALXwbhCQ1vO1WLv4CqyihMHOwFaICYqN/N6ylaBlVAM4MAoG
|
|
||||||
CCqGSM49BAMDA2gAMGUCMFdgjOXGl+hE2ABDsAeuNq8wi34yTMUHk0KMTOjRAfy9
|
|
||||||
rOCGQqvP0myoYlyzXOH9uQIxAMdkG1ZWBZS1dHavbPf1I/MjYpzX6gy0jVHIXXu5
|
|
||||||
aYWylBi/Uf2RPj0LWFZh8tNa1Q==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -7,6 +7,7 @@ const util = require('./../src/util');
|
|||||||
|
|
||||||
const pebbleManagementUrl = process.env.ACME_PEBBLE_MANAGEMENT_URL || null;
|
const pebbleManagementUrl = process.env.ACME_PEBBLE_MANAGEMENT_URL || null;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pebble
|
* Pebble
|
||||||
*/
|
*/
|
||||||
@@ -25,6 +26,7 @@ async function getPebbleCertIssuers() {
|
|||||||
return info.map((i) => i.issuer.commonName);
|
return info.map((i) => i.issuer.commonName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get certificate issuers
|
* Get certificate issuers
|
||||||
*/
|
*/
|
||||||
|
|||||||
59
packages/core/acme-client/test/retry.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const { assert } = require('chai');
|
||||||
|
const Promise = require('bluebird');
|
||||||
|
const util = require('../src/util');
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
async function apiRequest() {
|
||||||
|
count += 1;
|
||||||
|
console.log(new Date(), 'retry count:', count);
|
||||||
|
await Promise.delay(2000);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForValidStatus() {
|
||||||
|
const verifyFn = async (abort) => {
|
||||||
|
const resp = await apiRequest();
|
||||||
|
|
||||||
|
/* Verify status */
|
||||||
|
console.log(new Date(), 'Item has status', resp);
|
||||||
|
if (count < 3) {
|
||||||
|
abort();
|
||||||
|
throw new Error(`${new Date()}error`, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(new Date(), 'success');
|
||||||
|
return 'success';
|
||||||
|
|
||||||
|
// if (resp.data.status === 'invalid') {
|
||||||
|
// abort();
|
||||||
|
// throw new Error(util.formatResponseError(resp));
|
||||||
|
// }
|
||||||
|
// else if (resp.data.status === 'pending') {
|
||||||
|
// throw new Error('Operation is pending');
|
||||||
|
// }
|
||||||
|
// else if (resp.data.status === 'valid') {
|
||||||
|
// return resp.data;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// throw new Error(`Unexpected item status: ${resp.data.status}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(new Date(), 'Waiting for valid status from', this.backoffOpts);
|
||||||
|
return util.retry(verifyFn, this.backoffOpts);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
describe('util', () => {
|
||||||
|
it('retry', async function() {
|
||||||
|
this.timeout(100000);
|
||||||
|
try {
|
||||||
|
await waitForValidStatus();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('1111', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// await Promise.delay(100000);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,12 +7,14 @@ const chai = require('chai');
|
|||||||
const chaiAsPromised = require('chai-as-promised');
|
const chaiAsPromised = require('chai-as-promised');
|
||||||
const axios = require('./../src/axios');
|
const axios = require('./../src/axios');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add promise support to Chai
|
* Add promise support to Chai
|
||||||
*/
|
*/
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Challenge test server ports
|
* Challenge test server ports
|
||||||
*/
|
*/
|
||||||
@@ -29,12 +31,6 @@ if (process.env.ACME_TLSALPN_PORT) {
|
|||||||
axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT;
|
axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Greatly reduce retry duration while testing
|
|
||||||
*/
|
|
||||||
|
|
||||||
axios.defaults.acmeSettings.retryMaxAttempts = 3;
|
|
||||||
axios.defaults.acmeSettings.retryDefaultDelay = 1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* External account binding
|
* External account binding
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { assert } = require('chai');
|
|||||||
const spec = {};
|
const spec = {};
|
||||||
module.exports = spec;
|
module.exports = spec;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACME
|
* ACME
|
||||||
*/
|
*/
|
||||||
@@ -119,6 +120,7 @@ spec.rfc8555.challenge = (obj) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crypto
|
* Crypto
|
||||||
*/
|
*/
|
||||||
@@ -148,6 +150,7 @@ spec.crypto.certificateInfo = (obj) => {
|
|||||||
assert.strictEqual(Object.prototype.toString.call(obj.notAfter), '[object Date]');
|
assert.strictEqual(Object.prototype.toString.call(obj.notAfter), '[object Date]');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWK
|
* JWK
|
||||||
*/
|
*/
|
||||||
|
|||||||
12
packages/core/acme-client/types/index.d.ts
vendored
@@ -15,6 +15,7 @@ export type PublicKeyString = string;
|
|||||||
export type CertificateString = string;
|
export type CertificateString = string;
|
||||||
export type CsrString = string;
|
export type CsrString = string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Augmented ACME interfaces
|
* Augmented ACME interfaces
|
||||||
*/
|
*/
|
||||||
@@ -27,6 +28,7 @@ export interface Authorization extends rfc8555.Authorization {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client
|
* Client
|
||||||
*/
|
*/
|
||||||
@@ -49,7 +51,7 @@ export interface ClientExternalAccountBindingOptions {
|
|||||||
export interface ClientAutoOptions {
|
export interface ClientAutoOptions {
|
||||||
csr: CsrBuffer | CsrString;
|
csr: CsrBuffer | CsrString;
|
||||||
challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<any>;
|
challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<any>;
|
||||||
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<any>;
|
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string, recordRes:any) => Promise<any>;
|
||||||
email?: string;
|
email?: string;
|
||||||
termsOfServiceAgreed?: boolean;
|
termsOfServiceAgreed?: boolean;
|
||||||
skipChallengeVerification?: boolean;
|
skipChallengeVerification?: boolean;
|
||||||
@@ -78,6 +80,7 @@ export class Client {
|
|||||||
auto(opts: ClientAutoOptions): Promise<string>;
|
auto(opts: ClientAutoOptions): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directory URLs
|
* Directory URLs
|
||||||
*/
|
*/
|
||||||
@@ -87,10 +90,6 @@ export const directory: {
|
|||||||
staging: string,
|
staging: string,
|
||||||
production: string
|
production: string
|
||||||
},
|
},
|
||||||
google: {
|
|
||||||
staging: string,
|
|
||||||
production: string
|
|
||||||
},
|
|
||||||
letsencrypt: {
|
letsencrypt: {
|
||||||
staging: string,
|
staging: string,
|
||||||
production: string
|
production: string
|
||||||
@@ -100,6 +99,7 @@ export const directory: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crypto
|
* Crypto
|
||||||
*/
|
*/
|
||||||
@@ -177,12 +177,14 @@ export interface CryptoLegacyInterface {
|
|||||||
|
|
||||||
export const forge: CryptoLegacyInterface;
|
export const forge: CryptoLegacyInterface;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Axios
|
* Axios
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const axios: AxiosInstance;
|
export const axios: AxiosInstance;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger
|
* Logger
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import * as acme from 'acme-client';
|
import * as acme from 'acme-client';
|
||||||
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
/* Client */
|
/* Client */
|
||||||
const accountKey = await acme.crypto.createPrivateKey();
|
const accountKey = await acme.crypto.createPrivateKey();
|
||||||
|
|||||||
4
packages/core/acme-client/types/rfc8555.d.ts
vendored
@@ -27,6 +27,7 @@ export interface AccountUpdateRequest {
|
|||||||
termsOfServiceAgreed?: boolean;
|
termsOfServiceAgreed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Order
|
* Order
|
||||||
*
|
*
|
||||||
@@ -52,6 +53,7 @@ export interface OrderCreateRequest {
|
|||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorization
|
* Authorization
|
||||||
*
|
*
|
||||||
@@ -71,6 +73,7 @@ export interface Identifier {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Challenge
|
* Challenge
|
||||||
*
|
*
|
||||||
@@ -99,6 +102,7 @@ export interface DnsChallenge extends ChallengeAbstract {
|
|||||||
|
|
||||||
export type Challenge = HttpChallenge | DnsChallenge;
|
export type Challenge = HttpChallenge | DnsChallenge;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Certificate
|
* Certificate
|
||||||
*
|
*
|
||||||
|
|||||||
22
packages/core/pipeline/.eslintrc
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
// "no-unused-expressions": "off",
|
||||||
|
"max-len": [0, 160, 2, { "ignoreUrls": true }]
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/core/pipeline/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
test/user.secret.ts
|
||||||
5
packages/core/pipeline/.mocharc.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extension": ["ts"],
|
||||||
|
"spec": "test/**/*.test.ts",
|
||||||
|
"require": "ts-node/register"
|
||||||
|
}
|
||||||