Compare commits
878 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68feda29c | ||
|
|
579c411900 | ||
|
|
699f847d4a | ||
|
|
c370ac0609 | ||
|
|
5db18ab749 | ||
|
|
14ce139135 | ||
|
|
1745907bcb | ||
|
|
344c269f34 | ||
|
|
853feecfcc | ||
|
|
28647d6902 | ||
|
|
c89633fcd5 | ||
|
|
6e3d040127 | ||
|
|
a2ac836629 | ||
|
|
3c91f29a91 | ||
|
|
78600079a4 | ||
|
|
429f8ec85e | ||
|
|
8e5fce3e96 | ||
|
|
13238ae7b4 | ||
|
|
569f94e885 | ||
|
|
6e244d0657 | ||
|
|
97356328be | ||
|
|
f81fa2eb63 | ||
|
|
316a3c950f | ||
|
|
92528edbfb | ||
|
|
32fdb3ed88 | ||
|
|
63f824e3dd | ||
|
|
543d2d9d50 | ||
|
|
6f94f4d882 | ||
|
|
906141a415 | ||
|
|
2cfaf4e231 | ||
|
|
54217217aa | ||
|
|
c492e2de28 | ||
|
|
73fec61409 | ||
|
|
ea70429889 | ||
|
|
9febe47975 | ||
|
|
22d971db4b | ||
|
|
5139198691 | ||
|
|
0e1f720419 | ||
|
|
72896e052c | ||
|
|
469c24751e | ||
|
|
688a013d73 | ||
|
|
ff53866e9e | ||
|
|
1bac6174ad | ||
|
|
c451bf5e03 | ||
|
|
03d2f4ca32 | ||
|
|
46f02331fd | ||
|
|
7c3f2399c2 | ||
|
|
ea02190ad5 | ||
|
|
e2a148c25f | ||
|
|
b2eb5d2754 | ||
|
|
c72dc0d2c4 | ||
|
|
a40b078e9c | ||
|
|
61b7165bac | ||
|
|
a6f1f21c18 | ||
|
|
b734ffcf9d | ||
|
|
6d8301b159 | ||
|
|
66bdd923d7 | ||
|
|
879da92419 | ||
|
|
b9356a5653 | ||
|
|
d21c027db8 | ||
|
|
fe93334f86 | ||
|
|
d588e14a58 | ||
|
|
0b7b544d4e | ||
|
|
664bb692b6 | ||
|
|
970fba90e0 | ||
|
|
f6c338b50e | ||
|
|
3bc708b910 | ||
|
|
041325d67e | ||
|
|
51600e7ad0 | ||
|
|
4a0e3c9a69 | ||
|
|
41ff0241af | ||
|
|
12d62bde98 | ||
|
|
138e08e985 | ||
|
|
94408e8a9f | ||
|
|
f3c7d096bc | ||
|
|
774ed5d31e | ||
|
|
b07174b533 | ||
|
|
45de2cf1db | ||
|
|
81fe230be4 | ||
|
|
6673871db2 | ||
|
|
316bd58b68 | ||
|
|
ac4c375243 | ||
|
|
5da142ab83 | ||
|
|
cbf711ee60 | ||
|
|
4f5c1dc6d7 | ||
|
|
75c89b3d0b | ||
|
|
b8513eb0b6 | ||
|
|
a74ec95a6a | ||
|
|
0bc40fd676 | ||
|
|
b9e28db089 | ||
|
|
1f6b33f4f6 | ||
|
|
049707acdc | ||
|
|
886f166e66 | ||
|
|
3f9fda8a2d | ||
|
|
d32fce98ae | ||
|
|
5b9e39a449 | ||
|
|
4b931f782e | ||
|
|
24b591ed62 | ||
|
|
a41ee9c3ca | ||
|
|
5f5c835533 | ||
|
|
bc29cce645 | ||
|
|
98f4f1cc99 | ||
|
|
d11fc1c07e | ||
|
|
e019bfe136 | ||
|
|
57f8db010b | ||
|
|
0e1a964e7c | ||
|
|
469d4b35c1 | ||
|
|
a78a815ccc | ||
|
|
9f7cffce21 | ||
|
|
5ee5460612 | ||
|
|
1651cda5b4 | ||
|
|
9370f9d68f | ||
|
|
2a7be1b24d | ||
|
|
2965fb2b47 | ||
|
|
6c3c29dd11 | ||
|
|
adb43dfee1 | ||
|
|
c0386b153e | ||
|
|
5cabceb08e | ||
|
|
b67049f9aa | ||
|
|
7a2fc5e2fd | ||
|
|
5f213b5f51 | ||
|
|
97c73aae16 | ||
|
|
101d77e4ae | ||
|
|
0f945881a1 | ||
|
|
bee4ba10cb | ||
|
|
7e0f575e0a | ||
|
|
79c1da6d14 | ||
|
|
8dc86209df | ||
|
|
c61b2d2d3f | ||
|
|
c1f2437998 | ||
|
|
1039591677 | ||
|
|
d5568608f5 | ||
|
|
6bdcfaaef0 | ||
|
|
101d55bafa | ||
|
|
fa8ba061fb | ||
|
|
1b362673c0 | ||
|
|
11d654e902 | ||
|
|
e6e964aa8c | ||
|
|
c0dc9b1882 | ||
|
|
5b613bcf84 | ||
|
|
c71d14cafa | ||
|
|
60a13aaf17 | ||
|
|
c1f77dd92f | ||
|
|
ce4c590b1c | ||
|
|
3e1ecd60a1 | ||
|
|
2171faa330 | ||
|
|
d5e4ea385d | ||
|
|
d28b89f03e | ||
|
|
6adcc61447 | ||
|
|
ecde12ec23 | ||
|
|
c66027ae8a | ||
|
|
32f9c95dd0 | ||
|
|
1568e5a2a7 | ||
|
|
e4a534cb7c | ||
|
|
ee2cca17fe | ||
|
|
0869eaafdd | ||
|
|
69d4b3f93d | ||
|
|
61b37f38e2 | ||
|
|
c69c560de0 | ||
|
|
0d3e426dff | ||
|
|
4fe68d3b9f | ||
|
|
dab6ad917c | ||
|
|
a20b82b9cf | ||
|
|
087fd81879 | ||
|
|
d1dbbae101 | ||
|
|
3a2baba746 | ||
|
|
831f0ee5d9 | ||
|
|
e10fb64d6b | ||
|
|
8ecb71fb55 | ||
|
|
dea4106569 | ||
|
|
2dd8fb2ee2 | ||
|
|
2218be5d34 | ||
|
|
d712f07b96 | ||
|
|
b657405e46 | ||
|
|
974c320925 | ||
|
|
dd236b925d | ||
|
|
e264d71048 | ||
|
|
23f83b9377 | ||
|
|
db68721834 | ||
|
|
6a9cf2ed28 | ||
|
|
3dd79d447b | ||
|
|
e87ac72281 | ||
|
|
e430109228 | ||
|
|
e7e123af0d | ||
|
|
7b75dacb03 | ||
|
|
493f2a508b | ||
|
|
70b8aaf845 | ||
|
|
ab1c9bfdbc | ||
|
|
643d820965 | ||
|
|
8aa5c3ca65 | ||
|
|
7160589ac7 | ||
|
|
21cc1d43de | ||
|
|
793289ad97 | ||
|
|
bea2f00a90 | ||
|
|
d08da18b4a | ||
|
|
1e03fcff67 | ||
|
|
ffb516d343 | ||
|
|
45f9913bdb | ||
|
|
a6e9cc03c0 | ||
|
|
b5db2d565a | ||
|
|
e5518b1067 | ||
|
|
a5c9ed8d17 | ||
|
|
b5094a3cc9 | ||
|
|
99c5c8339d | ||
|
|
9f7e0f8a26 | ||
|
|
75bcbe52fd | ||
|
|
8c4a239631 | ||
|
|
503d9c34f8 | ||
|
|
d9f38c38a6 | ||
|
|
598d0705fb | ||
|
|
a0c08e841d | ||
|
|
8ed2b2475c | ||
|
|
e4e0a24a06 | ||
|
|
9839e2bb60 | ||
|
|
db10ed8378 | ||
|
|
ebffac7ba4 | ||
|
|
f99dd4f89a | ||
|
|
6badc0f419 | ||
|
|
aa1b69d7f2 | ||
|
|
eb3fec1ac0 | ||
|
|
0f772d55ab | ||
|
|
9cea6775d1 | ||
|
|
7e376071f5 | ||
|
|
9a937fa072 | ||
|
|
84c36a4eec | ||
|
|
155371cdd0 | ||
|
|
87e1749553 | ||
|
|
4ba7237326 | ||
|
|
6f1a375fee | ||
|
|
350160833b | ||
|
|
e4c51aece4 | ||
|
|
dfc192cb68 | ||
|
|
2a68372713 | ||
|
|
8af5235e4d | ||
|
|
7cf96d7d7e | ||
|
|
9c4831fa3f | ||
|
|
8cf1ffd38b | ||
|
|
3c70a4f455 | ||
|
|
ddb6a88392 | ||
|
|
61843a4997 | ||
|
|
3b9a7fe805 | ||
|
|
b686579acc | ||
|
|
01ede08a79 | ||
|
|
ae11d5ee3d | ||
|
|
9246878d0e | ||
|
|
5387c373e0 | ||
|
|
da76d1065e | ||
|
|
2213399f5e | ||
|
|
52dfa5e8c3 | ||
|
|
90058b2dae | ||
|
|
e695c8ee5c | ||
|
|
849e065bb2 | ||
|
|
b7cd07c996 | ||
|
|
52ee3863ae | ||
|
|
5ce5a08e41 | ||
|
|
8a16893082 | ||
|
|
c6a8f923e4 | ||
|
|
b6dd2248c8 | ||
|
|
1588179bc9 | ||
|
|
7f36621882 | ||
|
|
e256d36cd1 | ||
|
|
b2417ad902 | ||
|
|
67a32a98a9 | ||
|
|
78d9d5159a | ||
|
|
e2d29b8fa2 | ||
|
|
880c8819b4 | ||
|
|
6075cc5c95 | ||
|
|
5c1854948c | ||
|
|
7bd0cbce10 | ||
|
|
9c645a1efa | ||
|
|
6f088fd76a | ||
|
|
cb7a465d6c | ||
|
|
416f5e0986 | ||
|
|
86133ba52b | ||
|
|
047479426a | ||
|
|
fb2d292cbf | ||
|
|
75cf552e72 | ||
|
|
77537e7005 | ||
|
|
dae6ad2951 | ||
|
|
8a816ba44f | ||
|
|
a9d918aa95 | ||
|
|
a980904e38 | ||
|
|
4008c2bfd5 | ||
|
|
1184e52ba9 | ||
|
|
adbf40914e | ||
|
|
9b9083dfa1 | ||
|
|
6bd3b4998e | ||
|
|
1f602c00be | ||
|
|
4d0f7c2e02 | ||
|
|
c9024c5611 | ||
|
|
a92dc2bbe6 | ||
|
|
401fa3dcdd | ||
|
|
4e5373de73 | ||
|
|
956fbb7833 | ||
|
|
6217d3aacd | ||
|
|
8b1ae309fb | ||
|
|
52d24ff2f2 | ||
|
|
7a66bdf139 | ||
|
|
16bc12c15b | ||
|
|
0556d68a4e | ||
|
|
586c7fa927 | ||
|
|
9ef16ebcf9 | ||
|
|
d509445519 | ||
|
|
d7bff599b7 | ||
|
|
cda54085b9 | ||
|
|
984aae1ca6 | ||
|
|
695c99119f | ||
|
|
d7e205aee7 | ||
|
|
09919cb3cb | ||
|
|
ba73e04046 | ||
|
|
88cbf30fde | ||
|
|
ed37add29f | ||
|
|
6d25f9c205 | ||
|
|
01d30bb742 | ||
|
|
a1fec5f6ac | ||
|
|
ef9ddd27a5 | ||
|
|
b6203e57ed | ||
|
|
3fcea4ba2f | ||
|
|
a51f85826c | ||
|
|
c846945905 | ||
|
|
e2af21e0e1 | ||
|
|
929250810f | ||
|
|
cb162e063d | ||
|
|
63ffb9df14 | ||
|
|
a917d6c2c5 | ||
|
|
9e1e0dee1d | ||
|
|
7c1a2d5f91 | ||
|
|
cae33cfc4f | ||
|
|
d143df3f9f | ||
|
|
84a3817b15 | ||
|
|
525eb83d1e | ||
|
|
8b7295d77e | ||
|
|
df57c196e9 | ||
|
|
5ea5473bdd | ||
|
|
85faf8d517 | ||
|
|
abe6dbb5a2 | ||
|
|
afa446aabe | ||
|
|
2712f9a3f4 | ||
|
|
c40de5d3b2 | ||
|
|
2b1da81b98 | ||
|
|
d693d26323 | ||
|
|
2374bb56fa | ||
|
|
df71782719 | ||
|
|
599e718003 | ||
|
|
1cad816b17 | ||
|
|
0fa6d2980b | ||
|
|
c27818b3b0 | ||
|
|
047b3bc079 | ||
|
|
70e6920288 | ||
|
|
b5739c663d | ||
|
|
220d98a668 | ||
|
|
419b6eb626 | ||
|
|
9764fb481f | ||
|
|
ba01edc691 | ||
|
|
fba647313d | ||
|
|
2f146fffdc | ||
|
|
7654c79d12 | ||
|
|
cf35dbfd6e | ||
|
|
a6c002146c | ||
|
|
bb3009a124 | ||
|
|
b744363736 | ||
|
|
35c25987cd | ||
|
|
ca91a9e089 | ||
|
|
83ba3d4450 | ||
|
|
8fe0d342aa | ||
|
|
a4eff0b408 | ||
|
|
7b85da901d | ||
|
|
be27789ea8 | ||
|
|
07a443f6c4 | ||
|
|
588e89e8fe | ||
|
|
789c120fc9 | ||
|
|
fdfe54b6da | ||
|
|
3b50741f19 | ||
|
|
c5498b92a2 | ||
|
|
048150d779 | ||
|
|
7db933199a | ||
|
|
5c6be439e8 | ||
|
|
4e0134b70a | ||
|
|
d6ddf8e9f4 | ||
|
|
2facb160aa | ||
|
|
b44b8d09b2 | ||
|
|
65d9c6fe2f | ||
|
|
c522196029 | ||
|
|
a5d097e860 | ||
|
|
668f6ee36f | ||
|
|
4f2363230d | ||
|
|
2b93552d1d | ||
|
|
124af0b76d | ||
|
|
972df7c167 | ||
|
|
b4c17a6a12 | ||
|
|
92c03cbdf9 | ||
|
|
220478cd31 | ||
|
|
df905ade88 | ||
|
|
9ff3e22c80 | ||
|
|
37df882ed3 | ||
|
|
47050769fc | ||
|
|
65df759275 | ||
|
|
86761bd3a0 | ||
|
|
a842b6b925 | ||
|
|
09e4b24445 | ||
|
|
4916757d59 | ||
|
|
30b66adc3b | ||
|
|
13582d1a7b | ||
|
|
0b9312b549 | ||
|
|
bde51d8d38 | ||
|
|
643a666853 | ||
|
|
2d10fa0218 | ||
|
|
a59184ae5f | ||
|
|
82807fcc1b | ||
|
|
a6c93ef9b8 | ||
|
|
6a151865f7 | ||
|
|
414d8d140e | ||
|
|
51fb9dca58 | ||
|
|
6367785b1b | ||
|
|
03b2a9da66 | ||
|
|
aa7fb7da06 | ||
|
|
26d11de249 | ||
|
|
a9d5b53460 | ||
|
|
0daa9f1882 | ||
|
|
f799740d70 | ||
|
|
56886dcfe9 | ||
|
|
81e1e4a7ff | ||
|
|
9b5256716f | ||
|
|
446bf80f1d | ||
|
|
775b12aec1 | ||
|
|
6a80455c6c | ||
|
|
43b2ff7957 | ||
|
|
295b7779ee | ||
|
|
d1df088662 | ||
|
|
2b0f7aaf8a | ||
|
|
3265dd76ab | ||
|
|
d1d7b44303 | ||
|
|
56eced3813 | ||
|
|
bde2147dd3 | ||
|
|
c853f2976f | ||
|
|
8901f5d40e | ||
|
|
b66931003f | ||
|
|
9a75d2ac8f | ||
|
|
9132d47f4d | ||
|
|
42c5aea3f7 | ||
|
|
e2fd9c4cee | ||
|
|
f847b7ff62 | ||
|
|
9eae8f5077 | ||
|
|
2bacf76664 | ||
|
|
b2030caedc | ||
|
|
956c975c6d | ||
|
|
41bd321a4f | ||
|
|
952e9687d0 | ||
|
|
c298f8b952 | ||
|
|
e2562a5251 | ||
|
|
dbdb40baf9 | ||
|
|
52f40d982d | ||
|
|
fd04cec606 | ||
|
|
2ff923dd1b | ||
|
|
f4f13f91f2 | ||
|
|
034aa980e6 | ||
|
|
6ac7a51ce0 | ||
|
|
cf0c0e3e2c | ||
|
|
1b899575e0 | ||
|
|
23e5cb5669 | ||
|
|
ee9578b273 | ||
|
|
e4ba4c9b37 | ||
|
|
9ed64bdc9a | ||
|
|
e9b6fb55ff | ||
|
|
80caf881ae | ||
|
|
35c0ed2ba5 | ||
|
|
c36db3545f | ||
|
|
1ea0ba18cd | ||
|
|
327c83cbc8 | ||
|
|
a367585ab4 | ||
|
|
2994cb5c65 | ||
|
|
1bedb31a3c | ||
|
|
8fecebc254 | ||
|
|
44497a0969 | ||
|
|
5362371bda | ||
|
|
8b04e96a7d | ||
|
|
5d93334426 | ||
|
|
be84f3314f | ||
|
|
150b666d4b | ||
|
|
94579d65c4 | ||
|
|
551b06b4e8 | ||
|
|
76fc47a274 | ||
|
|
07b5760986 | ||
|
|
35e1bfcd7f | ||
|
|
b06ffc0eef | ||
|
|
24df7913fe | ||
|
|
83674e4b35 | ||
|
|
22d3aeb7b5 | ||
|
|
8809eef2ce | ||
|
|
cf005711c0 | ||
|
|
0a00d0c52f | ||
|
|
9aa17a0395 | ||
|
|
e4d190f1e7 | ||
|
|
65ecdf7dc2 | ||
|
|
0dfa5994cc | ||
|
|
5d2844fdb6 | ||
|
|
44332b9d07 | ||
|
|
9b8e73f1de | ||
|
|
20a23e148c | ||
|
|
076f0d5de9 | ||
|
|
0bcb6206f4 | ||
|
|
943b9827ee | ||
|
|
741f3ec212 | ||
|
|
613b6839b8 | ||
|
|
8549a17675 | ||
|
|
718cfccbea | ||
|
|
2458fa26d8 | ||
|
|
ac24684d2b | ||
|
|
106dbd9538 | ||
|
|
f9efb2b800 | ||
|
|
897d124d5b | ||
|
|
34daf9ccac | ||
|
|
269a97e81e | ||
|
|
2fd57621d8 | ||
|
|
76de837214 | ||
|
|
1e41020728 | ||
|
|
8a78e49bf0 | ||
|
|
e6726e4c02 | ||
|
|
76330a4a1a | ||
|
|
7e5f0097e4 | ||
|
|
18e1c02d1c | ||
|
|
28992f178e | ||
|
|
c41f34c352 | ||
|
|
6b5580a30c | ||
|
|
1dee14e32d | ||
|
|
1e3c4881d0 | ||
|
|
657964cda4 | ||
|
|
893aac916c | ||
|
|
68da6cf3ae | ||
|
|
0d96ea9eef | ||
|
|
0ceb44a7cd | ||
|
|
4fec0036cb | ||
|
|
f82eee4636 | ||
|
|
260cfb96ec | ||
|
|
f71a519674 | ||
|
|
369c146eca | ||
|
|
83264a6946 | ||
|
|
3c3d4e9109 | ||
|
|
ce55365292 | ||
|
|
be495839b6 | ||
|
|
a27a9f55a7 | ||
|
|
10e14caf35 | ||
|
|
59af246479 | ||
|
|
1f52eaca01 | ||
|
|
d833f4b5ff | ||
|
|
bfee39049d | ||
|
|
b4599df6c6 | ||
|
|
261c6f6956 | ||
|
|
b97d77c848 | ||
|
|
c1cefe0e7f | ||
|
|
55b77fdf5c | ||
|
|
16967c4ab1 | ||
|
|
61a4fd8657 | ||
|
|
67ca7e3097 | ||
|
|
26fa8e75bd | ||
|
|
aeaa45b713 | ||
|
|
edeac86f06 | ||
|
|
4e0c23165f | ||
|
|
feb851a3fc | ||
|
|
3103d60508 | ||
|
|
53be6b5f5b | ||
|
|
9d3e0d1090 | ||
|
|
f8aef129cf | ||
|
|
c419b2c8b4 | ||
|
|
e1a3a3e7c7 | ||
|
|
b47a1a13cb | ||
|
|
3397f424bc | ||
|
|
48672d1a44 | ||
|
|
38dc8a63d9 | ||
|
|
009e8fb976 | ||
|
|
6d7a91f49b | ||
|
|
9d4d14db06 | ||
|
|
c9f347f77a | ||
|
|
0396d8222e | ||
|
|
305f3de50f | ||
|
|
ffacfe0f42 | ||
|
|
be9e66c7d3 | ||
|
|
1238508bdb | ||
|
|
1ab5c4035a | ||
|
|
67fa9d91bf | ||
|
|
dc5f9abf20 | ||
|
|
7240a42fbc | ||
|
|
6fbb6d4992 | ||
|
|
86838f305b | ||
|
|
1b1b5939c5 | ||
|
|
ffdd61b5ee | ||
|
|
adad5d86ba | ||
|
|
e7870e2b05 | ||
|
|
548cbbfdd4 | ||
|
|
da4715e6dc | ||
|
|
506ab4f18e | ||
|
|
d87026d5be | ||
|
|
1690963aaf | ||
|
|
20d2c5699c | ||
|
|
e660e9cad1 | ||
|
|
26d7b0ba03 | ||
|
|
ee097b3135 | ||
|
|
f5052e9a58 | ||
|
|
3b3376899c | ||
|
|
a24a3595fa | ||
|
|
6a14d801f1 | ||
|
|
332c5c5127 | ||
|
|
f9568f1a4a | ||
|
|
b458720dca | ||
|
|
935a320100 | ||
|
|
361d0de17c | ||
|
|
024b3c936e | ||
|
|
dc720a5d99 | ||
|
|
af3e20709d | ||
|
|
ea9e9165b6 | ||
|
|
ee531dd186 | ||
|
|
51abe8de56 | ||
|
|
e2254faf15 | ||
|
|
cea6be37dc | ||
|
|
46dccb176e | ||
|
|
5411b9cb92 | ||
|
|
9f6ea410af | ||
|
|
528a3d9da8 | ||
|
|
564eb48ebe | ||
|
|
92a6b179d4 | ||
|
|
83393a4ee1 | ||
|
|
6875151717 | ||
|
|
2a8c6cf033 | ||
|
|
7544286b0f | ||
|
|
7c685646da | ||
|
|
d82a9c9253 | ||
|
|
59584a2961 | ||
|
|
195aa54cdc | ||
|
|
4b324e6a22 | ||
|
|
0e575a0ce7 | ||
|
|
7ab8517a93 | ||
|
|
1dca6ecf8d | ||
|
|
8bec234fe8 | ||
|
|
bff18a7be7 | ||
|
|
bac00491fe | ||
|
|
f8da3ded0d | ||
|
|
b01849eb0c | ||
|
|
c9eb487953 | ||
|
|
dc383644d6 | ||
|
|
a8f718afa0 | ||
|
|
cd76d170b2 | ||
|
|
7b129c11e9 | ||
|
|
f7972d5b68 | ||
|
|
b1a0d84033 | ||
|
|
969fba8a57 | ||
|
|
63865b5fbd | ||
|
|
46c32f15e3 | ||
|
|
5f62c887c0 | ||
|
|
c85beaa52b | ||
|
|
885cdfaec9 | ||
|
|
011130432c | ||
|
|
062d66222a | ||
|
|
e53749e16e | ||
|
|
dbfb84ec6d | ||
|
|
265842feeb | ||
|
|
0c35928eee | ||
|
|
ea4bcb4aaf | ||
|
|
716f5f1426 | ||
|
|
4e86c1eb45 | ||
|
|
0576a8bec3 | ||
|
|
97f334b5ab | ||
|
|
18a7bf0d66 | ||
|
|
908d33f186 | ||
|
|
68b9171390 | ||
|
|
45005a5073 | ||
|
|
028eb088a5 | ||
|
|
8f98664665 | ||
|
|
699385a8c4 | ||
|
|
64b7ed00f5 | ||
|
|
2c75d2bfde | ||
|
|
9c41b0e357 | ||
|
|
ec6f10053a | ||
|
|
0095600615 | ||
|
|
b031f00764 | ||
|
|
a4fc8dfc56 | ||
|
|
f168bd903d | ||
|
|
fc55e37454 | ||
|
|
84e2fd4f5c | ||
|
|
0037659462 | ||
|
|
364289894e | ||
|
|
f6a3f4edfa | ||
|
|
560d21c854 | ||
|
|
4719f99155 | ||
|
|
3a213dc9c3 | ||
|
|
0d07c7c234 | ||
|
|
21670f64d1 | ||
|
|
f0e7fe695d | ||
|
|
2bab727569 | ||
|
|
8d41a9aae7 | ||
|
|
896b5d3a13 | ||
|
|
88e64717cd | ||
|
|
2d275a14ab | ||
|
|
1b796cffd1 | ||
|
|
f53e54c8de | ||
|
|
a9b9be96cb | ||
|
|
1033885c99 | ||
|
|
c45ad3c901 | ||
|
|
24192b61c1 | ||
|
|
1562e92e74 | ||
|
|
17f72eb9cb | ||
|
|
57ae6d5b40 | ||
|
|
467e4c4634 | ||
|
|
d6d296b546 | ||
|
|
499bbe4fa7 | ||
|
|
ae814766f3 | ||
|
|
17cfeee374 | ||
|
|
efa394e9bd | ||
|
|
94ca0f27bf | ||
|
|
e08df5e6d8 | ||
|
|
952c6ef73d | ||
|
|
b9902c926f | ||
|
|
7fa6ea1797 | ||
|
|
be3cdbf585 | ||
|
|
6225969d4c | ||
|
|
4382474449 | ||
|
|
678ef9c232 | ||
|
|
3d535320b9 | ||
|
|
77d3e40ffb | ||
|
|
5dca64d3d3 | ||
|
|
02d582b564 | ||
|
|
8e906cbf23 | ||
|
|
411b7bbfe2 | ||
|
|
3093fc6b02 | ||
|
|
3c4b7d251a | ||
|
|
f87a1be192 | ||
|
|
9b91cbd67e | ||
|
|
9b5e1052a1 | ||
|
|
0d47d7cfd0 | ||
|
|
ef87975c80 | ||
|
|
9db757fbbb | ||
|
|
f6ef305441 | ||
|
|
004c6a8506 | ||
|
|
a035ba192a | ||
|
|
d90319bb53 | ||
|
|
51db5e1ed0 | ||
|
|
1ce2a52d70 | ||
|
|
fbbea22eee | ||
|
|
71f43c5bd4 | ||
|
|
79dce124a7 | ||
|
|
0bc042ae31 | ||
|
|
704d2eed32 | ||
|
|
3348301493 | ||
|
|
afeae4269c | ||
|
|
a4d1c9a7c0 | ||
|
|
26be47d072 | ||
|
|
cf3de10eff | ||
|
|
7ef885319e | ||
|
|
b0923d54ee | ||
|
|
be15f2b6a6 | ||
|
|
0c1d3341f4 | ||
|
|
1a9cad355c | ||
|
|
2c97fa929a | ||
|
|
e397793153 | ||
|
|
9bd279a8a0 | ||
|
|
dd7897feff | ||
|
|
d851a86a9b | ||
|
|
62133a91a6 | ||
|
|
5b30fc8aba | ||
|
|
2ed94bf509 | ||
|
|
1928a47961 | ||
|
|
19f5348802 | ||
|
|
f148240bcf | ||
|
|
f914931bc9 | ||
|
|
8c1033634d | ||
|
|
781b79f529 | ||
|
|
7d74e1d41e | ||
|
|
ad91703492 | ||
|
|
a007c81e9a | ||
|
|
39bffe3389 | ||
|
|
3b06c7b0a6 | ||
|
|
3f2767b28b | ||
|
|
312c6e685a | ||
|
|
d2b6ab75b7 | ||
|
|
9f1b00f04c | ||
|
|
dc16294b3d | ||
|
|
77dfcef168 | ||
|
|
30ef5841d6 | ||
|
|
217ba85ff8 | ||
|
|
71e2555391 | ||
|
|
f036eb1cf2 | ||
|
|
1347066549 | ||
|
|
7fc149f67d | ||
|
|
dfba5ee638 | ||
|
|
9ba79f996f | ||
|
|
cd85000908 | ||
|
|
995349ab3e | ||
|
|
4fa8031318 | ||
|
|
3f45bb1629 | ||
|
|
0e139e6284 | ||
|
|
82dbfc6de3 | ||
|
|
9b2937d601 | ||
|
|
3375839a40 | ||
|
|
7f5ff6fab5 | ||
|
|
5160b4c3d9 | ||
|
|
85234b21c7 | ||
|
|
223af9e09d | ||
|
|
49fdf8213a | ||
|
|
7a48101015 | ||
|
|
6b85b4a0c9 | ||
|
|
3c56a53e91 | ||
|
|
9797a0835d | ||
|
|
b6dc57f3e4 | ||
|
|
78ac21c767 | ||
|
|
1e2d8fa027 | ||
|
|
e7e2e4786d | ||
|
|
9acdd15c1e | ||
|
|
fcc0dd93fd | ||
|
|
5eba437732 | ||
|
|
0d0fcfccf3 | ||
|
|
993ef7bf57 | ||
|
|
46080b311a | ||
|
|
1fbe6b55c1 | ||
|
|
07795568bf | ||
|
|
e820e5599b | ||
|
|
cb8636faec | ||
|
|
aa1046c39a | ||
|
|
7e94ba0875 | ||
|
|
077b365458 | ||
|
|
b3f1e1e444 | ||
|
|
ead8e1fec5 | ||
|
|
5be1139c1a | ||
|
|
45e218dd5b | ||
|
|
0abb030889 | ||
|
|
85df8eb09d | ||
|
|
c6291b42fc | ||
|
|
2634789769 | ||
|
|
253075e7c0 | ||
|
|
363fbdee00 | ||
|
|
a9fdceca6f | ||
|
|
f9cb605eb4 | ||
|
|
d5867d0971 | ||
|
|
f0faee34a4 | ||
|
|
31e9f08b47 | ||
|
|
ac4904fb9a | ||
|
|
4c9095400e | ||
|
|
38d975a3bb | ||
|
|
5422f17fab | ||
|
|
2a1af1e7cd | ||
|
|
5eec1cf5ca | ||
|
|
a259ccdfec | ||
|
|
48c1c1e996 | ||
|
|
2cca82eb95 | ||
|
|
30beee6027 | ||
|
|
b649348162 | ||
|
|
b912c5e688 | ||
|
|
5981200df2 | ||
|
|
f9e7bfd606 | ||
|
|
7f6549bdf3 | ||
|
|
2af26dbfe0 | ||
|
|
b7f382e16f | ||
|
|
7762955989 | ||
|
|
1ab603b506 | ||
|
|
b432cbfd3f | ||
|
|
e4d76113f8 | ||
|
|
12a3adc559 | ||
|
|
e50f1a74d6 | ||
|
|
ba6a504588 | ||
|
|
2d37c42584 | ||
|
|
f4b3a8cf81 | ||
|
|
0390ac3eda | ||
|
|
1977201051 | ||
|
|
2efe0de0cf | ||
|
|
34e40e5e54 | ||
|
|
e7e269dfb0 | ||
|
|
fa85580e35 | ||
|
|
500fce6180 | ||
|
|
f501df2804 | ||
|
|
6c1b1fb72b | ||
|
|
505cfc5c1e | ||
|
|
ac14924a73 | ||
|
|
7550aec904 | ||
|
|
139a6980ac | ||
|
|
d5a6411e26 | ||
|
|
1fcd9897be | ||
|
|
99d4bf2624 | ||
|
|
844347acf9 | ||
|
|
eeae9b4405 | ||
|
|
feef94f873 |
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
indent_size = 2
|
||||
indent_style = tab
|
||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: ["https://profile.ikit.fun/sponsors/"]
|
||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: 创建一个报告来帮助我们改进
|
||||
title: "[Bug] 标题简要描述问题"
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**描述问题**
|
||||
简要描述问题是什么,1 个 ISSUE 只描述一个问题。
|
||||
|
||||
**复现步骤**
|
||||
复现该问题的步骤:
|
||||
|
||||
1. 去到 '...'
|
||||
2. 点击 '...'
|
||||
3. 滚动到 '...'
|
||||
4. 发现问题
|
||||
|
||||
**期望的结果**
|
||||
简要描述你期望发生的事情。
|
||||
|
||||
**截图**
|
||||
如有可能,请添加截图以帮助解释问题。
|
||||
|
||||
**环境**
|
||||
|
||||
- 操作系统: [e.g. Windows, macOS]
|
||||
- 浏览器: [e.g. Chrome, Safari]
|
||||
- 仓库版本: [e.g. v1.0.0]
|
||||
|
||||
**其他信息**
|
||||
在此处添加关于该问题的任何其他信息。
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 加入频道讨论
|
||||
url: https://t.me/+ZXphsppxUg41YmVl
|
||||
about: 加入到电报频道寻求更多帮助
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: 提出一个新功能请求
|
||||
title: "[Feature] 简要描述你希望实现的功能"
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**功能描述**
|
||||
简要描述你希望添加的功能和相关问题,1 个 ISSUE 只描述一个功能。
|
||||
|
||||
**动机**
|
||||
为什么这个功能对项目有帮助?
|
||||
|
||||
**替代方案**
|
||||
描述你已经考虑过的替代方案。
|
||||
|
||||
**其他信息**
|
||||
在这里添加任何相关的附加信息或截图。
|
||||
39
.github/workflows/push_image.yml
vendored
39
.github/workflows/push_image.yml
vendored
@@ -1,9 +1,17 @@
|
||||
name: Docker Image CI
|
||||
name: Docker Image CI (stable versions)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "v[0-9]*"
|
||||
- "!v*alpha*"
|
||||
- "!v*beta*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag version to be used for Docker image"
|
||||
required: true
|
||||
default: "latest"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -19,25 +27,32 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
usual2970/certimate
|
||||
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
|
||||
|
||||
- name: Log in to DOCKERHUB
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Log in to ALIYUNCS
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-shanghai.aliyuncs.com
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract Git tag
|
||||
id: get_tag
|
||||
run: echo "tag=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile_build
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
registry.cn-shanghai.aliyuncs.com/usual2970/certimate:${{ env.tag }}
|
||||
registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
61
.github/workflows/push_image_next.yml
vendored
Normal file
61
.github/workflows/push_image_next.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Docker Image CI (preview versions)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]*-alpha*"
|
||||
- "v[0-9]*-beta*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag version to be used for Docker image"
|
||||
required: true
|
||||
default: "next"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
usual2970/certimate
|
||||
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
|
||||
tags: |
|
||||
type=ref,event=tag,pattern={{version}}
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: Log in to DOCKERHUB
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Log in to ALIYUNCS
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-shanghai.aliyuncs.com
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: basebuild
|
||||
name: Base Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "v[0-9]*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
@@ -22,9 +22,9 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.22.5"
|
||||
go-version: ">=1.23.0"
|
||||
|
||||
- name: Build Admin dashboard UI
|
||||
- name: Build WebUI
|
||||
run: npm --prefix=./ui ci && npm --prefix=./ui run build
|
||||
|
||||
- name: Run GoReleaser
|
||||
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,14 +1,7 @@
|
||||
__debug_bin*
|
||||
vendor
|
||||
pb_data
|
||||
main
|
||||
./certimate
|
||||
build
|
||||
/docker/data
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/settings.tailwind.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
@@ -16,5 +9,12 @@ build
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
__debug_bin*
|
||||
|
||||
./dist
|
||||
vendor
|
||||
pb_data
|
||||
build
|
||||
main
|
||||
/dist
|
||||
/docker/data
|
||||
/certimate
|
||||
|
||||
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
21
.vscode/settings.json
vendored
Normal file
21
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"css.customData": [
|
||||
".vscode/settings.tailwind.json"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"go.useLanguageServer": true,
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true,
|
||||
},
|
||||
"typescript.tsdk": "ui/node_modules/typescript/lib",
|
||||
"[go]": {
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
55
.vscode/settings.tailwind.json
vendored
Normal file
55
.vscode/settings.tailwind.json
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1 @@
|
||||
## v0.0.3
|
||||
|
||||
- 解决一些 bug
|
||||
- 添加 README.md
|
||||
|
||||
## v0.0.1
|
||||
|
||||
- Initial release
|
||||
A full changelog of past releases is available on [GitHub Releases](https://github.com/usual2970/certimate/releases) page.
|
||||
|
||||
76
CONTRIBUTING.md
Normal file
76
CONTRIBUTING.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 向 Certimate 贡献代码
|
||||
|
||||
感谢你抽出时间来改进 Certimate!以下是向 Certimate 主仓库提交 PR(Pull Request)时的操作指南。
|
||||
|
||||
- [向 Certimate 贡献代码](#向-certimate-贡献代码)
|
||||
- [前提条件](#前提条件)
|
||||
- [修改 Go 代码](#修改-go-代码)
|
||||
- [修改管理页面 (Admin UI)](#修改管理页面-admin-ui)
|
||||
|
||||
## 前提条件
|
||||
|
||||
- Go 1.22+ (用于修改 Go 代码)
|
||||
- Node 20+ (用于修改 UI)
|
||||
|
||||
如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your_username/certimate.git
|
||||
```
|
||||
|
||||
> **重要提示:**
|
||||
> 建议为每个 Bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR,请保持不同的改动在独立分支中,以便更容易进行代码审查并最终合并。
|
||||
> 保持一个 PR 只实现一个功能。
|
||||
|
||||
## 修改 Go 代码
|
||||
|
||||
假设你已经对 Certimate 的 Go 代码做了一些修改,现在你想要运行它:
|
||||
|
||||
1. 进入根目录
|
||||
2. 运行以下命令启动服务:
|
||||
|
||||
```bash
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
这将启动一个 Web 服务器,默认运行在 `http://localhost:8090`,并使用来自 `ui/dist` 的预构建管理页面。
|
||||
|
||||
**在向主仓库提交 PR 之前,建议你:**
|
||||
|
||||
- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 对你的代码进行格式化。
|
||||
|
||||
- 为你的改动添加单元测试或集成测试(Certimate 使用 Go 的标准 `testing` 包)。你可以通过以下命令运行测试(在项目根目录下):
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## 修改管理页面 (Admin UI)
|
||||
|
||||
Certimate 的管理页面是一个基于 React 和 Vite 构建的单页应用(SPA)。
|
||||
|
||||
要启动 Admin UI:
|
||||
|
||||
1. 进入 `ui` 项目目录。
|
||||
|
||||
2. 运行 `npm install` 安装依赖。
|
||||
|
||||
3. 启动 Vite 开发服务器:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
你可以通过浏览器访问 `http://localhost:5173` 来查看运行中的管理页面。
|
||||
|
||||
由于 Admin UI 只是一个客户端应用,运行时需要 Certimate 的后端服务作为支撑。你可以手动运行 Certimate,或者使用预构建的可执行文件。
|
||||
|
||||
所有对 Admin UI 的修改将会自动反映在浏览器中,无需手动刷新页面。
|
||||
|
||||
完成修改后,运行以下命令来构建 Admin UI,以便它能被嵌入到 Go 包中:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
完成所有步骤后,你可以将改动提交 PR 到 Certimate 主仓库。
|
||||
81
CONTRIBUTING_EN.md
Normal file
81
CONTRIBUTING_EN.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Contributing to Certimate
|
||||
|
||||
Thank you for taking the time to improve Certimate! Below is a guide for submitting a PR (Pull Request) to the main Certimate repository.
|
||||
|
||||
- [Contributing to Certimate](#contributing-to-certimate)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Making Changes in the Go Code](#making-changes-in-the-go-code)
|
||||
- [Making Changes in the Admin UI](#making-changes-in-the-admin-ui)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.22+ (for Go code changes)
|
||||
- Node 20+ (for Admin UI changes)
|
||||
|
||||
If you haven't done so already, you can fork the Certimate repository and clone your fork to work locally:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your_username/certimate.git
|
||||
```
|
||||
|
||||
> **Important:**
|
||||
> It is recommended to create a new branch from `main` for each bug fix or feature. If you plan to submit multiple PRs, ensure the changes are in separate branches for easier review and eventual merge.
|
||||
> Keep each PR focused on a single feature or fix.
|
||||
|
||||
## Making Changes in the Go Code
|
||||
|
||||
Once you have made changes to the Go code in Certimate, follow these steps to run the project:
|
||||
|
||||
1. Navigate to the root directory.
|
||||
|
||||
2. Start the service by running:
|
||||
|
||||
```bash
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
This will start a web server at `http://localhost:8090` using the prebuilt Admin UI located in `ui/dist`.
|
||||
|
||||
**Before submitting a PR to the main repository, consider:**
|
||||
|
||||
- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt).
|
||||
|
||||
- Adding unit or integration tests for your changes. Certimate uses Go’s standard `testing` package. You can run tests using the following command (while in the root project directory):
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Making Changes in the Admin UI
|
||||
|
||||
Certimate’s Admin UI is a single-page application (SPA) built using React and Vite.
|
||||
|
||||
To start the Admin UI:
|
||||
|
||||
1. Navigate to the `ui` project directory.
|
||||
|
||||
2. Install the necessary dependencies by running:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the Vite development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You can now access the running Admin UI at `http://localhost:5173` in your browser.
|
||||
|
||||
Since the Admin UI is a client-side application, you will also need to have the Certimate backend running. You can either manually run Certimate or use a prebuilt executable.
|
||||
|
||||
Any changes you make in the Admin UI will be automatically reflected in the browser without requiring a page reload.
|
||||
|
||||
After completing your changes, build the Admin UI so it can be embedded into the Go package:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Once all steps are completed, you are ready to submit a PR to the main Certimate repository.
|
||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM node:20-alpine3.19 AS front-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN \
|
||||
cd /app/ui && \
|
||||
npm install && \
|
||||
npm run build
|
||||
|
||||
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../. /app/
|
||||
|
||||
RUN rm -rf /app/ui/dist
|
||||
|
||||
COPY --from=front-builder /app/ui/dist /app/ui/dist
|
||||
|
||||
RUN go build -o certimate
|
||||
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/certimate .
|
||||
|
||||
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM golang:1.22-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../. /app/
|
||||
|
||||
RUN go build -o certimate
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/certimate .
|
||||
|
||||
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
|
||||
5
Makefile
5
Makefile
@@ -34,4 +34,7 @@ help:
|
||||
@echo " make clean - 清理构建文件"
|
||||
@echo " make help - 显示此帮助信息"
|
||||
|
||||
.PHONY: all build clean help
|
||||
.PHONY: all build clean help
|
||||
|
||||
local.run:
|
||||
go mod vendor&& npm --prefix=./ui install && npm --prefix=./ui run build && go run main.go serve --http 127.0.0.1:8090
|
||||
|
||||
193
README.md
193
README.md
@@ -1,167 +1,114 @@
|
||||
<h1 align="center">🔒 Certimate</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
# 🔒Certimate
|
||||
[](https://github.com/usual2970/certimate)
|
||||
[](https://github.com/usual2970/certimate)
|
||||
[](https://hub.docker.com/r/usual2970/certimate)
|
||||
[](https://github.com/usual2970/certimate/releases)
|
||||
[](https://mit-license.org/)
|
||||
|
||||
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
|
||||
</div>
|
||||
|
||||
1. 😱麻烦:申请、部署证书虽不复杂,但也挺麻烦的,尤其是你维护多个域名的时候。
|
||||
2. 😭易忘:当前免费证书有效期仅90天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
|
||||
<div align="center">
|
||||
|
||||
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
|
||||
中文 | [English](README_EN.md)
|
||||
|
||||
1. 操作简单:自动申请、部署 SSL 证书,并在证书即将过期时自动续期,无需人工干预。
|
||||
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行审计。
|
||||
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
|
||||
</div>
|
||||
|
||||
> [!WARNING]
|
||||
> 当前分支为 `next`,是 v0.3.x 的开发分支,目前还没有稳定,请勿在生产环境中使用。
|
||||
>
|
||||
> 如需访问之前的版本,请切换至 `main` 分支。
|
||||
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问[https://docs.certimate.me](https://docs.certimate.me)
|
||||
---
|
||||
|
||||
- [🔒Certimate](#certimate)
|
||||
- [一、安装](#一安装)
|
||||
- [1. 二进制文件](#1-二进制文件)
|
||||
- [2. Docker 安装](#2-docker-安装)
|
||||
- [3. 源代码安装](#3-源代码安装)
|
||||
- [二、使用](#二使用)
|
||||
- [三、支持的服务商列表](#三支持的服务商列表)
|
||||
- [四、系统截图](#四系统截图)
|
||||
- [五、概念](#五概念)
|
||||
- [1. 域名](#1-域名)
|
||||
- [2. dns 服务商授权信息](#2-dns-服务商授权信息)
|
||||
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
|
||||
- [六、常见问题](#六常见问题)
|
||||
- [七、许可证](#七许可证)
|
||||
## 🚩 项目简介
|
||||
|
||||
做个人产品或者在中小企业里负责运维的同学,会遇到要管理多个域名的情况,需要给域名申请证书。但是手动申请证书有以下缺点:
|
||||
|
||||
- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,犹其是你有多个域名需要维护的时候。
|
||||
- 😭 易忘:另外当前免费证书的有效期只有 90 天,这就要求你定期的操作,增加了工作量的同时,你也很容易忘掉续期,从而导致网站访问不了。
|
||||
|
||||
## 一、安装
|
||||
Certimate 就是为了解决上述问题而产生的,它具有以下优势:
|
||||
|
||||
安装 Certimate 非常简单,你可以选择以下方式之一进行安装:
|
||||
- **本地部署**:一键安装,只需要下载二进制文件,然后直接运行即可。同时也支持 Docker 部署、源代码部署等方式。
|
||||
- **数据安全**:由于是私有部署,所有数据均存储在自己的服务器上,不会经过第三方,确保数据的隐私和安全。
|
||||
- **操作简单**:简单配置即可轻松申请 SSL 证书并部署到指定的目标上,在证书即将过期前自动续期,从申请证书到使用证书完全自动化,无需人工操作。
|
||||
|
||||
### 1. 二进制文件
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。
|
||||
|
||||
你可以直接从[Releases 页](https://github.com/usual2970/certimate/releases)下载预先编译好的二进制文件,解压后执行:
|
||||
## 💡 功能特性
|
||||
|
||||
- 灵活的工作流编排方式,证书从申请到部署完全自动化;
|
||||
- 支持泛域名、多域名证书,可选 RSA、ECC 签名算法;
|
||||
- 支持 20+ 域名托管商(如阿里云、腾讯云、Cloudflare 等);
|
||||
- 支持 50+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等);
|
||||
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
|
||||
- 支持 Let's Encrypt、ZeroSSL、Google Trust Services 等多种 ACME 证书颁发机构;
|
||||
- 更多特性等待探索。
|
||||
|
||||
## ⏱️ 快速启动
|
||||
|
||||
**5 分钟部署 Certimate!**
|
||||
|
||||
以二进制部署为例,从 [GitHub Releases](https://github.com/usual2970/certimate/releases) 页面下载预先编译好的二进制可执行文件压缩包,解压缩后在终端中执行:
|
||||
|
||||
```bash
|
||||
./certimate serve
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> MacOS 在执行二进制文件时会提示:无法打开“certimate”,因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
|
||||
浏览器中访问 `http://127.0.0.1:8090`。
|
||||
|
||||
初始的管理员账号及密码:
|
||||
|
||||
### 2. Docker 安装
|
||||
- 账号:`admin@certimate.fun`
|
||||
- 密码:`1234567890`
|
||||
|
||||
```bash
|
||||
即刻使用 Certimate。
|
||||
|
||||
git clone git@github.com:usual2970/certimate.git && cd certimate/docker && docker compose up -d
|
||||
如何使用 Docker 或其他部署方式请参考文档。
|
||||
|
||||
```
|
||||
## 📄 技术文档
|
||||
|
||||
### 3. 源代码安装
|
||||
请访问 [docs.certimate.me](https://docs.certimate.me/) 以阅读技术文档。
|
||||
|
||||
```bash
|
||||
git clone EMAIL:usual2970/certimate.git
|
||||
cd certimate
|
||||
go run main.go serve
|
||||
```
|
||||
相关文章:
|
||||
|
||||
- [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0)
|
||||
- [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
|
||||
## 二、使用
|
||||
## ⭐ 运行界面
|
||||
|
||||
执行完上述安装操作后,在浏览器中访问 `http://127.0.0.1:8090` 即可访问 Certimate 管理页面。
|
||||
[](https://www.bilibili.com/video/BV1xockeZEm2)
|
||||
|
||||
```bash
|
||||
用户名:admin@certimate.fun
|
||||
密码:1234567890
|
||||
```
|
||||
## 🤝 参与贡献
|
||||
|
||||

|
||||
Certimate 是一个免费且开源的项目,采用 [MIT License](./LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
|
||||
|
||||
## 三、支持的服务商列表
|
||||
你可以通过以下方式来支持 Certimate 的开发:
|
||||
|
||||
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|
||||
|------|------|-----|------|
|
||||
| 阿里云| 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
|
||||
| 腾讯云| 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
|
||||
| 七牛云| 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|
||||
|CloudFlare| 是 | 否 | 支持 CloudFlare 注册的域名,CloudFlare 服务自带SSL证书 |
|
||||
|SSH| 否 | 是 | 支持部署到 SSH 服务器 |
|
||||
|WEBHOOK| 否 | 是 | 支持回调到 WEBHOOK |
|
||||
- 提交代码:如果你发现了 Bug 或有新的功能需求,而你又有相关经验,可以[提交代码](CONTRIBUTING.md)给我们。
|
||||
- 提交 Issue:功能建议或者 Bug 可以[提交 Issue](https://github.com/usual2970/certimate/issues) 给我们。
|
||||
|
||||
支持更多提供商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家参与贡献。
|
||||
|
||||
## ⛔ 免责声明
|
||||
|
||||
Certimate 基于 [MIT License](https://opensource.org/licenses/MIT) 发布,完全免费提供,旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任,包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
|
||||
|
||||
## 四、系统截图
|
||||
**无任何保证**:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
|
||||
|
||||

|
||||
**用户责任**:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
|
||||
|
||||

|
||||
## 🌐 加入社群
|
||||
|
||||

|
||||
- [Telegram](https://t.me/+ZXphsppxUg41YmVl)
|
||||
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
|
||||
|
||||

|
||||
<img src="https://i.imgur.com/8xwsLTA.png" width="240"/>
|
||||
|
||||

|
||||
## 🚀 Star 趋势图
|
||||
|
||||
|
||||
## 五、概念
|
||||
|
||||
Certimate 的工作流程如下:
|
||||
|
||||
* 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
|
||||
* Certimate 向证书场商的 API 发起申请请求,获取 SSL 证书。
|
||||
* Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
|
||||
* Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
|
||||
|
||||
这就涉及域名、dns 服务商的授权信息、部署服务商的授权信息等。
|
||||
|
||||
### 1. 域名
|
||||
|
||||
就是要申请证书的域名。
|
||||
|
||||
### 2. dns 服务商授权信息
|
||||
|
||||
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 记录。
|
||||
|
||||
Certimate 会自动添加一个 TXT 记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
|
||||
|
||||
比如你在阿里云购买的域名,授权信息如下:
|
||||
|
||||
```bash
|
||||
accessKeyId: xxx
|
||||
accessKeySecret: TOKEN
|
||||
```
|
||||
|
||||
在腾讯云购买的域名,授权信息如下:
|
||||
|
||||
```bash
|
||||
secretId: xxx
|
||||
secretKey: TOKEN
|
||||
```
|
||||
|
||||
### 3. 部署服务商授权信息
|
||||
|
||||
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN 这时你需要填写阿里云的授权信息。Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
|
||||
|
||||
部署服务商授权信息和 dns 服务商授权信息一致,区别在于 dns 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
|
||||
Q: 提供saas服务吗?
|
||||
|
||||
> A: 不提供,目前仅支持self-hosted(私有部署)。
|
||||
|
||||
Q: 数据安全?
|
||||
|
||||
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外Certimate源码也开源,二进制包及Docker镜像打包过程全部使用Github actions进行,过程透明可见,可自行审计。
|
||||
|
||||
Q: 自动续期证书?
|
||||
|
||||
> A: 已经申请的证书会在过期前10天自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
|
||||
|
||||
|
||||
|
||||
## 七、许可证
|
||||
|
||||
Certimate 采用 MIT 许可证,详情请查看 [LICENSE](LICENSE.md) 文件。
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
|
||||
112
README_EN.md
Normal file
112
README_EN.md
Normal file
@@ -0,0 +1,112 @@
|
||||
<h1 align="center">🔒 Certimate</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/usual2970/certimate)
|
||||
[](https://github.com/usual2970/certimate)
|
||||
[](https://hub.docker.com/r/usual2970/certimate)
|
||||
[](https://github.com/usual2970/certimate/releases)
|
||||
[](https://mit-license.org/)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[中文](README.md) | English
|
||||
|
||||
</div>
|
||||
|
||||
> [!WARNING]
|
||||
> The current branch is `next`, which is the development branch for v0.3.x. It is currently unstable and should not be used in production environments.
|
||||
>
|
||||
> To access the previous versions, please switch to the `main` branch.
|
||||
|
||||
---
|
||||
|
||||
## 🚩 Introduction
|
||||
|
||||
For individuals managing personal projects or those responsible for IT operations in small businesses who need to manage multiple domain names, applying for certificates manually comes with several drawbacks:
|
||||
|
||||
- 😱 Troublesome: Applying for and deploying certificates isn’t difficult, but it can be quite a hassle, especially when managing multiple domains.
|
||||
- 😭 Easily forgotten: The current free certificate has a validity period of only 90 days, requiring regular renewal operations. This increases the workload and makes it easy to forget, which can result in the website becoming inaccessible.
|
||||
|
||||
Certimate was created to solve the above-mentioned issues and has the following advantages:
|
||||
|
||||
- **Local Deployment**: Simply to install, download the binary and run it directly. Supports Docker deployment and source code deployment for added flexibility.
|
||||
- **Data Security**: With private deployment, all data is stored on your own servers, ensuring it never resides on third-party systems and maintaining full control over your data.
|
||||
- **Easy Operation**: Effortlessly apply and deploy SSL certificates with minimal configuration. The system automatically renews certificates before expiration, providing a fully automated workflow, no manual intervention required.
|
||||
|
||||
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution.
|
||||
|
||||
## 💡 Features
|
||||
|
||||
- Flexible workflow orchestration, fully automated from certificate application to deployment;
|
||||
- Supports wildcard, multi-domain certificates, with options for RSA or ECC.
|
||||
- Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc.);
|
||||
- Supports more than 50+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc.);
|
||||
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
|
||||
- Supports multiple certificate authorities including Let's Encrypt, ZeroSSL, Google Trust Services, and more;
|
||||
- More features waiting to be discovered.
|
||||
|
||||
## ⏱️ Fast Track
|
||||
|
||||
**Deploy Certimate in 5 minutes!**
|
||||
|
||||
Download the archived package of precompiled binary files directly from [GitHub Releases](https://github.com/usual2970/certimate/releases), extract and then execute:
|
||||
|
||||
```bash
|
||||
./certimate serve
|
||||
```
|
||||
|
||||
Visit `http://127.0.0.1:8090` in your browser.
|
||||
|
||||
Initial administrator account:
|
||||
|
||||
- Username: `admin@certimate.fun`
|
||||
- Password: `1234567890`
|
||||
|
||||
Work with Certimate right now. Or read other content in the documentation to learn more.
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
Please visit [docs.certimate.me](https://docs.certimate.me/en/).
|
||||
|
||||
Related articles:
|
||||
|
||||
- [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0)
|
||||
- [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
|
||||
## ⭐ Screenshot
|
||||
|
||||
[](https://www.youtube.com/watch?v=am_yzdfyNOE)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Certimate is a free and open-source project, licensed under the [MIT License](./LICENSE.md). You can use it for anything you want, even offering it as a paid service to users.
|
||||
|
||||
You can support the development of Certimate in the following ways:
|
||||
|
||||
- **Submit Code**: If you find a bug or have new feature requests, and you have relevant experience, [you can submit code to us](CONTRIBUTING_EN.md).
|
||||
- **Submit an Issue**: For feature suggestions or bugs, you can [submit an issue](https://github.com/usual2970/certimate/issues) to us.
|
||||
|
||||
Support for more service providers, UI enhancements, bug fixes, and documentation improvements are all welcome. We encourage everyone to contribute.
|
||||
|
||||
## ⛔ Disclaimer
|
||||
|
||||
This software is provided under the [MIT License](https://opensource.org/licenses/MIT) and distributed “as-is” without any warranty of any kind. The authors and contributors are not responsible for any damages or losses resulting from the use or inability to use this software, including but not limited to data loss, business interruption, or any other potential harm.
|
||||
|
||||
**No Warranties**: This software comes without any express or implied warranties, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement.
|
||||
|
||||
**User Responsibilities**: By using this software, you agree to take full responsibility for any outcomes resulting from its use.
|
||||
|
||||
## 🌐 Join the Community
|
||||
|
||||
- [Telegram](https://t.me/+ZXphsppxUg41YmVl)
|
||||
- Wechat Group
|
||||
|
||||
<img src="https://i.imgur.com/zSHEoIm.png" width="240"/>
|
||||
|
||||
## 🚀 Star History
|
||||
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
@@ -1,10 +1,10 @@
|
||||
version: '3.0'
|
||||
version: "3.0"
|
||||
services:
|
||||
certimate:
|
||||
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
|
||||
container_name: certimate_server
|
||||
ports:
|
||||
certimate:
|
||||
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
|
||||
container_name: certimate_server
|
||||
ports:
|
||||
- 8090:8090
|
||||
volumes:
|
||||
volumes:
|
||||
- ./data:/app/pb_data
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
|
||||
288
go.mod
288
go.mod
@@ -1,133 +1,223 @@
|
||||
module certimate
|
||||
module github.com/usual2970/certimate
|
||||
|
||||
go 1.22
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.22.5
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0
|
||||
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8
|
||||
github.com/alibabacloud-go/tea v1.2.1
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5
|
||||
github.com/go-acme/lego/v4 v4.17.4
|
||||
github.com/gojek/heimdall/v7 v7.0.3
|
||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pocketbase/dbx v1.10.1
|
||||
github.com/pocketbase/pocketbase v0.22.18
|
||||
github.com/qiniu/go-sdk/v7 v7.22.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992
|
||||
golang.org/x/crypto v0.26.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
|
||||
github.com/alibabacloud-go/alb-20200616/v2 v2.2.7
|
||||
github.com/alibabacloud-go/cas-20200407/v3 v3.0.4
|
||||
github.com/alibabacloud-go/cdn-20180510/v5 v5.2.2
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
|
||||
github.com/alibabacloud-go/esa-20240910/v2 v2.13.0
|
||||
github.com/alibabacloud-go/live-20161101 v1.1.1
|
||||
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3
|
||||
github.com/alibabacloud-go/slb-20140515/v4 v4.0.10
|
||||
github.com/alibabacloud-go/tea v1.2.2
|
||||
github.com/alibabacloud-go/waf-openapi-20211001/v5 v5.0.4
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/aws/aws-sdk-go-v2/service/acm v1.30.18
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10
|
||||
github.com/baidubce/bce-sdk-go v0.9.217
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.41
|
||||
github.com/go-acme/lego/v4 v4.22.2
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.136
|
||||
github.com/nikoksr/notify v1.3.0
|
||||
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
|
||||
github.com/pkg/sftp v1.13.7
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.25.4
|
||||
github.com/povsister/scp v0.0.0-20240802064259-28781e87b246
|
||||
github.com/qiniu/go-sdk/v7 v7.25.2
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1096
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1096
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1102
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1096
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1096
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1096
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.31
|
||||
github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.9
|
||||
github.com/volcengine/volc-sdk-golang v1.0.195
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.180
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
k8s.io/api v0.32.1
|
||||
k8s.io/apimachinery v0.32.1
|
||||
k8s.io/client-go v0.32.1
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0
|
||||
)
|
||||
|
||||
require github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
|
||||
github.com/G-Core/gcorelabscdn-go v1.0.26 // indirect
|
||||
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-rpc v1.1.3 // indirect
|
||||
github.com/alibabacloud-go/tea-rpc-utils v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/alibabacloud-go/vod-20170321 v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/vod-20170321/v4 v4.6.1 // indirect
|
||||
github.com/avast/retry-go v3.0.0+incompatible // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-lark/lark v1.15.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.16.0 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/jdcloud-api/jdcloud-sdk-go v1.62.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/qiniu/dyn v1.3.0 // indirect
|
||||
github.com/qiniu/x v1.10.5 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.0.1102 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.0.1099 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 // indirect
|
||||
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.2 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.3 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/dcdn-20180115/v3 v3.5.0
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 // indirect
|
||||
github.com/aliyun/credentials-go v1.3.1 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.83 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.3 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.1
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.6.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.114.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1084 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.37.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/term v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
google.golang.org/api v0.189.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gocloud.dev v0.40.0 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.220.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect
|
||||
google.golang.org/grpc v1.70.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.31.1 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
modernc.org/libc v1.61.11 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
modernc.org/sqlite v1.34.5 // indirect
|
||||
)
|
||||
|
||||
replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0
|
||||
|
||||
replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1
|
||||
|
||||
32
internal/app/app.go
Normal file
32
internal/app/app.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
var instance core.App
|
||||
|
||||
var intanceOnce sync.Once
|
||||
|
||||
func GetApp() core.App {
|
||||
intanceOnce.Do(func() {
|
||||
instance = pocketbase.NewWithConfig(pocketbase.Config{
|
||||
HideStartBanner: true,
|
||||
})
|
||||
})
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
func GetDB() dbx.Builder {
|
||||
return GetApp().DB()
|
||||
}
|
||||
|
||||
func GetLogger() *slog.Logger {
|
||||
return GetApp().Logger()
|
||||
}
|
||||
@@ -2,17 +2,24 @@ package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
)
|
||||
|
||||
var schedulerOnce sync.Once
|
||||
|
||||
var scheduler *cron.Cron
|
||||
|
||||
var schedulerOnce sync.Once
|
||||
|
||||
func GetScheduler() *cron.Cron {
|
||||
scheduler = GetApp().Cron()
|
||||
schedulerOnce.Do(func() {
|
||||
scheduler = cron.New()
|
||||
location, err := time.LoadLocation("Local")
|
||||
if err == nil {
|
||||
scheduler.Stop()
|
||||
scheduler.SetTimezone(location)
|
||||
scheduler.Start()
|
||||
}
|
||||
})
|
||||
|
||||
return scheduler
|
||||
38
internal/applicant/acme_ca.go
Normal file
38
internal/applicant/acme_ca.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package applicant
|
||||
|
||||
const (
|
||||
sslProviderLetsEncrypt = "letsencrypt"
|
||||
sslProviderLetsEncryptStaging = "letsencrypt_staging"
|
||||
sslProviderZeroSSL = "zerossl"
|
||||
sslProviderGoogleTrustServices = "gts"
|
||||
)
|
||||
const defaultSSLProvider = sslProviderLetsEncrypt
|
||||
|
||||
const (
|
||||
letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
letsencryptStagingUrl = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
zerosslUrl = "https://acme.zerossl.com/v2/DV90"
|
||||
gtsUrl = "https://dv.acme-v02.api.pki.goog/directory"
|
||||
)
|
||||
|
||||
var sslProviderUrls = map[string]string{
|
||||
sslProviderLetsEncrypt: letsencryptUrl,
|
||||
sslProviderLetsEncryptStaging: letsencryptStagingUrl,
|
||||
sslProviderZeroSSL: zerosslUrl,
|
||||
sslProviderGoogleTrustServices: gtsUrl,
|
||||
}
|
||||
|
||||
type acmeSSLProviderConfig struct {
|
||||
Config acmeSSLProviderConfigContent `json:"config"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
type acmeSSLProviderConfigContent struct {
|
||||
ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"`
|
||||
GoogleTrustServices acmeSSLProviderEabConfig `json:"gts"`
|
||||
}
|
||||
|
||||
type acmeSSLProviderEabConfig struct {
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
EabKid string `json:"eabKid"`
|
||||
}
|
||||
140
internal/applicant/acme_user.go
Normal file
140
internal/applicant/acme_user.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certs"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
)
|
||||
|
||||
type acmeUser struct {
|
||||
CA string
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
|
||||
privkey string
|
||||
}
|
||||
|
||||
func newAcmeUser(ca, email string) (*acmeUser, error) {
|
||||
repo := repository.NewAcmeAccountRepository()
|
||||
|
||||
applyUser := &acmeUser{
|
||||
CA: ca,
|
||||
Email: email,
|
||||
}
|
||||
|
||||
acmeAccount, err := repo.GetByCAAndEmail(ca, email)
|
||||
if err != nil {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyPEM, err := certs.ConvertECPrivateKeyToPEM(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
applyUser.privkey = keyPEM
|
||||
return applyUser, nil
|
||||
}
|
||||
|
||||
applyUser.Registration = acmeAccount.Resource
|
||||
applyUser.privkey = acmeAccount.Key
|
||||
|
||||
return applyUser, nil
|
||||
}
|
||||
|
||||
func (u *acmeUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u acmeUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
func (u *acmeUser) GetPrivateKey() crypto.PrivateKey {
|
||||
rs, _ := certs.ParseECPrivateKeyFromPEM(u.privkey)
|
||||
return rs
|
||||
}
|
||||
|
||||
func (u *acmeUser) hasRegistration() bool {
|
||||
return u.Registration != nil
|
||||
}
|
||||
|
||||
func (u *acmeUser) getPrivateKeyPEM() string {
|
||||
return u.privkey
|
||||
}
|
||||
|
||||
type acmeAccountRepository interface {
|
||||
GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error)
|
||||
Save(ca, email, key string, resource *registration.Resource) error
|
||||
}
|
||||
|
||||
var registerGroup singleflight.Group
|
||||
|
||||
func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) {
|
||||
resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) {
|
||||
return registerAcmeUser(client, sslProviderConfig, user)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.(*registration.Resource), nil
|
||||
}
|
||||
|
||||
func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) {
|
||||
var reg *registration.Resource
|
||||
var err error
|
||||
switch sslProviderConfig.Provider {
|
||||
case sslProviderZeroSSL:
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: sslProviderConfig.Config.ZeroSSL.EabKid,
|
||||
HmacEncoded: sslProviderConfig.Config.ZeroSSL.EabHmacKey,
|
||||
})
|
||||
case sslProviderGoogleTrustServices:
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid,
|
||||
HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey,
|
||||
})
|
||||
case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
default:
|
||||
err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := repository.NewAcmeAccountRepository()
|
||||
resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail())
|
||||
if err == nil {
|
||||
user.privkey = resp.Key
|
||||
return resp.Resource, nil
|
||||
}
|
||||
|
||||
if _, err := repo.Save(context.Background(), &domain.AcmeAccount{
|
||||
CA: sslProviderConfig.Provider,
|
||||
Email: user.GetEmail(),
|
||||
Key: user.getPrivateKeyPEM(),
|
||||
Resource: reg,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to save registration: %w", err)
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
)
|
||||
|
||||
type aliyun struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewAliyun(option *ApplyOption) Applicant {
|
||||
return &aliyun{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aliyun) Apply() (*Certificate, error) {
|
||||
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
||||
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
|
||||
dnsProvider, err := alidns.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
@@ -1,131 +1,233 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
uslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
configTypeTencent = "tencent"
|
||||
configTypeAliyun = "aliyun"
|
||||
configTypeCloudflare = "cloudflare"
|
||||
configTypeNamesilo = "namesilo"
|
||||
)
|
||||
|
||||
type Certificate struct {
|
||||
CertUrl string `json:"certUrl"`
|
||||
CertStableUrl string `json:"certStableUrl"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Certificate string `json:"certificate"`
|
||||
IssuerCertificate string `json:"issuerCertificate"`
|
||||
Csr string `json:"csr"`
|
||||
}
|
||||
|
||||
type ApplyOption struct {
|
||||
Email string `json:"email"`
|
||||
Domain string `json:"domain"`
|
||||
Access string `json:"access"`
|
||||
}
|
||||
|
||||
type MyUser struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
func (u *MyUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
func (u MyUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
|
||||
return u.key
|
||||
type ApplyCertResult struct {
|
||||
CertificateFullChain string
|
||||
IssuerCertificate string
|
||||
PrivateKey string
|
||||
ACMEAccountUrl string
|
||||
ACMECertUrl string
|
||||
ACMECertStableUrl string
|
||||
CSR string
|
||||
}
|
||||
|
||||
type Applicant interface {
|
||||
Apply() (*Certificate, error)
|
||||
Apply() (*ApplyCertResult, error)
|
||||
}
|
||||
|
||||
func Get(record *models.Record) (Applicant, error) {
|
||||
access := record.ExpandedOne("access")
|
||||
option := &ApplyOption{
|
||||
Email: "536464346@qq.com",
|
||||
Domain: record.GetString("domain"),
|
||||
Access: access.GetString("config"),
|
||||
}
|
||||
switch access.GetString("configType") {
|
||||
case configTypeTencent:
|
||||
return NewTencent(option), nil
|
||||
case configTypeAliyun:
|
||||
return NewAliyun(option), nil
|
||||
case configTypeCloudflare:
|
||||
return NewCloudflare(option), nil
|
||||
case configTypeNamesilo:
|
||||
return NewNamesilo(option), nil
|
||||
default:
|
||||
return nil, errors.New("unknown config type")
|
||||
}
|
||||
|
||||
type applicantOptions struct {
|
||||
Domains []string
|
||||
ContactEmail string
|
||||
Provider domain.ApplyDNSProviderType
|
||||
ProviderAccessConfig map[string]any
|
||||
ProviderApplyConfig map[string]any
|
||||
KeyAlgorithm string
|
||||
Nameservers []string
|
||||
DnsPropagationTimeout int32
|
||||
DnsTTL int32
|
||||
DisableFollowCNAME bool
|
||||
ReplacedARIAcctId string
|
||||
ReplacedARICertId string
|
||||
}
|
||||
|
||||
func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, error) {
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
|
||||
if node.Type != domain.WorkflowNodeTypeApply {
|
||||
return nil, fmt.Errorf("node type is not apply")
|
||||
}
|
||||
|
||||
nodeConfig := node.GetConfigForApply()
|
||||
options := &applicantOptions{
|
||||
Domains: uslices.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
|
||||
ContactEmail: nodeConfig.ContactEmail,
|
||||
Provider: domain.ApplyDNSProviderType(nodeConfig.Provider),
|
||||
ProviderApplyConfig: nodeConfig.ProviderConfig,
|
||||
KeyAlgorithm: nodeConfig.KeyAlgorithm,
|
||||
Nameservers: uslices.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
|
||||
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
|
||||
DnsTTL: nodeConfig.DnsTTL,
|
||||
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
|
||||
}
|
||||
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
|
||||
} else {
|
||||
accessConfig, err := access.UnmarshalConfigToMap()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
options.ProviderAccessConfig = accessConfig
|
||||
}
|
||||
|
||||
certRepo := repository.NewCertificateRepository()
|
||||
lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), node.Id)
|
||||
if lastCertificate != nil {
|
||||
newCertSan := slices.Clone(options.Domains)
|
||||
oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";")
|
||||
slices.Sort(newCertSan)
|
||||
slices.Sort(oldCertSan)
|
||||
|
||||
if slices.Equal(newCertSan, oldCertSan) {
|
||||
lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
|
||||
if lastCertX509 != nil {
|
||||
replacedARICertId, _ := certificate.MakeARICertID(lastCertX509)
|
||||
options.ReplacedARIAcctId = lastCertificate.ACMEAccountUrl
|
||||
options.ReplacedARICertId = replacedARICertId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applicant, err := createApplicant(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
myUser := MyUser{
|
||||
Email: option.Email,
|
||||
key: privateKey,
|
||||
return &proxyApplicant{
|
||||
applicant: applicant,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) {
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
|
||||
|
||||
sslProviderConfig := &acmeSSLProviderConfig{
|
||||
Config: acmeSSLProviderConfigContent{},
|
||||
Provider: defaultSSLProvider,
|
||||
}
|
||||
if settings != nil {
|
||||
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
config := lego.NewConfig(&myUser)
|
||||
if sslProviderConfig.Provider == "" {
|
||||
sslProviderConfig.Provider = defaultSSLProvider
|
||||
}
|
||||
|
||||
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
|
||||
config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// A client facilitates communication with the CA server.
|
||||
// Some unified lego environment variables are configured here.
|
||||
// link: https://github.com/go-acme/lego/issues/1867
|
||||
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME))
|
||||
|
||||
// Create an ACME client config
|
||||
config := lego.NewConfig(acmeUser)
|
||||
config.CADirURL = sslProviderUrls[sslProviderConfig.Provider]
|
||||
config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
|
||||
|
||||
// Create an ACME client
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.Challenge.SetDNS01Provider(provider)
|
||||
|
||||
// New users will need to register
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Set the DNS01 challenge provider
|
||||
challengeOptions := make([]dns01.ChallengeOption, 0)
|
||||
if len(options.Nameservers) > 0 {
|
||||
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(dns01.ParseNameservers(options.Nameservers)))
|
||||
challengeOptions = append(challengeOptions, dns01.DisableAuthoritativeNssPropagationRequirement())
|
||||
}
|
||||
myUser.Registration = reg
|
||||
client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...)
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{option.Domain},
|
||||
// New users need to register first
|
||||
if !acmeUser.hasRegistration() {
|
||||
reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register: %w", err)
|
||||
}
|
||||
acmeUser.Registration = reg
|
||||
}
|
||||
|
||||
// Obtain a certificate
|
||||
certRequest := certificate.ObtainRequest{
|
||||
Domains: options.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != acmeUser.Registration.URI {
|
||||
certRequest.ReplacesCertID = options.ReplacedARICertId
|
||||
}
|
||||
certResource, err := client.Certificate.Obtain(certRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Certificate{
|
||||
CertUrl: certificates.CertURL,
|
||||
CertStableUrl: certificates.CertStableURL,
|
||||
PrivateKey: string(certificates.PrivateKey),
|
||||
Certificate: string(certificates.Certificate),
|
||||
IssuerCertificate: string(certificates.IssuerCertificate),
|
||||
Csr: string(certificates.CSR),
|
||||
return &ApplyCertResult{
|
||||
CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)),
|
||||
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
|
||||
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
|
||||
ACMEAccountUrl: acmeUser.Registration.URI,
|
||||
ACMECertUrl: certResource.CertURL,
|
||||
ACMECertStableUrl: certResource.CertStableURL,
|
||||
CSR: strings.TrimSpace(string(certResource.CSR)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyType {
|
||||
switch algo {
|
||||
case domain.CertificateKeyAlgorithmTypeRSA2048:
|
||||
return certcrypto.RSA2048
|
||||
case domain.CertificateKeyAlgorithmTypeRSA3072:
|
||||
return certcrypto.RSA3072
|
||||
case domain.CertificateKeyAlgorithmTypeRSA4096:
|
||||
return certcrypto.RSA4096
|
||||
case domain.CertificateKeyAlgorithmTypeRSA8192:
|
||||
return certcrypto.RSA8192
|
||||
case domain.CertificateKeyAlgorithmTypeEC256:
|
||||
return certcrypto.EC256
|
||||
case domain.CertificateKeyAlgorithmTypeEC384:
|
||||
return certcrypto.EC384
|
||||
}
|
||||
|
||||
return certcrypto.RSA2048
|
||||
}
|
||||
|
||||
// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑
|
||||
type proxyApplicant struct {
|
||||
applicant challenge.Provider
|
||||
options *applicantOptions
|
||||
}
|
||||
|
||||
var limiters sync.Map
|
||||
|
||||
const (
|
||||
limitBurst = 300
|
||||
limitRate float64 = float64(1) / float64(36)
|
||||
)
|
||||
|
||||
func getLimiter(key string) *rate.Limiter {
|
||||
limiter, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Limit(limitRate), 300))
|
||||
return limiter.(*rate.Limiter)
|
||||
}
|
||||
|
||||
func (d *proxyApplicant) Apply() (*ApplyCertResult, error) {
|
||||
limiter := getLimiter(fmt.Sprintf("apply_%s", d.options.ContactEmail))
|
||||
limiter.Wait(context.Background())
|
||||
return apply(d.applicant, d.options)
|
||||
}
|
||||
|
||||
44
internal/applicant/applicant_test.go
Normal file
44
internal/applicant/applicant_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package applicant_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
burst int
|
||||
rate rate.Limit
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
burst: 300,
|
||||
rate: rate.Limit(float64(1) / float64(20)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rl := rate.NewLimiter(tt.rate, tt.burst)
|
||||
if rl.Burst() != tt.burst {
|
||||
t.Errorf("Burst() = %v, want %v", rl.Burst(), tt.burst)
|
||||
}
|
||||
if rl.Limit() != tt.rate {
|
||||
t.Errorf("Limit() = %v, want %v", rl.Limit(), tt.rate)
|
||||
}
|
||||
|
||||
t.Log("consume all tokens at once", rl.AllowN(time.Now(), tt.burst))
|
||||
|
||||
t.Log("consume more", rl.Allow())
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
t.Log("consume after 5 seconds", rl.Allow())
|
||||
|
||||
time.Sleep(time.Second * 20)
|
||||
t.Log("consume after 20 seconds", rl.Allow())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
)
|
||||
|
||||
type cloudflare struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewCloudflare(option *ApplyOption) Applicant {
|
||||
return &cloudflare{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cloudflare) Apply() (*Certificate, error) {
|
||||
access := &domain.CloudflareAccess{}
|
||||
json.Unmarshal([]byte(c.option.Access), access)
|
||||
|
||||
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
||||
|
||||
provider, err := cf.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(c.option, provider)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
)
|
||||
|
||||
type namesilo struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewNamesilo(option *ApplyOption) Applicant {
|
||||
return &namesilo{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *namesilo) Apply() (*Certificate, error) {
|
||||
|
||||
access := &domain.NameSiloAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
||||
|
||||
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(a.option, dnsProvider)
|
||||
}
|
||||
413
internal/applicant/providers.go
Normal file
413
internal/applicant/providers.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
pACMEHttpReq "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/acmehttpreq"
|
||||
pAliyun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun"
|
||||
pAWSRoute53 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aws-route53"
|
||||
pAzureDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/azure-dns"
|
||||
pBaiduCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud"
|
||||
pCloudflare "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudflare"
|
||||
pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns"
|
||||
pCMCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud"
|
||||
pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla"
|
||||
pGcore "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gcore"
|
||||
pGname "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname"
|
||||
pGoDaddy "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/godaddy"
|
||||
pHuaweiCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/huaweicloud"
|
||||
pJDCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/jdcloud"
|
||||
pNamecheap "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap"
|
||||
pNameDotCom "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namedotcom"
|
||||
pNameSilo "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namesilo"
|
||||
pNS1 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ns1"
|
||||
pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns"
|
||||
pRainYun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/rainyun"
|
||||
pTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud"
|
||||
pVolcEngine "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/volcengine"
|
||||
pWestcn "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
func createApplicant(options *applicantOptions) (challenge.Provider, error) {
|
||||
/*
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
switch options.Provider {
|
||||
case domain.ApplyDNSProviderTypeACMEHttpReq:
|
||||
{
|
||||
access := domain.AccessConfigForACMEHttpReq{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pACMEHttpReq.NewChallengeProvider(&pACMEHttpReq.ChallengeProviderConfig{
|
||||
Endpoint: access.Endpoint,
|
||||
Mode: access.Mode,
|
||||
Username: access.Username,
|
||||
Password: access.Password,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeAliyun, domain.ApplyDNSProviderTypeAliyunDNS:
|
||||
{
|
||||
access := domain.AccessConfigForAliyun{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pAliyun.NewChallengeProvider(&pAliyun.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeAWS, domain.ApplyDNSProviderTypeAWSRoute53:
|
||||
{
|
||||
access := domain.AccessConfigForAWS{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderApplyConfig, "region"),
|
||||
HostedZoneId: maps.GetValueAsString(options.ProviderApplyConfig, "hostedZoneId"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeAzure, domain.ApplyDNSProviderTypeAzureDNS:
|
||||
{
|
||||
access := domain.AccessConfigForAzure{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pAzureDNS.NewChallengeProvider(&pAzureDNS.ChallengeProviderConfig{
|
||||
TenantId: access.TenantId,
|
||||
ClientId: access.ClientId,
|
||||
ClientSecret: access.ClientSecret,
|
||||
CloudName: access.CloudName,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeBaiduCloud, domain.ApplyDNSProviderTypeBaiduCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForBaiduCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pBaiduCloud.NewChallengeProvider(&pBaiduCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeCloudflare:
|
||||
{
|
||||
access := domain.AccessConfigForCloudflare{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pCloudflare.NewChallengeProvider(&pCloudflare.ChallengeProviderConfig{
|
||||
DnsApiToken: access.DnsApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeClouDNS:
|
||||
{
|
||||
access := domain.AccessConfigForClouDNS{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pClouDNS.NewChallengeProvider(&pClouDNS.ChallengeProviderConfig{
|
||||
AuthId: access.AuthId,
|
||||
AuthPassword: access.AuthPassword,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeCMCCCloud:
|
||||
{
|
||||
access := domain.AccessConfigForCMCCCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pCMCCCloud.NewChallengeProvider(&pCMCCCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeDNSLA:
|
||||
{
|
||||
access := domain.AccessConfigForDNSLA{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pDNSLA.NewChallengeProvider(&pDNSLA.ChallengeProviderConfig{
|
||||
ApiId: access.ApiId,
|
||||
ApiSecret: access.ApiSecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeGcore:
|
||||
{
|
||||
access := domain.AccessConfigForGcore{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pGcore.NewChallengeProvider(&pGcore.ChallengeProviderConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeGname:
|
||||
{
|
||||
access := domain.AccessConfigForGname{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pGname.NewChallengeProvider(&pGname.ChallengeProviderConfig{
|
||||
AppId: access.AppId,
|
||||
AppKey: access.AppKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeGoDaddy:
|
||||
{
|
||||
access := domain.AccessConfigForGoDaddy{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pGoDaddy.NewChallengeProvider(&pGoDaddy.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
ApiSecret: access.ApiSecret,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeHuaweiCloud, domain.ApplyDNSProviderTypeHuaweiCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForHuaweiCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderApplyConfig, "region"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeJDCloud, domain.ApplyDNSProviderTypeJDCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForJDCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
RegionId: maps.GetValueAsString(options.ProviderApplyConfig, "region_id"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeNamecheap:
|
||||
{
|
||||
access := domain.AccessConfigForNamecheap{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNamecheap.NewChallengeProvider(&pNamecheap.ChallengeProviderConfig{
|
||||
Username: access.Username,
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeNameDotCom:
|
||||
{
|
||||
access := domain.AccessConfigForNameDotCom{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNameDotCom.NewChallengeProvider(&pNameDotCom.ChallengeProviderConfig{
|
||||
Username: access.Username,
|
||||
ApiToken: access.ApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeNameSilo:
|
||||
{
|
||||
access := domain.AccessConfigForNameSilo{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNameSilo.NewChallengeProvider(&pNameSilo.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeNS1:
|
||||
{
|
||||
access := domain.AccessConfigForNS1{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pNS1.NewChallengeProvider(&pNS1.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypePowerDNS:
|
||||
{
|
||||
access := domain.AccessConfigForPowerDNS{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pPowerDNS.NewChallengeProvider(&pPowerDNS.ChallengeProviderConfig{
|
||||
ApiUrl: access.ApiUrl,
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeRainYun:
|
||||
{
|
||||
access := domain.AccessConfigForRainYun{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pRainYun.NewChallengeProvider(&pRainYun.ChallengeProviderConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS:
|
||||
{
|
||||
access := domain.AccessConfigForTencentCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeVolcEngine, domain.ApplyDNSProviderTypeVolcEngineDNS:
|
||||
{
|
||||
access := domain.AccessConfigForVolcEngine{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pVolcEngine.NewChallengeProvider(&pVolcEngine.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
|
||||
case domain.ApplyDNSProviderTypeWestcn:
|
||||
{
|
||||
access := domain.AccessConfigForWestcn{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
applicant, err := pWestcn.NewChallengeProvider(&pWestcn.ChallengeProviderConfig{
|
||||
Username: access.Username,
|
||||
ApiPassword: access.ApiPassword,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
return applicant, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported applicant provider: %s", string(options.Provider))
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
)
|
||||
|
||||
type tencent struct {
|
||||
option *ApplyOption
|
||||
}
|
||||
|
||||
func NewTencent(option *ApplyOption) Applicant {
|
||||
return &tencent{
|
||||
option: option,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tencent) Apply() (*Certificate, error) {
|
||||
|
||||
access := &domain.TencentAccess{}
|
||||
json.Unmarshal([]byte(t.option.Access), access)
|
||||
|
||||
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
||||
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
|
||||
dnsProvider, err := tencentcloud.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apply(t.option, dnsProvider)
|
||||
}
|
||||
256
internal/certificate/service.go
Normal file
256
internal/certificate/service.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/usual2970/certimate/internal/app"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain/dtos"
|
||||
"github.com/usual2970/certimate/internal/notify"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certs"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultExpireSubject = "有 ${COUNT} 张证书即将过期"
|
||||
defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!"
|
||||
)
|
||||
|
||||
type certificateRepository interface {
|
||||
ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error)
|
||||
GetById(ctx context.Context, id string) (*domain.Certificate, error)
|
||||
}
|
||||
|
||||
type CertificateService struct {
|
||||
certRepo certificateRepository
|
||||
}
|
||||
|
||||
func NewCertificateService(certRepo certificateRepository) *CertificateService {
|
||||
return &CertificateService{
|
||||
certRepo: certRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificateService) InitSchedule(ctx context.Context) error {
|
||||
app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() {
|
||||
certificates, err := s.certRepo.ListExpireSoon(context.Background())
|
||||
if err != nil {
|
||||
app.GetLogger().Error("failed to get certificates which expire soon", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
notification := buildExpireSoonNotification(certificates)
|
||||
if notification == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := notify.SendToAllChannels(notification.Subject, notification.Message); err != nil {
|
||||
app.GetLogger().Error("failed to send notification", "err", err)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) (*dtos.CertificateArchiveFileResp, error) {
|
||||
certificate, err := s.certRepo.GetById(ctx, req.CertificateId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
zipWriter := zip.NewWriter(&buf)
|
||||
defer zipWriter.Close()
|
||||
|
||||
resp := &dtos.CertificateArchiveFileResp{
|
||||
FileFormat: "zip",
|
||||
}
|
||||
|
||||
switch strings.ToUpper(req.Format) {
|
||||
case "", "PEM":
|
||||
{
|
||||
certWriter, err := zipWriter.Create("certbundle.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = certWriter.Write([]byte(certificate.Certificate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyWriter, err := zipWriter.Create("privkey.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = keyWriter.Write([]byte(certificate.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.FileBytes = buf.Bytes()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
case "PFX":
|
||||
{
|
||||
const pfxPassword = "certimate"
|
||||
|
||||
certPFX, err := certs.TransformCertificateFromPEMToPFX(certificate.Certificate, certificate.PrivateKey, pfxPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certWriter, err := zipWriter.Create("cert.pfx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = certWriter.Write(certPFX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyWriter, err := zipWriter.Create("pfx-password.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = keyWriter.Write([]byte(pfxPassword))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.FileBytes = buf.Bytes()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
case "JKS":
|
||||
{
|
||||
const jksPassword = "certimate"
|
||||
|
||||
certJKS, err := certs.TransformCertificateFromPEMToJKS(certificate.Certificate, certificate.PrivateKey, jksPassword, jksPassword, jksPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certWriter, err := zipWriter.Create("cert.jks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = certWriter.Write(certJKS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyWriter, err := zipWriter.Create("jks-password.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = keyWriter.Write([]byte(jksPassword))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.FileBytes = buf.Bytes()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, domain.ErrInvalidParams
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CertificateService) ValidateCertificate(ctx context.Context, req *dtos.CertificateValidateCertificateReq) (*dtos.CertificateValidateCertificateResp, error) {
|
||||
certX509, err := certs.ParseCertificateFromPEM(req.Certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if time.Now().After(certX509.NotAfter) {
|
||||
return nil, fmt.Errorf("certificate has expired at %s", certX509.NotAfter.UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
return &dtos.CertificateValidateCertificateResp{
|
||||
IsValid: true,
|
||||
Domains: strings.Join(certX509.DNSNames, ";"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) (*dtos.CertificateValidatePrivateKeyResp, error) {
|
||||
_, err := certcrypto.ParsePEMPrivateKey([]byte(req.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dtos.CertificateValidatePrivateKeyResp{
|
||||
IsValid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildExpireSoonNotification(certificates []*domain.Certificate) *struct {
|
||||
Subject string
|
||||
Message string
|
||||
} {
|
||||
if len(certificates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
subject := defaultExpireSubject
|
||||
message := defaultExpireMessage
|
||||
|
||||
// 查询模板信息
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
settings, err := settingsRepo.GetByName(context.Background(), "notifyTemplates")
|
||||
if err == nil {
|
||||
var templates *domain.NotifyTemplatesSettingsContent
|
||||
json.Unmarshal([]byte(settings.Content), &templates)
|
||||
|
||||
if templates != nil && len(templates.NotifyTemplates) > 0 {
|
||||
subject = templates.NotifyTemplates[0].Subject
|
||||
message = templates.NotifyTemplates[0].Message
|
||||
}
|
||||
}
|
||||
|
||||
// 替换变量
|
||||
count := len(certificates)
|
||||
domains := make([]string, count)
|
||||
for i, record := range certificates {
|
||||
domains[i] = record.SubjectAltNames
|
||||
}
|
||||
countStr := strconv.Itoa(count)
|
||||
domainStr := strings.Join(domains, ";")
|
||||
subject = strings.ReplaceAll(subject, "${COUNT}", countStr)
|
||||
subject = strings.ReplaceAll(subject, "${DOMAINS}", domainStr)
|
||||
message = strings.ReplaceAll(message, "${COUNT}", countStr)
|
||||
message = strings.ReplaceAll(message, "${DOMAINS}", domainStr)
|
||||
|
||||
// 返回消息
|
||||
return &struct {
|
||||
Subject string
|
||||
Message string
|
||||
}{Subject: subject, Message: message}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cas20200407 "github.com/alibabacloud-go/cas-20200407/v2/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type aliyun struct {
|
||||
client *cas20200407.Client
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewAliyun(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
a := &aliyun{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}
|
||||
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.client = client
|
||||
return a, nil
|
||||
|
||||
}
|
||||
|
||||
func (a *aliyun) GetInfo() []string {
|
||||
return a.infos
|
||||
}
|
||||
|
||||
func (a *aliyun) Deploy(ctx context.Context) error {
|
||||
|
||||
// 查询有没有对应的资源
|
||||
resource, err := a.resource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("查询对应的资源", resource))
|
||||
|
||||
// 查询有没有对应的联系人
|
||||
contacts, err := a.contacts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("查询联系人", contacts))
|
||||
|
||||
// 上传证书
|
||||
certId, err := a.uploadCert(&a.option.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("上传证书", certId))
|
||||
|
||||
// 部署证书
|
||||
jobId, err := a.deploy(resource, certId, contacts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("创建部署证书任务", jobId))
|
||||
|
||||
// 等待部署成功
|
||||
err = a.updateDeployStatus(*jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 部署成功后删除旧的证书
|
||||
a.deleteCert(resource)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aliyun) updateDeployStatus(jobId int64) error {
|
||||
// 查询部署状态
|
||||
req := &cas20200407.UpdateDeploymentJobStatusRequest{
|
||||
JobId: tea.Int64(jobId),
|
||||
}
|
||||
|
||||
resp, err := a.client.UpdateDeploymentJobStatus(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.infos = append(a.infos, toStr("查询对应的资源", resp))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aliyun) deleteCert(resource *cas20200407.ListCloudResourcesResponseBodyData) error {
|
||||
// 查询有没有对应的资源
|
||||
if resource.CertId == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除证书
|
||||
_, err := a.client.DeleteUserCertificate(&cas20200407.DeleteUserCertificateRequest{
|
||||
CertId: resource.CertId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aliyun) contacts() ([]*cas20200407.ListContactResponseBodyContactList, error) {
|
||||
listContactRequest := &cas20200407.ListContactRequest{}
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.ListContactWithOptions(listContactRequest, runtime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body.TotalCount == nil {
|
||||
return nil, errors.New("no contact found")
|
||||
}
|
||||
|
||||
return resp.Body.ContactList, nil
|
||||
}
|
||||
|
||||
func (a *aliyun) deploy(resource *cas20200407.ListCloudResourcesResponseBodyData, certId int64, contacts []*cas20200407.ListContactResponseBodyContactList) (*int64, error) {
|
||||
contactIds := make([]string, 0, len(contacts))
|
||||
for _, contact := range contacts {
|
||||
contactIds = append(contactIds, fmt.Sprintf("%d", *contact.ContactId))
|
||||
}
|
||||
// 部署证书
|
||||
createCloudResourceRequest := &cas20200407.CreateDeploymentJobRequest{
|
||||
CertIds: tea.String(fmt.Sprintf("%d", certId)),
|
||||
Name: tea.String(a.option.Domain + rand.RandStr(6)),
|
||||
JobType: tea.String("user"),
|
||||
ResourceIds: tea.String(fmt.Sprintf("%d", *resource.Id)),
|
||||
ContactIds: tea.String(strings.Join(contactIds, ",")),
|
||||
}
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.CreateDeploymentJobWithOptions(createCloudResourceRequest, runtime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body.JobId, nil
|
||||
}
|
||||
|
||||
func (a *aliyun) uploadCert(cert *applicant.Certificate) (int64, error) {
|
||||
uploadUserCertificateRequest := &cas20200407.UploadUserCertificateRequest{
|
||||
Cert: &cert.Certificate,
|
||||
Key: &cert.PrivateKey,
|
||||
Name: tea.String(a.option.Domain + rand.RandStr(6)),
|
||||
}
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.UploadUserCertificateWithOptions(uploadUserCertificateRequest, runtime)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return *resp.Body.CertId, nil
|
||||
}
|
||||
|
||||
func (a *aliyun) createClient(accessKeyId, accessKeySecret string) (_result *cas20200407.Client, _err error) {
|
||||
config := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
config.Endpoint = tea.String("cas.aliyuncs.com")
|
||||
_result = &cas20200407.Client{}
|
||||
_result, _err = cas20200407.NewClient(config)
|
||||
return _result, _err
|
||||
}
|
||||
|
||||
func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, error) {
|
||||
|
||||
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
|
||||
CloudProduct: tea.String(a.option.Product),
|
||||
Keyword: tea.String(a.option.Domain),
|
||||
}
|
||||
|
||||
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *resp.Body.Total == 0 {
|
||||
return nil, errors.New("no resource found")
|
||||
}
|
||||
|
||||
return resp.Body.Data[0], nil
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cdn20180510 "github.com/alibabacloud-go/cdn-20180510/v5/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type AliyunCdn struct {
|
||||
client *cdn20180510.Client
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
|
||||
access := &domain.AliyunAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
a := &AliyunCdn{
|
||||
option: option,
|
||||
}
|
||||
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AliyunCdn{
|
||||
client: client,
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AliyunCdn) GetInfo() []string {
|
||||
return a.infos
|
||||
}
|
||||
|
||||
func (a *AliyunCdn) Deploy(ctx context.Context) error {
|
||||
|
||||
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
|
||||
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(a.option.Domain),
|
||||
CertName: tea.String(certName),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(a.option.Certificate.Certificate),
|
||||
SSLPri: tea.String(a.option.Certificate.PrivateKey),
|
||||
CertRegion: tea.String("cn-hangzhou"),
|
||||
}
|
||||
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
resp, err := a.client.SetCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.infos = append(a.infos, toStr("cdn设置证书", resp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AliyunCdn) createClient(accessKeyId, accessKeySecret string) (_result *cdn20180510.Client, _err error) {
|
||||
config := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
config.Endpoint = tea.String("cdn.aliyuncs.com")
|
||||
_result = &cdn20180510.Client{}
|
||||
_result, _err = cdn20180510.NewClient(config)
|
||||
return _result, _err
|
||||
}
|
||||
@@ -1,84 +1,73 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/logger"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
targetAliyunOss = "aliyun-oss"
|
||||
targetAliyunCdn = "aliyun-cdn"
|
||||
targetSSH = "ssh"
|
||||
targetWebhook = "webhook"
|
||||
targetTencentCdn = "tencent-cdn"
|
||||
targetQiniuCdn = "qiniu-cdn"
|
||||
)
|
||||
|
||||
type DeployerOption struct {
|
||||
DomainId string `json:"domainId"`
|
||||
Domain string `json:"domain"`
|
||||
Product string `json:"product"`
|
||||
Access string `json:"access"`
|
||||
Certificate applicant.Certificate `json:"certificate"`
|
||||
}
|
||||
|
||||
type Deployer interface {
|
||||
Deploy(ctx context.Context) error
|
||||
GetInfo() []string
|
||||
}
|
||||
|
||||
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
||||
access := record.ExpandedOne("targetAccess")
|
||||
option := &DeployerOption{
|
||||
DomainId: record.Id,
|
||||
Domain: record.GetString("domain"),
|
||||
Product: getProduct(record),
|
||||
Access: access.GetString("config"),
|
||||
}
|
||||
if cert != nil {
|
||||
option.Certificate = *cert
|
||||
} else {
|
||||
option.Certificate = applicant.Certificate{
|
||||
Certificate: record.GetString("certificate"),
|
||||
PrivateKey: record.GetString("privateKey"),
|
||||
}
|
||||
}
|
||||
|
||||
switch record.GetString("targetType") {
|
||||
case targetAliyunOss:
|
||||
return NewAliyun(option)
|
||||
case targetAliyunCdn:
|
||||
return NewAliyunCdn(option)
|
||||
case targetSSH:
|
||||
return NewSSH(option)
|
||||
case targetWebhook:
|
||||
return NewWebhook(option)
|
||||
case targetTencentCdn:
|
||||
return NewTencentCdn(option)
|
||||
case targetQiniuCdn:
|
||||
return NewQiNiu(option)
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
type deployerOptions struct {
|
||||
Provider domain.DeployProviderType
|
||||
ProviderAccessConfig map[string]any
|
||||
ProviderDeployConfig map[string]any
|
||||
}
|
||||
|
||||
func getProduct(record *models.Record) string {
|
||||
targetType := record.GetString("targetType")
|
||||
rs := strings.Split(targetType, "-")
|
||||
if len(rs) < 2 {
|
||||
return ""
|
||||
func NewWithDeployNode(node *domain.WorkflowNode, certdata struct {
|
||||
Certificate string
|
||||
PrivateKey string
|
||||
},
|
||||
) (Deployer, error) {
|
||||
if node.Type != domain.WorkflowNodeTypeDeploy {
|
||||
return nil, fmt.Errorf("node type is not deploy")
|
||||
}
|
||||
return rs[1]
|
||||
|
||||
nodeConfig := node.GetConfigForDeploy()
|
||||
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
|
||||
}
|
||||
|
||||
accessConfig, err := access.UnmarshalConfigToMap()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := createDeployer(&deployerOptions{
|
||||
Provider: domain.DeployProviderType(nodeConfig.Provider),
|
||||
ProviderAccessConfig: accessConfig,
|
||||
ProviderDeployConfig: nodeConfig.ProviderConfig,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proxyDeployer{
|
||||
logger: logger.NewNilLogger(),
|
||||
deployer: deployer,
|
||||
deployCertificate: certdata.Certificate,
|
||||
deployPrivateKey: certdata.PrivateKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toStr(tag string, data any) string {
|
||||
if data == nil {
|
||||
return tag
|
||||
}
|
||||
byts, _ := json.Marshal(data)
|
||||
return tag + ":" + string(byts)
|
||||
// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑
|
||||
type proxyDeployer struct {
|
||||
logger logger.Logger
|
||||
deployer deployer.Deployer
|
||||
deployCertificate string
|
||||
deployPrivateKey string
|
||||
}
|
||||
|
||||
func (d *proxyDeployer) Deploy(ctx context.Context) error {
|
||||
_, err := d.deployer.Deploy(ctx, d.deployCertificate, d.deployPrivateKey)
|
||||
return err
|
||||
}
|
||||
|
||||
803
internal/deployer/providers.go
Normal file
803
internal/deployer/providers.go
Normal file
@@ -0,0 +1,803 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
pAliyunALB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
|
||||
pAliyunCASDeploy "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cas-deploy"
|
||||
pAliyunCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn"
|
||||
pAliyunCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb"
|
||||
pAliyunDCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn"
|
||||
pAliyunESA "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-esa"
|
||||
pAliyunLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-live"
|
||||
pAliyunNLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb"
|
||||
pAliyunOSS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
|
||||
pAliyunVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-vod"
|
||||
pAliyunWAF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-waf"
|
||||
pAWSCloudFront "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-cloudfront"
|
||||
pBaiduCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
|
||||
pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn"
|
||||
pBaotaPanelConsole "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-console"
|
||||
pBaotaPanelSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-site"
|
||||
pBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
|
||||
pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly"
|
||||
pCdnfly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cdnfly"
|
||||
pDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
|
||||
pEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications"
|
||||
pGcoreCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/gcore-cdn"
|
||||
pHuaweiCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
|
||||
pHuaweiCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
|
||||
pHuaweiCloudWAF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-waf"
|
||||
pJDCloudALB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-alb"
|
||||
pJDCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-cdn"
|
||||
pJDCloudLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-live"
|
||||
pJDCloudVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-vod"
|
||||
pK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
|
||||
pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
|
||||
pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
|
||||
pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili"
|
||||
pSafeLine "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline"
|
||||
pSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
|
||||
pTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
|
||||
pTencentCloudCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb"
|
||||
pTencentCloudCOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos"
|
||||
pTencentCloudCSS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-css"
|
||||
pTencentCloudECDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
|
||||
pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-eo"
|
||||
pTencentCloudSSLDeploy "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy"
|
||||
pTencentCloudVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-vod"
|
||||
pTencentCloudWAF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-waf"
|
||||
pUCloudUCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ucloud-ucdn"
|
||||
pUCloudUS3 "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ucloud-us3"
|
||||
pVolcEngineCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
|
||||
pVolcEngineCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-clb"
|
||||
pVolcEngineDCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-dcdn"
|
||||
pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex"
|
||||
pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
|
||||
pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos"
|
||||
pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/slices"
|
||||
)
|
||||
|
||||
func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
|
||||
/*
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeAliyunALB, domain.DeployProviderTypeAliyunCASDeploy, domain.DeployProviderTypeAliyunCDN, domain.DeployProviderTypeAliyunCLB, domain.DeployProviderTypeAliyunDCDN, domain.DeployProviderTypeAliyunESA, domain.DeployProviderTypeAliyunLive, domain.DeployProviderTypeAliyunNLB, domain.DeployProviderTypeAliyunOSS, domain.DeployProviderTypeAliyunVOD, domain.DeployProviderTypeAliyunWAF:
|
||||
{
|
||||
access := domain.AccessConfigForAliyun{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeAliyunALB:
|
||||
deployer, err := pAliyunALB.NewDeployer(&pAliyunALB.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: pAliyunALB.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(options.ProviderDeployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(options.ProviderDeployConfig, "listenerId"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunCASDeploy:
|
||||
deployer, err := pAliyunCASDeploy.NewDeployer(&pAliyunCASDeploy.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceIds: slices.Filter(strings.Split(maps.GetValueAsString(options.ProviderDeployConfig, "resourceIds"), ";"), func(s string) bool { return s != "" }),
|
||||
ContactIds: slices.Filter(strings.Split(maps.GetValueAsString(options.ProviderDeployConfig, "contactIds"), ";"), func(s string) bool { return s != "" }),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunCDN:
|
||||
deployer, err := pAliyunCDN.NewDeployer(&pAliyunCDN.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunCLB:
|
||||
deployer, err := pAliyunCLB.NewDeployer(&pAliyunCLB.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: pAliyunCLB.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(options.ProviderDeployConfig, "loadbalancerId"),
|
||||
ListenerPort: maps.GetValueOrDefaultAsInt32(options.ProviderDeployConfig, "listenerPort", 443),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunDCDN:
|
||||
deployer, err := pAliyunDCDN.NewDeployer(&pAliyunDCDN.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunESA:
|
||||
deployer, err := pAliyunESA.NewDeployer(&pAliyunESA.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
SiteId: maps.GetValueAsInt64(options.ProviderDeployConfig, "siteId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunLive:
|
||||
deployer, err := pAliyunLive.NewDeployer(&pAliyunLive.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunNLB:
|
||||
deployer, err := pAliyunNLB.NewDeployer(&pAliyunNLB.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: pAliyunNLB.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(options.ProviderDeployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(options.ProviderDeployConfig, "listenerId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunOSS:
|
||||
deployer, err := pAliyunOSS.NewDeployer(&pAliyunOSS.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(options.ProviderDeployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunVOD:
|
||||
deployer, err := pAliyunVOD.NewDeployer(&pAliyunVOD.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeAliyunWAF:
|
||||
deployer, err := pAliyunWAF.NewDeployer(&pAliyunWAF.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
InstanceId: maps.GetValueAsString(options.ProviderDeployConfig, "instanceId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeAWSCloudFront:
|
||||
{
|
||||
access := domain.AccessConfigForAWS{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeAWSCloudFront:
|
||||
deployer, err := pAWSCloudFront.NewDeployer(&pAWSCloudFront.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
DistributionId: maps.GetValueAsString(options.ProviderDeployConfig, "distributionId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeBaiduCloudCDN:
|
||||
{
|
||||
access := domain.AccessConfigForBaiduCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeBaiduCloudCDN:
|
||||
deployer, err := pBaiduCloudCDN.NewDeployer(&pBaiduCloudCDN.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeBaishanCDN:
|
||||
{
|
||||
access := domain.AccessConfigForBaishan{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeBaishanCDN:
|
||||
deployer, err := pBaishanCDN.NewDeployer(&pBaishanCDN.DeployerConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeBaotaPanelConsole, domain.DeployProviderTypeBaotaPanelSite:
|
||||
{
|
||||
access := domain.AccessConfigForBaotaPanel{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeBaotaPanelConsole:
|
||||
deployer, err := pBaotaPanelConsole.NewDeployer(&pBaotaPanelConsole.DeployerConfig{
|
||||
ApiUrl: access.ApiUrl,
|
||||
ApiKey: access.ApiKey,
|
||||
AutoRestart: maps.GetValueAsBool(options.ProviderDeployConfig, "autoRestart"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeBaotaPanelSite:
|
||||
deployer, err := pBaotaPanelSite.NewDeployer(&pBaotaPanelSite.DeployerConfig{
|
||||
ApiUrl: access.ApiUrl,
|
||||
ApiKey: access.ApiKey,
|
||||
SiteType: maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "siteType", "other"),
|
||||
SiteName: maps.GetValueAsString(options.ProviderDeployConfig, "siteName"),
|
||||
SiteNames: slices.Filter(strings.Split(maps.GetValueAsString(options.ProviderDeployConfig, "siteNames"), ";"), func(s string) bool { return s != "" }),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeBytePlusCDN:
|
||||
{
|
||||
access := domain.AccessConfigForBytePlus{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeBytePlusCDN:
|
||||
deployer, err := pBytePlusCDN.NewDeployer(&pBytePlusCDN.DeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeCacheFly:
|
||||
{
|
||||
access := domain.AccessConfigForCacheFly{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pCacheFly.NewDeployer(&pCacheFly.DeployerConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeCdnfly:
|
||||
{
|
||||
access := domain.AccessConfigForCdnfly{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pCdnfly.NewDeployer(&pCdnfly.DeployerConfig{
|
||||
ApiUrl: access.ApiUrl,
|
||||
ApiKey: access.ApiKey,
|
||||
ApiSecret: access.ApiSecret,
|
||||
ResourceType: pCdnfly.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
SiteId: maps.GetValueAsString(options.ProviderDeployConfig, "siteId"),
|
||||
CertificateId: maps.GetValueAsString(options.ProviderDeployConfig, "certificateId"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeDogeCloudCDN:
|
||||
{
|
||||
access := domain.AccessConfigForDogeCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pDogeCDN.NewDeployer(&pDogeCDN.DeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeEdgioApplications:
|
||||
{
|
||||
access := domain.AccessConfigForEdgio{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pEdgioApplications.NewDeployer(&pEdgioApplications.DeployerConfig{
|
||||
ClientId: access.ClientId,
|
||||
ClientSecret: access.ClientSecret,
|
||||
EnvironmentId: maps.GetValueAsString(options.ProviderDeployConfig, "environmentId"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeGcoreCDN:
|
||||
{
|
||||
access := domain.AccessConfigForGcore{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeGcoreCDN:
|
||||
deployer, err := pGcoreCDN.NewDeployer(&pGcoreCDN.DeployerConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
ResourceId: maps.GetValueAsInt64(options.ProviderDeployConfig, "resourceId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB, domain.DeployProviderTypeHuaweiCloudWAF:
|
||||
{
|
||||
access := domain.AccessConfigForHuaweiCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeHuaweiCloudCDN:
|
||||
deployer, err := pHuaweiCloudCDN.NewDeployer(&pHuaweiCloudCDN.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeHuaweiCloudELB:
|
||||
deployer, err := pHuaweiCloudELB.NewDeployer(&pHuaweiCloudELB.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: pHuaweiCloudELB.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
CertificateId: maps.GetValueAsString(options.ProviderDeployConfig, "certificateId"),
|
||||
LoadbalancerId: maps.GetValueAsString(options.ProviderDeployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(options.ProviderDeployConfig, "listenerId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeHuaweiCloudWAF:
|
||||
deployer, err := pHuaweiCloudWAF.NewDeployer(&pHuaweiCloudWAF.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: pHuaweiCloudWAF.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
CertificateId: maps.GetValueAsString(options.ProviderDeployConfig, "certificateId"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeJDCloudALB, domain.DeployProviderTypeJDCloudCDN, domain.DeployProviderTypeJDCloudLive, domain.DeployProviderTypeJDCloudVOD:
|
||||
{
|
||||
access := domain.AccessConfigForJDCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeJDCloudALB:
|
||||
deployer, err := pJDCloudALB.NewDeployer(&pJDCloudALB.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
RegionId: maps.GetValueAsString(options.ProviderDeployConfig, "regionId"),
|
||||
ResourceType: pJDCloudALB.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(options.ProviderDeployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(options.ProviderDeployConfig, "listenerId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeJDCloudCDN:
|
||||
deployer, err := pJDCloudCDN.NewDeployer(&pJDCloudCDN.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeJDCloudLive:
|
||||
deployer, err := pJDCloudLive.NewDeployer(&pJDCloudLive.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeJDCloudVOD:
|
||||
deployer, err := pJDCloudVOD.NewDeployer(&pJDCloudVOD.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeLocal:
|
||||
{
|
||||
deployer, err := pLocal.NewDeployer(&pLocal.DeployerConfig{
|
||||
ShellEnv: pLocal.ShellEnvType(maps.GetValueAsString(options.ProviderDeployConfig, "shellEnv")),
|
||||
PreCommand: maps.GetValueAsString(options.ProviderDeployConfig, "preCommand"),
|
||||
PostCommand: maps.GetValueAsString(options.ProviderDeployConfig, "postCommand"),
|
||||
OutputFormat: pLocal.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(pLocal.OUTPUT_FORMAT_PEM))),
|
||||
OutputCertPath: maps.GetValueAsString(options.ProviderDeployConfig, "certPath"),
|
||||
OutputKeyPath: maps.GetValueAsString(options.ProviderDeployConfig, "keyPath"),
|
||||
PfxPassword: maps.GetValueAsString(options.ProviderDeployConfig, "pfxPassword"),
|
||||
JksAlias: maps.GetValueAsString(options.ProviderDeployConfig, "jksAlias"),
|
||||
JksKeypass: maps.GetValueAsString(options.ProviderDeployConfig, "jksKeypass"),
|
||||
JksStorepass: maps.GetValueAsString(options.ProviderDeployConfig, "jksStorepass"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeKubernetesSecret:
|
||||
{
|
||||
access := domain.AccessConfigForKubernetes{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pK8sSecret.NewDeployer(&pK8sSecret.DeployerConfig{
|
||||
KubeConfig: access.KubeConfig,
|
||||
Namespace: maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "namespace", "default"),
|
||||
SecretName: maps.GetValueAsString(options.ProviderDeployConfig, "secretName"),
|
||||
SecretType: maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "secretType", "kubernetes.io/tls"),
|
||||
SecretDataKeyForCrt: maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "secretDataKeyForCrt", "tls.crt"),
|
||||
SecretDataKeyForKey: maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "secretDataKeyForKey", "tls.key"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeQiniuCDN, domain.DeployProviderTypeQiniuPili:
|
||||
{
|
||||
access := domain.AccessConfigForQiniu{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeQiniuCDN:
|
||||
deployer, err := pQiniuCDN.NewDeployer(&pQiniuCDN.DeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeQiniuPili:
|
||||
deployer, err := pQiniuPili.NewDeployer(&pQiniuPili.DeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Hub: maps.GetValueAsString(options.ProviderDeployConfig, "hub"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeSafeLine:
|
||||
{
|
||||
access := domain.AccessConfigForSafeLine{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pSafeLine.NewDeployer(&pSafeLine.DeployerConfig{
|
||||
ApiUrl: access.ApiUrl,
|
||||
ApiToken: access.ApiToken,
|
||||
ResourceType: pSafeLine.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
CertificateId: maps.GetValueAsInt32(options.ProviderDeployConfig, "certificateId"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeSSH:
|
||||
{
|
||||
access := domain.AccessConfigForSSH{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pSSH.NewDeployer(&pSSH.DeployerConfig{
|
||||
SshHost: access.Host,
|
||||
SshPort: access.Port,
|
||||
SshUsername: access.Username,
|
||||
SshPassword: access.Password,
|
||||
SshKey: access.Key,
|
||||
SshKeyPassphrase: access.KeyPassphrase,
|
||||
UseSCP: maps.GetValueAsBool(options.ProviderDeployConfig, "useSCP"),
|
||||
PreCommand: maps.GetValueAsString(options.ProviderDeployConfig, "preCommand"),
|
||||
PostCommand: maps.GetValueAsString(options.ProviderDeployConfig, "postCommand"),
|
||||
OutputFormat: pSSH.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))),
|
||||
OutputCertPath: maps.GetValueAsString(options.ProviderDeployConfig, "certPath"),
|
||||
OutputKeyPath: maps.GetValueAsString(options.ProviderDeployConfig, "keyPath"),
|
||||
PfxPassword: maps.GetValueAsString(options.ProviderDeployConfig, "pfxPassword"),
|
||||
JksAlias: maps.GetValueAsString(options.ProviderDeployConfig, "jksAlias"),
|
||||
JksKeypass: maps.GetValueAsString(options.ProviderDeployConfig, "jksKeypass"),
|
||||
JksStorepass: maps.GetValueAsString(options.ProviderDeployConfig, "jksStorepass"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudCDN, domain.DeployProviderTypeTencentCloudCLB, domain.DeployProviderTypeTencentCloudCOS, domain.DeployProviderTypeTencentCloudCSS, domain.DeployProviderTypeTencentCloudECDN, domain.DeployProviderTypeTencentCloudEO, domain.DeployProviderTypeTencentCloudSSLDeploy, domain.DeployProviderTypeTencentCloudVOD, domain.DeployProviderTypeTencentCloudWAF:
|
||||
{
|
||||
access := domain.AccessConfigForTencentCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeTencentCloudCDN:
|
||||
deployer, err := pTencentCloudCDN.NewDeployer(&pTencentCloudCDN.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudCLB:
|
||||
deployer, err := pTencentCloudCLB.NewDeployer(&pTencentCloudCLB.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: pTencentCloudCLB.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(options.ProviderDeployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(options.ProviderDeployConfig, "listenerId"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudCOS:
|
||||
deployer, err := pTencentCloudCOS.NewDeployer(&pTencentCloudCOS.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(options.ProviderDeployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudCSS:
|
||||
deployer, err := pTencentCloudCSS.NewDeployer(&pTencentCloudCSS.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudECDN:
|
||||
deployer, err := pTencentCloudECDN.NewDeployer(&pTencentCloudECDN.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudEO:
|
||||
deployer, err := pTencentCloudEO.NewDeployer(&pTencentCloudEO.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
ZoneId: maps.GetValueAsString(options.ProviderDeployConfig, "zoneId"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudSSLDeploy:
|
||||
deployer, err := pTencentCloudSSLDeploy.NewDeployer(&pTencentCloudSSLDeploy.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: maps.GetValueAsString(options.ProviderDeployConfig, "resourceType"),
|
||||
ResourceIds: slices.Filter(strings.Split(maps.GetValueAsString(options.ProviderDeployConfig, "resourceIds"), ";"), func(s string) bool { return s != "" }),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudVOD:
|
||||
deployer, err := pTencentCloudVOD.NewDeployer(&pTencentCloudVOD.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
SubAppId: maps.GetValueAsInt64(options.ProviderDeployConfig, "subAppId"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeTencentCloudWAF:
|
||||
deployer, err := pTencentCloudWAF.NewDeployer(&pTencentCloudWAF.DeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
DomainId: maps.GetValueAsString(options.ProviderDeployConfig, "domainId"),
|
||||
InstanceId: maps.GetValueAsString(options.ProviderDeployConfig, "instanceId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeUCloudUCDN, domain.DeployProviderTypeUCloudUS3:
|
||||
{
|
||||
access := domain.AccessConfigForUCloud{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeUCloudUCDN:
|
||||
deployer, err := pUCloudUCDN.NewDeployer(&pUCloudUCDN.DeployerConfig{
|
||||
PrivateKey: access.PrivateKey,
|
||||
PublicKey: access.PublicKey,
|
||||
ProjectId: access.ProjectId,
|
||||
DomainId: maps.GetValueAsString(options.ProviderDeployConfig, "domainId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeUCloudUS3:
|
||||
deployer, err := pUCloudUS3.NewDeployer(&pUCloudUS3.DeployerConfig{
|
||||
PrivateKey: access.PrivateKey,
|
||||
PublicKey: access.PublicKey,
|
||||
ProjectId: access.ProjectId,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(options.ProviderDeployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineImageX, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS:
|
||||
{
|
||||
access := domain.AccessConfigForVolcEngine{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeVolcEngineCDN:
|
||||
deployer, err := pVolcEngineCDN.NewDeployer(&pVolcEngineCDN.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeVolcEngineCLB:
|
||||
deployer, err := pVolcEngineCLB.NewDeployer(&pVolcEngineCLB.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ResourceType: pVolcEngineCLB.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")),
|
||||
ListenerId: maps.GetValueAsString(options.ProviderDeployConfig, "listenerId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeVolcEngineDCDN:
|
||||
deployer, err := pVolcEngineDCDN.NewDeployer(&pVolcEngineDCDN.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeVolcEngineImageX:
|
||||
deployer, err := pVolcEngineImageX.NewDeployer(&pVolcEngineImageX.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
ServiceId: maps.GetValueAsString(options.ProviderDeployConfig, "serviceId"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeVolcEngineLive:
|
||||
deployer, err := pVolcEngineLive.NewDeployer(&pVolcEngineLive.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
case domain.DeployProviderTypeVolcEngineTOS:
|
||||
deployer, err := pVolcEngineTOS.NewDeployer(&pVolcEngineTOS.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(options.ProviderDeployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(options.ProviderDeployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeWebhook:
|
||||
{
|
||||
access := domain.AccessConfigForWebhook{}
|
||||
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := pWebhook.NewDeployer(&pWebhook.DeployerConfig{
|
||||
WebhookUrl: access.Url,
|
||||
WebhookData: maps.GetValueAsString(options.ProviderDeployConfig, "webhookData"),
|
||||
})
|
||||
return deployer, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported deployer provider: %s", string(options.Provider))
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"certimate/internal/domain"
|
||||
xhttp "certimate/internal/utils/http"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
)
|
||||
|
||||
const qiniuGateway = "http://api.qiniu.com"
|
||||
|
||||
type qiuniu struct {
|
||||
option *DeployerOption
|
||||
info []string
|
||||
credentials *auth.Credentials
|
||||
}
|
||||
|
||||
func NewQiNiu(option *DeployerOption) (*qiuniu, error) {
|
||||
access := &domain.QiniuAccess{}
|
||||
json.Unmarshal([]byte(option.Access), access)
|
||||
|
||||
return &qiuniu{
|
||||
option: option,
|
||||
info: make([]string, 0),
|
||||
|
||||
credentials: auth.New(access.AccessKey, access.SecretKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q *qiuniu) GetInfo() []string {
|
||||
return q.info
|
||||
}
|
||||
|
||||
func (q *qiuniu) Deploy(ctx context.Context) error {
|
||||
|
||||
// 上传证书
|
||||
certId, err := q.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("uploadCert failed: %w", err)
|
||||
}
|
||||
|
||||
// 获取域名信息
|
||||
domainInfo, err := q.getDomainInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDomainInfo failed: %w", err)
|
||||
}
|
||||
|
||||
// 判断域名是否启用 https
|
||||
|
||||
if domainInfo.Https != nil && domainInfo.Https.CertID != "" {
|
||||
// 启用了 https
|
||||
// 修改域名证书
|
||||
err = q.modifyDomainCert(certId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("modifyDomainCert failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// 没启用 https
|
||||
// 启用 https
|
||||
|
||||
err = q.enableHttps(certId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enableHttps failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *qiuniu) enableHttps(certId string) error {
|
||||
path := fmt.Sprintf("/domain/%s/sslize", q.option.Domain)
|
||||
|
||||
body := &modifyDomainCertReq{
|
||||
CertID: certId,
|
||||
ForceHttps: true,
|
||||
Http2Enable: true,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable https failed: %w", err)
|
||||
}
|
||||
|
||||
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable https failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type domainInfo struct {
|
||||
Https *modifyDomainCertReq `json:"https"`
|
||||
}
|
||||
|
||||
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
|
||||
path := fmt.Sprintf("/domain/%s", q.option.Domain)
|
||||
|
||||
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req failed: %w", err)
|
||||
}
|
||||
|
||||
resp := &domainInfo{}
|
||||
err = json.Unmarshal(res, resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json.Unmarshal failed: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type uploadCertReq struct {
|
||||
Name string `json:"name"`
|
||||
CommonName string `json:"common_name"`
|
||||
Pri string `json:"pri"`
|
||||
Ca string `json:"ca"`
|
||||
}
|
||||
|
||||
type uploadCertResp struct {
|
||||
CertID string `json:"certID"`
|
||||
}
|
||||
|
||||
func (q *qiuniu) uploadCert() (string, error) {
|
||||
path := "/sslcert"
|
||||
|
||||
body := &uploadCertReq{
|
||||
Name: q.option.Domain,
|
||||
CommonName: q.option.Domain,
|
||||
Pri: q.option.Certificate.PrivateKey,
|
||||
Ca: q.option.Certificate.Certificate,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("json.Marshal failed: %w", err)
|
||||
}
|
||||
|
||||
res, err := q.req(qiniuGateway+path, http.MethodPost, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("req failed: %w", err)
|
||||
}
|
||||
resp := &uploadCertResp{}
|
||||
err = json.Unmarshal(res, resp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("json.Unmarshal failed: %w", err)
|
||||
}
|
||||
|
||||
return resp.CertID, nil
|
||||
}
|
||||
|
||||
type modifyDomainCertReq struct {
|
||||
CertID string `json:"certId"`
|
||||
ForceHttps bool `json:"forceHttps"`
|
||||
Http2Enable bool `json:"http2Enable"`
|
||||
}
|
||||
|
||||
func (q *qiuniu) modifyDomainCert(certId string) error {
|
||||
path := fmt.Sprintf("/domain/%s/httpsconf", q.option.Domain)
|
||||
|
||||
body := &modifyDomainCertReq{
|
||||
CertID: certId,
|
||||
ForceHttps: true,
|
||||
Http2Enable: true,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json.Marshal failed: %w", err)
|
||||
}
|
||||
|
||||
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("req failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *qiuniu) req(url, method string, body io.Reader) ([]byte, error) {
|
||||
req := xhttp.BuildReq(url, method, body, map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
|
||||
if err := q.credentials.AddToken(auth.TokenQBox, req); err != nil {
|
||||
return nil, fmt.Errorf("credentials.AddToken failed: %w", err)
|
||||
}
|
||||
|
||||
respBody, err := xhttp.ToRequest(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ToRequest failed: %w", err)
|
||||
}
|
||||
|
||||
defer respBody.Close()
|
||||
|
||||
res, err := io.ReadAll(respBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("io.ReadAll failed: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"testing"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
)
|
||||
|
||||
func Test_qiuniu_uploadCert(t *testing.T) {
|
||||
type fields struct {
|
||||
option *DeployerOption
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
fields: fields{
|
||||
option: &DeployerOption{
|
||||
DomainId: "1",
|
||||
Domain: "example.com",
|
||||
Product: "test",
|
||||
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
||||
Certificate: applicant.Certificate{
|
||||
Certificate: "",
|
||||
PrivateKey: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q, _ := NewQiNiu(tt.fields.option)
|
||||
got, err := q.uploadCert()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("qiuniu.uploadCert() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("qiuniu.uploadCert() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_qiuniu_modifyDomainCert(t *testing.T) {
|
||||
type fields struct {
|
||||
option *DeployerOption
|
||||
info []string
|
||||
credentials *auth.Credentials
|
||||
}
|
||||
type args struct {
|
||||
certId string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
fields: fields{
|
||||
option: &DeployerOption{
|
||||
DomainId: "1",
|
||||
Domain: "jt1.ikit.fun",
|
||||
Product: "test",
|
||||
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q, _ := NewQiNiu(tt.fields.option)
|
||||
if err := q.modifyDomainCert(tt.args.certId); (err != nil) != tt.wantErr {
|
||||
t.Errorf("qiuniu.modifyDomainCert() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
xpath "path"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
sshPkg "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type ssh struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
type sshAccess struct {
|
||||
Host string `json:"host"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
Port string `json:"port"`
|
||||
Command string `json:"command"`
|
||||
CertPath string `json:"certPath"`
|
||||
KeyPath string `json:"keyPath"`
|
||||
}
|
||||
|
||||
func NewSSH(option *DeployerOption) (Deployer, error) {
|
||||
return &ssh{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ssh) GetInfo() []string {
|
||||
return s.infos
|
||||
}
|
||||
|
||||
func (s *ssh) Deploy(ctx context.Context) error {
|
||||
access := &sshAccess{}
|
||||
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
|
||||
return err
|
||||
}
|
||||
// 连接
|
||||
client, err := s.getClient(access)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh连接成功", nil))
|
||||
|
||||
// 上传
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh创建session成功", nil))
|
||||
|
||||
// 上传证书
|
||||
if err := s.upload(client, s.option.Certificate.Certificate, access.CertPath); err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
|
||||
|
||||
// 上传私钥
|
||||
if err := s.upload(client, s.option.Certificate.PrivateKey, access.KeyPath); err != nil {
|
||||
return fmt.Errorf("failed to upload private key: %w", err)
|
||||
}
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
|
||||
|
||||
// 执行命令
|
||||
var stdoutBuf bytes.Buffer
|
||||
session.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
session.Stderr = &stderrBuf
|
||||
|
||||
if err := session.Run(access.Command); err != nil {
|
||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdoutBuf.String(), stderrBuf.String())
|
||||
}
|
||||
|
||||
s.infos = append(s.infos, toStr("ssh执行命令成功", []string{stdoutBuf.String()}))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ssh) upload(client *sshPkg.Client, content, path string) error {
|
||||
|
||||
sftpCli, err := sftp.NewClient(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sftp client: %w", err)
|
||||
}
|
||||
defer sftpCli.Close()
|
||||
|
||||
if err := sftpCli.MkdirAll(xpath.Dir(path)); err != nil {
|
||||
return fmt.Errorf("failed to create remote directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open remote file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write([]byte(content))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to remote file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ssh) getClient(access *sshAccess) (*sshPkg.Client, error) {
|
||||
|
||||
var authMethod sshPkg.AuthMethod
|
||||
|
||||
if access.Key != "" {
|
||||
signer, err := sshPkg.ParsePrivateKey([]byte(access.Key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authMethod = sshPkg.PublicKeys(signer)
|
||||
} else {
|
||||
authMethod = sshPkg.Password(access.Password)
|
||||
}
|
||||
|
||||
return sshPkg.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &sshPkg.ClientConfig{
|
||||
User: access.Username,
|
||||
Auth: []sshPkg.AuthMethod{
|
||||
authMethod,
|
||||
},
|
||||
HostKeyCallback: sshPkg.InsecureIgnoreHostKey(),
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
dir := path.Dir("./a/b/c")
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813"
|
||||
)
|
||||
|
||||
type tencentCdn struct {
|
||||
option *DeployerOption
|
||||
credential *common.Credential
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewTencentCdn(option *DeployerOption) (Deployer, error) {
|
||||
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
|
||||
}
|
||||
|
||||
credential := common.NewCredential(
|
||||
access.SecretId,
|
||||
access.SecretKey,
|
||||
)
|
||||
|
||||
return &tencentCdn{
|
||||
option: option,
|
||||
credential: credential,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) GetInfo() []string {
|
||||
return t.infos
|
||||
}
|
||||
|
||||
func (t *tencentCdn) Deploy(ctx context.Context) error {
|
||||
|
||||
// 查询有没有对应的资源
|
||||
resource, err := t.resource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get resource: %w", err)
|
||||
}
|
||||
|
||||
t.infos = append(t.infos, toStr("查询对应的资源", resource))
|
||||
|
||||
// 上传证书
|
||||
certId, err := t.uploadCert()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
t.infos = append(t.infos, toStr("上传证书", certId))
|
||||
|
||||
if err := t.deploy(resource, certId); err != nil {
|
||||
return fmt.Errorf("failed to deploy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) uploadCert() (string, error) {
|
||||
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
|
||||
client, _ := ssl.NewClient(t.credential, "", cpf)
|
||||
|
||||
request := ssl.NewUploadCertificateRequest()
|
||||
|
||||
request.CertificatePublicKey = common.StringPtr(t.option.Certificate.Certificate)
|
||||
request.CertificatePrivateKey = common.StringPtr(t.option.Certificate.PrivateKey)
|
||||
request.Alias = common.StringPtr(t.option.Domain)
|
||||
request.Repeatable = common.BoolPtr(true)
|
||||
|
||||
response, err := client.UploadCertificate(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload certificate: %w", err)
|
||||
}
|
||||
|
||||
return *response.Response.CertificateId, nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) deploy(resource *tag.ResourceTagMapping, certId string) error {
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
client, _ := ssl.NewClient(t.credential, "", cpf)
|
||||
|
||||
resourceId, err := getResourceId(resource)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get resource id: %w", err)
|
||||
}
|
||||
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
request := ssl.NewDeployCertificateInstanceRequest()
|
||||
|
||||
request.CertificateId = common.StringPtr(certId)
|
||||
request.InstanceIdList = common.StringPtrs([]string{resourceId})
|
||||
request.ResourceType = common.StringPtr("cdn")
|
||||
request.Status = common.Int64Ptr(1)
|
||||
|
||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||
resp, err := client.DeployCertificateInstance(request)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy certificate: %w", err)
|
||||
}
|
||||
t.infos = append(t.infos, toStr("部署证书", resp.Response))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tencentCdn) resource() (*tag.ResourceTagMapping, error) {
|
||||
request := tag.NewGetResourcesRequest()
|
||||
cpf := profile.NewClientProfile()
|
||||
cpf.HttpProfile.Endpoint = "tag.tencentcloudapi.com"
|
||||
|
||||
client, err := tag.NewClient(t.credential, "", cpf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
response, err := client.GetResources(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get resources: %w", err)
|
||||
}
|
||||
|
||||
for _, resource := range response.Response.ResourceTagMappingList {
|
||||
if t.compare(resource) {
|
||||
return resource, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("no resource found")
|
||||
|
||||
}
|
||||
|
||||
func (t *tencentCdn) compare(resource *tag.ResourceTagMapping) bool {
|
||||
slices := strings.Split(*resource.Resource, "/")
|
||||
if len(slices) != 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
typeSlices := strings.Split(slices[0], "::")
|
||||
if len(typeSlices) != 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
if typeSlices[1] != "cdn" || slices[2] != t.option.Domain {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func getResourceId(resource *tag.ResourceTagMapping) (string, error) {
|
||||
slices := strings.Split(*resource.Resource, "/")
|
||||
if len(slices) != 3 {
|
||||
return "", errors.New("invalid resource")
|
||||
}
|
||||
return slices[2], nil
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
xhttp "certimate/internal/utils/http"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type webhookAccess struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type hookData struct {
|
||||
Domain string `json:"domain"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
}
|
||||
|
||||
type webhook struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
}
|
||||
|
||||
func NewWebhook(option *DeployerOption) (Deployer, error) {
|
||||
|
||||
return &webhook{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *webhook) GetInfo() []string {
|
||||
return w.infos
|
||||
}
|
||||
|
||||
func (w *webhook) Deploy(ctx context.Context) error {
|
||||
access := &webhookAccess{}
|
||||
if err := json.Unmarshal([]byte(w.option.Access), access); err != nil {
|
||||
return fmt.Errorf("failed to parse hook access config: %w", err)
|
||||
}
|
||||
|
||||
data := &hookData{
|
||||
Domain: w.option.Domain,
|
||||
Certificate: w.option.Certificate.Certificate,
|
||||
PrivateKey: w.option.Certificate.PrivateKey,
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(data)
|
||||
|
||||
resp, err := xhttp.Req(access.Url, http.MethodPost, bytes.NewReader(body), map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send hook request: %w", err)
|
||||
}
|
||||
|
||||
w.infos = append(w.infos, toStr("webhook response", string(resp)))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,24 +1,208 @@
|
||||
package domain
|
||||
|
||||
type AliyunAccess struct {
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CollectionNameAccess = "access"
|
||||
|
||||
type Access struct {
|
||||
Meta
|
||||
Name string `json:"name" db:"name"`
|
||||
Provider string `json:"provider" db:"provider"`
|
||||
Config string `json:"config" db:"config"`
|
||||
DeletedAt *time.Time `json:"deleted" db:"deleted"`
|
||||
}
|
||||
|
||||
func (a *Access) UnmarshalConfigToMap() (map[string]any, error) {
|
||||
config := make(map[string]any)
|
||||
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type AccessConfigForACMEHttpReq struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForAliyun struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type TencentAccess struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
type AccessConfigForAWS struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type CloudflareAccess struct {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
type AccessConfigForAzure struct {
|
||||
TenantId string `json:"tenantId"`
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
CloudName string `json:"cloudName,omitempty"`
|
||||
}
|
||||
|
||||
type QiniuAccess struct {
|
||||
type AccessConfigForBaiduCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForBaishan struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForBaotaPanel struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForBytePlus struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type NameSiloAccess struct {
|
||||
type AccessConfigForCacheFly struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForCdnfly struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForCloudflare struct {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForClouDNS struct {
|
||||
AuthId string `json:"authId"`
|
||||
AuthPassword string `json:"authPassword"`
|
||||
}
|
||||
|
||||
type AccessConfigForCMCCCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForDNSLA struct {
|
||||
ApiId string `json:"apiId"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForDogeCloud struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForEdgio struct {
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForGcore struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForGname struct {
|
||||
AppId string `json:"appId"`
|
||||
AppKey string `json:"appKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForGoDaddy struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForHuaweiCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForJDCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForKubernetes struct {
|
||||
KubeConfig string `json:"kubeConfig,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForLocal struct{}
|
||||
|
||||
type AccessConfigForNamecheap struct {
|
||||
Username string `json:"username"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForNameDotCom struct {
|
||||
Username string `json:"username"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForNameSilo struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForNS1 struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForPowerDNS struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForQiniu struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForRainYun struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForSafeLine struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type AccessConfigForSSH struct {
|
||||
Host string `json:"host"`
|
||||
Port int32 `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
KeyPassphrase string `json:"keyPassphrase,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForTencentCloud struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForUCloud struct {
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
ProjectId string `json:"projectId,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForVolcEngine struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForWebhook struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type AccessConfigForWestcn struct {
|
||||
Username string `json:"username"`
|
||||
ApiPassword string `json:"password"`
|
||||
}
|
||||
|
||||
15
internal/domain/acme_account.go
Normal file
15
internal/domain/acme_account.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
const CollectionNameAcmeAccount = "acme_accounts"
|
||||
|
||||
type AcmeAccount struct {
|
||||
Meta
|
||||
CA string `json:"ca" db:"ca"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Resource *registration.Resource `json:"resource" db:"resource"`
|
||||
Key string `json:"key" db:"key"`
|
||||
}
|
||||
94
internal/domain/certificate.go
Normal file
94
internal/domain/certificate.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certs"
|
||||
)
|
||||
|
||||
const CollectionNameCertificate = "certificate"
|
||||
|
||||
type Certificate struct {
|
||||
Meta
|
||||
Source CertificateSourceType `json:"source" db:"source"`
|
||||
SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"`
|
||||
SerialNumber string `json:"serialNumber" db:"serialNumber"`
|
||||
Certificate string `json:"certificate" db:"certificate"`
|
||||
PrivateKey string `json:"privateKey" db:"privateKey"`
|
||||
Issuer string `json:"issuer" db:"issuer"`
|
||||
IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"`
|
||||
KeyAlgorithm CertificateKeyAlgorithmType `json:"keyAlgorithm" db:"keyAlgorithm"`
|
||||
EffectAt time.Time `json:"effectAt" db:"effectAt"`
|
||||
ExpireAt time.Time `json:"expireAt" db:"expireAt"`
|
||||
ACMEAccountUrl string `json:"acmeAccountUrl" db:"acmeAccountUrl"`
|
||||
ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"`
|
||||
ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"`
|
||||
WorkflowId string `json:"workflowId" db:"workflowId"`
|
||||
WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"`
|
||||
WorkflowRunId string `json:"workflowRunId" db:"workflowRunId"`
|
||||
WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"`
|
||||
DeletedAt *time.Time `json:"deleted" db:"deleted"`
|
||||
}
|
||||
|
||||
func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate {
|
||||
c.SubjectAltNames = strings.Join(certX509.DNSNames, ";")
|
||||
c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16))
|
||||
c.Issuer = strings.Join(certX509.Issuer.Organization, ";")
|
||||
c.EffectAt = certX509.NotBefore
|
||||
c.ExpireAt = certX509.NotAfter
|
||||
|
||||
switch certX509.SignatureAlgorithm {
|
||||
case x509.SHA256WithRSA, x509.SHA256WithRSAPSS:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA2048
|
||||
case x509.SHA384WithRSA, x509.SHA384WithRSAPSS:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA3072
|
||||
case x509.SHA512WithRSA, x509.SHA512WithRSAPSS:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA4096
|
||||
case x509.ECDSAWithSHA256:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC256
|
||||
case x509.ECDSAWithSHA384:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC384
|
||||
case x509.ECDSAWithSHA512:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC512
|
||||
default:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("")
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate {
|
||||
c.Certificate = certPEM
|
||||
c.PrivateKey = privkeyPEM
|
||||
|
||||
_, issuerCertPEM, _ := certs.ExtractCertificatesFromPEM(certPEM)
|
||||
c.IssuerCertificate = issuerCertPEM
|
||||
|
||||
certX509, _ := certs.ParseCertificateFromPEM(certPEM)
|
||||
if certX509 != nil {
|
||||
c.PopulateFromX509(certX509)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type CertificateSourceType string
|
||||
|
||||
const (
|
||||
CertificateSourceTypeWorkflow = CertificateSourceType("workflow")
|
||||
CertificateSourceTypeUpload = CertificateSourceType("upload")
|
||||
)
|
||||
|
||||
type CertificateKeyAlgorithmType string
|
||||
|
||||
const (
|
||||
CertificateKeyAlgorithmTypeRSA2048 = CertificateKeyAlgorithmType("RSA2048")
|
||||
CertificateKeyAlgorithmTypeRSA3072 = CertificateKeyAlgorithmType("RSA3072")
|
||||
CertificateKeyAlgorithmTypeRSA4096 = CertificateKeyAlgorithmType("RSA4096")
|
||||
CertificateKeyAlgorithmTypeRSA8192 = CertificateKeyAlgorithmType("RSA8192")
|
||||
CertificateKeyAlgorithmTypeEC256 = CertificateKeyAlgorithmType("EC256")
|
||||
CertificateKeyAlgorithmTypeEC384 = CertificateKeyAlgorithmType("EC384")
|
||||
CertificateKeyAlgorithmTypeEC512 = CertificateKeyAlgorithmType("EC512")
|
||||
)
|
||||
28
internal/domain/dtos/certificate.go
Normal file
28
internal/domain/dtos/certificate.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package dtos
|
||||
|
||||
type CertificateArchiveFileReq struct {
|
||||
CertificateId string `json:"-"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type CertificateArchiveFileResp struct {
|
||||
FileBytes []byte `json:"fileBytes"`
|
||||
FileFormat string `json:"fileFormat"`
|
||||
}
|
||||
|
||||
type CertificateValidateCertificateReq struct {
|
||||
Certificate string `json:"certificate"`
|
||||
}
|
||||
|
||||
type CertificateValidateCertificateResp struct {
|
||||
IsValid bool `json:"isValid"`
|
||||
Domains string `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
type CertificateValidatePrivateKeyReq struct {
|
||||
PrivateKey string `json:"privateKey"`
|
||||
}
|
||||
|
||||
type CertificateValidatePrivateKeyResp struct {
|
||||
IsValid bool `json:"isValid"`
|
||||
}
|
||||
7
internal/domain/dtos/notify.go
Normal file
7
internal/domain/dtos/notify.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package dtos
|
||||
|
||||
import "github.com/usual2970/certimate/internal/domain"
|
||||
|
||||
type NotifyTestPushReq struct {
|
||||
Channel domain.NotifyChannelType `json:"channel"`
|
||||
}
|
||||
13
internal/domain/dtos/workflow.go
Normal file
13
internal/domain/dtos/workflow.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package dtos
|
||||
|
||||
import "github.com/usual2970/certimate/internal/domain"
|
||||
|
||||
type WorkflowStartRunReq struct {
|
||||
WorkflowId string `json:"-"`
|
||||
RunTrigger domain.WorkflowTriggerType `json:"trigger"`
|
||||
}
|
||||
|
||||
type WorkflowCancelRunReq struct {
|
||||
WorkflowId string `json:"-"`
|
||||
RunId string `json:"-"`
|
||||
}
|
||||
30
internal/domain/error.go
Normal file
30
internal/domain/error.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package domain
|
||||
|
||||
var (
|
||||
ErrInvalidParams = NewError(400, "invalid params")
|
||||
ErrRecordNotFound = NewError(404, "record not found")
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewError(code int, msg string) *Error {
|
||||
if code == 0 {
|
||||
code = -1
|
||||
}
|
||||
|
||||
return &Error{code, msg}
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func IsRecordNotFoundError(err error) bool {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Code == ErrRecordNotFound.Code
|
||||
}
|
||||
return false
|
||||
}
|
||||
9
internal/domain/meta.go
Normal file
9
internal/domain/meta.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Meta struct {
|
||||
Id string `json:"id" db:"id"`
|
||||
CreatedAt time.Time `json:"created" db:"created"`
|
||||
UpdatedAt time.Time `json:"updated" db:"updated"`
|
||||
}
|
||||
20
internal/domain/notify.go
Normal file
20
internal/domain/notify.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package domain
|
||||
|
||||
type NotifyChannelType string
|
||||
|
||||
/*
|
||||
消息通知渠道常量值。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
NotifyChannelTypeBark = NotifyChannelType("bark")
|
||||
NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk")
|
||||
NotifyChannelTypeEmail = NotifyChannelType("email")
|
||||
NotifyChannelTypeLark = NotifyChannelType("lark")
|
||||
NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
|
||||
NotifyChannelTypeTelegram = NotifyChannelType("telegram")
|
||||
NotifyChannelTypeWebhook = NotifyChannelType("webhook")
|
||||
NotifyChannelTypeWeCom = NotifyChannelType("wecom")
|
||||
)
|
||||
164
internal/domain/provider.go
Normal file
164
internal/domain/provider.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package domain
|
||||
|
||||
type AccessProviderType string
|
||||
|
||||
/*
|
||||
授权提供商类型常量值。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
AccessProviderType1Panel = AccessProviderType("1panel") // 1Panel(预留)
|
||||
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
|
||||
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
|
||||
AccessProviderTypeAliyun = AccessProviderType("aliyun")
|
||||
AccessProviderTypeAWS = AccessProviderType("aws")
|
||||
AccessProviderTypeAzure = AccessProviderType("azure")
|
||||
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
|
||||
AccessProviderTypeBaishan = AccessProviderType("baishan")
|
||||
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
|
||||
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
|
||||
AccessProviderTypeCacheFly = AccessProviderType("cachefly")
|
||||
AccessProviderTypeCdnfly = AccessProviderType("cdnfly")
|
||||
AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
|
||||
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
|
||||
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
|
||||
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留)
|
||||
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留)
|
||||
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
|
||||
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
|
||||
AccessProviderTypeEdgio = AccessProviderType("edgio")
|
||||
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留)
|
||||
AccessProviderTypeGname = AccessProviderType("gname")
|
||||
AccessProviderTypeGcore = AccessProviderType("gcore")
|
||||
AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
|
||||
AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge(预留)
|
||||
AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud")
|
||||
AccessProviderTypeJDCloud = AccessProviderType("jdcloud")
|
||||
AccessProviderTypeKubernetes = AccessProviderType("k8s")
|
||||
AccessProviderTypeLocal = AccessProviderType("local")
|
||||
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
|
||||
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
|
||||
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
|
||||
AccessProviderTypeNS1 = AccessProviderType("ns1")
|
||||
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
|
||||
AccessProviderTypeQiniu = AccessProviderType("qiniu")
|
||||
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
|
||||
AccessProviderTypeRainYun = AccessProviderType("rainyun")
|
||||
AccessProviderTypeSafeLine = AccessProviderType("safeline")
|
||||
AccessProviderTypeSSH = AccessProviderType("ssh")
|
||||
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
|
||||
AccessProviderTypeUCloud = AccessProviderType("ucloud")
|
||||
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
|
||||
AccessProviderTypeWebhook = AccessProviderType("webhook")
|
||||
AccessProviderTypeWestcn = AccessProviderType("westcn")
|
||||
)
|
||||
|
||||
type ApplyDNSProviderType string
|
||||
|
||||
/*
|
||||
申请证书 DNS 提供商常量值。
|
||||
短横线前的部分始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
ApplyDNSProviderTypeACMEHttpReq = ApplyDNSProviderType("acmehttpreq")
|
||||
ApplyDNSProviderTypeAliyun = ApplyDNSProviderType("aliyun") // 兼容旧值,等同于 [ApplyDNSProviderTypeAliyunDNS]
|
||||
ApplyDNSProviderTypeAliyunDNS = ApplyDNSProviderType("aliyun-dns")
|
||||
ApplyDNSProviderTypeAWS = ApplyDNSProviderType("aws") // 兼容旧值,等同于 [ApplyDNSProviderTypeAWSRoute53]
|
||||
ApplyDNSProviderTypeAWSRoute53 = ApplyDNSProviderType("aws-route53")
|
||||
ApplyDNSProviderTypeAzure = ApplyDNSProviderType("azure") // 兼容旧值,等同于 [ApplyDNSProviderTypeAzure]
|
||||
ApplyDNSProviderTypeAzureDNS = ApplyDNSProviderType("azure-dns")
|
||||
ApplyDNSProviderTypeBaiduCloud = ApplyDNSProviderType("baiducloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeBaiduCloudDNS]
|
||||
ApplyDNSProviderTypeBaiduCloudDNS = ApplyDNSProviderType("baiducloud-dns")
|
||||
ApplyDNSProviderTypeCloudflare = ApplyDNSProviderType("cloudflare")
|
||||
ApplyDNSProviderTypeClouDNS = ApplyDNSProviderType("cloudns")
|
||||
ApplyDNSProviderTypeCMCCCloud = ApplyDNSProviderType("cmcccloud")
|
||||
ApplyDNSProviderTypeDNSLA = ApplyDNSProviderType("dnsla")
|
||||
ApplyDNSProviderTypeGcore = ApplyDNSProviderType("gcore")
|
||||
ApplyDNSProviderTypeGname = ApplyDNSProviderType("gname")
|
||||
ApplyDNSProviderTypeGoDaddy = ApplyDNSProviderType("godaddy")
|
||||
ApplyDNSProviderTypeHuaweiCloud = ApplyDNSProviderType("huaweicloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeHuaweiCloudDNS]
|
||||
ApplyDNSProviderTypeHuaweiCloudDNS = ApplyDNSProviderType("huaweicloud-dns")
|
||||
ApplyDNSProviderTypeJDCloud = ApplyDNSProviderType("jdcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeJDCloudDNS]
|
||||
ApplyDNSProviderTypeJDCloudDNS = ApplyDNSProviderType("jdcloud-dns")
|
||||
ApplyDNSProviderTypeNamecheap = ApplyDNSProviderType("namecheap")
|
||||
ApplyDNSProviderTypeNameDotCom = ApplyDNSProviderType("namedotcom")
|
||||
ApplyDNSProviderTypeNameSilo = ApplyDNSProviderType("namesilo")
|
||||
ApplyDNSProviderTypeNS1 = ApplyDNSProviderType("ns1")
|
||||
ApplyDNSProviderTypePowerDNS = ApplyDNSProviderType("powerdns")
|
||||
ApplyDNSProviderTypeRainYun = ApplyDNSProviderType("rainyun")
|
||||
ApplyDNSProviderTypeTencentCloud = ApplyDNSProviderType("tencentcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeTencentCloudDNS]
|
||||
ApplyDNSProviderTypeTencentCloudDNS = ApplyDNSProviderType("tencentcloud-dns")
|
||||
ApplyDNSProviderTypeVolcEngine = ApplyDNSProviderType("volcengine") // 兼容旧值,等同于 [ApplyDNSProviderTypeVolcEngineDNS]
|
||||
ApplyDNSProviderTypeVolcEngineDNS = ApplyDNSProviderType("volcengine-dns")
|
||||
ApplyDNSProviderTypeWestcn = ApplyDNSProviderType("westcn")
|
||||
)
|
||||
|
||||
type DeployProviderType string
|
||||
|
||||
/*
|
||||
部署目标提供商常量值。
|
||||
短横线前的部分始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
DeployProviderTypeAliyunALB = DeployProviderType("aliyun-alb")
|
||||
DeployProviderTypeAliyunCASDeploy = DeployProviderType("aliyun-casdeploy")
|
||||
DeployProviderTypeAliyunCDN = DeployProviderType("aliyun-cdn")
|
||||
DeployProviderTypeAliyunCLB = DeployProviderType("aliyun-clb")
|
||||
DeployProviderTypeAliyunDCDN = DeployProviderType("aliyun-dcdn")
|
||||
DeployProviderTypeAliyunESA = DeployProviderType("aliyun-esa")
|
||||
DeployProviderTypeAliyunLive = DeployProviderType("aliyun-live")
|
||||
DeployProviderTypeAliyunNLB = DeployProviderType("aliyun-nlb")
|
||||
DeployProviderTypeAliyunOSS = DeployProviderType("aliyun-oss")
|
||||
DeployProviderTypeAliyunVOD = DeployProviderType("aliyun-vod")
|
||||
DeployProviderTypeAliyunWAF = DeployProviderType("aliyun-waf")
|
||||
DeployProviderTypeAWSCloudFront = DeployProviderType("aws-cloudfront")
|
||||
DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn")
|
||||
DeployProviderTypeBaishanCDN = DeployProviderType("baishan-cdn")
|
||||
DeployProviderTypeBaotaPanelConsole = DeployProviderType("baotapanel-console")
|
||||
DeployProviderTypeBaotaPanelSite = DeployProviderType("baotapanel-site")
|
||||
DeployProviderTypeBytePlusCDN = DeployProviderType("byteplus-cdn")
|
||||
DeployProviderTypeCacheFly = DeployProviderType("cachefly")
|
||||
DeployProviderTypeCdnfly = DeployProviderType("cdnfly")
|
||||
DeployProviderTypeDogeCloudCDN = DeployProviderType("dogecloud-cdn")
|
||||
DeployProviderTypeEdgioApplications = DeployProviderType("edgio-applications")
|
||||
DeployProviderTypeGcoreCDN = DeployProviderType("gcore-cdn")
|
||||
DeployProviderTypeHuaweiCloudCDN = DeployProviderType("huaweicloud-cdn")
|
||||
DeployProviderTypeHuaweiCloudELB = DeployProviderType("huaweicloud-elb")
|
||||
DeployProviderTypeHuaweiCloudWAF = DeployProviderType("huaweicloud-waf")
|
||||
DeployProviderTypeJDCloudALB = DeployProviderType("jdcloud-alb")
|
||||
DeployProviderTypeJDCloudCDN = DeployProviderType("jdcloud-cdn")
|
||||
DeployProviderTypeJDCloudLive = DeployProviderType("jdcloud-live")
|
||||
DeployProviderTypeJDCloudVOD = DeployProviderType("jdcloud-vod")
|
||||
DeployProviderTypeKubernetesSecret = DeployProviderType("k8s-secret")
|
||||
DeployProviderTypeLocal = DeployProviderType("local")
|
||||
DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn")
|
||||
DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili")
|
||||
DeployProviderTypeSafeLine = DeployProviderType("safeline")
|
||||
DeployProviderTypeSSH = DeployProviderType("ssh")
|
||||
DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn")
|
||||
DeployProviderTypeTencentCloudCLB = DeployProviderType("tencentcloud-clb")
|
||||
DeployProviderTypeTencentCloudCOS = DeployProviderType("tencentcloud-cos")
|
||||
DeployProviderTypeTencentCloudCSS = DeployProviderType("tencentcloud-css")
|
||||
DeployProviderTypeTencentCloudECDN = DeployProviderType("tencentcloud-ecdn")
|
||||
DeployProviderTypeTencentCloudEO = DeployProviderType("tencentcloud-eo")
|
||||
DeployProviderTypeTencentCloudSSLDeploy = DeployProviderType("tencentcloud-ssldeploy")
|
||||
DeployProviderTypeTencentCloudVOD = DeployProviderType("tencentcloud-vod")
|
||||
DeployProviderTypeTencentCloudWAF = DeployProviderType("tencentcloud-waf")
|
||||
DeployProviderTypeUCloudUCDN = DeployProviderType("ucloud-ucdn")
|
||||
DeployProviderTypeUCloudUS3 = DeployProviderType("ucloud-us3")
|
||||
DeployProviderTypeVolcEngineCDN = DeployProviderType("volcengine-cdn")
|
||||
DeployProviderTypeVolcEngineCLB = DeployProviderType("volcengine-clb")
|
||||
DeployProviderTypeVolcEngineDCDN = DeployProviderType("volcengine-dcdn")
|
||||
DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex")
|
||||
DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live")
|
||||
DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos")
|
||||
DeployProviderTypeWebhook = DeployProviderType("webhook")
|
||||
)
|
||||
39
internal/domain/settings.go
Normal file
39
internal/domain/settings.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const CollectionNameSettings = "settings"
|
||||
|
||||
type Settings struct {
|
||||
Meta
|
||||
Name string `json:"name" db:"name"`
|
||||
Content string `json:"content" db:"content"`
|
||||
}
|
||||
|
||||
type NotifyTemplatesSettingsContent struct {
|
||||
NotifyTemplates []NotifyTemplate `json:"notifyTemplates"`
|
||||
}
|
||||
|
||||
type NotifyTemplate struct {
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type NotifyChannelsSettingsContent map[string]map[string]any
|
||||
|
||||
func (s *Settings) GetNotifyChannelConfig(channel string) (map[string]any, error) {
|
||||
conf := &NotifyChannelsSettingsContent{}
|
||||
if err := json.Unmarshal([]byte(s.Content), conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, ok := (*conf)[channel]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("channel \"%s\" not found", channel)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
11
internal/domain/statistics.go
Normal file
11
internal/domain/statistics.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package domain
|
||||
|
||||
type Statistics struct {
|
||||
CertificateTotal int `json:"certificateTotal"`
|
||||
CertificateExpireSoon int `json:"certificateExpireSoon"`
|
||||
CertificateExpired int `json:"certificateExpired"`
|
||||
|
||||
WorkflowTotal int `json:"workflowTotal"`
|
||||
WorkflowEnabled int `json:"workflowEnabled"`
|
||||
WorkflowDisabled int `json:"workflowDisabled"`
|
||||
}
|
||||
184
internal/domain/workflow.go
Normal file
184
internal/domain/workflow.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
const CollectionNameWorkflow = "workflow"
|
||||
|
||||
type Workflow struct {
|
||||
Meta
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Trigger WorkflowTriggerType `json:"trigger" db:"trigger"`
|
||||
TriggerCron string `json:"triggerCron" db:"triggerCron"`
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
Content *WorkflowNode `json:"content" db:"content"`
|
||||
Draft *WorkflowNode `json:"draft" db:"draft"`
|
||||
HasDraft bool `json:"hasDraft" db:"hasDraft"`
|
||||
LastRunId string `json:"lastRunId" db:"lastRunId"`
|
||||
LastRunStatus WorkflowRunStatusType `json:"lastRunStatus" db:"lastRunStatus"`
|
||||
LastRunTime time.Time `json:"lastRunTime" db:"lastRunTime"`
|
||||
}
|
||||
|
||||
type WorkflowNodeType string
|
||||
|
||||
const (
|
||||
WorkflowNodeTypeStart = WorkflowNodeType("start")
|
||||
WorkflowNodeTypeEnd = WorkflowNodeType("end")
|
||||
WorkflowNodeTypeApply = WorkflowNodeType("apply")
|
||||
WorkflowNodeTypeUpload = WorkflowNodeType("upload")
|
||||
WorkflowNodeTypeDeploy = WorkflowNodeType("deploy")
|
||||
WorkflowNodeTypeNotify = WorkflowNodeType("notify")
|
||||
WorkflowNodeTypeBranch = WorkflowNodeType("branch")
|
||||
WorkflowNodeTypeCondition = WorkflowNodeType("condition")
|
||||
WorkflowNodeTypeExecuteResultBranch = WorkflowNodeType("execute_result_branch")
|
||||
WorkflowNodeTypeExecuteSuccess = WorkflowNodeType("execute_success")
|
||||
WorkflowNodeTypeExecuteFailure = WorkflowNodeType("execute_failure")
|
||||
)
|
||||
|
||||
type WorkflowTriggerType string
|
||||
|
||||
const (
|
||||
WorkflowTriggerTypeAuto = WorkflowTriggerType("auto")
|
||||
WorkflowTriggerTypeManual = WorkflowTriggerType("manual")
|
||||
)
|
||||
|
||||
type WorkflowNode struct {
|
||||
Id string `json:"id"`
|
||||
Type WorkflowNodeType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
|
||||
Config map[string]any `json:"config"`
|
||||
Inputs []WorkflowNodeIO `json:"inputs"`
|
||||
Outputs []WorkflowNodeIO `json:"outputs"`
|
||||
|
||||
Next *WorkflowNode `json:"next,omitempty"`
|
||||
Branches []WorkflowNode `json:"branches,omitempty"`
|
||||
|
||||
Validated bool `json:"validated"`
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForApply struct {
|
||||
Domains string `json:"domains"` // 域名列表,以半角逗号分隔
|
||||
ContactEmail string `json:"contactEmail"` // 联系邮箱
|
||||
ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01
|
||||
Provider string `json:"provider"` // DNS 提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置
|
||||
KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法
|
||||
Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角逗号分隔
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值)
|
||||
DnsTTL int32 `json:"dnsTTL"` // DNS TTL(零值取决于提供商的默认值)
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随
|
||||
DisableARI bool `json:"disableARI"` // 是否关闭 ARI
|
||||
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30)
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForUpload struct {
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Domains string `json:"domains"`
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForDeploy struct {
|
||||
Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate”
|
||||
Provider string `json:"provider"` // 主机提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // 主机提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig"` // 主机提供商额外配置
|
||||
SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForNotify struct {
|
||||
Channel string `json:"channel"` // 通知渠道
|
||||
Subject string `json:"subject"` // 通知主题
|
||||
Message string `json:"message"` // 通知内容
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigValueAsString(key string) string {
|
||||
return maps.GetValueAsString(n.Config, key)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigValueAsBool(key string) bool {
|
||||
return maps.GetValueAsBool(n.Config, key)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigValueAsInt32(key string) int32 {
|
||||
return maps.GetValueAsInt32(n.Config, key)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigValueAsMap(key string) map[string]any {
|
||||
if val, ok := n.Config[key]; ok {
|
||||
if result, ok := val.(map[string]any); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
||||
skipBeforeExpiryDays := n.getConfigValueAsInt32("skipBeforeExpiryDays")
|
||||
if skipBeforeExpiryDays == 0 {
|
||||
skipBeforeExpiryDays = 30
|
||||
}
|
||||
|
||||
return WorkflowNodeConfigForApply{
|
||||
Domains: n.getConfigValueAsString("domains"),
|
||||
ContactEmail: n.getConfigValueAsString("contactEmail"),
|
||||
Provider: n.getConfigValueAsString("provider"),
|
||||
ProviderAccessId: n.getConfigValueAsString("providerAccessId"),
|
||||
ProviderConfig: n.getConfigValueAsMap("providerConfig"),
|
||||
KeyAlgorithm: n.getConfigValueAsString("keyAlgorithm"),
|
||||
Nameservers: n.getConfigValueAsString("nameservers"),
|
||||
DnsPropagationTimeout: n.getConfigValueAsInt32("dnsPropagationTimeout"),
|
||||
DnsTTL: n.getConfigValueAsInt32("dnsTTL"),
|
||||
DisableFollowCNAME: n.getConfigValueAsBool("disableFollowCNAME"),
|
||||
DisableARI: n.getConfigValueAsBool("disableARI"),
|
||||
SkipBeforeExpiryDays: skipBeforeExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload {
|
||||
return WorkflowNodeConfigForUpload{
|
||||
Certificate: n.getConfigValueAsString("certificate"),
|
||||
PrivateKey: n.getConfigValueAsString("privateKey"),
|
||||
Domains: n.getConfigValueAsString("domains"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
|
||||
return WorkflowNodeConfigForDeploy{
|
||||
Certificate: n.getConfigValueAsString("certificate"),
|
||||
Provider: n.getConfigValueAsString("provider"),
|
||||
ProviderAccessId: n.getConfigValueAsString("providerAccessId"),
|
||||
ProviderConfig: n.getConfigValueAsMap("providerConfig"),
|
||||
SkipOnLastSucceeded: n.getConfigValueAsBool("skipOnLastSucceeded"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
|
||||
return WorkflowNodeConfigForNotify{
|
||||
Channel: n.getConfigValueAsString("channel"),
|
||||
Subject: n.getConfigValueAsString("subject"),
|
||||
Message: n.getConfigValueAsString("message"),
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowNodeIO struct {
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Value any `json:"value"`
|
||||
ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"`
|
||||
}
|
||||
|
||||
type WorkflowNodeIOValueSelector struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
const WorkflowNodeIONameCertificate string = "certificate"
|
||||
13
internal/domain/workflow_output.go
Normal file
13
internal/domain/workflow_output.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package domain
|
||||
|
||||
const CollectionNameWorkflowOutput = "workflow_output"
|
||||
|
||||
type WorkflowOutput struct {
|
||||
Meta
|
||||
WorkflowId string `json:"workflowId" db:"workflow"`
|
||||
RunId string `json:"runId" db:"runId"`
|
||||
NodeId string `json:"nodeId" db:"nodeId"`
|
||||
Node *WorkflowNode `json:"node" db:"node"`
|
||||
Outputs []WorkflowNodeIO `json:"outputs" db:"outputs"`
|
||||
Succeeded bool `json:"succeeded" db:"succeeded"`
|
||||
}
|
||||
65
internal/domain/workflow_run.go
Normal file
65
internal/domain/workflow_run.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CollectionNameWorkflowRun = "workflow_run"
|
||||
|
||||
type WorkflowRun struct {
|
||||
Meta
|
||||
WorkflowId string `json:"workflowId" db:"workflowId"`
|
||||
Status WorkflowRunStatusType `json:"status" db:"status"`
|
||||
Trigger WorkflowTriggerType `json:"trigger" db:"trigger"`
|
||||
StartedAt time.Time `json:"startedAt" db:"startedAt"`
|
||||
EndedAt time.Time `json:"endedAt" db:"endedAt"`
|
||||
Logs []WorkflowRunLog `json:"logs" db:"logs"`
|
||||
Error string `json:"error" db:"error"`
|
||||
}
|
||||
|
||||
type WorkflowRunStatusType string
|
||||
|
||||
const (
|
||||
WorkflowRunStatusTypePending WorkflowRunStatusType = "pending"
|
||||
WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running"
|
||||
WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded"
|
||||
WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed"
|
||||
WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled"
|
||||
)
|
||||
|
||||
type WorkflowRunLog struct {
|
||||
NodeId string `json:"nodeId"`
|
||||
NodeName string `json:"nodeName"`
|
||||
Records []WorkflowRunLogRecord `json:"records"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type WorkflowRunLogRecord struct {
|
||||
Time string `json:"time"`
|
||||
Level WorkflowRunLogLevel `json:"level"`
|
||||
Content string `json:"content"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type WorkflowRunLogLevel string
|
||||
|
||||
const (
|
||||
WorkflowRunLogLevelDebug WorkflowRunLogLevel = "DEBUG"
|
||||
WorkflowRunLogLevelInfo WorkflowRunLogLevel = "INFO"
|
||||
WorkflowRunLogLevelWarn WorkflowRunLogLevel = "WARN"
|
||||
WorkflowRunLogLevelError WorkflowRunLogLevel = "ERROR"
|
||||
)
|
||||
|
||||
type WorkflowRunLogs []WorkflowRunLog
|
||||
|
||||
func (r WorkflowRunLogs) ErrorString() string {
|
||||
var builder strings.Builder
|
||||
for _, log := range r {
|
||||
if log.Error != "" {
|
||||
builder.WriteString(log.Error)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/deployer"
|
||||
"certimate/internal/utils/app"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
checkPhase Phase = "check"
|
||||
applyPhase Phase = "apply"
|
||||
deployPhase Phase = "deploy"
|
||||
)
|
||||
|
||||
func deploy(ctx context.Context, record *models.Record) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
app.GetApp().Logger().Error("部署失败", "err", r)
|
||||
}
|
||||
}()
|
||||
var certificate *applicant.Certificate
|
||||
|
||||
history := NewHistory(record)
|
||||
defer history.commit()
|
||||
|
||||
// ############1.检查域名配置
|
||||
history.record(checkPhase, "开始检查", nil)
|
||||
|
||||
currRecord, err := app.GetApp().Dao().FindRecordById("domains", record.Id)
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("获取记录失败", "err", err)
|
||||
history.record(checkPhase, "获取域名配置失败", &RecordInfo{Err: err})
|
||||
return err
|
||||
}
|
||||
history.record(checkPhase, "获取记录成功", nil)
|
||||
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess"}, nil); len(errs) > 0 {
|
||||
|
||||
errList := make([]error, 0)
|
||||
for name, err := range errs {
|
||||
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
|
||||
}
|
||||
err = errors.Join(errList...)
|
||||
app.GetApp().Logger().Error("展开记录失败", "err", err)
|
||||
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
|
||||
return err
|
||||
}
|
||||
history.record(checkPhase, "获取授权信息成功", nil)
|
||||
|
||||
cert := currRecord.GetString("certificate")
|
||||
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
|
||||
app.GetApp().Logger().Info("证书在有效期内")
|
||||
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
}, true)
|
||||
return err
|
||||
}
|
||||
history.record(checkPhase, "检查通过", nil, true)
|
||||
|
||||
// ############2.申请证书
|
||||
history.record(applyPhase, "开始申请", nil)
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
|
||||
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
})
|
||||
} else {
|
||||
applicant, err := applicant.Get(currRecord)
|
||||
if err != nil {
|
||||
history.record(applyPhase, "获取applicant失败", &RecordInfo{Err: err})
|
||||
app.GetApp().Logger().Error("获取applicant失败", "err", err)
|
||||
return err
|
||||
}
|
||||
certificate, err = applicant.Apply()
|
||||
if err != nil {
|
||||
history.record(applyPhase, "申请证书失败", &RecordInfo{Err: err})
|
||||
app.GetApp().Logger().Error("申请证书失败", "err", err)
|
||||
return err
|
||||
}
|
||||
history.record(applyPhase, "申请证书成功", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书地址: %s", certificate.CertUrl)},
|
||||
})
|
||||
history.setCert(certificate)
|
||||
}
|
||||
|
||||
history.record(applyPhase, "保存证书成功", nil, true)
|
||||
|
||||
// ############3.部署证书
|
||||
history.record(deployPhase, "开始部署", nil, false)
|
||||
deployer, err := deployer.Get(currRecord, certificate)
|
||||
if err != nil {
|
||||
history.record(deployPhase, "获取deployer失败", &RecordInfo{Err: err})
|
||||
app.GetApp().Logger().Error("获取deployer失败", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = deployer.Deploy(ctx); err != nil {
|
||||
|
||||
app.GetApp().Logger().Error("部署失败", "err", err)
|
||||
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
|
||||
return err
|
||||
}
|
||||
|
||||
app.GetApp().Logger().Info("部署成功")
|
||||
history.record(deployPhase, "部署成功", &RecordInfo{
|
||||
Info: deployer.GetInfo(),
|
||||
}, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func create(ctx context.Context, record *models.Record) error {
|
||||
if !record.GetBool("enabled") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if record.GetBool("rightnow") {
|
||||
go func() {
|
||||
if err := deploy(ctx, record); err != nil {
|
||||
app.GetApp().Logger().Error("deploy failed", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
scheduler := app.GetScheduler()
|
||||
|
||||
err := scheduler.Add(record.Id, record.GetString("crontab"), func() {
|
||||
deploy(ctx, record)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("add cron job failed", "err", err)
|
||||
return fmt.Errorf("add cron job failed: %w", err)
|
||||
}
|
||||
app.GetApp().Logger().Error("add cron job failed", "domain", record.GetString("domain"))
|
||||
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func update(ctx context.Context, record *models.Record) error {
|
||||
scheduler := app.GetScheduler()
|
||||
scheduler.Remove(record.Id)
|
||||
if !record.GetBool("enabled") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if record.GetBool("rightnow") {
|
||||
|
||||
go func() {
|
||||
if err := deploy(ctx, record); err != nil {
|
||||
app.GetApp().Logger().Error("deploy failed", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
err := scheduler.Add(record.Id, record.GetString("crontab"), func() {
|
||||
deploy(ctx, record)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("update cron job failed", "err", err)
|
||||
return fmt.Errorf("update cron job failed: %w", err)
|
||||
}
|
||||
app.GetApp().Logger().Info("update cron job success", "domain", record.GetString("domain"))
|
||||
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func delete(_ context.Context, record *models.Record) error {
|
||||
scheduler := app.GetScheduler()
|
||||
|
||||
scheduler.Remove(record.Id)
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRightnow(ctx context.Context, record *models.Record, ok bool) error {
|
||||
record.Set("rightnow", ok)
|
||||
return app.GetApp().Dao().SaveRecord(record)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
const tableName = "domains"
|
||||
|
||||
func AddEvent() error {
|
||||
app := app.GetApp()
|
||||
|
||||
app.OnRecordAfterCreateRequest(tableName).Add(func(e *core.RecordCreateEvent) error {
|
||||
return create(e.HttpContext.Request().Context(), e.Record)
|
||||
})
|
||||
|
||||
app.OnRecordAfterUpdateRequest(tableName).Add(func(e *core.RecordUpdateEvent) error {
|
||||
return update(e.HttpContext.Request().Context(), e.Record)
|
||||
})
|
||||
|
||||
app.OnRecordAfterDeleteRequest(tableName).Add(func(e *core.RecordDeleteEvent) error {
|
||||
return delete(e.HttpContext.Request().Context(), e.Record)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/utils/app"
|
||||
"certimate/internal/utils/xtime"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
type historyItem struct {
|
||||
Time string `json:"time"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error"`
|
||||
Info []string `json:"info"`
|
||||
}
|
||||
|
||||
type RecordInfo struct {
|
||||
Err error `json:"err"`
|
||||
Info []string `json:"info"`
|
||||
}
|
||||
|
||||
type history struct {
|
||||
Domain string `json:"domain"`
|
||||
Log map[Phase][]historyItem `json:"log"`
|
||||
Phase Phase `json:"phase"`
|
||||
PhaseSuccess bool `json:"phaseSuccess"`
|
||||
DeployedAt string `json:"deployedAt"`
|
||||
Cert *applicant.Certificate `json:"cert"`
|
||||
}
|
||||
|
||||
func NewHistory(record *models.Record) *history {
|
||||
return &history{
|
||||
Domain: record.Id,
|
||||
DeployedAt: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
Log: make(map[Phase][]historyItem),
|
||||
Phase: checkPhase,
|
||||
PhaseSuccess: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *history) record(phase Phase, msg string, info *RecordInfo, pass ...bool) {
|
||||
if info == nil {
|
||||
info = &RecordInfo{}
|
||||
}
|
||||
a.Phase = phase
|
||||
if len(pass) > 0 {
|
||||
a.PhaseSuccess = pass[0]
|
||||
}
|
||||
|
||||
errMsg := ""
|
||||
if info.Err != nil {
|
||||
errMsg = info.Err.Error()
|
||||
a.PhaseSuccess = false
|
||||
}
|
||||
|
||||
a.Log[phase] = append(a.Log[phase], historyItem{
|
||||
Message: msg,
|
||||
Error: errMsg,
|
||||
Info: info.Info,
|
||||
Time: xtime.BeijingTimeStr(),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a *history) setCert(cert *applicant.Certificate) {
|
||||
a.Cert = cert
|
||||
}
|
||||
|
||||
func (a *history) commit() error {
|
||||
collection, err := app.GetApp().Dao().FindCollectionByNameOrId("deployments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := models.NewRecord(collection)
|
||||
|
||||
record.Set("domain", a.Domain)
|
||||
record.Set("deployedAt", a.DeployedAt)
|
||||
record.Set("log", a.Log)
|
||||
record.Set("phase", string(a.Phase))
|
||||
record.Set("phaseSuccess", a.PhaseSuccess)
|
||||
|
||||
if err := app.GetApp().Dao().SaveRecord(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
domainRecord, err := app.GetApp().Dao().FindRecordById("domains", a.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
domainRecord.Set("lastDeployedAt", a.DeployedAt)
|
||||
domainRecord.Set("lastDeployment", record.Id)
|
||||
domainRecord.Set("rightnow", false)
|
||||
if a.Phase == deployPhase && a.PhaseSuccess {
|
||||
domainRecord.Set("deployed", true)
|
||||
}
|
||||
cert := a.Cert
|
||||
if cert != nil {
|
||||
domainRecord.Set("certUrl", cert.CertUrl)
|
||||
domainRecord.Set("certStableUrl", cert.CertStableUrl)
|
||||
domainRecord.Set("privateKey", cert.PrivateKey)
|
||||
domainRecord.Set("certificate", cert.Certificate)
|
||||
domainRecord.Set("issuerCertificate", cert.IssuerCertificate)
|
||||
domainRecord.Set("csr", cert.Csr)
|
||||
domainRecord.Set("expiredAt", time.Now().Add(time.Hour*24*90))
|
||||
}
|
||||
|
||||
if err := app.GetApp().Dao().SaveRecord(domainRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"certimate/internal/utils/app"
|
||||
"context"
|
||||
)
|
||||
|
||||
func InitSchedule() {
|
||||
// 查询所有启用的域名
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "enabled=true", "-id", 500, 0)
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("查询所有启用的域名失败", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 加入到定时任务
|
||||
for _, record := range records {
|
||||
if err := app.GetScheduler().Add(record.Id, record.GetString("crontab"), func() {
|
||||
if err := deploy(context.Background(), record); err != nil {
|
||||
app.GetApp().Logger().Error("部署失败", "err", err)
|
||||
return
|
||||
}
|
||||
}); err != nil {
|
||||
app.GetApp().Logger().Error("加入到定时任务失败", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动定时任务
|
||||
app.GetScheduler().Start()
|
||||
app.GetApp().Logger().Info("定时任务启动成功", "total", app.GetScheduler().Total())
|
||||
|
||||
}
|
||||
78
internal/notify/notify.go
Normal file
78
internal/notify/notify.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
)
|
||||
|
||||
func SendToAllChannels(subject, message string) error {
|
||||
notifiers, err := getEnabledNotifiers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(notifiers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var eg errgroup.Group
|
||||
for _, n := range notifiers {
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
_, err := n.Notify(context.Background(), subject, message)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
|
||||
notifier, err := createNotifier(domain.NotifyChannelType(channel), channelConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = notifier.Notify(context.Background(), subject, message)
|
||||
return err
|
||||
}
|
||||
|
||||
func getEnabledNotifiers() ([]notifier.Notifier, error) {
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
settings, err := settingsRepo.GetByName(context.Background(), "notifyChannels")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find notifyChannels error: %w", err)
|
||||
}
|
||||
|
||||
rs := make(map[string]map[string]any)
|
||||
if err := json.Unmarshal([]byte(settings.Content), &rs); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err)
|
||||
}
|
||||
|
||||
notifiers := make([]notifier.Notifier, 0)
|
||||
for k, v := range rs {
|
||||
if !maps.GetValueAsBool(v, "enabled") {
|
||||
continue
|
||||
}
|
||||
|
||||
notifier, err := createNotifier(domain.NotifyChannelType(k), v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
notifiers = append(notifiers, notifier)
|
||||
}
|
||||
|
||||
return notifiers, nil
|
||||
}
|
||||
76
internal/notify/providers.go
Normal file
76
internal/notify/providers.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
|
||||
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
|
||||
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
|
||||
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
|
||||
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
|
||||
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
|
||||
pWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]any) (notifier.Notifier, error) {
|
||||
/*
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
switch channel {
|
||||
case domain.NotifyChannelTypeBark:
|
||||
return pBark.NewNotifier(&pBark.NotifierConfig{
|
||||
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
|
||||
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeDingTalk:
|
||||
return pDingTalk.NewNotifier(&pDingTalk.NotifierConfig{
|
||||
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
|
||||
Secret: maps.GetValueAsString(channelConfig, "secret"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeEmail:
|
||||
return pEmail.NewNotifier(&pEmail.NotifierConfig{
|
||||
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
|
||||
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
|
||||
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
|
||||
Username: maps.GetValueOrDefaultAsString(channelConfig, "username", maps.GetValueAsString(channelConfig, "senderAddress")),
|
||||
Password: maps.GetValueAsString(channelConfig, "password"),
|
||||
SenderAddress: maps.GetValueAsString(channelConfig, "senderAddress"),
|
||||
ReceiverAddress: maps.GetValueAsString(channelConfig, "receiverAddress"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeLark:
|
||||
return pLark.NewNotifier(&pLark.NotifierConfig{
|
||||
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeServerChan:
|
||||
return pServerChan.NewNotifier(&pServerChan.NotifierConfig{
|
||||
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeTelegram:
|
||||
return pTelegram.NewNotifier(&pTelegram.NotifierConfig{
|
||||
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
|
||||
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeWebhook:
|
||||
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
|
||||
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeWeCom:
|
||||
return pWeCom.NewNotifier(&pWeCom.NotifierConfig{
|
||||
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
|
||||
})
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported notifier channel: %s", channelConfig)
|
||||
}
|
||||
42
internal/notify/service.go
Normal file
42
internal/notify/service.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/domain/dtos"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyTestTitle = "测试通知"
|
||||
notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。"
|
||||
)
|
||||
|
||||
type settingsRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Settings, error)
|
||||
}
|
||||
|
||||
type NotifyService struct {
|
||||
settingsRepo settingsRepository
|
||||
}
|
||||
|
||||
func NewNotifyService(settingsRepo settingsRepository) *NotifyService {
|
||||
return &NotifyService{
|
||||
settingsRepo: settingsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NotifyService) Test(ctx context.Context, req *dtos.NotifyTestPushReq) error {
|
||||
settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get notify channels settings: %w", err)
|
||||
}
|
||||
|
||||
channelConfig, err := settings.GetNotifyChannelConfig(string(req.Channel))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
|
||||
}
|
||||
|
||||
return SendToChannel(notifyTestTitle, notifyTestBody, string(req.Channel), channelConfig)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package acmehttpreq
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/httpreq"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Mode string `json:"mode"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
endpoint, _ := url.Parse(config.Endpoint)
|
||||
providerConfig := httpreq.NewDefaultConfig()
|
||||
providerConfig.Endpoint = endpoint
|
||||
providerConfig.Mode = config.Mode
|
||||
providerConfig.Username = config.Username
|
||||
providerConfig.Password = config.Password
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
|
||||
provider, err := httpreq.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package aliyun
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := alidns.NewDefaultConfig()
|
||||
providerConfig.APIKey = config.AccessKeyId
|
||||
providerConfig.SecretKey = config.AccessKeySecret
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := alidns.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package awsroute53
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/route53"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
Region string `json:"region"`
|
||||
HostedZoneId string `json:"hostedZoneId"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := route53.NewDefaultConfig()
|
||||
providerConfig.AccessKeyID = config.AccessKeyId
|
||||
providerConfig.SecretAccessKey = config.SecretAccessKey
|
||||
providerConfig.Region = config.Region
|
||||
providerConfig.HostedZoneID = config.HostedZoneId
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := route53.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package azuredns
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azuredns"
|
||||
|
||||
azcommon "github.com/usual2970/certimate/internal/pkg/vendors/azure-sdk/common"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
TenantId string `json:"tenantId"`
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
CloudName string `json:"cloudName,omitempty"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := azuredns.NewDefaultConfig()
|
||||
providerConfig.TenantID = config.TenantId
|
||||
providerConfig.ClientID = config.ClientId
|
||||
providerConfig.ClientSecret = config.ClientSecret
|
||||
if config.CloudName != "" {
|
||||
env, err := azcommon.GetCloudEnvironmentConfiguration(config.CloudName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providerConfig.Environment = env
|
||||
}
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := azuredns.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package baiducloud
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := internal.NewDefaultConfig()
|
||||
providerConfig.AccessKeyID = config.AccessKeyId
|
||||
providerConfig.SecretAccessKey = config.SecretAccessKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = config.DnsTTL
|
||||
}
|
||||
|
||||
provider, err := internal.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package lego_baiducloud
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bceDns "github.com/baidubce/bce-sdk-go/services/dns"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
envNamespace = "BAIDUCLOUD_"
|
||||
|
||||
EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
|
||||
EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
|
||||
|
||||
type Config struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int32
|
||||
HTTPTimeout time.Duration
|
||||
}
|
||||
|
||||
type DNSProvider struct {
|
||||
client *bceDns.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("baiducloud: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.AccessKeyID = values[EnvAccessKeyID]
|
||||
config.SecretAccessKey = values[EnvSecretAccessKey]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("baiducloud: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client, err := bceDns.NewClient(config.AccessKeyID, config.SecretAccessKey, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if client.Config != nil {
|
||||
client.Config.ConnectionTimeoutInMillis = int(config.HTTPTimeout.Milliseconds())
|
||||
}
|
||||
}
|
||||
|
||||
return &DNSProvider{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("baiducloud: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("baiducloud: %w", err)
|
||||
}
|
||||
|
||||
if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil {
|
||||
return fmt.Errorf("baiducloud: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("baiducloud: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("baiducloud: %w", err)
|
||||
}
|
||||
|
||||
if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil {
|
||||
return fmt.Errorf("baiducloud: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*bceDns.Record, error) {
|
||||
pageMarker := ""
|
||||
pageSize := 1000
|
||||
for {
|
||||
request := &bceDns.ListRecordRequest{}
|
||||
request.Rr = subDomain
|
||||
request.Marker = pageMarker
|
||||
request.MaxKeys = pageSize
|
||||
|
||||
response, err := d.client.ListRecord(zoneName, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, record := range response.Records {
|
||||
if record.Type == "TXT" && record.Rr == subDomain {
|
||||
return &record, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(response.Records) < pageSize {
|
||||
break
|
||||
}
|
||||
|
||||
pageMarker = response.NextMarker
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error {
|
||||
record, err := d.getDNSRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
request := &bceDns.CreateRecordRequest{
|
||||
Type: "TXT",
|
||||
Rr: subDomain,
|
||||
Value: value,
|
||||
Ttl: &d.config.TTL,
|
||||
}
|
||||
err := d.client.CreateRecord(zoneName, request, d.generateClientToken())
|
||||
return err
|
||||
} else {
|
||||
request := &bceDns.UpdateRecordRequest{
|
||||
Type: "TXT",
|
||||
Rr: subDomain,
|
||||
Value: value,
|
||||
Ttl: &d.config.TTL,
|
||||
}
|
||||
err := d.client.UpdateRecord(zoneName, record.Id, request, d.generateClientToken())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error {
|
||||
record, err := d.getDNSRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
return nil
|
||||
} else {
|
||||
err = d.client.DeleteRecord(zoneName, record.Id, d.generateClientToken())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSProvider) generateClientToken() string {
|
||||
return strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cloudflare
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := cloudflare.NewDefaultConfig()
|
||||
providerConfig.AuthToken = config.DnsApiToken
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := cloudflare.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cloudns
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudns"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AuthId string `json:"authId"`
|
||||
AuthPassword string `json:"authPassword"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := cloudns.NewDefaultConfig()
|
||||
providerConfig.AuthID = config.AuthId
|
||||
providerConfig.AuthPassword = config.AuthPassword
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := cloudns.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cmcccloud
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := internal.NewDefaultConfig()
|
||||
providerConfig.AccessKey = config.AccessKeyId
|
||||
providerConfig.SecretKey = config.AccessKeySecret
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = config.DnsTTL
|
||||
}
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
|
||||
provider, err := internal.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
"gitlab.ecloud.com/ecloud/ecloudsdkclouddns"
|
||||
"gitlab.ecloud.com/ecloud/ecloudsdkclouddns/model"
|
||||
"gitlab.ecloud.com/ecloud/ecloudsdkcore/config"
|
||||
)
|
||||
|
||||
const (
|
||||
envNamespace = "CMCCCLOUD_"
|
||||
|
||||
EnvAccessKey = envNamespace + "ACCESS_KEY"
|
||||
EnvSecretKey = envNamespace + "SECRET_KEY"
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvReadTimeOut = envNamespace + "READ_TIMEOUT"
|
||||
EnvConnectTimeout = envNamespace + "CONNECT_TIMEOUT"
|
||||
)
|
||||
|
||||
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
|
||||
|
||||
type Config struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
ReadTimeOut int
|
||||
ConnectTimeout int
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int32
|
||||
}
|
||||
|
||||
type DNSProvider struct {
|
||||
client *ecloudsdkclouddns.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
ReadTimeOut: env.GetOrDefaultInt(EnvReadTimeOut, 30),
|
||||
ConnectTimeout: env.GetOrDefaultInt(EnvConnectTimeout, 30),
|
||||
TTL: int32(env.GetOrDefaultInt(EnvTTL, 600)),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAccessKey, EnvSecretKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cmccecloud: %w", err)
|
||||
}
|
||||
|
||||
cfg := NewDefaultConfig()
|
||||
cfg.AccessKey = values[EnvAccessKey]
|
||||
cfg.SecretKey = values[EnvSecretKey]
|
||||
|
||||
return NewDNSProviderConfig(cfg)
|
||||
}
|
||||
|
||||
func NewDNSProviderConfig(cfg *Config) (*DNSProvider, error) {
|
||||
if cfg == nil {
|
||||
return nil, errors.New("cmccecloud: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client := ecloudsdkclouddns.NewClient(&config.Config{
|
||||
AccessKey: cfg.AccessKey,
|
||||
SecretKey: cfg.SecretKey,
|
||||
// 资源池常量见: https://ecloud.10086.cn/op-help-center/doc/article/54462
|
||||
// 默认全局
|
||||
PoolId: "CIDC-CORE-00",
|
||||
ReadTimeOut: cfg.ReadTimeOut,
|
||||
ConnectTimeout: cfg.ConnectTimeout,
|
||||
})
|
||||
|
||||
return &DNSProvider{
|
||||
client: client,
|
||||
config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
zoneName, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmccecloud: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmccecloud: %w", err)
|
||||
}
|
||||
|
||||
readDomain := strings.Trim(zoneName, ".")
|
||||
record, err := d.getDomainRecord(readDomain, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record == nil {
|
||||
// add new record
|
||||
resp, err := d.client.CreateRecordOpenapi(&model.CreateRecordOpenapiRequest{
|
||||
CreateRecordOpenapiBody: &model.CreateRecordOpenapiBody{
|
||||
LineId: "0", // 默认线路
|
||||
Rr: subDomain,
|
||||
DomainName: readDomain,
|
||||
Description: "from certimate",
|
||||
Type: model.CreateRecordOpenapiBodyTypeEnumTxt,
|
||||
Value: info.Value,
|
||||
Ttl: &d.config.TTL,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("lego: %w", err)
|
||||
}
|
||||
if resp.State != model.CreateRecordOpenapiResponseStateEnumOk {
|
||||
return fmt.Errorf("lego: create record failed, response state: %s, message: %s, code: %s", resp.State, resp.ErrorMessage, resp.ErrorCode)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
// update record
|
||||
resp, err := d.client.ModifyRecordOpenapi(&model.ModifyRecordOpenapiRequest{
|
||||
ModifyRecordOpenapiBody: &model.ModifyRecordOpenapiBody{
|
||||
RecordId: record.RecordId,
|
||||
Rr: subDomain,
|
||||
DomainName: readDomain,
|
||||
Description: "from certmate",
|
||||
LineId: "0",
|
||||
Type: model.ModifyRecordOpenapiBodyTypeEnumTxt,
|
||||
Value: info.Value,
|
||||
Ttl: &d.config.TTL,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("lego: %w", err)
|
||||
}
|
||||
if resp.State != model.ModifyRecordOpenapiResponseStateEnumOk {
|
||||
return fmt.Errorf("lego: create record failed, response state: %s", resp.State)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
challengeInfo := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
zoneName, err := dns01.FindZoneByFqdn(challengeInfo.FQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmccecloud: %w", err)
|
||||
}
|
||||
subDomain, err := dns01.ExtractSubDomain(challengeInfo.FQDN, zoneName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmccecloud: %w", err)
|
||||
}
|
||||
readDomain := strings.Trim(zoneName, ".")
|
||||
record, err := d.getDomainRecord(readDomain, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record == nil {
|
||||
return nil
|
||||
}
|
||||
resp, err := d.client.DeleteRecordOpenapi(&model.DeleteRecordOpenapiRequest{
|
||||
DeleteRecordOpenapiBody: &model.DeleteRecordOpenapiBody{
|
||||
RecordIdList: []string{record.RecordId},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("lego: %w", err)
|
||||
}
|
||||
if resp.State != model.DeleteRecordOpenapiResponseStateEnumOk {
|
||||
return fmt.Errorf("lego: delete record failed, response state: %s", resp.State)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getDomainRecord(domain string, rr string) (*model.ListRecordOpenapiResponseData, error) {
|
||||
pageSize := int32(50)
|
||||
page := int32(1)
|
||||
for {
|
||||
resp, err := d.client.ListRecordOpenapi(&model.ListRecordOpenapiRequest{
|
||||
ListRecordOpenapiBody: &model.ListRecordOpenapiBody{
|
||||
DomainName: domain,
|
||||
},
|
||||
ListRecordOpenapiQuery: &model.ListRecordOpenapiQuery{
|
||||
PageSize: &pageSize,
|
||||
Page: &page,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.State != model.ListRecordOpenapiResponseStateEnumOk {
|
||||
respStr, _ := json.Marshal(resp)
|
||||
return nil, fmt.Errorf("request error. %s", string(respStr))
|
||||
}
|
||||
if resp.Body.Data != nil {
|
||||
for _, item := range *resp.Body.Data {
|
||||
if item.Rr == rr {
|
||||
return &item, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.Body.TotalPages == nil || page >= *resp.Body.TotalPages {
|
||||
return nil, nil
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package dnsla
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla/internal"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
ApiId string `json:"apiId"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := internal.NewDefaultConfig()
|
||||
providerConfig.APIId = config.ApiId
|
||||
providerConfig.APISecret = config.ApiSecret
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := internal.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package lego_dnsla
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
|
||||
dnslasdk "github.com/usual2970/certimate/internal/pkg/vendors/dnsla-sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
envNamespace = "DNSLA_"
|
||||
|
||||
EnvAPIId = envNamespace + "API_ID"
|
||||
EnvAPISecret = envNamespace + "API_KEY"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
|
||||
|
||||
type Config struct {
|
||||
APIId string
|
||||
APISecret string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPTimeout time.Duration
|
||||
}
|
||||
|
||||
type DNSProvider struct {
|
||||
client *dnslasdk.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, 300),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAPIId, EnvAPISecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dnsla: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.APIId = values[EnvAPIId]
|
||||
config.APISecret = values[EnvAPISecret]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("dnsla: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client := dnslasdk.NewClient(config.APIId, config.APISecret).
|
||||
WithTimeout(config.HTTPTimeout)
|
||||
|
||||
return &DNSProvider{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnsla: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnsla: %w", err)
|
||||
}
|
||||
|
||||
if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil {
|
||||
return fmt.Errorf("dnsla: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnsla: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnsla: %w", err)
|
||||
}
|
||||
|
||||
if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil {
|
||||
return fmt.Errorf("dnsla: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getDNSZone(zoneName string) (*dnslasdk.DomainInfo, error) {
|
||||
pageIndex := 1
|
||||
pageSize := 100
|
||||
for {
|
||||
request := &dnslasdk.ListDomainsRequest{
|
||||
PageIndex: int32(pageIndex),
|
||||
PageSize: int32(pageSize),
|
||||
}
|
||||
response, err := d.client.ListDomains(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.Data != nil {
|
||||
for _, item := range response.Data.Results {
|
||||
if strings.TrimRight(item.Domain, ".") == zoneName || strings.TrimRight(item.DisplayDomain, ".") == zoneName {
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if response.Data == nil || len(response.Data.Results) < pageSize {
|
||||
break
|
||||
}
|
||||
|
||||
pageIndex++
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("dnsla: zone %s not found", zoneName)
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getDNSZoneAndRecord(zoneName, subDomain string) (*dnslasdk.DomainInfo, *dnslasdk.RecordInfo, error) {
|
||||
zone, err := d.getDNSZone(zoneName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pageIndex := 1
|
||||
pageSize := 100
|
||||
for {
|
||||
request := &dnslasdk.ListRecordsRequest{
|
||||
DomainId: zone.Id,
|
||||
Host: &subDomain,
|
||||
PageIndex: int32(pageIndex),
|
||||
PageSize: int32(pageSize),
|
||||
}
|
||||
response, err := d.client.ListRecords(request)
|
||||
if err != nil {
|
||||
return zone, nil, err
|
||||
}
|
||||
|
||||
if response.Data != nil {
|
||||
for _, record := range response.Data.Results {
|
||||
if record.Type == 16 && (record.Host == subDomain || record.DisplayHost == subDomain) {
|
||||
return zone, record, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if response.Data == nil || len(response.Data.Results) < pageSize {
|
||||
break
|
||||
}
|
||||
|
||||
pageIndex++
|
||||
}
|
||||
|
||||
return zone, nil, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error {
|
||||
zone, record, err := d.getDNSZoneAndRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
request := &dnslasdk.CreateRecordRequest{
|
||||
DomainId: zone.Id,
|
||||
Type: 16,
|
||||
Host: subDomain,
|
||||
Data: value,
|
||||
Ttl: int32(d.config.TTL),
|
||||
}
|
||||
_, err := d.client.CreateRecord(request)
|
||||
return err
|
||||
} else {
|
||||
reqType := int32(16)
|
||||
reqTtl := int32(d.config.TTL)
|
||||
request := &dnslasdk.UpdateRecordRequest{
|
||||
Id: record.Id,
|
||||
Type: &reqType,
|
||||
Host: &subDomain,
|
||||
Data: &value,
|
||||
Ttl: &reqTtl,
|
||||
}
|
||||
_, err := d.client.UpdateRecord(request)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error {
|
||||
_, record, err := d.getDNSZoneAndRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
return nil
|
||||
} else {
|
||||
request := &dnslasdk.DeleteRecordRequest{
|
||||
Id: record.Id,
|
||||
}
|
||||
_, err = d.client.DeleteRecord(request)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package gcore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gcore"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := gcore.NewDefaultConfig()
|
||||
providerConfig.APIToken = config.ApiToken
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := gcore.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package gname
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AppId string `json:"appId"`
|
||||
AppKey string `json:"appKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := internal.NewDefaultConfig()
|
||||
providerConfig.AppID = config.AppId
|
||||
providerConfig.AppKey = config.AppKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := internal.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package lego_gname
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
|
||||
gnamesdk "github.com/usual2970/certimate/internal/pkg/vendors/gname-sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
envNamespace = "GNAME_"
|
||||
|
||||
EnvAppID = envNamespace + "APP_ID"
|
||||
EnvAppKey = envNamespace + "APP_KEY"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
|
||||
|
||||
type Config struct {
|
||||
AppID string
|
||||
AppKey string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPTimeout time.Duration
|
||||
}
|
||||
|
||||
type DNSProvider struct {
|
||||
client *gnamesdk.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, 300),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAppID, EnvAppKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gname: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.AppID = values[EnvAppID]
|
||||
config.AppKey = values[EnvAppKey]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("gname: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client := gnamesdk.NewClient(config.AppID, config.AppKey).
|
||||
WithTimeout(config.HTTPTimeout)
|
||||
|
||||
return &DNSProvider{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gname: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gname: %w", err)
|
||||
}
|
||||
|
||||
if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil {
|
||||
return fmt.Errorf("gname: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gname: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gname: %w", err)
|
||||
}
|
||||
|
||||
if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil {
|
||||
return fmt.Errorf("gname: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*gnamesdk.ResolutionRecord, error) {
|
||||
page := 1
|
||||
pageSize := 20
|
||||
for {
|
||||
request := &gnamesdk.ListDomainResolutionRequest{}
|
||||
request.ZoneName = zoneName
|
||||
request.Page = &page
|
||||
request.PageSize = &pageSize
|
||||
|
||||
response, err := d.client.ListDomainResolution(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, record := range response.Data {
|
||||
if record.RecordType == "TXT" && record.RecordName == subDomain {
|
||||
return record, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(response.Data) == 0 {
|
||||
break
|
||||
}
|
||||
if response.Page*response.PageSize >= response.Count {
|
||||
break
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error {
|
||||
record, err := d.getDNSRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
request := &gnamesdk.AddDomainResolutionRequest{
|
||||
ZoneName: zoneName,
|
||||
RecordType: "TXT",
|
||||
RecordName: subDomain,
|
||||
RecordValue: value,
|
||||
TTL: d.config.TTL,
|
||||
}
|
||||
_, err := d.client.AddDomainResolution(request)
|
||||
return err
|
||||
} else {
|
||||
request := &gnamesdk.ModifyDomainResolutionRequest{
|
||||
ID: record.ID,
|
||||
ZoneName: zoneName,
|
||||
RecordType: "TXT",
|
||||
RecordName: subDomain,
|
||||
RecordValue: value,
|
||||
TTL: d.config.TTL,
|
||||
}
|
||||
_, err := d.client.ModifyDomainResolution(request)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error {
|
||||
record, err := d.getDNSRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
request := &gnamesdk.DeleteDomainResolutionRequest{
|
||||
ZoneName: zoneName,
|
||||
RecordID: record.ID,
|
||||
}
|
||||
_, err = d.client.DeleteDomainResolution(request)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package godaddy
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := godaddy.NewDefaultConfig()
|
||||
providerConfig.APIKey = config.ApiKey
|
||||
providerConfig.APISecret = config.ApiSecret
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := godaddy.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package huaweicloud
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
hwc "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
Region string `json:"region"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
region := config.Region
|
||||
if region == "" {
|
||||
// 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
||||
region = "cn-north-1"
|
||||
}
|
||||
|
||||
providerConfig := hwc.NewDefaultConfig()
|
||||
providerConfig.AccessKeyID = config.AccessKeyId
|
||||
providerConfig.SecretAccessKey = config.SecretAccessKey
|
||||
providerConfig.Region = region
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = config.DnsTTL
|
||||
}
|
||||
|
||||
provider, err := hwc.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package lego_jdcloud
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
jdCore "github.com/jdcloud-api/jdcloud-sdk-go/core"
|
||||
jdDnsApi "github.com/jdcloud-api/jdcloud-sdk-go/services/domainservice/apis"
|
||||
jdDnsClient "github.com/jdcloud-api/jdcloud-sdk-go/services/domainservice/client"
|
||||
jdDnsModel "github.com/jdcloud-api/jdcloud-sdk-go/services/domainservice/models"
|
||||
)
|
||||
|
||||
const (
|
||||
envNamespace = "JDCLOUD_"
|
||||
|
||||
EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
|
||||
EnvAccessKeySecret = envNamespace + "ACCESS_KEY_SECRET"
|
||||
EnvRegionId = envNamespace + "REGION_ID"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
|
||||
|
||||
type Config struct {
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
RegionId string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int32
|
||||
HTTPTimeout time.Duration
|
||||
}
|
||||
|
||||
type DNSProvider struct {
|
||||
client *jdDnsClient.DomainserviceClient
|
||||
config *Config
|
||||
}
|
||||
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAccessKeyID, EnvAccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jdcloud: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.AccessKeyID = values[EnvAccessKeyID]
|
||||
config.AccessKeySecret = values[EnvAccessKeySecret]
|
||||
config.RegionId = values[EnvRegionId]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("jdcloud: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
clientCredentials := jdCore.NewCredentials(config.AccessKeyID, config.AccessKeySecret)
|
||||
client := jdDnsClient.NewDomainserviceClient(clientCredentials)
|
||||
clientConfig := &client.Config
|
||||
clientConfig.SetTimeout(config.HTTPTimeout)
|
||||
client.SetConfig(clientConfig)
|
||||
client.SetLogger(jdCore.NewDefaultLogger(jdCore.LogWarn))
|
||||
|
||||
return &DNSProvider{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jdcloud: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jdcloud: %w", err)
|
||||
}
|
||||
|
||||
if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil {
|
||||
return fmt.Errorf("jdcloud: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jdcloud: %w", err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jdcloud: %w", err)
|
||||
}
|
||||
|
||||
if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil {
|
||||
return fmt.Errorf("jdcloud: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getDNSZone(zoneName string) (*jdDnsModel.DomainInfo, error) {
|
||||
pageNumber := 1
|
||||
pageSize := 10
|
||||
for {
|
||||
request := jdDnsApi.NewDescribeDomainsRequest(d.config.RegionId, pageNumber, pageSize)
|
||||
request.SetDomainName(zoneName)
|
||||
|
||||
response, err := d.client.DescribeDomains(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range response.Result.DataList {
|
||||
if item.DomainName == zoneName {
|
||||
return &item, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(response.Result.DataList) < pageSize {
|
||||
break
|
||||
}
|
||||
|
||||
pageNumber++
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("jdcloud: zone %s not found", zoneName)
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getDNSZoneAndRecord(zoneName, subDomain string) (*jdDnsModel.DomainInfo, *jdDnsModel.RRInfo, error) {
|
||||
zone, err := d.getDNSZone(zoneName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pageNumber := 1
|
||||
pageSize := 10
|
||||
for {
|
||||
request := jdDnsApi.NewDescribeResourceRecordRequest(d.config.RegionId, fmt.Sprintf("%d", zone.Id))
|
||||
request.SetSearch(subDomain)
|
||||
request.SetPageNumber(pageNumber)
|
||||
request.SetPageSize(pageSize)
|
||||
|
||||
response, err := d.client.DescribeResourceRecord(request)
|
||||
if err != nil {
|
||||
return zone, nil, err
|
||||
}
|
||||
|
||||
for _, record := range response.Result.DataList {
|
||||
if record.Type == "TXT" && record.HostRecord == subDomain {
|
||||
return zone, &record, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(response.Result.DataList) < pageSize {
|
||||
break
|
||||
}
|
||||
|
||||
pageNumber++
|
||||
}
|
||||
|
||||
return zone, nil, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error {
|
||||
zone, record, err := d.getDNSZoneAndRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
request := jdDnsApi.NewCreateResourceRecordRequest(d.config.RegionId, fmt.Sprintf("%d", zone.Id), &jdDnsModel.AddRR{
|
||||
Type: "TXT",
|
||||
HostRecord: subDomain,
|
||||
HostValue: value,
|
||||
Ttl: int(d.config.TTL),
|
||||
ViewValue: -1,
|
||||
})
|
||||
_, err := d.client.CreateResourceRecord(request)
|
||||
return err
|
||||
} else {
|
||||
request := jdDnsApi.NewModifyResourceRecordRequest(d.config.RegionId, fmt.Sprintf("%d", zone.Id), fmt.Sprintf("%d", record.Id), &jdDnsModel.UpdateRR{
|
||||
Type: "TXT",
|
||||
HostRecord: subDomain,
|
||||
HostValue: value,
|
||||
Ttl: int(d.config.TTL),
|
||||
ViewValue: -1,
|
||||
})
|
||||
_, err := d.client.ModifyResourceRecord(request)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error {
|
||||
zone, record, err := d.getDNSZoneAndRecord(zoneName, subDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
return nil
|
||||
} else {
|
||||
request := jdDnsApi.NewDeleteResourceRecordRequest(d.config.RegionId, fmt.Sprintf("%d", zone.Id), fmt.Sprintf("%d", record.Id))
|
||||
_, err = d.client.DeleteResourceRecord(request)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package jdcloud
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
||||
internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/jdcloud/internal"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
RegionId string `json:"regionId"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
regionId := config.RegionId
|
||||
if regionId == "" {
|
||||
// 京东云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
||||
regionId = "cn-north-1"
|
||||
}
|
||||
|
||||
providerConfig := internal.NewDefaultConfig()
|
||||
providerConfig.AccessKeyID = config.AccessKeyId
|
||||
providerConfig.AccessKeySecret = config.AccessKeySecret
|
||||
providerConfig.RegionId = regionId
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = config.DnsTTL
|
||||
}
|
||||
|
||||
provider, err := internal.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package namedotcom
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namecheap"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
Username string `json:"username"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := namecheap.NewDefaultConfig()
|
||||
providerConfig.APIUser = config.Username
|
||||
providerConfig.APIKey = config.ApiKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := namecheap.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package namedotcom
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namedotcom"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
Username string `json:"username"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := namedotcom.NewDefaultConfig()
|
||||
providerConfig.Username = config.Username
|
||||
providerConfig.APIToken = config.ApiToken
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := namedotcom.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package namesilo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := namesilo.NewDefaultConfig()
|
||||
providerConfig.APIKey = config.ApiKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := namesilo.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ns1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ns1"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := ns1.NewDefaultConfig()
|
||||
providerConfig.APIKey = config.ApiKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := ns1.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package namesilo
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/pdns"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
host, _ := url.Parse(config.ApiUrl)
|
||||
providerConfig := pdns.NewDefaultConfig()
|
||||
providerConfig.Host = host
|
||||
providerConfig.APIKey = config.ApiKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := pdns.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package rainyun
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rainyun"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := rainyun.NewDefaultConfig()
|
||||
providerConfig.APIKey = config.ApiKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := rainyun.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package tencentcloud
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := tencentcloud.NewDefaultConfig()
|
||||
providerConfig.SecretID = config.SecretId
|
||||
providerConfig.SecretKey = config.SecretKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := tencentcloud.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package volcengine
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/volcengine"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := volcengine.NewDefaultConfig()
|
||||
providerConfig.AccessKey = config.AccessKeyId
|
||||
providerConfig.SecretKey = config.SecretAccessKey
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := volcengine.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package westcn
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/westcn"
|
||||
)
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
Username string `json:"username"`
|
||||
ApiPassword string `json:"apiPassword"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
|
||||
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
providerConfig := westcn.NewDefaultConfig()
|
||||
providerConfig.Username = config.Username
|
||||
providerConfig.Password = config.ApiPassword
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
if config.DnsTTL != 0 {
|
||||
providerConfig.TTL = int(config.DnsTTL)
|
||||
}
|
||||
|
||||
provider, err := westcn.NewDNSProviderConfig(providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
24
internal/pkg/core/deployer/deployer.go
Normal file
24
internal/pkg/core/deployer/deployer.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package deployer
|
||||
|
||||
import "context"
|
||||
|
||||
// 表示定义证书部署器的抽象类型接口。
|
||||
// 注意与 `Uploader` 区分,“部署”通常为“上传”的后置操作。
|
||||
type Deployer interface {
|
||||
// 部署证书。
|
||||
//
|
||||
// 入参:
|
||||
// - ctx:上下文。
|
||||
// - certPem:证书 PEM 内容。
|
||||
// - privkeyPem:私钥 PEM 内容。
|
||||
//
|
||||
// 出参:
|
||||
// - res:部署结果。
|
||||
// - err: 错误。
|
||||
Deploy(ctx context.Context, certPem string, privkeyPem string) (res *DeployResult, err error)
|
||||
}
|
||||
|
||||
// 表示证书部署结果的数据结构。
|
||||
type DeployResult struct {
|
||||
ExtendedData map[string]any `json:"extendedData,omitempty"`
|
||||
}
|
||||
451
internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go
Normal file
451
internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package aliyunalb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client"
|
||||
aliyunCas "github.com/alibabacloud-go/cas-20200407/v3/client"
|
||||
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/logger"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
|
||||
)
|
||||
|
||||
type DeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType ResourceType `json:"resourceType"`
|
||||
// 负载均衡实例 ID。
|
||||
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
// SNI 域名(支持泛域名)。
|
||||
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
type DeployerProvider struct {
|
||||
config *DeployerConfig
|
||||
logger logger.Logger
|
||||
sdkClients *wSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*DeployerProvider)(nil)
|
||||
|
||||
type wSdkClients struct {
|
||||
alb *aliyunAlb.Client
|
||||
cas *aliyunCas.Client
|
||||
}
|
||||
|
||||
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
clients, err := createSdkClients(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &DeployerProvider{
|
||||
config: config,
|
||||
logger: logger.NewNilLogger(),
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) WithLogger(logger logger.Logger) *DeployerProvider {
|
||||
d.logger = logger
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CAS
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case RESOURCE_TYPE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case RESOURCE_TYPE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
|
||||
getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(d.config.LoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClients.alb.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp)
|
||||
|
||||
// 查询 HTTPS 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listenerIds := make([]string, 0)
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("HTTPS"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClients.alb.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", listenerIds)
|
||||
|
||||
// 查询 QUIC 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listListenersToken = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("QUIC"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClients.alb.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", listenerIds)
|
||||
|
||||
// 遍历更新监听证书
|
||||
if len(listenerIds) == 0 {
|
||||
return errors.New("listener not found")
|
||||
} else {
|
||||
var errs []error
|
||||
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
|
||||
// 查询监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute
|
||||
getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClients.alb.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 监听配置", getListenerAttributeResp)
|
||||
|
||||
if d.config.Domain == "" {
|
||||
// 未指定 SNI,只需部署到监听器
|
||||
|
||||
// 修改监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
|
||||
updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{
|
||||
CertificateId: tea.String(cloudCertId),
|
||||
}},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClients.alb.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 ALB 监听配置", updateListenerAttributeResp)
|
||||
} else {
|
||||
// 指定 SNI,需部署到扩展域名
|
||||
|
||||
// 查询监听证书列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates
|
||||
listenerCertificates := make([]aliyunAlb.ListListenerCertificatesResponseBodyCertificates, 0)
|
||||
listListenerCertificatesLimit := int32(100)
|
||||
var listListenerCertificatesToken *string = nil
|
||||
for {
|
||||
listListenerCertificatesReq := &aliyunAlb.ListListenerCertificatesRequest{
|
||||
NextToken: listListenerCertificatesToken,
|
||||
MaxResults: tea.Int32(listListenerCertificatesLimit),
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
CertificateType: tea.String("Server"),
|
||||
}
|
||||
listListenerCertificatesResp, err := d.sdkClients.alb.ListListenerCertificates(listListenerCertificatesReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListenerCertificates'")
|
||||
}
|
||||
|
||||
if listListenerCertificatesResp.Body.Certificates != nil {
|
||||
for _, listenerCertificate := range listListenerCertificatesResp.Body.Certificates {
|
||||
listenerCertificates = append(listenerCertificates, *listenerCertificate)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenerCertificatesResp.Body.Certificates) == 0 || listListenerCertificatesResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenerCertificatesToken = listListenerCertificatesResp.Body.NextToken
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 监听下全部证书", listenerCertificates)
|
||||
|
||||
// 遍历查询监听证书,并找出需要解除关联的证书
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates
|
||||
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail
|
||||
certificateIsAssociated := false
|
||||
certificateIdsExpired := make([]string, 0)
|
||||
if len(listenerCertificates) > 0 {
|
||||
var errs []error
|
||||
|
||||
for _, listenerCertificate := range listenerCertificates {
|
||||
if *listenerCertificate.CertificateId == cloudCertId {
|
||||
certificateIsAssociated = true
|
||||
continue
|
||||
}
|
||||
|
||||
if *listenerCertificate.IsDefault || !strings.EqualFold(*listenerCertificate.Status, "Associated") {
|
||||
continue
|
||||
}
|
||||
|
||||
listenerCertificateId, err := strconv.ParseInt(*listenerCertificate.CertificateId, 10, 64)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
getUserCertificateDetailReq := &aliyunCas.GetUserCertificateDetailRequest{
|
||||
CertId: tea.Int64(listenerCertificateId),
|
||||
}
|
||||
getUserCertificateDetailResp, err := d.sdkClients.cas.GetUserCertificateDetail(getUserCertificateDetailReq)
|
||||
if err != nil {
|
||||
errs = append(errs, xerrors.Wrap(err, "failed to execute sdk request 'cas.GetUserCertificateDetail'"))
|
||||
continue
|
||||
}
|
||||
|
||||
certCnMatched := getUserCertificateDetailResp.Body.Common != nil && *getUserCertificateDetailResp.Body.Common == d.config.Domain
|
||||
certSanMatched := getUserCertificateDetailResp.Body.Sans != nil && slices.Contains(strings.Split(*getUserCertificateDetailResp.Body.Sans, ","), d.config.Domain)
|
||||
if !certCnMatched && !certSanMatched {
|
||||
continue
|
||||
}
|
||||
|
||||
certEndDate, _ := time.Parse("2006-01-02", *getUserCertificateDetailResp.Body.EndDate)
|
||||
if time.Now().Before(certEndDate) {
|
||||
continue
|
||||
}
|
||||
|
||||
certificateIdsExpired = append(certificateIdsExpired, *listenerCertificate.CertificateId)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
// 关联监听和扩展证书
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-associateadditionalcertificateswithlistener
|
||||
if !certificateIsAssociated {
|
||||
associateAdditionalCertificatesFromListenerReq := &aliyunAlb.AssociateAdditionalCertificatesWithListenerRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
Certificates: []*aliyunAlb.AssociateAdditionalCertificatesWithListenerRequestCertificates{
|
||||
{
|
||||
CertificateId: tea.String(cloudCertId),
|
||||
},
|
||||
},
|
||||
}
|
||||
associateAdditionalCertificatesFromListenerResp, err := d.sdkClients.alb.AssociateAdditionalCertificatesWithListener(associateAdditionalCertificatesFromListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.AssociateAdditionalCertificatesWithListener'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已关联 ALB 监听和扩展证书", associateAdditionalCertificatesFromListenerResp)
|
||||
}
|
||||
|
||||
// 解除关联监听和扩展证书
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-dissociateadditionalcertificatesfromlistener
|
||||
if len(certificateIdsExpired) > 0 {
|
||||
dissociateAdditionalCertificates := make([]*aliyunAlb.DissociateAdditionalCertificatesFromListenerRequestCertificates, 0)
|
||||
for _, certificateId := range certificateIdsExpired {
|
||||
dissociateAdditionalCertificates = append(dissociateAdditionalCertificates, &aliyunAlb.DissociateAdditionalCertificatesFromListenerRequestCertificates{
|
||||
CertificateId: tea.String(certificateId),
|
||||
})
|
||||
}
|
||||
|
||||
dissociateAdditionalCertificatesFromListenerReq := &aliyunAlb.DissociateAdditionalCertificatesFromListenerRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
Certificates: dissociateAdditionalCertificates,
|
||||
}
|
||||
dissociateAdditionalCertificatesFromListenerResp, err := d.sdkClients.alb.DissociateAdditionalCertificatesFromListener(dissociateAdditionalCertificatesFromListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.DissociateAdditionalCertificatesFromListener'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已解除关联 ALB 监听和扩展证书", dissociateAdditionalCertificatesFromListenerResp)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients, error) {
|
||||
// 接入点一览 https://www.alibabacloud.com/help/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-albEndpoint
|
||||
var albEndpoint string
|
||||
switch region {
|
||||
case "cn-hangzhou-finance":
|
||||
albEndpoint = "alb.cn-hangzhou.aliyuncs.com"
|
||||
default:
|
||||
albEndpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
albConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String(albEndpoint),
|
||||
}
|
||||
albClient, err := aliyunAlb.NewClient(albConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 接入点一览 https://help.aliyun.com/zh/ssl-certificate/developer-reference/endpoints
|
||||
var casEndpoint string
|
||||
if !strings.HasPrefix(region, "cn-") {
|
||||
casEndpoint = "cas.ap-southeast-1.aliyuncs.com"
|
||||
} else {
|
||||
casEndpoint = "cas.aliyuncs.com"
|
||||
}
|
||||
|
||||
casConfig := &aliyunOpen.Config{
|
||||
Endpoint: tea.String(casEndpoint),
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
casClient, err := aliyunCas.NewClient(casConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wSdkClients{
|
||||
alb: albClient,
|
||||
cas: casClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) {
|
||||
casRegion := region
|
||||
if casRegion != "" {
|
||||
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
|
||||
// 国内版固定接入点:华东一杭州
|
||||
// 国际版固定接入点:亚太东南一新加坡
|
||||
if casRegion != "" && !strings.HasPrefix(casRegion, "cn-") {
|
||||
casRegion = "ap-southeast-1"
|
||||
} else {
|
||||
casRegion = "cn-hangzhou"
|
||||
}
|
||||
}
|
||||
|
||||
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
|
||||
AccessKeyId: accessKeyId,
|
||||
AccessKeySecret: accessKeySecret,
|
||||
Region: casRegion,
|
||||
})
|
||||
return uploader, err
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package aliyunalb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNALB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./aliyun_alb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_LOADBALANCERID="your-alb-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_LISTENERID="your-alb-listener-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_DOMAIN="your-alb-sni-domain"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.RESOURCE_TYPE_LISTENER,
|
||||
ListenerId: fListenerId,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user