Compare commits

..

955 Commits

Author SHA1 Message Date
RHQYZ
2aac79af54 Merge pull request #810 from fudiwei/bugfix
build: fix go cross build
2025-06-18 23:12:52 +08:00
Fu Diwei
813fc562e3 build: fix go cross build 2025-06-18 22:27:15 +08:00
RHQYZ
3fb9b00f66 bump vertion to v0.3.18 2025-06-15 22:28:44 +08:00
RHQYZ
a492fbe18c Merge pull request #799 from fudiwei/bugfix
bugfix
2025-06-15 22:23:24 +08:00
Fu Diwei
0383233315 fix: int64 overflow 2025-06-15 22:21:36 +08:00
Fu Diwei
0434f95a1e fix: tsc build error 2025-06-15 20:51:40 +08:00
Fu Diwei
769b24aa88 refactor: clean code 2025-06-15 20:51:28 +08:00
RHQYZ
94a0292fad Merge pull request #797 from usual2970/hotfix/cer_expire
fix: correct certificate expiration check logic
2025-06-15 20:41:08 +08:00
RHQYZ
410231cd1f Merge pull request #796 from bigsk05/main
fixed 1panel system ssl deploy
2025-06-15 20:41:00 +08:00
Yoan.liu
1d4e048777 refactor code 2025-06-14 21:51:10 +08:00
RHQYZ
c16fe1a807 Merge pull request #791 from usual2970/feat/panic_recover
处理panic避免影响到其它工作流
2025-06-14 21:48:23 +08:00
PBK Bin
e8ed831b28 Merge pull request #789 from PBK-B/bin/fix_remove_email_setting
feat(ui): support deleting email input box auto-complete items #174
2025-06-14 21:48:12 +08:00
RHQYZ
c7c89efbe7 Merge pull request #784 from PBK-B/bin/chore_add_gitkeep
chore: add ui/dist/.gitkeep file
2025-06-14 21:47:57 +08:00
RHQYZ
d7f3d9c512 Merge pull request #783 from fudiwei/feat/providers
new providers
2025-06-14 21:47:37 +08:00
RHQYZ
ce67cc5a39 Merge pull request #782 from fudiwei/bugfix
bugfix
2025-06-14 21:47:26 +08:00
Fu Diwei
945d0da36f update README 2025-06-14 21:41:58 +08:00
Fu Diwei
898311c6e5 feat: new deployment provider: ctcccloud elb 2025-06-14 21:41:58 +08:00
Fu Diwei
5d6bc03f21 feat: new deployment provider: ctcccloud lvdn 2025-06-14 21:41:58 +08:00
Fu Diwei
018743299b feat: new deployment provider: ctcccloud cms 2025-06-14 21:41:58 +08:00
Fu Diwei
18e7238067 feat: new deployment provider: ctcccloud ao 2025-06-14 21:41:58 +08:00
Fu Diwei
b7d1ff8960 feat: new deployment provider: ctcccloud icdn 2025-06-14 21:41:58 +08:00
Fu Diwei
0d44373de6 feat: new deployment provider: ctcccloud cdn 2025-06-14 21:41:52 +08:00
Yoan.liu
3d680e50e2 fix: correct certificate expiration check logic 2025-06-14 21:16:58 +08:00
Bigsk
9542079e20 fixed 1panel system ssl deploy 2025-06-13 23:43:24 +08:00
Yoan.liu
081e83e0bf recover from panics 2025-06-13 13:37:14 +08:00
Fu Diwei
9c8ab98efb feat: new acme dns-01 provider: statecloud smartdns 2025-06-12 23:21:03 +08:00
Fu Diwei
bf26db77cb fix: #786 2025-06-12 14:13:43 +08:00
Fu Diwei
08ea915d24 fix: #786 2025-06-12 14:03:29 +08:00
Fu Diwei
fb62f1e105 feat: ipv6 connect 2025-06-12 13:39:01 +08:00
PBK-B
261b3a8546 chore: add ui/dist/.gitkeep file 2025-06-11 23:25:14 +08:00
Fu Diwei
563adbec2a feat: allow configuring smtp sender display name 2025-06-11 23:10:57 +08:00
Fu Diwei
c6dfe11bdb feat: add preset scripts for qnap on deployment to ssh 2025-06-11 23:09:56 +08:00
Fu Diwei
e4bfa90a77 feat: new deployment provider: apisix 2025-06-11 22:17:07 +08:00
Fu Diwei
b833d09466 feat: new deployment provider: tencentcloud gaap 2025-06-11 20:53:19 +08:00
Fu Diwei
3ec0ba7052 fix: #781 2025-06-11 19:58:27 +08:00
Fu Diwei
57eb66b889 chore: remove unused files 2025-06-10 21:52:56 +08:00
RHQYZ
a048eb95a9 Merge pull request #779 from fudiwei/main
build: keep release files as before
2025-06-10 21:41:10 +08:00
Fu Diwei
23e58b914e build: keep release files as before 2025-06-10 21:38:47 +08:00
Fu Diwei
1170f635fd build: keep release files as before 2025-06-10 21:10:27 +08:00
Yoan.liu
553aceac44 retain executable permission 2025-06-10 11:08:29 +08:00
Yoan.liu
e24de70c02 split release into multiple jobs 2025-06-09 23:40:24 +08:00
Yoan.liu
bba2b25757 split release into multiple jobs 2025-06-09 23:11:50 +08:00
Yoan.liu
4132ec3617 split release into multiple jobs 2025-06-09 22:40:43 +08:00
Fu Diwei
9b3c7e16c0 fix: #769 2025-06-09 21:34:03 +08:00
Fu Diwei
0448538073 feat: bump version to v0.3.17 2025-06-09 21:16:58 +08:00
RHQYZ
80157496d5 Merge pull request #774 from lfffffy/main
fix(docs): 修复腾讯云文档链接使用错误的 com.cn 域名
2025-06-09 21:09:39 +08:00
RHQYZ
62e2ed2fb8 Merge pull request #768 from fudiwei/main
enhance & bugfix
2025-06-09 21:09:28 +08:00
Fu Diwei
a750592eb5 feat: duplicate workflow 2025-06-09 21:07:52 +08:00
Fu Diwei
5e6d729631 fix: #769 2025-06-09 20:44:48 +08:00
Fu Diwei
24fe824757 feat: allow skip notify nodes when all previous nodes were skipped 2025-06-09 20:40:06 +08:00
Fu Diwei
84a3f3346a fix: #769 2025-06-09 19:11:04 +08:00
Fu Diwei
bd26dfecb8 feat: improve workflow log 2025-06-09 10:06:41 +08:00
leun
43182de732 fix(docs): 修复腾讯云文档链接使用错误的 com.cn 域名
将文档链接中的 cloud.tencent.com.cn 统一替换为正确的 cloud.tencent.com,
以避免链接跳转失败或文档加载异常的问题。
2025-06-08 15:00:28 +08:00
Fu Diwei
d58109f4be feat: support wildcard domains on deployment to wangsu cdn 2025-06-05 20:28:50 +08:00
Fu Diwei
59935df6b1 fix: #766 2025-06-05 20:24:43 +08:00
Fu Diwei
252da5d7e1 refactor(ui): clean code 2025-06-05 10:24:39 +08:00
RHQYZ
c3e7590f53 bump version to v0.3.16 2025-06-03 23:50:48 +08:00
RHQYZ
65cd1dc850 Merge pull request #761 from fudiwei/feat/providers
new providers
2025-06-03 23:48:27 +08:00
RHQYZ
2203bb5268 Merge pull request #760 from fudiwei/bugfix
bugfix: could not add branches in workflows
2025-06-03 23:48:13 +08:00
Fu Diwei
8e5c36968a chore: improve error 2025-06-03 23:45:00 +08:00
Fu Diwei
9ad0e6fb57 feat: support ssh challenge-response 2025-06-03 23:39:48 +08:00
Fu Diwei
7d55383cf7 feat: new deployment provider: aws iam 2025-06-03 23:39:48 +08:00
Fu Diwei
6dc65eea2f feat: new acme dns-01 provider: ucloud udnr 2025-06-03 23:39:48 +08:00
Fu Diwei
7210f63884 feat: new acme dns-01 provider: constellix 2025-06-03 23:39:42 +08:00
Fu Diwei
f94db675fb fix: could not add branches in workflows 2025-06-03 22:37:03 +08:00
RHQYZ
e6cf4d3e07 bump version to v0.3.15 2025-06-02 23:10:34 +08:00
RHQYZ
cc5098c4bc Merge pull request #755 from fudiwei/main
Support duplicating workflow nodes or branches
2025-06-02 23:09:14 +08:00
Fu Diwei
025e606db4 feat(ui): duplicate workflow node 2025-06-02 23:06:18 +08:00
RHQYZ
d3e8bacd58 Merge pull request #742 from tailorvii/wangsu
fix: wangsu get certificate list api error #741
2025-06-01 23:05:10 +08:00
RHQYZ
308b21bb33 Merge pull request #734 from fudiwei/bugfix
bugfix
2025-06-01 23:04:53 +08:00
RHQYZ
262c1d7fcb Merge pull request #735 from fudiwei/feat/providers
enhance providers
2025-06-01 23:04:42 +08:00
RHQYZ
722c3a0e83 Merge pull request #712 from usual2970/feat/condition
workflow conditional branch & monitoring node
2025-06-01 23:04:31 +08:00
Fu Diwei
f885b49daf feat: add certtest workflow template 2025-06-01 22:59:24 +08:00
Fu Diwei
6731c465e7 refactor: workflow condition node
refactor: workflow condition node
2025-05-31 17:30:37 +08:00
Fu Diwei
28811c46d8 fix: #746 2025-05-31 16:29:17 +08:00
tailor
599cf17c9e fix: wangsu get certificate list api error 2025-05-29 11:24:30 +08:00
Fu Diwei
f0af36b59e refactor: clean code 2025-05-28 22:43:18 +08:00
Fu Diwei
e73e2739c1 feat: use discard handler as default providers logger 2025-05-28 22:21:41 +08:00
Fu Diwei
efdeacf01a feat: add preset webhook template for serverchan3 2025-05-28 21:39:02 +08:00
Fu Diwei
3a829ad53b refactor: workflow monitor(aka inspect) node 2025-05-28 21:05:56 +08:00
Yoan.liu
605de595b1 remove upx compression 2025-05-28 20:34:18 +08:00
Fu Diwei
daf22b7f15 feat: initialize aliyun fc ssl protocol 2025-05-28 16:44:11 +08:00
Fu Diwei
0e8ebaa885 fix: #732 2025-05-28 14:51:18 +08:00
Fu Diwei
829fa29cf1 feat: add user-agent http header for thirdparty sdks 2025-05-28 10:46:02 +08:00
Fu Diwei
ddb46f9dda refactor: clean code 2025-05-28 10:17:33 +08:00
Fu Diwei
df1f216b5b feat: support configuring aliyun resource group id 2025-05-27 21:19:06 +08:00
Fu Diwei
b8b94dfd77 feat: support configuring huaweicloud enterprise project id 2025-05-27 21:19:02 +08:00
Fu Diwei
4489096e57 Merge branch 'main' into feat/condition 2025-05-27 05:36:42 +08:00
Fu Diwei
a4f736e0f3 build: fix goreleaser.yml 2025-05-27 05:04:35 +08:00
Fu Diwei
d8935337d6 build: fix goreleaser.yml 2025-05-27 04:59:38 +08:00
Fu Diwei
211f66dc0a fix: tsc build error 2025-05-27 04:43:44 +08:00
RHQYZ
d964b129b0 bump version to v0.3.14 2025-05-27 04:35:10 +08:00
RHQYZ
a758b1d6d4 Merge pull request #727 from fudiwei/feat/providers
new providers
2025-05-27 04:34:07 +08:00
Fu Diwei
037305d8cd update README 2025-05-27 04:31:26 +08:00
Fu Diwei
cfdd3c621f feat: new deployment provider: unicloud webhost 2025-05-27 04:29:51 +08:00
Fu Diwei
5339963524 feat(ui): improve i18n 2025-05-26 23:02:57 +08:00
RHQYZ
af7d05e669 Merge pull request #726 from fudiwei/feat/providers
new providers
2025-05-26 21:52:02 +08:00
Fu Diwei
bf1d03a30e feat(migration): tracer 2025-05-26 21:49:37 +08:00
Fu Diwei
3bb88d9f93 chore(deps): upgrade npm dependencies 2025-05-26 17:04:40 +08:00
Fu Diwei
b0eb71421f chore(deps): upgrade go mod dependencies 2025-05-26 16:55:15 +08:00
Fu Diwei
e82a59289b feat: new notification provider: slack bot 2025-05-26 16:41:44 +08:00
Fu Diwei
8e23b14bf3 feat: new notification provider: discord bot 2025-05-26 16:41:38 +08:00
Fu Diwei
cd9dac7765 feat: new acme dns-01 provider: duckdns 2025-05-26 13:59:22 +08:00
Fu Diwei
40f4488009 feat: new acme dns-01 provider: digitalocean 2025-05-26 13:59:22 +08:00
Fu Diwei
4c13a3e86a feat: new acme dns-01 provider: hetzner 2025-05-26 13:59:16 +08:00
Fu Diwei
b139139f50 Merge branch 'main' into feat/providers 2025-05-26 11:37:01 +08:00
RHQYZ
55e16f4b17 Merge pull request #725 from fudiwei/bugfix
bugfix
2025-05-26 11:30:31 +08:00
Fu Diwei
46b4ff73c9 fix: ari double renewal error 2025-05-26 11:29:04 +08:00
Fu Diwei
970a1f0f79 fix: i18n error in AccessEditModal 2025-05-26 10:17:25 +08:00
Fu Diwei
11ff80ab28 fix: #723 2025-05-26 09:27:33 +08:00
Fu Diwei
7cd036f41e fix: #724 2025-05-26 09:26:11 +08:00
Fu Diwei
0909671be6 refactor: clean code 2025-05-25 23:32:41 +08:00
Fu Diwei
b798b824db refactor(ui): MultipleSplitValueInput 2025-05-25 23:05:08 +08:00
Fu Diwei
71c093c042 refactor: clean code 2025-05-25 21:54:39 +08:00
Yoan.liu
9878c12512 Merge pull request #722 from KukiSa/main
Fix Netlify Site Inversion of ca_certificates and key
2025-05-25 08:30:14 +08:00
Signaliks
b43797a0fb Update models.go 2025-05-24 21:32:34 +08:00
Yoan.liu
312589ab1c reduce the binary size 2025-05-23 17:23:36 +08:00
Yoan.liu
bff8add010 Merge branch 'main' of github.com:usual2970/certimate 2025-05-23 16:48:54 +08:00
Yoan.liu
6b9f295167 update Dockerfile 2025-05-23 16:48:41 +08:00
Yoan.liu
9cdc59b272 refactor code 2025-05-22 17:09:14 +08:00
RHQYZ
0e8b271e8d Merge pull request #716 from fudiwei/bugfix
bugfix
2025-05-21 20:31:09 +08:00
Fu Diwei
87aae4087c fix: #706 2025-05-21 20:28:21 +08:00
Yoan.liu
75326b1ddd refactor code 2025-05-21 15:59:02 +08:00
Yoan.liu
7d8dd523a2 Merge branch 'main' into feat/condition 2025-05-21 13:51:23 +08:00
Yoan.liu
993ca36755 add certificate mornitoring node 2025-05-21 13:48:54 +08:00
RHQYZ
c34346cb31 bump version to v0.3.13 2025-05-21 00:20:08 +08:00
RHQYZ
7643975ef9 Merge pull request #714 from fudiwei/bugfix
bugfix
2025-05-20 23:14:06 +08:00
Fu Diwei
799ad61dcc fix: #713 2025-05-20 23:10:44 +08:00
Fu Diwei
1c3cb1b21b fix: #710 2025-05-20 23:04:34 +08:00
Fu Diwei
9d9ca88ebe fix: tsc build error 2025-05-20 23:04:28 +08:00
Yoan.liu
faad7cb6d7 improve condition evaluate 2025-05-20 22:54:41 +08:00
Fu Diwei
d81a33f24a fix: #704 2025-05-20 21:52:33 +08:00
Fu Diwei
398337826e fix: #706 2025-05-20 21:51:38 +08:00
RHQYZ
7469310fdb Merge pull request #699 from Lensual/feat-ssh-jumpserver
feat: ssh jump server support (#49) (#95)
2025-05-20 21:42:53 +08:00
RHQYZ
fe993b54f3 Merge branch 'main' into feat-ssh-jumpserver 2025-05-20 21:42:38 +08:00
RHQYZ
1ed1c62f76 Merge pull request #697 from fudiwei/main
new providers
2025-05-20 21:41:07 +08:00
Fu Diwei
524c4fd1e8 feat: new deployment provider: baotawaf console 2025-05-20 21:27:26 +08:00
Fu Diwei
591df58992 feat: new deployment provider: baotawaf site 2025-05-20 21:27:26 +08:00
Fu Diwei
4ad08d983a refactor: clean code 2025-05-20 21:27:26 +08:00
Fu Diwei
7a663d31cb feat: new deployment provider: wangsu cdn 2025-05-20 21:27:26 +08:00
Fu Diwei
e6fc92eccb feat: new deployment provider: wangsu certificate management 2025-05-20 21:27:19 +08:00
Yoan.liu
97d692910b expression evaluate 2025-05-20 18:09:42 +08:00
Yoan.liu
b546cf3ad0 multi language support 2025-05-20 14:55:48 +08:00
Yoan.liu
6353f0139b improve variable types 2025-05-20 11:01:04 +08:00
Fu Diwei
c9e6bd0c2f feat: new deployment provider: lecdn 2025-05-19 22:51:17 +08:00
Yoan.liu
1e67e9333e condition render 2025-05-19 21:59:37 +08:00
Yoan.liu
6f054ee594 update readme 2025-05-19 18:16:21 +08:00
Yoan.liu
05d43f38ce improve previous variables 2025-05-19 18:15:04 +08:00
Yoan.liu
b8ab077b57 improve ui 2025-05-19 17:41:39 +08:00
神楽坂ニャン
36dd4ef3eb update: update formSchema for jump server & fix username validate message 2025-05-19 14:27:35 +08:00
Fu Diwei
a66e1c04c9 feat(ui): allow clear input when field is optional 2025-05-19 11:39:45 +08:00
Fu Diwei
3098f6a82f feat: rename certificate props 'issuer' to 'issuerOrg' 2025-05-19 10:35:05 +08:00
神楽坂ニャン
4a8eaa9ffa feat: ssh jump server support 2025-05-17 19:47:31 +08:00
Fu Diwei
3462e454d0 feat: new deployment provider: aliyun ga 2025-05-16 23:30:03 +08:00
Fu Diwei
eabd16dd71 feat: new deployment provider: flexcdn 2025-05-16 22:18:03 +08:00
Fu Diwei
122d766cab feat: new ca provider: custom acme ca 2025-05-16 21:40:40 +08:00
Fu Diwei
980d1ee0b9 feat: new deployment providers: ratpanel console & site 2025-05-16 20:15:59 +08:00
RHQYZ
e9f248d8ec Merge branch 'usual2970:main' into main 2025-05-16 19:23:03 +08:00
RHQYZ
2906576de0 Merge pull request #696 from devhaozi/main
feat: backend support for ratpanel
2025-05-16 19:22:15 +08:00
Fu Diwei
fd875feef3 feat: support 1panel v2 2025-05-16 18:57:39 +08:00
耗子
871d3ece90 fix: test case 2025-05-16 18:51:03 +08:00
耗子
8014abc836 feat: add test cases 2025-05-16 18:43:50 +08:00
Fu Diwei
0840454143 update README 2025-05-16 15:10:44 +08:00
RHQYZ
06fd95782a Merge pull request #693 from fudiwei/main
refactor
2025-05-16 15:07:21 +08:00
Fu Diwei
ef0f0f6b43 refactor: remove nikoksr/notify deps 2025-05-16 15:04:39 +08:00
Fu Diwei
b15bf8ef98 refactor: clean code 2025-05-16 13:56:45 +08:00
Fu Diwei
dd2087b101 refactor: clean code 2025-05-16 11:55:07 +08:00
RHQYZ
12d0b66c61 Merge pull request #691 from fudiwei/main
refactor
2025-05-16 11:15:38 +08:00
Fu Diwei
f3bbb9e8b2 refactor: optimize third-party sdks 2025-05-16 11:10:43 +08:00
RHQYZ
dcd646b465 Merge branch 'usual2970:main' into main 2025-05-16 02:52:51 +08:00
耗子
bd4aa4806f feat: backend support for ratpanel 2025-05-16 02:33:46 +08:00
Fu Diwei
b122bbced9 build: config goreleaser 2025-05-16 02:26:38 +08:00
Fu Diwei
5edae845cb build: config goreleaser 2025-05-16 02:03:49 +08:00
Fu Diwei
b7af3c10e4 build: config goreleaser 2025-05-16 01:58:53 +08:00
Fu Diwei
2453048288 build: config goreleaser 2025-05-16 01:50:43 +08:00
Fu Diwei
221b6a6fc6 feat(ui): improve i18n 2025-05-16 01:34:05 +08:00
Fu Diwei
851ad70a6c bump version to v0.3.12 2025-05-15 23:52:21 +08:00
Fu Diwei
1844e9a9db update README 2025-05-15 23:41:25 +08:00
RHQYZ
04e9f4e909 Merge pull request #689 from fudiwei/feat/providers
new providers
2025-05-15 23:35:48 +08:00
Fu Diwei
e6e2587bfc feat: new deployment provider: netlify site 2025-05-15 23:25:48 +08:00
Fu Diwei
70bd2f0581 feat: new acme dns-01 provider: netlify 2025-05-15 23:25:48 +08:00
Fu Diwei
9e08cfd1d1 feat: support replacing old certificate on deployment to aws acm 2025-05-15 23:25:48 +08:00
Fu Diwei
cd93a2d72c feat: new acme dns-01 provider: netcup 2025-05-15 23:25:42 +08:00
RHQYZ
11a4d4f55c Merge pull request #684 from fudiwei/main
enhance & bugfix
2025-05-15 21:16:17 +08:00
RHQYZ
f33570d514 Merge branch 'usual2970:main' into main 2025-05-15 21:15:49 +08:00
Fu Diwei
1ee3b64a19 feat: config goedge api user role 2025-05-15 21:05:22 +08:00
Fu Diwei
a3a56f3346 feat: add preset scripts for synologydsm and fnos on deployment to ssh 2025-05-15 20:52:43 +08:00
Fu Diwei
564ed8f0d3 fix: #686 2025-05-15 17:52:24 +08:00
Fu Diwei
268ec4bd7f feat(ui): browser happy detecting 2025-05-15 02:20:47 +08:00
Fu Diwei
e55e6cc512 fix(ui): tsc build error 2025-05-15 01:57:00 +08:00
Fu Diwei
6c6bb78568 feat: deploy server certificate or intermedia certificate 2025-05-15 01:40:37 +08:00
Fu Diwei
178c62512d fix(ui): fix incorrect webhook guide 2025-05-15 01:16:29 +08:00
Fu Diwei
9eaf9fd933 feat(ui): CodeInput 2025-05-15 00:58:02 +08:00
Fu Diwei
04abf9dd76 feat(ui): TextFileInput 2025-05-14 00:42:59 +08:00
Fu Diwei
355059df3c fix(ui): disallow to create access of builtin providers 2025-05-13 22:32:33 +08:00
Fu Diwei
4a6c32877f chore: github issue templates 2025-05-13 22:00:15 +08:00
Fu Diwei
258f6b5001 fix: resolve #681 2025-05-13 21:33:59 +08:00
Fu Diwei
78d9501fce fix: fix typo 2025-05-13 21:22:01 +08:00
RHQYZ
15975bb92c Merge branch 'usual2970:main' into main 2025-05-13 21:19:13 +08:00
Fu Diwei
fef8c3cb1a build: set go version to 1.24 2025-05-13 01:33:39 +08:00
RHQYZ
4b226d7730 Merge branch 'usual2970:main' into main 2025-05-13 01:00:07 +08:00
RHQYZ
41abc8cab8 bump version to v0.3.11 2025-05-13 00:54:08 +08:00
Fu Diwei
4d3b3834f5 fix(ui): tsc build error 2025-05-13 00:52:41 +08:00
Fu Diwei
9b1ba574ff fix(ui): scale origin 2025-05-13 00:52:41 +08:00
Fu Diwei
ee7d950c15 feat(ui): max content height on AccessEditModal 2025-05-13 00:52:41 +08:00
Fu Diwei
8ca41f18be feat(ui): show search on AccessSelect 2025-05-13 00:52:41 +08:00
Fu Diwei
9d522bd9ef feat(ui): AccessProviderPicker 2025-05-13 00:52:41 +08:00
Fu Diwei
f9633616e2 feat: support replacing old certificate on deployment to gcore cdn 2025-05-13 00:52:41 +08:00
Fu Diwei
9a0dd53cd7 chore(deps): upgrade gomod dependencies 2025-05-13 00:52:41 +08:00
Fu Diwei
6bbcec6163 chore(deps): upgrade npm dependencies 2025-05-13 00:52:41 +08:00
Fu Diwei
fdee69bdaf feat(ui): move settings page entry from topbar to sider-menu 2025-05-13 00:52:41 +08:00
Fu Diwei
2ec4adf7d5 fix(ui): tsc build error 2025-05-13 00:52:05 +08:00
Fu Diwei
709684d00f fix(ui): scale origin 2025-05-13 00:43:58 +08:00
Fu Diwei
989fd1ec6d feat(ui): max content height on AccessEditModal 2025-05-13 00:39:18 +08:00
Fu Diwei
7d57c5abc0 feat(ui): show search on AccessSelect 2025-05-13 00:33:48 +08:00
Fu Diwei
0c42bb845d feat(ui): AccessProviderPicker 2025-05-13 00:28:58 +08:00
Fu Diwei
07037e8d49 feat: support replacing old certificate on deployment to gcore cdn 2025-05-12 23:19:13 +08:00
Fu Diwei
a1fc3841df chore(deps): upgrade gomod dependencies 2025-05-12 23:00:39 +08:00
Fu Diwei
1fee156457 chore(deps): upgrade npm dependencies 2025-05-12 22:12:59 +08:00
Fu Diwei
82bdccf7e7 feat(ui): move settings page entry from topbar to sider-menu 2025-05-12 21:34:00 +08:00
Fu Diwei
7936c34472 fix #672 2025-05-12 20:19:00 +08:00
RHQYZ
365fd0bd5b Merge pull request #670 from fudiwei/feat/providers
new providers
2025-05-09 21:52:26 +08:00
Fu Diwei
f63ee41405 feat: new deployment provider: proxmoxve 2025-05-09 20:53:25 +08:00
Fu Diwei
26359b9d16 feat: allow insecure connections in cdnfly, goedge, powerdns 2025-05-09 16:35:58 +08:00
RHQYZ
5abdb577fb Merge pull request #666 from fudiwei/feat/providers
new providers
2025-05-09 11:00:32 +08:00
RHQYZ
c67c4741e5 bump version to v0.3.10 2025-05-08 21:04:14 +08:00
RHQYZ
60bc4be9ea Merge pull request #664 from fudiwei/bugfix
bugfix
2025-05-08 21:02:18 +08:00
RHQYZ
b83f87b6c8 Merge pull request #665 from fudiwei/main
Support configuring dns propagation waiting time
2025-05-08 21:01:50 +08:00
Fu Diwei
0a73ba1a53 feat: add preset scripts 2025-05-08 20:43:09 +08:00
Fu Diwei
a4d397f24b fix: fix typo 2025-05-08 15:36:51 +08:00
Fu Diwei
809f231981 feat: set the default max workers to the number of available CPU cores 2025-05-07 22:15:11 +08:00
Fu Diwei
1499c637ee feat: new deployment provider: goedge 2025-05-07 22:06:43 +08:00
Fu Diwei
e5805a028b feat: new acme dns-01 provider: aliyun esa 2025-05-07 22:06:43 +08:00
Fu Diwei
5cb0463cf6 feat: set the default max workers to the number of available CPU cores 2025-05-07 22:06:43 +08:00
Fu Diwei
12c208cad4 feat: new deployment provider: aliyun ddoscoo 2025-05-07 22:06:31 +08:00
Fu Diwei
1946a4e68a feat: add dns propagation wait configuration to application node in workflows 2025-05-07 11:56:35 +08:00
Fu Diwei
dfed0af053 fix: #658 2025-05-07 10:12:13 +08:00
Yoan.liu
fc064aa9f8 Merge pull request #643 from fudiwei/main
Support configuring independent notification channel for each workflow
2025-04-30 21:37:03 +08:00
Fu Diwei
edaf08361f refactor: clean code 2025-04-28 12:09:28 +08:00
Fu Diwei
92c93822b9 feat: webhook general template 2025-04-28 10:52:58 +08:00
Fu Diwei
18a19096d3 feat: add dingtalk, lark, and wecom bot webhook 2025-04-27 23:44:01 +08:00
Fu Diwei
7e707cd973 feat: webhook preset template data 2025-04-27 22:44:10 +08:00
Fu Diwei
d33b8caf14 feat: support overwriting the default webhook data of deployers and notifiers 2025-04-27 22:06:04 +08:00
Fu Diwei
e533f9407f feat: reserve accesses for ca or notification 2025-04-27 11:41:09 +08:00
Fu Diwei
193a19b79c feat: support more content-type in webhook 2025-04-27 00:01:00 +08:00
Fu Diwei
609a252ee0 refactor: clean code 2025-04-26 21:27:53 +08:00
Fu Diwei
3be70c3696 feat: support configuring method and headers in webhook 2025-04-26 21:17:09 +08:00
Fu Diwei
3c2fbd720f feat: support overwriting the default config of notifiers 2025-04-25 11:51:19 +08:00
Fu Diwei
11b413d0dc feat: adapt email, mattermost, telegram, and webhook to new notifier module 2025-04-24 23:37:27 +08:00
Fu Diwei
a117fd7d93 refactor: clean code 2025-04-24 23:14:17 +08:00
Fu Diwei
794695c313 refactor: clean code 2025-04-24 21:04:55 +08:00
Fu Diwei
7478dd7f47 feat: deprecate old notification module and introduce new notifier module 2025-04-24 20:27:20 +08:00
Fu Diwei
2d17501072 refactor: clean code 2025-04-24 10:46:16 +08:00
Fu Diwei
034bb71b10 feat(ui): show ca provider global settings button only when not specified ca provider 2025-04-24 09:13:49 +08:00
Fu Diwei
97f102533c feat: enhance context cancellation handling 2025-04-23 19:32:21 +08:00
RHQYZ
a90b6a8589 Merge pull request #640 from fudiwei/main
field type conversion
2025-04-22 22:28:45 +08:00
Fu Diwei
e8f6c665f9 Merge branch 'upstream' 2025-04-22 22:16:01 +08:00
Fu Diwei
6dac89e9a1 refactor: remove pkg/errors 2025-04-22 22:12:58 +08:00
Fu Diwei
4f512a6cdd refactor: clean code 2025-04-22 21:51:36 +08:00
Fu Diwei
94f162c189 Merge branch 'sync-upstream' 2025-04-22 21:27:37 +08:00
Fu Diwei
f5807d215f style: format code 2025-04-22 21:24:48 +08:00
Fu Diwei
3189e65bad refactor: clean code 2025-04-22 21:18:16 +08:00
Fu Diwei
3febc53061 feat: change field provider's type of table access 2025-04-22 18:34:51 +08:00
Fu Diwei
efd8135341 chore(deps): upgrade gomod dependencies 2025-04-22 16:25:18 +08:00
Fu Diwei
56405eff6c chore(deps): upgrade npm dependencies 2025-04-22 16:25:08 +08:00
Fu Diwei
e0fad9d9a9 Merge branch 'main' of https://github.com/fudiwei/certimate 2025-04-22 10:27:08 +08:00
Yoan.liu
8fe942d8d5 update to version v0.3.9 2025-04-21 21:27:13 +08:00
Yoan.liu
cf98e789d8 Merge pull request #627 from fudiwei/bugfix
bugfix
2025-04-21 17:50:02 +08:00
Fu Diwei
ff58b9a317 fix: wangsu api error 2025-04-21 09:17:52 +08:00
Yoan.liu
65e7d390b8 Merge pull request #629 from fudiwei/feat/providers
new providers
2025-04-20 08:22:56 +08:00
Fu Diwei
54ae378e30 fix: handle deployment task status check on deployment to wangsu cdnpro 2025-04-19 19:37:31 +08:00
Fu Diwei
347695cf66 feat: update default certificate paths on deployment to local and ssh 2025-04-19 14:04:06 +08:00
Fu Diwei
8f4d854b0d feat: support replacing old certificate on deployment to 1panel site 2025-04-19 14:02:55 +08:00
Yoan.liu
94bd846726 Merge pull request #625 from fondoger/fondoger/fix-volcengine
修复火山云证书上传获取空值的bug
2025-04-19 10:46:26 +08:00
Yoan.liu
0365841549 Merge pull request #621 from fondoger/fondoger/keyvault
Fix Azure KeyVault bug & Support custom certificate name in Azure KeyVault
2025-04-19 10:46:07 +08:00
Yoan.liu
74bd1f64a0 Merge pull request #610 from imlonghao/feat/bunny
feat: support bunny as dns and cdn provider
2025-04-19 10:45:51 +08:00
Fu Diwei
5bce03410e feat: add aliyun apigw deployer 2025-04-18 20:51:23 +08:00
Fu Diwei
32ff658e84 fix: #626 2025-04-18 18:20:01 +08:00
Fu Diwei
c10ceed753 feat: improve log 2025-04-18 18:04:52 +08:00
Fu Diwei
eb45b56a87 fix: ignore wangsu api responses without content 2025-04-18 17:54:51 +08:00
Fu Diwei
283b150d60 refactor: re-impl azure keyvault deployer 2025-04-18 17:52:12 +08:00
Fu Diwei
7329a22132 chore(ui): improve i18n 2025-04-17 22:11:45 +08:00
Fu Diwei
50b48d956f fix: #617 2025-04-17 22:08:53 +08:00
Fu Diwei
55d7a05af8 fix: #615 2025-04-17 21:44:40 +08:00
RHQYZ
6c70b0655a chore: modify error log 2025-04-17 13:39:04 +08:00
fondoger
0004eac764 Modify code according to code suggestions 2025-04-17 13:13:23 +08:00
fondoger
5fe24465d7 修复火山云证书上传获取空值的bug 2025-04-17 12:54:43 +08:00
fondoger
364ceb2399 Fix Azure KeyVault bug 2025-04-16 21:53:19 +08:00
Yoan.liu
88b90986b1 update to version v0.3.8 2025-04-13 20:55:33 +08:00
imlonghao
5143823e43 feat: support bunny as dns and cdn provider 2025-04-13 15:47:23 +08:00
RHQYZ
363e23dd13 Merge branch 'usual2970:main' into main 2025-04-13 11:57:30 +08:00
Yoan.liu
44a6190e17 resolve build error 2025-04-13 09:14:08 +08:00
Yoan.liu
4475ed0dea resolve build error 2025-04-13 08:54:05 +08:00
Yoan.liu
6a23da3de3 Merge pull request #596 from redzl/redzl-patch-1
bugfix: tencent cloud ecdn deploy error
2025-04-13 08:24:43 +08:00
Yoan.liu
0f1d5a7730 Merge pull request #604 from banto6/main
feat(notify): add mattermost
2025-04-13 08:24:26 +08:00
Yoan.liu
5b4c3bb668 Merge branch 'main' into main 2025-04-13 08:24:16 +08:00
Yoan.liu
ad49f9d788 Merge pull request #607 from imlonghao/feat/pushover
feat: support pushover as notification
2025-04-13 08:18:59 +08:00
Yoan.liu
397ceefa02 Merge branch 'main' into feat/pushover 2025-04-13 08:18:47 +08:00
Yoan.liu
e11b1ca4e8 Merge pull request #597 from fudiwei/feat/providers
new providers
2025-04-13 08:14:50 +08:00
Yoan.liu
8e983e7286 Merge pull request #587 from fudiwei/bugfix
bugfix
2025-04-13 08:13:06 +08:00
Fu Diwei
f970ae7529 feat: add wangsu cdnpro deployer 2025-04-12 21:43:21 +08:00
Fu Diwei
b0973b5ca8 refactor: clean code 2025-04-12 20:54:02 +08:00
banto
4784bf9dba feat: add channelId tooltip 2025-04-12 20:01:03 +08:00
imlonghao
6b8dbf5235 feat: support pushover as notification 2025-04-12 13:05:37 +08:00
banto
48f698e84b style: fix code style 2025-04-12 12:45:03 +08:00
banto
ec0cdf8b96 feat(notify): add mattermost 2025-04-11 22:55:47 +08:00
Fu Diwei
2a6cc01eed feat(ui): adjust table scroll width in Dashboard 2025-04-10 21:57:22 +08:00
Fu Diwei
acc1365101 Merge branch 'feat/providers' of https://github.com/fudiwei/certimate into feat/providers 2025-04-09 23:12:52 +08:00
Fu Diwei
c5409c78ba refactor: edgio api sdk 2025-04-09 23:12:11 +08:00
RHQYZ
b97de6c06b Merge branch 'usual2970:main' into feat/providers 2025-04-09 22:56:43 +08:00
RHQYZ
abe755cb69 Merge branch 'usual2970:main' into main 2025-04-09 22:56:26 +08:00
RHQYZ
4e3f499d76 chore: github issue templates 2025-04-09 10:55:53 +08:00
Fu Diwei
3cebe51796 feat: add rainyun rcdn deployer 2025-04-08 21:53:16 +08:00
Fu Diwei
25bd17dc6e feat: add rainyun ssl center uploader 2025-04-08 21:53:05 +08:00
redzl
2525f54dc3 解决腾讯云ECDN部署报错的问题
ECDN部署的时候报错:failed to execute sdk request 'ssl.DeployCertificateInstance':[TencentCloudSDKError] Code=FailedOperation.CertificateHostResourceTypeInvalid, Message=云资源类型无效。
经排查'ssl.DeployCertificateInstance接口的ResourceType不支持ecdn类型,ecdn和cdn都需要传入cdn
2025-04-08 18:06:51 +08:00
Fu Diwei
2127bb7e69 Merge branch 'feat/providers' of https://github.com/fudiwei/certimate into feat/providers 2025-04-08 16:47:49 +08:00
Fu Diwei
ed6d74f1ba feat(ui): builtin providers tag 2025-04-08 16:44:10 +08:00
Fu Diwei
02dd11f196 chore(ui): improve i18n 2025-04-08 10:19:42 +08:00
Fu Diwei
37b9ae30e2 fix: #595 2025-04-08 09:41:16 +08:00
Fu Diwei
0463dbcc75 Merge branch 'bugfix' of https://github.com/fudiwei/certimate into bugfix 2025-04-07 15:32:12 +08:00
Fu Diwei
111ef97d9c fix: migration error 2025-04-07 15:31:20 +08:00
RHQYZ
e8e854e392 Merge branch 'usual2970:main' into bugfix 2025-04-07 12:42:22 +08:00
RHQYZ
cedbaf70c0 Merge branch 'usual2970:main' into main 2025-04-07 12:42:13 +08:00
Yoan.liu
ff43b9ab3e Update version.ts 2025-04-07 09:26:19 +08:00
Fu Diwei
47c4ba9dd6 feat(ui): workflow runs deleting warning 2025-04-05 21:23:55 +08:00
Fu Diwei
2899aa1b19 feat: set placeholder values in preset scripts 2025-04-03 21:12:21 +08:00
Fu Diwei
ecff16c89d chore: improve error messages 2025-04-03 21:03:43 +08:00
Fu Diwei
6ff738144a fix: #585 #586 2025-04-03 20:33:58 +08:00
Fu Diwei
26028bb1eb chore(ui): improve i18n 2025-04-03 20:30:44 +08:00
Yoan.liu
eb4d5ddfd5 Merge pull request #573 from fudiwei/main
Support configuring independent CA for each workflow
2025-04-03 17:42:42 +08:00
Yoan.liu
093ee006e4 Merge pull request #578 from fudiwei/feat/providers
Support cloudflare zone api token
2025-04-03 17:42:23 +08:00
Yoan.liu
9f8aa15af8 Merge pull request #579 from catfishlty/feat/gotify
feat(notify): add gotify
2025-04-03 17:42:00 +08:00
Yoan.liu
74d66a0131 Merge pull request #583 from catfishlty/feat/pushplus
feat(notify): add pushplus
2025-04-03 17:41:46 +08:00
catfishlty
626a86dea7 fix(notify): optimize gotify code and close unreleased resources. 2025-04-03 09:47:19 +08:00
catfishlty
9ab029a296 fix(notify): optimize pushplus code and close unreleased resources. 2025-04-03 09:34:23 +08:00
Fu Diwei
8e1a81ae53 chore: improve i18n 2025-04-02 21:57:33 +08:00
Fu Diwei
d76e1a3204 refactor: clean code 2025-04-02 21:46:27 +08:00
catfishlty
b585782007 feat(notify): add pushplus 2025-04-02 15:04:41 +08:00
catfishlty
2d198bcef7 fix(notify): add missing config for gotify 2025-04-02 15:00:46 +08:00
Fu Diwei
0edcd9174f feat(ui): download workflow run logs 2025-04-02 13:40:55 +08:00
Fu Diwei
daa5b44f8e refactor(ui): clean code 2025-04-02 12:54:51 +08:00
Fu Diwei
949660bc01 feat(ui): add AccessEditDrawer component 2025-04-02 11:02:09 +08:00
Fu Diwei
899a0b75b0 feat(ui): improve access provider tags appearance 2025-04-01 21:23:51 +08:00
Fu Diwei
8cdb2afa69 refactor: clean code 2025-04-01 20:44:45 +08:00
catfishlty
00ec2ce33e feat(notify): add gotify 2025-04-01 10:53:41 +08:00
Fu Diwei
2f7fd95684 feat: cloudflare zone api token 2025-03-31 21:13:07 +08:00
Fu Diwei
55b1794004 chore: improve i18n 2025-03-31 20:03:08 +08:00
Fu Diwei
e20972d4e7 chore: improve i18n 2025-03-31 20:00:03 +08:00
Fu Diwei
749d727f50 fix: could not obtain ecc certificates from sslcom 2025-03-31 10:24:35 +08:00
Fu Diwei
9b524728c0 update README 2025-03-30 22:46:33 +08:00
Fu Diwei
f81b4b9680 feat(ui): hide notification channel entry in AcessList for now 2025-03-30 22:33:05 +08:00
Fu Diwei
d2eaea7a44 feat: add buypass ca 2025-03-30 22:15:21 +08:00
Fu Diwei
f77c2dae23 feat: add ssl.com ca 2025-03-30 22:15:21 +08:00
Fu Diwei
a72737fdd5 feat(ui): different provider range of accesses in AccessList 2025-03-30 22:15:21 +08:00
Fu Diwei
4ab6b72e6f feat(ui): different provider range of accesses in AccessForm 2025-03-30 22:15:08 +08:00
Fu Diwei
1468e74a6c fix: ari 2025-03-30 14:02:43 +08:00
Fu Diwei
09b5a21af1 feat: make the builtin providers access field non mandatory 2025-03-30 13:57:26 +08:00
Fu Diwei
6ad0d8e42f feat: support configuring independent ca in workflows 2025-03-30 13:57:26 +08:00
Fu Diwei
deb3b2f412 feat: manage ca authorizations 2025-03-30 13:57:21 +08:00
Yoan.liu
893391a3d1 Merge pull request #566 from fudiwei/main
enhance & bugfix
2025-03-29 20:01:13 +08:00
Fu Diwei
7503d52857 refactor: clean code 2025-03-27 20:39:06 +08:00
Fu Diwei
fb860981d6 fix: #568 2025-03-27 15:53:29 +08:00
Fu Diwei
f302c7fb74 feat: support replacing old certificate on deployment to baishan cdn 2025-03-27 14:14:34 +08:00
Fu Diwei
a8be2a77cf fix: #565 2025-03-27 14:14:27 +08:00
Fu Diwei
c2345e6118 style: format 2025-03-27 09:47:16 +08:00
Yoan.liu
539f8f3343 update to version v0.3.6 2025-03-26 21:51:13 +08:00
Yoan.liu
9a06c1e35b Merge pull request #558 from fudiwei/main
new providers & bugfix
2025-03-26 17:59:27 +08:00
Fu Diwei
382de0d6d6 refactor: clean code 2025-03-26 10:43:37 +08:00
Fu Diwei
4883b3bb88 feat(ui): make request error friendly 2025-03-26 10:18:27 +08:00
Fu Diwei
0a90523d61 fix(ui): login theme error in dark mode 2025-03-25 20:31:02 +08:00
Fu Diwei
fa63f2a838 feat: add tencentcloud-eo dns-01 applicant 2025-03-25 20:28:05 +08:00
Fu Diwei
fd8ac3ae37 feat(ui): allow select dns-01 provider on application 2025-03-25 19:52:09 +08:00
Fu Diwei
51c1b193e5 fix: #559 2025-03-25 19:41:55 +08:00
Fu Diwei
ee99bcf8a1 Merge branch 'main' of https://github.com/usual2970/certimate 2025-03-25 17:20:28 +08:00
Fu Diwei
324086ca49 chore: github issue templates 2025-03-25 17:19:28 +08:00
Fu Diwei
e9610eaede fix: #556 2025-03-25 16:17:35 +08:00
Fu Diwei
7d5c714211 feat: improve i18n 2025-03-25 13:54:00 +08:00
Fu Diwei
24e275fdb3 feat: add volcengine certcenter deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
597b9d0e17 feat: add huaweicloud scm deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
4d710a1aaf feat: add baiducloud cert deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
5de033814b feat: add baiducloud appblb deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
aaec840d8c feat: add baiducloud blb deployer 2025-03-25 13:53:54 +08:00
Fu Diwei
e579cf6ceb feat: add baiducloud cert uploader 2025-03-25 13:53:39 +08:00
Yoan.liu
798e72f663 Merge pull request #543 from fudiwei/main
new providers & bugfix
2025-03-25 09:23:54 +08:00
Fu Diwei
e79d862256 chore: github issue templates 2025-03-24 20:55:06 +08:00
Fu Diwei
53133db456 refactor: clean code 2025-03-24 12:37:20 +08:00
Fu Diwei
39f8484b2a fix: #544 2025-03-24 12:35:30 +08:00
Fu Diwei
892256c0b9 fix: #544 2025-03-24 11:25:02 +08:00
Fu Diwei
0545945697 refactor: clean code 2025-03-24 10:31:41 +08:00
Fu Diwei
ad0125fe0d feat: add vercel dns-01 applicant 2025-03-23 22:42:59 +08:00
Fu Diwei
fb325b5447 feat: add desec dns-01 applicant 2025-03-23 22:42:59 +08:00
Fu Diwei
56ff9e6344 feat: add porkbun dns-01 applicant 2025-03-23 22:42:59 +08:00
Fu Diwei
74b431481d feat: add volcengine alb deployer 2025-03-23 22:42:51 +08:00
Fu Diwei
12102ef641 refactor: clean code 2025-03-23 10:19:24 +08:00
Fu Diwei
445541c38f fix: #548 2025-03-23 00:42:58 +08:00
Fu Diwei
820f03e162 feat: support wildcard domain on deployment to aliyun fc 2025-03-22 20:54:54 +08:00
Fu Diwei
516a958c66 fix: #544 2025-03-22 17:51:27 +08:00
Yoan.liu
7da101d5b7 update to version v0.3.5 2025-03-21 20:57:57 +08:00
Fu Diwei
9667f3309b fix: #542 2025-03-21 20:13:05 +08:00
Fu Diwei
82735f3c02 refactor: clean code 2025-03-21 19:55:29 +08:00
Fu Diwei
752acb591f refactor: clean code 2025-03-21 18:11:17 +08:00
Yoan.liu
43d851c7ef Merge pull request #541 from fudiwei/main
new providers & bugfix
2025-03-21 07:00:57 +08:00
Fu Diwei
95e1fc6b5f fix: #539 2025-03-21 01:30:35 +08:00
Fu Diwei
a8a12a3b91 chore(deps): upgrade gomod dependencies 2025-03-20 23:55:25 +08:00
Fu Diwei
63a95723ac chore(deps): upgrade npm dependencies 2025-03-20 23:55:20 +08:00
Fu Diwei
02f806ab99 feat: preset script for backup files on deployment to local and ssh 2025-03-20 23:36:20 +08:00
Fu Diwei
da6526d5fa feat: add dynv6 dns-01 applicant 2025-03-20 23:36:20 +08:00
Fu Diwei
347d166250 feat: add aliyun cas, tencentcloud ssl, aws acm, azure keyvault deployer 2025-03-20 23:36:19 +08:00
Fu Diwei
ef22d9d07b feat: add qiniu kodo deployer 2025-03-20 23:36:19 +08:00
Fu Diwei
e4fd1e78f5 feat: add upyun file deployer 2025-03-20 23:36:19 +08:00
Fu Diwei
4acbbf6e13 feat: add upyun cdn deployer 2025-03-20 23:36:19 +08:00
Fu Diwei
16f20dc01d feat: add upyun ssl uploader 2025-03-20 23:36:19 +08:00
Fu Diwei
8e4b3d12bd fix: #527 2025-03-20 23:36:19 +08:00
Fu Diwei
09b1bf6e2d fix: #523 2025-03-20 23:36:19 +08:00
Fu Diwei
7e4aa24459 fix: #539 2025-03-20 23:36:13 +08:00
Yoan.liu
7c94999efc Merge pull request #525 from fudiwei/main
new workflow logging
2025-03-20 21:26:39 +08:00
Fu Diwei
e27d4f11ee feat: auto cleanup workflow history runs and expired certificates 2025-03-19 17:12:24 +08:00
Fu Diwei
914c5b4870 refactor: clean code 2025-03-19 10:30:12 +08:00
Fu Diwei
882f802585 feat(ui): enhance workflow logs display 2025-03-19 10:09:30 +08:00
Yoan.liu
5579780b12 Merge pull request #532 from Anbool/main
修复白山云API 400209错误
2025-03-19 09:16:12 +08:00
Fu Diwei
fd6e41c566 feat(ui): workflow logs 2025-03-18 20:02:39 +08:00
RHQYZ
984d2a47b8 style: format code 2025-03-18 18:23:46 +08:00
root
92bae0c439 修复白山云API 400209错误 2025-03-18 16:48:19 +08:00
Fu Diwei
af5d7465a1 feat: adapt new logging to workflow node processors 2025-03-17 22:50:57 +08:00
Fu Diwei
b620052b88 feat: adapt new logging to uploader, deployer and notifier providers 2025-03-17 22:50:47 +08:00
Fu Diwei
c13a7a7873 feat: logging 2025-03-16 18:43:54 +08:00
Yoan.liu
65b199d392 update to version v0.3.4 2025-03-14 11:25:58 +08:00
Yoan.liu
dc0b86281e Merge pull request #517 from fudiwei/main
bugfix
2025-03-14 07:11:00 +08:00
Yoan.liu
b8796991b5 Merge pull request #518 from usual2970/hotfix/display
解决分支过多内容展示不完整的问题
2025-03-14 07:10:13 +08:00
Yoan.liu
83447fff62 fix the issue where content is not fully displayed when there are too many branches. 2025-03-13 17:22:34 +08:00
Fu Diwei
4a02c252d5 feat(ui): auto complete tencentcloud ssl deploy resource type 2025-03-13 16:20:03 +08:00
Fu Diwei
cb88df04b0 fix: #516 2025-03-13 16:03:35 +08:00
Yoan.liu
e888df2b9f Merge pull request #515 from fudiwei/main
enhance & bugfix
2025-03-12 21:44:12 +08:00
Fu Diwei
17af07e4bb feat: support sni on deployment to aliyun waf 2025-03-12 19:58:58 +08:00
Fu Diwei
d1aed36154 fix: #512 2025-03-12 19:36:14 +08:00
Fu Diwei
eb97f7a661 refactor: clean code 2025-03-12 16:56:02 +08:00
Yoan.liu
be822ccc93 update readme 2025-03-12 16:55:33 +08:00
Yoan.liu
e2b52eed61 Merge pull request #511 from LeoChen98/feat-rdp-preset
add feat: Windows RDP binding preset
2025-03-11 22:15:36 +08:00
Leo Chen
21717985ac add feat: Windows RDP binding preset 2025-03-11 21:38:22 +08:00
Yoan.liu
3c4ffee7d3 Update push_image.yml 2025-03-11 06:35:41 +08:00
Yoan.liu
3e1a457609 update to version v0.3.3 2025-03-10 21:33:06 +08:00
Yoan.liu
b28f0dc5e4 Merge pull request #504 from fudiwei/main
bugfix
2025-03-10 21:15:23 +08:00
Yoan.liu
29561ed75d Merge pull request #505 from usual2970/feat/image_tags
镜像增加大版本 tag
2025-03-10 21:14:47 +08:00
Yoan.liu
2e931d1f67 when tagging the image, also tag the major version 2025-03-10 16:33:28 +08:00
Fu Diwei
c907f22275 fix: wrong detection results of certificate key algorithm 2025-03-10 16:18:30 +08:00
Fu Diwei
19ccac5c05 build: set timezone in docker-compose 2025-03-10 15:22:25 +08:00
Fu Diwei
f9e3797cdd feat: default set autoRestart on deployment to 1panel or baotapanel 2025-03-10 15:13:41 +08:00
RHQYZ
a30379bfdb Merge branch 'usual2970:main' into main 2025-03-10 13:48:47 +08:00
Yoan.liu
dad1b4dfa6 update to version v0.3.2 2025-03-10 06:49:57 +08:00
Fu Diwei
643e09a4e6 fix: typo 2025-03-09 13:04:27 +08:00
Fu Diwei
56fc2d8b44 fix: invalid version checker 2025-03-09 12:57:01 +08:00
Yoan.liu
786f2f8678 Merge pull request #498 from usual2970/hotfix/workflow
fix the issue where the deployment node could not set the certificate…
2025-03-09 12:42:22 +08:00
Yoan.liu
ed689dba41 restore currentlength 2025-03-09 12:40:32 +08:00
Yoan.liu
f779117ed6 fix the issue where the deployment node could not set the certificate source. 2025-03-09 12:23:14 +08:00
Fu Diwei
c9e7e00f42 update README 2025-03-09 11:03:32 +08:00
Yoan.liu
6019945d83 Merge branch 'main' of github.com:usual2970/certimate 2025-03-09 07:22:13 +08:00
Yoan.liu
e0aed060aa update to version 0.3.1 2025-03-09 07:21:56 +08:00
Yoan.liu
1b03be774d Merge pull request #494 from fudiwei/main
enhance & new providers
2025-03-09 07:14:19 +08:00
Fu Diwei
c7ad61e319 feat: add tencentcloud scf deployer 2025-03-08 14:58:40 +08:00
Fu Diwei
563a32ed62 fix: #495 2025-03-08 14:32:22 +08:00
Fu Diwei
1d4b88339e feat: add aliyun fc deployer 2025-03-08 14:30:01 +08:00
Fu Diwei
1e2e88e299 feat: allow insecure connections on deployment to some self-hosted services 2025-03-07 21:04:57 +08:00
Fu Diwei
29dda4ec66 feat: add 1panel deployer 2025-03-07 21:04:50 +08:00
Fu Diwei
6ccbdeb89a feat(ui): update default standard workflow template 2025-03-07 12:27:22 +08:00
Yoan.liu
5ae460c922 Merge pull request #488 from fudiwei/bugfix
serveral bugfix
2025-03-07 06:38:41 +08:00
Fu Diwei
48f3cc419b chore(ui): upgrade cron-parser 2025-03-07 00:18:39 +08:00
Fu Diwei
52e9341dab fix: inappropriate workflow node config unsaved reminder 2025-03-06 21:41:19 +08:00
Fu Diwei
411c39b148 fix: #485 2025-03-06 21:41:19 +08:00
Fu Diwei
574ad0445e refactor(ui): clean code 2025-03-06 21:41:16 +08:00
Fu Diwei
5b2bc6bff9 chore(ui): improve i18n 2025-03-06 18:28:43 +08:00
Fu Diwei
8a113e2bcb fix: missing parameter on deployment to tencentcloud ssl 2025-03-06 18:28:43 +08:00
Fu Diwei
9aaf3ff5d8 fix: #478 2025-03-06 18:28:43 +08:00
Fu Diwei
6d612f42a8 fix: #482 2025-03-06 18:28:43 +08:00
Fu Diwei
e2fdc29ca0 chore(deps): upgrade npm dependencies 2025-03-06 18:28:43 +08:00
Fu Diwei
b17dd04329 chore(deps): upgrade gomod dependencies 2025-03-06 18:28:31 +08:00
Fu Diwei
9a81d4a293 update README 2025-03-05 23:45:42 +08:00
Yoan.liu
5f971ea7e8 update version to v0.3.0 2025-03-05 21:17:58 +08:00
Yoan.liu
a68feda29c Merge pull request #475 from fudiwei/next
Reconfigure Github Actions for Docker Image CI
2025-03-03 21:48:24 +08:00
Fu Diwei
579c411900 build: config github actions workflow 2025-03-02 23:55:30 +08:00
RHQYZ
699f847d4a Merge branch 'usual2970:next' into next 2025-03-02 23:31:26 +08:00
RHQYZ
c370ac0609 chore: config editorconfig 2025-03-02 23:11:38 +08:00
RHQYZ
5db18ab749 update README 2025-03-02 00:31:04 +08:00
Fu Diwei
14ce139135 Merge branch 'next' of https://github.com/fudiwei/certimate into next 2025-03-02 00:29:16 +08:00
Fu Diwei
1745907bcb update README 2025-03-02 00:29:04 +08:00
RHQYZ
344c269f34 Merge pull request #469 from fudiwei/next
improve i18n
2025-03-01 01:44:08 +08:00
Fu Diwei
853feecfcc chore: improve i18n 2025-03-01 01:33:30 +08:00
Yoan.liu
28647d6902 Merge pull request #465 from fudiwei/hotfix
bugfix
2025-02-28 09:12:35 +08:00
Fu Diwei
c89633fcd5 fix: config or logger not be set in deployers 2025-02-26 12:51:17 +08:00
Fu Diwei
6e3d040127 feat: ptr util func 2025-02-25 18:41:23 +08:00
Fu Diwei
a2ac836629 feat: add azure keyvault uploader 2025-02-25 17:12:55 +08:00
Fu Diwei
3c91f29a91 fix: incorrect azure cloud environment 2025-02-24 20:37:38 +08:00
Fu Diwei
78600079a4 fix: incorrect content-type in baota sdk 2025-02-24 16:42:16 +08:00
Fu Diwei
429f8ec85e Merge branch 'next' into hotfix 2025-02-24 15:34:59 +08:00
Fu Diwei
8e5fce3e96 feat(ui): improve i18n 2025-02-24 15:32:53 +08:00
Yoan.liu
13238ae7b4 update version 2025-02-23 21:02:14 +08:00
Yoan.liu
569f94e885 Merge pull request #456 from fudiwei/feat/providers
feat: more providers
2025-02-22 14:47:22 +08:00
Yoan.liu
6e244d0657 Merge pull request #459 from fudiwei/bugfix/overlength-fields-error
bugfix #458
2025-02-22 14:46:29 +08:00
Fu Diwei
97356328be refactor: clean code 2025-02-21 17:28:58 +08:00
Fu Diwei
f81fa2eb63 feat: add dns.la dns-01 applicant 2025-02-21 17:21:39 +08:00
Fu Diwei
316a3c950f fix: nil poiner dereference 2025-02-21 14:48:04 +08:00
Fu Diwei
92528edbfb chore: set default jdcloud logger level to warning 2025-02-21 14:15:23 +08:00
Fu Diwei
32fdb3ed88 fix: typo 2025-02-21 13:53:44 +08:00
Fu Diwei
63f824e3dd feat: add namecheap dns-01 applicant 2025-02-21 10:32:30 +08:00
Fu Diwei
543d2d9d50 feat: add cmcccloud dns-01 applicant 2025-02-20 23:42:07 +08:00
Fu Diwei
6f94f4d882 refactor: reimpl custom lego dns providers 2025-02-20 22:20:18 +08:00
Fu Diwei
906141a415 feat: add jdcloud vod deployer 2025-02-20 22:05:58 +08:00
Fu Diwei
2cfaf4e231 feat: add tencentcloud vod deployer 2025-02-20 21:41:24 +08:00
Fu Diwei
54217217aa fix: insufficient certificate length 2025-02-20 21:06:04 +08:00
Fu Diwei
c492e2de28 feat: add aliyun vod deployer 2025-02-20 21:02:24 +08:00
Fu Diwei
73fec61409 fix: #458 2025-02-20 19:30:26 +08:00
Fu Diwei
ea70429889 feat: add jdcloud alb deployer 2025-02-20 17:39:15 +08:00
Fu Diwei
9febe47975 feat: add jdcloud ssl uploader 2025-02-20 14:32:42 +08:00
Fu Diwei
22d971db4b feat: add jdcloud live video deployer 2025-02-20 10:13:02 +08:00
Fu Diwei
5139198691 feat: add jdcloud cdn deployer 2025-02-20 00:51:34 +08:00
Fu Diwei
0e1f720419 refactor: normalize providers constructors 2025-02-20 00:16:26 +08:00
Fu Diwei
72896e052c feat: add jdcloud dns-01 applicant 2025-02-19 23:57:22 +08:00
Fu Diwei
469c24751e refactor: reimpl 3rd sdks 2025-02-19 21:55:38 +08:00
Fu Diwei
688a013d73 feat(ui): version checker 2025-02-18 21:21:29 +08:00
Fu Diwei
ff53866e9e refactor: normalize providers constructors 2025-02-18 19:19:00 +08:00
Fu Diwei
1bac6174ad feat: add baiducloud dns-01 applicant 2025-02-18 19:18:59 +08:00
Fu Diwei
c451bf5e03 feat: support multiple sites on deployment to baotapanel site 2025-02-18 19:18:59 +08:00
Fu Diwei
03d2f4ca32 feat: add cdnfly deployer 2025-02-18 19:18:56 +08:00
Fu Diwei
46f02331fd feat: add cachefly deployer 2025-02-18 15:16:24 +08:00
Fu Diwei
7c3f2399c2 feat: add gcore cdn deployer 2025-02-18 15:16:20 +08:00
Fu Diwei
ea02190ad5 feat: add gcore uploader 2025-02-18 15:16:19 +08:00
Fu Diwei
e2a148c25f feat: add gcore dns-01 applicant 2025-02-18 15:16:19 +08:00
Fu Diwei
b2eb5d2754 feat: add baishan cdn deployer 2025-02-18 15:16:19 +08:00
Fu Diwei
c72dc0d2c4 feat: add safeline deployer 2025-02-18 15:16:19 +08:00
Fu Diwei
a40b078e9c feat: add tencentcloud waf deployer 2025-02-18 15:16:19 +08:00
Fu Diwei
61b7165bac feat: add huaweicloud waf deployer 2025-02-18 15:16:19 +08:00
Fu Diwei
a6f1f21c18 feat: add huaweicloud waf uploader 2025-02-18 15:16:19 +08:00
Fu Diwei
b734ffcf9d feat: add baotapanel console deployer 2025-02-18 15:16:03 +08:00
Fu Diwei
6d8301b159 Merge branch 'feat/new-workflow' into feat/providers 2025-02-15 11:11:06 +08:00
Yoan.liu
66bdd923d7 Merge pull request #453 from fudiwei/feat/new-workflow
feat: enhance
2025-02-15 09:30:11 +08:00
Fu Diwei
879da92419 feat: add volcengine imagex deployer 2025-02-14 21:01:33 +08:00
Fu Diwei
b9356a5653 feat(ui): new deploy provider category website 2025-02-14 16:34:52 +08:00
Fu Diwei
d21c027db8 refactor: drop access field usage 2025-02-14 16:20:53 +08:00
Fu Diwei
fe93334f86 chore: create migration snapshot 2025-02-14 00:27:25 +08:00
Fu Diwei
d588e14a58 feat: reserved providers 2025-02-13 22:20:56 +08:00
Fu Diwei
0b7b544d4e feat: deploy provider category 2025-02-13 21:50:04 +08:00
Fu Diwei
664bb692b6 feat: search by keyword on AccessList, CertificateList, WorkflowList 2025-02-13 21:50:04 +08:00
Fu Diwei
970fba90e0 chore: rename 2025-02-13 21:50:00 +08:00
Fu Diwei
f6c338b50e chore(deps): upgrade npm denpendencies 2025-02-13 21:50:00 +08:00
Fu Diwei
3bc708b910 chore(deps): upgrade gomod denpendencies 2025-02-13 21:49:54 +08:00
Yoan.liu
041325d67e update version 2025-02-12 09:55:44 +08:00
Yoan.liu
51600e7ad0 Merge branch 'fudiwei-feat/providers' into next 2025-02-12 09:53:17 +08:00
Yoan.liu
4a0e3c9a69 fix conflict 2025-02-12 09:53:09 +08:00
Yoan.liu
41ff0241af Merge branch 'next' of github.com:usual2970/certimate into next 2025-02-12 09:48:42 +08:00
Yoan.liu
12d62bde98 Merge pull request #443 from fudiwei/feat/new-workflow
feat: enhance & bugfix
2025-02-12 09:48:06 +08:00
Yoan.liu
138e08e985 Merge branch 'feat/new-workflow' of github.com:fudiwei/certimate into next 2025-02-12 09:42:00 +08:00
Fu Diwei
94408e8a9f refactor: clean code 2025-02-11 19:31:24 +08:00
Fu Diwei
f3c7d096bc fix(ui): missing css 2025-02-11 19:17:38 +08:00
Fu Diwei
774ed5d31e feat: reserved field challengeType for apply node config 2025-02-11 19:03:58 +08:00
Fu Diwei
b07174b533 feat: cascade delete related runs and outputs when delete workflow 2025-02-11 16:45:51 +08:00
Fu Diwei
45de2cf1db edit README 2025-02-11 00:02:43 +08:00
Fu Diwei
81fe230be4 feat: add baota panel deployer 2025-02-11 00:02:40 +08:00
Fu Diwei
6673871db2 feat: add tencent cloud ssl-deploy deployer 2025-02-10 22:34:01 +08:00
Fu Diwei
316bd58b68 feat: add aliyun cas-deploy deployer 2025-02-10 22:33:41 +08:00
Fu Diwei
ac4c375243 feat: add aliyun esa deployer 2025-02-10 17:59:36 +08:00
Fu Diwei
5da142ab83 fix: memory leak 2025-02-10 16:27:01 +08:00
Fu Diwei
cbf711ee60 feat: save run logs when each workflow node completed 2025-02-10 16:19:04 +08:00
Fu Diwei
4f5c1dc6d7 refactor: new workflow run logs 2025-02-10 13:07:45 +08:00
Fu Diwei
75c89b3d0b feat(ui): display artifact certificates in WorkflowRunDetail 2025-02-10 13:07:45 +08:00
Fu Diwei
b8513eb0b6 fix: different cronexpr rules between ui and pocketbase 2025-02-10 13:07:41 +08:00
Fu Diwei
a74ec95a6a feat(ui): subscribe workflow runs status 2025-02-08 23:08:25 +08:00
Fu Diwei
0bc40fd676 feat: workflow run dispatcher 2025-02-08 23:08:21 +08:00
Fu Diwei
b9e28db089 fix: nil pointer dereference 2025-02-08 23:08:14 +08:00
Yoan.liu
1f6b33f4f6 update version 2025-02-08 09:00:15 +08:00
Yoan.liu
049707acdc Merge pull request #438 from hujingnb/fix/k8s_secret
fix: k8s secret not updated
2025-02-08 08:56:52 +08:00
Fu Diwei
886f166e66 refactor: clean code 2025-02-07 09:19:17 +08:00
Fu Diwei
3f9fda8a2d feat: support multiple workflow outputs 2025-02-06 23:37:44 +08:00
Fu Diwei
d32fce98ae feat: save related runId in certificates or workflow outputs 2025-02-06 23:37:44 +08:00
Fu Diwei
5b9e39a449 fix: #439 2025-02-06 23:37:44 +08:00
Fu Diwei
4b931f782e refactor(ui): clean code 2025-02-06 23:37:44 +08:00
Fu Diwei
24b591ed62 fix: nil pointer dereference 2025-02-06 23:37:44 +08:00
Fu Diwei
a41ee9c3ca feat: enhance certificate model 2025-02-06 23:37:44 +08:00
Fu Diwei
5f5c835533 feat: add ExtractCertificatesFromPEM util func 2025-02-06 23:37:44 +08:00
Fu Diwei
bc29cce645 chore(deps): upgrade gomod dependencies 2025-02-06 23:37:44 +08:00
Fu Diwei
98f4f1cc99 fix: conflict pocketbase superuser initializations 2025-02-06 23:37:44 +08:00
Fu Diwei
d11fc1c07e refactor: reimpl qiniu sdk 2025-02-06 23:37:38 +08:00
hujing
e019bfe136 fix: k8s secret not updated 2025-01-31 00:50:40 +08:00
Yoan.liu
57f8db010b Merge pull request #433 from fudiwei/feat/new-workflow
feat: more providers
2025-01-24 10:26:30 +08:00
Fu Diwei
0e1a964e7c feat: add gname applicant 2025-01-24 03:42:34 +08:00
Fu Diwei
469d4b35c1 feat: implement gname api sdk 2025-01-24 01:38:06 +08:00
Fu Diwei
a78a815ccc fix: typo 2025-01-23 23:54:45 +08:00
Fu Diwei
9f7cffce21 feat: allow fallback to use scp on deployment to ssh 2025-01-23 23:50:11 +08:00
Fu Diwei
5ee5460612 feat: add aws cloudfront deployer 2025-01-23 23:50:07 +08:00
Fu Diwei
1651cda5b4 feat: add aws acm uploader 2025-01-23 23:49:56 +08:00
Fu Diwei
9370f9d68f feat: add cloudns applicant 2025-01-23 23:49:56 +08:00
Fu Diwei
2a7be1b24d feat: add aliyun waf deployer 2025-01-23 23:49:56 +08:00
Fu Diwei
2965fb2b47 feat: add rainyun applicant 2025-01-23 23:49:56 +08:00
Fu Diwei
6c3c29dd11 feat: add westcn applicant 2025-01-23 23:49:56 +08:00
Fu Diwei
adb43dfee1 feat: add qiniu pili deployer 2025-01-23 23:49:49 +08:00
Yoan.liu
c0386b153e Merge pull request #430 from fudiwei/feat/new-workflow
feat: enhance workflow
2025-01-23 09:37:43 +08:00
Fu Diwei
5cabceb08e feat(ui): improve workflow elements scroll area 2025-01-23 03:02:59 +08:00
Fu Diwei
b67049f9aa refactor: clean code 2025-01-22 22:11:04 +08:00
Fu Diwei
7a2fc5e2fd Merge branch 'next' into feat/new-workflow 2025-01-22 20:21:32 +08:00
Yoan.liu
5f213b5f51 Adjustable scaling ratio 2025-01-22 10:52:46 +08:00
Yoan.liu
97c73aae16 Merge pull request #428 from usual2970/feat/upload_certificate
add upload certificate node
2025-01-22 10:03:55 +08:00
Yoan.liu
101d77e4ae parse privatekey using certcrypto 2025-01-22 10:03:13 +08:00
Fu Diwei
0f945881a1 feat: cancel workflow run 2025-01-22 04:13:16 +08:00
Fu Diwei
bee4ba10cb feat: generate run record at the beginning of the workflow execution 2025-01-22 03:48:58 +08:00
Fu Diwei
7e0f575e0a feat(ui): jump to workflow detail page in dashboard 2025-01-22 03:44:54 +08:00
Fu Diwei
79c1da6d14 feat: a new status for canceled workflow run 2025-01-22 03:13:31 +08:00
Fu Diwei
8dc86209df feat: support removing workflow runs 2025-01-21 23:11:48 +08:00
Fu Diwei
c61b2d2d3f fix: couldn't list expire soon certificates 2025-01-21 21:41:25 +08:00
Yoan.liu
c1f2437998 Create FUNDING.yml 2025-01-21 10:42:52 +08:00
Yoan.liu
1039591677 add upload certificate node 2025-01-21 08:02:46 +08:00
Fu Diwei
d5568608f5 refactor: clean code 2025-01-21 00:42:28 +08:00
Yoan.liu
6bdcfaaef0 Merge pull request #425 from usual2970/feat/result_branch
添加执行结果分支节点
2025-01-20 09:46:50 +08:00
Yoan.liu
101d55bafa Merge pull request #426 from fudiwei/feat/new-workflow
feat: support ARI
2025-01-20 09:46:32 +08:00
Fu Diwei
fa8ba061fb feat: support ARI 2025-01-20 02:28:40 +08:00
yoan
1b362673c0 fix conflict 2025-01-19 19:02:58 +08:00
Yoan.liu
11d654e902 Merge pull request #424 from fudiwei/feat/new-workflow
feat: enhance & bugfix
2025-01-19 18:54:34 +08:00
yoan
e6e964aa8c add execute result branch 2025-01-19 17:01:02 +08:00
Fu Diwei
c0dc9b1882 feat(ui): improve workflow runs history 2025-01-19 06:15:38 +08:00
Fu Diwei
5b613bcf84 feat: support configuring repeatable deploy in deployment 2025-01-19 06:02:49 +08:00
Fu Diwei
c71d14cafa feat: support configuring renewal interval in application 2025-01-19 05:37:28 +08:00
Fu Diwei
60a13aaf17 feat: support configuring dns ttl in application 2025-01-19 05:01:36 +08:00
Fu Diwei
c1f77dd92f refactor: clean code 2025-01-19 03:34:38 +08:00
Fu Diwei
ce4c590b1c refactor: clean code 2025-01-18 22:25:20 +08:00
Fu Diwei
3e1ecd60a1 chore(deps): upgrade npm dependencies 2025-01-18 18:37:04 +08:00
Fu Diwei
2171faa330 fix(ui): modal form input focus problem 2025-01-18 18:37:01 +08:00
Fu Diwei
d5e4ea385d feat: download certificate archive 2025-01-18 07:09:41 +08:00
Fu Diwei
d28b89f03e fix: couldn't trasform ecc certificate to pfx format 2025-01-18 07:09:41 +08:00
Fu Diwei
6adcc61447 refactor: clean code 2025-01-18 07:09:41 +08:00
Fu Diwei
ecde12ec23 feat(ui): improve ssl providers switch warning 2025-01-18 07:09:41 +08:00
Fu Diwei
c66027ae8a build: rebuilld pocketbase migration snapshot 2025-01-18 07:09:41 +08:00
Fu Diwei
32f9c95dd0 feat: migrate pocketbase to v0.23 2025-01-18 07:09:41 +08:00
Fu Diwei
1568e5a2a7 fix: incorrect config in deployment on aliyun oss 2025-01-18 07:09:27 +08:00
Fu Diwei
e4a534cb7c Merge branch 'next' into feat/new-workflow 2025-01-17 19:47:44 +08:00
Fu Diwei
ee2cca17fe Merge branch 'next' into feat/new-workflow 2025-01-17 18:07:50 +08:00
Fu Diwei
0869eaafdd refactor: clean code 2025-01-17 18:01:47 +08:00
yoan
69d4b3f93d fix dockerfile 2025-01-17 16:54:37 +08:00
yoan
61b37f38e2 update version 2025-01-17 16:22:11 +08:00
Yoan.liu
c69c560de0 Merge pull request #420 from usual2970/feat/async_apply
池化申请证书请求
2025-01-17 16:20:15 +08:00
Yoan.liu
0d3e426dff Merge pull request #421 from fudiwei/feat/new-workflow
feat: enhance workflow
2025-01-17 11:01:30 +08:00
yoan
4fe68d3b9f limit request rate 2025-01-17 10:56:09 +08:00
Fu Diwei
dab6ad917c refactor: remove unused code 2025-01-16 23:42:53 +08:00
Fu Diwei
a20b82b9cf feat: re-run workflow nodes when critical configurations changed 2025-01-16 23:02:08 +08:00
Fu Diwei
087fd81879 feat: support configuring pb-data-dir on app launch 2025-01-16 22:23:00 +08:00
Fu Diwei
d1dbbae101 feat(ui): show errmsg if table loaded error 2025-01-16 22:07:01 +08:00
Fu Diwei
3a2baba746 feat: support removing certificates 2025-01-16 21:53:51 +08:00
Fu Diwei
831f0ee5d9 feat(ui): improve responsive ui 2025-01-16 21:50:16 +08:00
Fu Diwei
e10fb64d6b Merge branch 'feat/new-workflow' of https://github.com/fudiwei/certimate into feat/new-workflow 2025-01-16 20:30:01 +08:00
Fu Diwei
8ecb71fb55 refactor: clean code 2025-01-16 20:29:28 +08:00
Fu Diwei
dea4106569 fix: couldn't return stdout or stderr during script execution if errors occur on deployment to local/ssh 2025-01-16 20:29:24 +08:00
yoan
2dd8fb2ee2 pool certificate issuance requests 2025-01-16 14:42:54 +08:00
Yoan.liu
2218be5d34 Merge pull request #419 from fudiwei/feat/new-workflow
feat: more providers
2025-01-16 11:23:47 +08:00
Fu Diwei
d712f07b96 refactor: reimplement webhook deployer 2025-01-15 23:04:43 +08:00
Fu Diwei
b657405e46 refactor: clean code 2025-01-15 22:45:34 +08:00
Fu Diwei
974c320925 feat: add edgio applications v7 deployer 2025-01-15 22:45:29 +08:00
Fu Diwei
dd236b925d feat: add ns1 applicant 2025-01-15 14:24:51 +08:00
Fu Diwei
e264d71048 refactor: slices utils 2025-01-15 14:24:48 +08:00
Yoan.liu
23f83b9377 Merge pull request #416 from fudiwei/feat/new-workflow
feat: more providers
2025-01-15 09:11:58 +08:00
Fu Diwei
db68721834 feat: add ucloud us3 deployer 2025-01-14 22:19:40 +08:00
Fu Diwei
6a9cf2ed28 feat(ui): improve responsive ui 2025-01-14 21:46:09 +08:00
Fu Diwei
3dd79d447b update README 2025-01-14 21:34:22 +08:00
Fu Diwei
e87ac72281 feat: add ucloud ucdn deployer 2025-01-14 21:31:10 +08:00
Fu Diwei
e430109228 feat: add ucloud ussl uploader 2025-01-14 21:02:08 +08:00
yoan
e7e123af0d Merge branch 'next' of github.com:usual2970/certimate into next 2025-01-14 12:09:55 +08:00
yoan
7b75dacb03 clean up unnecessary lines 2025-01-14 12:09:12 +08:00
Yoan.liu
493f2a508b Merge pull request #415 from fudiwei/feat/new-workflow
feat: more providers
2025-01-14 08:18:03 +08:00
Fu Diwei
70b8aaf845 update README 2025-01-13 22:26:21 +08:00
Fu Diwei
ab1c9bfdbc feat: add tencentcloud css deployer 2025-01-13 21:47:38 +08:00
Fu Diwei
643d820965 feat: add aliyun live deployer 2025-01-13 21:47:34 +08:00
Fu Diwei
8aa5c3ca65 refactor: clean code 2025-01-13 20:04:46 +08:00
Fu Diwei
7160589ac7 refactor: clean code 2025-01-13 20:03:07 +08:00
Fu Diwei
21cc1d43de feat: support sni domain on deployment to aliyun clb & alb 2025-01-13 20:02:57 +08:00
yoan
793289ad97 mod tidy 2025-01-13 15:27:15 +08:00
Yoan.liu
bea2f00a90 Merge pull request #414 from fudiwei/feat/new-workflow
feat: more providers
2025-01-13 15:12:17 +08:00
yoan
d08da18b4a update preview link 2025-01-13 11:38:20 +08:00
yoan
1e03fcff67 update readme 2025-01-13 11:12:07 +08:00
yoan
ffb516d343 update vedio 2025-01-13 10:53:40 +08:00
yoan
45f9913bdb update readme 2025-01-13 10:43:29 +08:00
Fu Diwei
a6e9cc03c0 Merge branch 'next' into feat/new-workflow 2025-01-12 21:27:35 +08:00
Fu Diwei
b5db2d565a feat: add azure dns applicant 2025-01-12 21:26:31 +08:00
Fu Diwei
e5518b1067 feat: add volcengine clb deployer 2025-01-12 21:26:31 +08:00
Fu Diwei
a5c9ed8d17 feat: add volcengine tos deployer 2025-01-12 21:26:31 +08:00
Fu Diwei
b5094a3cc9 feat: add volcengine dcdn deployer 2025-01-12 21:26:31 +08:00
Fu Diwei
99c5c8339d feat: add volcengine cert-center uploader 2025-01-12 21:26:19 +08:00
Yoan.liu
9f7e0f8a26 Merge pull request #413 from usual2970/feat/dashboard
refine the dashboard
2025-01-12 20:45:42 +08:00
yoan
75bcbe52fd Merge branch 'fudiwei-feat/new-workflow-ui' into next 2025-01-12 20:45:07 +08:00
yoan
8c4a239631 fix build error 2025-01-12 20:44:51 +08:00
yoan
503d9c34f8 refine the dashboard 2025-01-12 19:55:40 +08:00
Fu Diwei
d9f38c38a6 feat(ui): add prompt message during workflow running 2025-01-11 16:51:21 +08:00
Fu Diwei
598d0705fb feat: extract some configs from access to apply logic 2025-01-11 16:31:49 +08:00
Fu Diwei
a0c08e841d feat: separate access providers and dns providers 2025-01-11 16:31:44 +08:00
Fu Diwei
8ed2b2475c refactor: clean code 2025-01-10 21:22:22 +08:00
Fu Diwei
e4e0a24a06 Merge branch 'next' into feat/new-workflow-ui 2025-01-10 12:15:19 +08:00
Yoan.liu
9839e2bb60 Merge pull request #410 from usual2970/feat/async
异步执行workflow
2025-01-10 07:25:52 +08:00
yoan
db10ed8378 handle exit logic 2025-01-10 07:25:09 +08:00
yoan
ebffac7ba4 execute workflows asynchronously 2025-01-09 20:00:15 +08:00
Fu Diwei
f99dd4f89a test: improve example 2025-01-08 20:18:43 +08:00
RHQYZ
6badc0f419 fix: build error 2025-01-08 18:32:50 +08:00
Yoan.liu
aa1b69d7f2 Merge pull request #407 from fudiwei/feat/new-workflow-ui
bugfix #405 on v0.3.x
2025-01-08 18:10:20 +08:00
Fu Diwei
eb3fec1ac0 fix: incorrect nil check logic in tencentcloud cdn and ecdn deployment 2025-01-08 16:05:02 +08:00
Fu Diwei
0f772d55ab refactor(ui): clean code 2025-01-08 15:53:17 +08:00
Yoan.liu
9cea6775d1 Merge pull request #390 from fudiwei/feat/new-workflow-ui
feat: new UI
2025-01-08 09:37:37 +08:00
Fu Diwei
7e376071f5 fix: nil pointer 2025-01-07 01:10:26 +08:00
Fu Diwei
9a937fa072 feat(ui): shared workflow node dropdown menu 2025-01-07 00:57:10 +08:00
Fu Diwei
84c36a4eec feat: improve workflow node configuration 2025-01-06 23:46:14 +08:00
Fu Diwei
155371cdd0 feat: letsencrypt staging environment 2025-01-06 20:05:06 +08:00
Fu Diwei
87e1749553 fix(ui): antd nested form bugs 2025-01-06 19:10:29 +08:00
Fu Diwei
4ba7237326 feat(ui): close confirm when changes not saved 2025-01-06 00:44:06 +08:00
Fu Diwei
6f1a375fee refactor: clean code 2025-01-05 22:59:00 +08:00
Fu Diwei
350160833b feat(ui): new workflow node panel 2025-01-05 22:53:47 +08:00
Fu Diwei
e4c51aece4 refactor: clean code 2025-01-05 22:53:40 +08:00
Fu Diwei
dfc192cb68 refactor: clean code 2025-01-05 16:34:15 +08:00
Fu Diwei
2a68372713 refactor: clean code 2025-01-05 04:08:34 +08:00
Fu Diwei
8af5235e4d refactor: clean code 2025-01-05 03:34:46 +08:00
Fu Diwei
7cf96d7d7e feat: release and discard workflow changes 2025-01-05 02:38:01 +08:00
Fu Diwei
9c4831fa3f fix: couldn't skip certificate not found error 2025-01-05 01:39:47 +08:00
Fu Diwei
8cf1ffd38b fix: couldn't get certificate effect time or expire time 2025-01-05 01:27:21 +08:00
Fu Diwei
3c70a4f455 fix: couldn't save certificate source 2025-01-05 01:16:00 +08:00
Fu Diwei
ddb6a88392 fix(ui): wrong form initial values 2025-01-05 00:47:27 +08:00
Fu Diwei
61843a4997 refactor: clean code 2025-01-05 00:08:12 +08:00
Fu Diwei
3b9a7fe805 feat: workflow run status & time 2025-01-04 22:07:01 +08:00
Fu Diwei
b686579acc feat: rename workflow_run_log to workflow_run 2025-01-04 16:53:58 +08:00
Fu Diwei
01ede08a79 feat: rename input to inputs, output to outputs 2025-01-04 16:41:30 +08:00
Fu Diwei
ae11d5ee3d feat: rename san to subjectAltNames, workflow to workflowId, nodeId to workflowNodeId, output to workflowOutputId, log to logs, succeed to succeeded 2025-01-04 16:29:14 +08:00
Fu Diwei
9246878d0e feat: rename domain to subjectAltNames 2025-01-04 14:04:47 +08:00
Fu Diwei
5387c373e0 feat: rename email to contactEmail 2025-01-04 13:39:08 +08:00
Fu Diwei
da76d1065e feat: rename , executionMethod/type to trigger, crontab to triggerCron 2025-01-04 13:29:03 +08:00
Fu Diwei
2213399f5e feat(ui): disable nodes during workflow running 2025-01-04 12:58:45 +08:00
Fu Diwei
52dfa5e8c3 feat: rename access to providerAccessId 2025-01-04 12:37:34 +08:00
Fu Diwei
90058b2dae feat: support template variables in webhook deployment 2025-01-04 10:26:57 +08:00
Fu Diwei
e695c8ee5c feat: rename configType/providerType to provider 2025-01-03 22:20:34 +08:00
Fu Diwei
849e065bb2 refactor(ui): clean code 2025-01-03 21:58:05 +08:00
Fu Diwei
b7cd07c996 fix(ui): workflow branch edge 2025-01-03 21:35:12 +08:00
Fu Diwei
52ee3863ae feat(ui): enhance WorkflowNew 2025-01-03 21:31:17 +08:00
Fu Diwei
5ce5a08e41 style(ui): eslint-plugin-tailwindcss 2025-01-03 20:35:11 +08:00
Fu Diwei
8a16893082 feat(ui): new WorkflowElements using antd 2025-01-03 20:29:34 +08:00
Fu Diwei
c6a8f923e4 feat(ui): WorkflowNew page 2025-01-02 20:24:16 +08:00
Fu Diwei
b6dd2248c8 style(ui): eslint-sort-imports 2025-01-02 12:50:38 +08:00
Fu Diwei
1588179bc9 refactor(ui): clean code 2025-01-02 10:28:18 +08:00
Fu Diwei
7f36621882 refactor(ui): clean code 2025-01-02 10:11:35 +08:00
Fu Diwei
e256d36cd1 refactor(ui): clean code 2025-01-02 10:04:23 +08:00
Fu Diwei
b2417ad902 fix(ui): useEffect deps 2025-01-01 20:52:36 +08:00
Fu Diwei
67a32a98a9 fix(ui): date format 2025-01-01 20:52:18 +08:00
Fu Diwei
78d9d5159a style: eslint-plugin-import 2025-01-01 20:40:59 +08:00
Fu Diwei
e2d29b8fa2 feat: configure k8s secret type 2025-01-01 19:13:48 +08:00
Fu Diwei
880c8819b4 chore(deps): upgrade npm dependencies 2025-01-01 19:12:54 +08:00
Fu Diwei
6075cc5c95 feat(ui): release & run workflow 2025-01-01 17:22:19 +08:00
Fu Diwei
5c1854948c feat(ui): improve i18n 2025-01-01 14:22:23 +08:00
Fu Diwei
7bd0cbce10 feat(ui): improve i18n 2025-01-01 14:04:41 +08:00
Fu Diwei
9c645a1efa chore: remove unused code 2024-12-31 20:05:48 +08:00
Fu Diwei
6f088fd76a feat(ui): new DeployNodeForm using antd 2024-12-31 19:55:34 +08:00
Fu Diwei
cb7a465d6c refactor: clean code 2024-12-28 16:59:36 +08:00
Fu Diwei
416f5e0986 refactor: clean code 2024-12-28 16:26:01 +08:00
Fu Diwei
86133ba52b refactor: clean code 2024-12-27 19:35:50 +08:00
Fu Diwei
047479426a refactor(ui): clean code 2024-12-27 17:02:21 +08:00
Fu Diwei
fb2d292cbf feat(ui): multiple input domains & nameservers in ApplyNodeForm 2024-12-27 16:42:07 +08:00
Fu Diwei
75cf552e72 refactor(ui): useTriggerElement 2024-12-27 12:47:45 +08:00
Fu Diwei
77537e7005 refactor: rename Timeout to PropagationTimeout during ACME DNS-01 authentication 2024-12-27 09:50:54 +08:00
Fu Diwei
dae6ad2951 feat(ui): add @ant-design/icons 2024-12-26 13:02:22 +08:00
Fu Diwei
8a816ba44f feat(ui): new WorkflowApplyNodeForm using antd 2024-12-26 03:06:15 +08:00
Fu Diwei
a9d918aa95 refactor(ui): validators util 2024-12-26 01:07:47 +08:00
Fu Diwei
a980904e38 chore: remove unused code 2024-12-26 00:42:23 +08:00
Fu Diwei
4008c2bfd5 refactor(ui): clean code 2024-12-25 23:28:42 +08:00
Fu Diwei
1184e52ba9 refactor(ui): clean code 2024-12-25 23:20:09 +08:00
Fu Diwei
adbf40914e chore: remove unused code 2024-12-25 21:26:32 +08:00
Fu Diwei
9b9083dfa1 Merge branch 'next' into feat/new-workflow-ui 2024-12-25 21:06:59 +08:00
Fu Diwei
6bd3b4998e feat(ui): new WorkflowNotifyNodeForm using antd 2024-12-25 20:57:09 +08:00
Yoan.liu
1f602c00be Merge pull request #394 from RangerCD/feat-name-dot-com
feat(provider): add name.com
2024-12-25 15:35:20 +08:00
Fu Diwei
4d0f7c2e02 refactor(ui): useAntdForm 2024-12-25 14:51:32 +08:00
Fu Diwei
c9024c5611 feat(ui): new WorkflowStartNodeForm using antd 2024-12-25 00:36:02 +08:00
RangerCD
a92dc2bbe6 fix(provider): typo while adding name.com 2024-12-24 22:45:39 +08:00
Fu Diwei
401fa3dcdd feat(ui): new WorkflowRuns using antd 2024-12-24 22:00:17 +08:00
Fu Diwei
4e5373de73 feat(ui): new WorkflowDetail using antd 2024-12-24 20:39:01 +08:00
RHQYZ
956fbb7833 feat(ui): improve i18n 2024-12-24 19:29:15 +08:00
RangerCD
6217d3aacd feat(provider): add name.com 2024-12-24 19:02:09 +08:00
Fu Diwei
8b1ae309fb refactor(ui): useZustandShallowSelector 2024-12-24 15:07:39 +08:00
Fu Diwei
52d24ff2f2 feat: improve i18n 2024-12-23 22:46:07 +08:00
Fu Diwei
7a66bdf139 fix: fix typo 2024-12-23 22:46:01 +08:00
Fu Diwei
16bc12c15b feat update placeholder syntax in notify templates 2024-12-23 22:33:12 +08:00
Fu Diwei
0556d68a4e feat(ui): MultipleInput 2024-12-23 22:22:00 +08:00
Fu Diwei
586c7fa927 feat: create DNSProvider using independent config instead of envvar 2024-12-23 19:58:51 +08:00
Fu Diwei
9ef16ebcf9 refactor: clean code 2024-12-23 19:31:48 +08:00
Fu Diwei
d509445519 refactor: clean code 2024-12-23 15:31:41 +08:00
Fu Diwei
d7bff599b7 chore(deps): upgrade gomod dependencies 2024-12-23 15:05:25 +08:00
Fu Diwei
cda54085b9 chore(deps): upgrade npm dependencies 2024-12-23 13:36:18 +08:00
Fu Diwei
984aae1ca6 chore: remove unused code 2024-12-22 20:10:04 +08:00
Fu Diwei
695c99119f Merge branch 'next' into feat/new-workflow-ui 2024-12-22 19:48:34 +08:00
Fu Diwei
d7e205aee7 feat(ui): improve i18n 2024-12-22 19:45:01 +08:00
Fu Diwei
09919cb3cb fix(ui): couldn't save ssh key 2024-12-22 19:35:05 +08:00
yoan
ba73e04046 fix build error 2024-12-22 18:43:18 +08:00
yoan
88cbf30fde update version 2024-12-22 18:38:24 +08:00
yoan
ed37add29f Merge branch 'fudiwei-feat/new-workflow-ui' into next 2024-12-22 18:37:52 +08:00
yoan
6d25f9c205 fix build error 2024-12-22 18:37:03 +08:00
Fu Diwei
01d30bb742 feat: add wecom notifier 2024-12-22 11:25:08 +08:00
Fu Diwei
a1fec5f6ac chore: remove unused code 2024-12-21 19:00:20 +08:00
Fu Diwei
ef9ddd27a5 chore: remove unused code 2024-12-21 12:46:22 +08:00
Fu Diwei
b6203e57ed fix: fix typo 2024-12-20 23:11:45 +08:00
Fu Diwei
3fcea4ba2f refactor: clean code 2024-12-20 23:07:40 +08:00
Fu Diwei
a51f85826c chore: remove unused code 2024-12-20 23:00:05 +08:00
Fu Diwei
c846945905 refactor(deployer): reimplement deploy service 2024-12-20 22:59:04 +08:00
Fu Diwei
e2af21e0e1 fix: could not deploy again when certificate is not expired 2024-12-20 22:59:00 +08:00
Fu Diwei
929250810f chore: remove unused code 2024-12-20 21:45:07 +08:00
Fu Diwei
cb162e063d chore: remove unused code 2024-12-20 21:23:55 +08:00
Fu Diwei
63ffb9df14 fix(ui): tsc-check error 2024-12-20 21:01:24 +08:00
Fu Diwei
a917d6c2c5 feat(ui): new SettingsSSLProvider using antd 2024-12-20 20:42:46 +08:00
Fu Diwei
9e1e0dee1d fix(ui): couldn't detect form changed in NotifyChannels 2024-12-20 14:08:30 +08:00
Fu Diwei
7c1a2d5f91 feat(ui): new SettingsNotification using antd 2024-12-20 13:56:29 +08:00
Fu Diwei
cae33cfc4f fix(ui): duplicate form names 2024-12-20 12:06:30 +08:00
Fu Diwei
d143df3f9f fix(ui): deep compare when model change in AccessEditForm 2024-12-19 21:34:17 +08:00
Fu Diwei
84a3817b15 feat(ui): disable autocomplete in AccessEditForm 2024-12-19 13:11:32 +08:00
Fu Diwei
525eb83d1e feat(ui): new SettingsPassword using antd 2024-12-19 11:59:13 +08:00
Fu Diwei
8b7295d77e feat(ui): new SettingsAccount using antd 2024-12-19 10:50:42 +08:00
Fu Diwei
df57c196e9 build(ui): config babel 2024-12-19 10:18:04 +08:00
Fu Diwei
5ea5473bdd refactor(ui): clean code 2024-12-19 09:44:09 +08:00
Fu Diwei
85faf8d517 chore: remove unused code 2024-12-18 21:24:39 +08:00
Fu Diwei
abe6dbb5a2 feat(ui): new Settings layout using antd 2024-12-18 21:22:25 +08:00
Fu Diwei
afa446aabe feat(ui): fixed header & sider 2024-12-18 20:45:27 +08:00
Fu Diwei
2712f9a3f4 feat(ui): new AccessSelect component using antd 2024-12-18 16:10:47 +08:00
Fu Diwei
c40de5d3b2 build: vite.config.ts 2024-12-18 13:24:35 +08:00
Fu Diwei
2b1da81b98 chore 2024-12-18 13:19:55 +08:00
Fu Diwei
d693d26323 refactor: clean code 2024-12-18 10:27:55 +08:00
Fu Diwei
2374bb56fa refactor: clean code 2024-12-18 10:20:32 +08:00
Fu Diwei
df71782719 fix: #322 2024-12-17 19:25:06 +08:00
Fu Diwei
599e718003 feat(ui): props guard 2024-12-17 19:25:00 +08:00
Fu Diwei
1cad816b17 fix: render error when notify template is empty 2024-12-17 19:15:05 +08:00
Fu Diwei
0fa6d2980b chore: remove unused code 2024-12-17 19:11:28 +08:00
Fu Diwei
c27818b3b0 feat(ui): new AccessEditForm using antd 2024-12-17 19:11:19 +08:00
Fu Diwei
047b3bc079 feat: normalize provider names 2024-12-17 17:11:36 +08:00
Fu Diwei
70e6920288 refactor(ui): clean code 2024-12-16 13:37:10 +08:00
Fu Diwei
b5739c663d feat(ui): new AccessProviderSelect component using antd 2024-12-12 16:49:12 +08:00
Fu Diwei
220d98a668 feat(ui): show more details in CertificateDetail 2024-12-12 09:48:50 +08:00
yoan
419b6eb626 fix build error 2024-12-11 22:53:37 +08:00
yoan
9764fb481f update version 2024-12-11 22:29:10 +08:00
yoan
ba01edc691 fix build error 2024-12-11 22:28:04 +08:00
yoan
fba647313d update version 2024-12-11 22:00:01 +08:00
yoan
2f146fffdc Merge branch 'next' of github.com:usual2970/certimate into next 2024-12-11 21:57:57 +08:00
yoan
7654c79d12 Merge branch 'fudiwei-feat/new-workflow-ui' into next 2024-12-11 21:57:25 +08:00
yoan
cf35dbfd6e Update site name width 2024-12-11 21:56:01 +08:00
yoan
a6c002146c Merge branch 'feat/new-workflow-ui' of github.com:fudiwei/certimate into fudiwei-feat/new-workflow-ui 2024-12-11 21:26:42 +08:00
Fu Diwei
bb3009a124 refactor(ui): refactor accesses state using zustand store 2024-12-11 19:55:50 +08:00
Fu Diwei
b744363736 refactor(ui): refactor emails state using zustand store 2024-12-11 16:42:23 +08:00
RHQYZ
35c25987cd Update README_EN.md 2024-12-10 20:03:41 +08:00
RHQYZ
ca91a9e089 Update README.md 2024-12-10 20:01:27 +08:00
Fu Diwei
83ba3d4450 fix: #361 2024-12-10 19:23:48 +08:00
Fu Diwei
8fe0d342aa refactor(ui): improve i18n 2024-12-10 16:37:24 +08:00
Fu Diwei
a4eff0b408 feat(ui): enhance certificate downloading 2024-12-09 19:42:56 +08:00
Fu Diwei
7b85da901d feat(ui): responsive table 2024-12-09 19:32:09 +08:00
Fu Diwei
be27789ea8 feat(ui): responsive sider menu 2024-12-09 19:25:35 +08:00
Fu Diwei
07a443f6c4 refactor(ui): clean code 2024-12-09 17:48:44 +08:00
Fu Diwei
588e89e8fe feat(ui): copied to clipboard message 2024-12-09 17:38:34 +08:00
Fu Diwei
789c120fc9 feat(ui): antd theme 2024-12-09 17:04:02 +08:00
Fu Diwei
fdfe54b6da feat(ui): antd i18n 2024-12-09 16:09:35 +08:00
Fu Diwei
3b50741f19 chore(ui): clean code 2024-12-09 15:17:55 +08:00
Fu Diwei
c5498b92a2 feat(ui): optimize table UI 2024-12-09 13:19:25 +08:00
Fu Diwei
048150d779 feat(ui): new Dashboard UI using antd 2024-12-09 13:10:48 +08:00
Fu Diwei
7db933199a feat(ui): optimize table UI 2024-12-08 21:10:22 +08:00
Fu Diwei
5c6be439e8 feat(ui): optimize table UI 2024-12-08 11:55:30 +08:00
Fu Diwei
4e0134b70a feat(ui): new CertificateDetail UI using antd 2024-12-07 17:11:36 +08:00
Fu Diwei
d6ddf8e9f4 feat(ui): new Layout UI using antd 2024-12-07 13:47:02 +08:00
Fu Diwei
2facb160aa feat(ui): new Login UI using antd 2024-12-06 19:44:29 +08:00
Fu Diwei
b44b8d09b2 feat(ui): new AccessList UI using antd 2024-12-06 09:54:44 +08:00
Fu Diwei
65d9c6fe2f feat(ui): new CertificateList UI using antd 2024-12-05 21:52:27 +08:00
Fu Diwei
c522196029 feat(ui): new WorkflowList UI using antd 2024-12-04 21:55:52 +08:00
yoan
a5d097e860 Ensure branches execute independently without affecting each other 2024-12-02 08:54:25 +08:00
Fu Diwei
668f6ee36f feat(ui): use ant-design 2024-11-25 21:28:38 +08:00
Fu Diwei
4f2363230d chore: fix typo 2024-11-25 21:28:07 +08:00
Fu Diwei
2b93552d1d chore: comments 2024-11-25 21:22:20 +08:00
yoan
124af0b76d update workflow file 2024-11-24 21:11:46 +08:00
yoan
972df7c167 migration 2024-11-24 20:16:55 +08:00
yoan
b4c17a6a12 update version 2024-11-24 20:07:00 +08:00
yoan
92c03cbdf9 fix conflict 2024-11-24 20:06:30 +08:00
yoan
220478cd31 update version 2024-11-24 20:01:40 +08:00
yoan
df905ade88 workflow ajustment 2024-11-24 20:01:04 +08:00
yoan
9ff3e22c80 details improvement and unnecessary files deletion 2024-11-24 13:36:17 +08:00
yoan
37df882ed3 improve multi language 2024-11-23 12:55:31 +08:00
yoan
47050769fc fix conflict 2024-11-22 11:16:54 +08:00
Yoan.liu
65df759275 Merge pull request #348 from fudiwei/feat/deployer
feat: deployers
2024-11-22 11:14:24 +08:00
yoan
86761bd3a0 Certificate displaying and monitoring 2024-11-22 10:59:57 +08:00
Fu Diwei
a842b6b925 fix: illegal arguments 2024-11-21 20:23:01 +08:00
yoan
09e4b24445 certificate display 2024-11-21 13:17:39 +08:00
Fu Diwei
4916757d59 feat: add Deployer factory 2024-11-21 11:23:15 +08:00
Fu Diwei
30b66adc3b refactor: replace Append* to Log* in DeployerLogger 2024-11-21 10:35:45 +08:00
Fu Diwei
13582d1a7b test: add unit test cases 2024-11-21 10:29:04 +08:00
Fu Diwei
0b9312b549 feat: implement more Deployer 2024-11-20 23:51:26 +08:00
Fu Diwei
bde51d8d38 feat: implement more Deployer 2024-11-20 22:58:01 +08:00
Fu Diwei
643a666853 feat: implement more Deployer 2024-11-20 21:02:29 +08:00
yoan
2d10fa0218 Save and display execution records 2024-11-20 15:47:51 +08:00
Fu Diwei
a59184ae5f fix: update GetValueOrDefault util functions to return default value for zero values 2024-11-20 07:49:50 +08:00
Fu Diwei
82807fcc1b refactor: clean code 2024-11-19 22:43:15 +08:00
Fu Diwei
a6c93ef9b8 test: fix typo 2024-11-19 22:11:47 +08:00
Fu Diwei
6a151865f7 feat: implement k8s secret Deployer 2024-11-19 22:04:00 +08:00
Fu Diwei
414d8d140e test: use flag arguments in test cases for Notifier and Deployer 2024-11-19 21:18:36 +08:00
Fu Diwei
51fb9dca58 test: add some unit test cases for new Deployer 2024-11-19 20:03:51 +08:00
Fu Diwei
6367785b1b feat: implement local, ssh, webhook Deployer 2024-11-19 19:09:48 +08:00
yoan
03b2a9da66 Implement complete workflow execution process 2024-11-19 16:02:31 +08:00
Fu Diwei
aa7fb7da06 Merge branch 'main' into feat/deployer 2024-11-19 09:09:38 +08:00
Fu Diwei
26d11de249 feat: add deployer interface 2024-11-19 09:08:49 +08:00
yoan
a9d5b53460 Merge branch 'main' into feat/workflow 2024-11-19 09:07:37 +08:00
yoan
0daa9f1882 v0.2.21 2024-11-19 09:07:08 +08:00
yoan
f799740d70 fix conflict 2024-11-18 20:22:21 +08:00
yoan
56886dcfe9 Merge branch 'LeoChen98-fix-reapply-when-domain-list-changed' 2024-11-18 20:03:16 +08:00
yoan
81e1e4a7ff validity duration 2024-11-18 20:03:11 +08:00
yoan
9b5256716f Merge branch 'fix-reapply-when-domain-list-changed' of github.com:LeoChen98/certimate into LeoChen98-fix-reapply-when-domain-list-changed 2024-11-18 19:58:36 +08:00
usual2970
446bf80f1d Merge pull request #346 from jarod/main
feat: add deployer BytePlus CDN
2024-11-18 19:43:58 +08:00
yoan
775b12aec1 Add workflow execution process 2024-11-18 19:40:24 +08:00
Jarod Liu
6a80455c6c fix: byteplus access provider 2024-11-18 10:51:51 +08:00
Fu Diwei
43b2ff7957 refactor: extract x509 transformer utils 2024-11-18 09:12:15 +08:00
Fu Diwei
295b7779ee refactor: clean code 2024-11-18 09:10:28 +08:00
Jarod Liu
d1df088662 fix: 补充Provider Access 的 UI 实现 2024-11-16 09:52:28 +08:00
Jarod Liu
2b0f7aaf8a feat: add deployer BytePlus CDN 2024-11-16 09:18:58 +08:00
Leo Chen
3265dd76ab edit comments for the forward changes 2024-11-15 20:45:08 +08:00
Leo Chen
d1d7b44303 Invert the changed logic to match the function name 2024-11-15 20:37:36 +08:00
Leo Chen
56eced3813 Invert the boolean value to match the function name 2024-11-15 20:36:47 +08:00
yoan
bde2147dd3 fix conflict 2024-11-15 10:27:10 +08:00
yoan
c853f2976f v0.2.20 2024-11-15 08:07:37 +08:00
yoan
8901f5d40e improve data display 2024-11-15 08:06:39 +08:00
usual2970
b66931003f Merge pull request #342 from belier-cn/volcengine-cdn
feat: add volcengine cdn deployer
2024-11-15 08:05:27 +08:00
Leo Chen
9a75d2ac8f add key algorithm check 2024-11-15 00:33:09 +08:00
yoan
9132d47f4d display workflow data 2024-11-14 14:52:02 +08:00
belier
42c5aea3f7 docs: update README_EN.md 2024-11-14 14:28:39 +08:00
belier
e2fd9c4cee style: modify variable name 2024-11-14 14:28:35 +08:00
belier
f847b7ff62 improvement: improve certificate fingerprint comparison 2024-11-14 14:19:00 +08:00
belier
9eae8f5077 feat: add volcengine cdn deployer 2024-11-14 13:39:23 +08:00
usual2970
2bacf76664 Merge pull request #339 from belier-cn/main
feat: add volcengine dns provider and add volcengine live deployer
2024-11-14 09:42:26 +08:00
usual2970
b2030caedc Merge pull request #337 from fudiwei/bugfix/syntax-error
fix switch-case syntax error
2024-11-14 09:35:58 +08:00
usual2970
956c975c6d Merge pull request #333 from JiangJamm/feat/notify_setting_expand
feat: 使系统设置中的消息推送设置列表打开后能够关闭
2024-11-14 09:34:52 +08:00
Leo Chen
41bd321a4f fixed: not reapply when domain list changed
fixed #334
2024-11-13 18:52:29 +08:00
Leo Chen
952e9687d0 fix misspelling var name 2024-11-13 17:58:56 +08:00
belier
c298f8b952 docs: Add Volcengine Information to README.md 2024-11-13 16:18:04 +08:00
belier
e2562a5251 feat: add volcengine dns provider and add volcengine live deployer 2024-11-13 15:36:46 +08:00
Fu Diwei
dbdb40baf9 fix: fix switch-case syntax error 2024-11-13 13:44:44 +08:00
yoan
52f40d982d add access to deployment 2024-11-13 13:20:47 +08:00
yoan
fd04cec606 Merge branch 'main' into feat/workflow 2024-11-13 08:41:25 +08:00
yoan
2ff923dd1b v0.2.19 2024-11-13 08:16:19 +08:00
usual2970
f4f13f91f2 Merge pull request #331 from fudiwei/bugfix/qiniu-wildcard-domain
bugfix #330
2024-11-13 08:14:32 +08:00
usual2970
034aa980e6 Merge pull request #329 from fudiwei/bugfix/aliyun-clb-deploy-error
bugfix #326
2024-11-13 08:14:19 +08:00
usual2970
6ac7a51ce0 Merge pull request #328 from fudiwei/bugfix/tencentcloud-deploy-config-not-saving
bugfix #324
2024-11-13 08:14:05 +08:00
usual2970
cf0c0e3e2c Merge pull request #327 from LeoChen98/fix-tencent-cos-instance-not-found
fixed: instance not found when deploying tencent COS
2024-11-13 08:13:49 +08:00
JiangJamm
1b899575e0 feat: 使系统设置中的消息推送设置列表打开后能够关闭 2024-11-13 01:10:26 +08:00
Fu Diwei
23e5cb5669 fix: #330 2024-11-12 21:41:06 +08:00
yoan
ee9578b273 delete mail 2024-11-12 21:40:02 +08:00
Fu Diwei
e4ba4c9b37 fix: #326 2024-11-12 20:35:31 +08:00
Fu Diwei
9ed64bdc9a fix: #324 2024-11-12 20:20:54 +08:00
Leo Chen
e9b6fb55ff fixed: instance possible not found when deploying tencent CLB via SSL api
修复了重构导致腾讯云CLB通过SSL接口部署时可能找不到实例的bug
2024-11-12 17:59:13 +08:00
Leo Chen
80caf881ae fixed: instance not found when deploying tencent COS
修复了重构导致腾讯云COS部署时找不到实例的bug
2024-11-12 17:56:41 +08:00
yoan
35c0ed2ba5 workflow data save 2024-11-12 13:16:23 +08:00
usual2970
c36db3545f Merge pull request #321 from fudiwei/feat/notifier
feat: notifiers
2024-11-11 18:16:30 +08:00
Fu Diwei
2994cb5c65 test: add unit test case for email notifier 2024-11-10 20:28:01 +08:00
Fu Diwei
1bedb31a3c fix: fix typo 2024-11-10 20:06:18 +08:00
Fu Diwei
8fecebc254 feat: show loading button when pushing test notifications 2024-11-10 20:00:19 +08:00
Fu Diwei
44497a0969 feat: new UI for notify settings 2024-11-10 19:52:50 +08:00
Fu Diwei
8b04e96a7d feat: new UI for email notify settings 2024-11-10 18:21:43 +08:00
Fu Diwei
5d93334426 refactor: re-implement logic of notify 2024-11-10 18:03:20 +08:00
Fu Diwei
150b666d4b refactor: maps utils 2024-11-09 20:46:49 +08:00
Fu Diwei
94579d65c4 refactor: clean code 2024-11-09 20:29:13 +08:00
Fu Diwei
551b06b4e8 feat: notifier 2024-11-09 20:06:22 +08:00
Fu Diwei
76fc47a274 Merge branch 'main' into feat/notifier 2024-11-09 12:14:21 +08:00
Fu Diwei
83674e4b35 refactor: ensure compile-time check for Uploader implementations 2024-11-09 09:47:14 +08:00
1301 changed files with 101054 additions and 35578 deletions

View File

@@ -8,3 +8,9 @@ indent_size = 2
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
[*.go]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = tab

15
.github/FUNDING.yml vendored Normal file
View 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/"]

80
.github/ISSUE_TEMPLATE/1-bug_report.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: "🐞 Bug Report"
description: "报告缺陷来帮助我们完善。 / Create a report to help us improve."
title: "[Bug] 简要描述你发现的缺陷"
labels:
- bug
body:
- type: markdown
attributes:
value: |
## Welcome!
**在提交 Issue 之前,请确认以下事项**
1. 我**确认**已尝试过使用当前最新版本,并能复现问题。由于开发者精力有限,非当前最新版本的问题将被直接关闭,感谢理解。
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个缺陷报告。如果有多个缺陷,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release and can reproduce the issue. Issues that are not in the latest version will be closed directly.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
5. Please limit one report per issue.
- type: input
attributes:
label: 软件版本 / Release Version
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
placeholder: (e.g. v1.0.0)
validations:
required: true
- type: textarea
attributes:
label: 缺陷描述 / Description
description: 请详细清晰地描述你发现的缺陷或故障,如果可能请上传截图。 / Describe the bug you found in detail and clearly, and upload screenshots if possible.
validations:
required: true
- type: textarea
attributes:
label: 复现步骤 / Steps to reproduce
description: 请提供可复现的完整步骤。 / Please walk us through it step by step.
placeholder: |
1. ...
2. ...
3. ...
...
validations:
required: true
- type: textarea
attributes:
label: 日志 / Logs
description: 在此处添加日志信息(如果有的话)。 / Add logs here if available.
value: |-
<details>
```console
# 请在此粘贴日志 / Paste logs here
```
</details>
validations:
required: false
- type: textarea
attributes:
label: 其他 / Miscellaneous
description: 在此处添加关于该 Issue 的任何其他信息。 / Add any other context about the issue here.
validations:
required: false
- type: checkboxes
attributes:
label: 贡献 / Contribution
options:
- label: 我乐意为此贡献代码! / I am interested in contributing to this issue!
required: false

View File

@@ -0,0 +1,52 @@
name: "💡 Feature Request"
description: "提出新功能请求或改进意见。 / Suggest an idea for this project."
title: "[Feature] 简要描述你希望实现的功能"
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
## Welcome!
**在提交 Issue 之前,请确认以下事项**
1. 我**确认**是基于当前最新大版本而提出的新功能请求或改进意见。
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个功能请求。如果有多个需求,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
5. Please limit one request per issue.
- type: textarea
attributes:
label: 功能描述 / Description
description: 请详细清晰地描述你希望添加的功能,如果可能请上传截图。 / Describe the feature you'd like to add in detail and clearly, and upload screenshots if possible.
validations:
required: true
- type: textarea
attributes:
label: 请求动机 / Motivation
description: 为什么这个功能对项目有帮助? / Why is this feature helpful to the project?
validations:
required: true
- type: textarea
attributes:
label: 其他 / Miscellaneous
description: 在此处添加关于该 Issue 的任何其他信息(新增提供商请求请补充 API 文档链接等资料)。 / Add any other context about the problem here.
validations:
required: false
- type: checkboxes
attributes:
label: 贡献 / Contribution
options:
- label: 我乐意为此贡献代码! / I am interested in contributing to this issue!
required: false

44
.github/ISSUE_TEMPLATE/3-questions.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: "❓ Questions"
description: "遇到了困难需要求助? / Have problem in use and need help?"
title: "简要描述你遇到的问题"
body:
- type: markdown
attributes:
value: |
## Welcome!
**在提交 Issue 之前,请确认以下事项**
1. 我**确认**正在使用的是当前最新版本。
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个问题求助。如果有多个问题,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
5. Please limit one question per issue.
- type: input
attributes:
label: 软件版本 / Release Version
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
placeholder: (e.g. v1.0.0)
validations:
required: true
- type: textarea
attributes:
label: 问题描述 / Description
description: 请详细清晰地描述你遇到的问题,如果可能请上传截图。 / Describe the problem you encountered in detail and clearly, and upload screenshots if possible.
validations:
required: true
- type: textarea
attributes:
label: 其他 / Miscellaneous
description: 在此处添加关于该问题的任何其他信息。 / Add any other context about the problem here.
validations:
required: false

View File

@@ -1,33 +0,0 @@
---
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]
**其他信息**
在此处添加关于该问题的任何其他信息。

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 加入频道讨论
url: https://t.me/+ZXphsppxUg41YmVl
about: 加入到电报频道寻求更多帮助
- name: "🌐 加入频道讨论"
about: "加入到电报频道寻求更多帮助。 / Join in our Telegram channel."
url: "https://t.me/+ZXphsppxUg41YmVl"
- name: "📖 常见问题"
about: "请先阅读文档 FAQ可能会有你需要的答案。 / Please take a look to FAQs."
url: "https://docs.certimate.me/docs/reference/faq"

View File

@@ -1,19 +0,0 @@
---
name: Feature request
about: 提出一个新功能请求
title: "[Feature] 简要描述你希望实现的功能"
labels: enhancement
assignees: ""
---
**功能描述**
简要描述你希望添加的功能和相关问题1 个 ISSUE 只描述一个功能。
**动机**
为什么这个功能对项目有帮助?
**替代方案**
描述你已经考虑过的替代方案。
**其他信息**
在这里添加任何相关的附加信息或截图。

View File

@@ -1,15 +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: "v0.1.9"
default: "latest"
jobs:
build-and-push:
@@ -32,17 +34,20 @@ jobs:
images: |
usual2970/certimate
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
- 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 }}

61
.github/workflows/push_image_next.yml vendored Normal file
View 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 }}

View File

@@ -1,12 +1,12 @@
name: basebuild
name: Release
on:
push:
tags:
- "*"
- "v[0-9]*"
jobs:
goreleaser:
prepare-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -19,19 +19,192 @@ jobs:
with:
node-version: 20.11.0
- name: Build WebUI
run: |
npm --prefix=./ui ci
npm --prefix=./ui run build
- name: Upload UI build artifacts
uses: actions/upload-artifact@v4
with:
name: ui-build
path: ./ui/dist
retention-days: 1
build-linux:
needs: prepare-ui
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ">=1.22.5"
go-version-file: "go.mod"
- name: Build Admin dashboard UI
run: npm --prefix=./ui ci && npm --prefix=./ui run build
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
- name: Download UI build artifacts
uses: actions/download-artifact@v4
with:
distribution: goreleaser
version: latest
args: release --clean
name: ui-build
path: ./ui/dist
- name: Build Linux binaries
env:
CGO_ENABLED: 0
GOOS: linux
run: |
mkdir -p dist/linux
for ARCH in amd64 arm64 armv7; do
if [ "$ARCH" == "armv7" ]; then
go env -w GOARCH=arm
go env -w GOARM=7
else
go env -w GOARCH=$ARCH
go env -u GOARM
fi
go build -ldflags="-s -w -X github.com/usual2970/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/linux/certimate_${GITHUB_REF#refs/tags/}_linux_$ARCH
done
- name: Upload Linux binaries
uses: actions/upload-artifact@v4
with:
name: linux-binaries
path: dist/linux/
retention-days: 1
build-macos:
needs: prepare-ui
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Download UI build artifacts
uses: actions/download-artifact@v4
with:
name: ui-build
path: ./ui/dist
- name: Build macOS binaries
env:
CGO_ENABLED: 0
GOOS: darwin
run: |
mkdir -p dist/darwin
for ARCH in amd64 arm64; do
go env -w GOARCH=$ARCH
go build -ldflags="-s -w -X github.com/usual2970/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/darwin/certimate_${GITHUB_REF#refs/tags/}_darwin_$ARCH
done
- name: Upload macOS binaries
uses: actions/upload-artifact@v4
with:
name: macos-binaries
path: dist/darwin/
retention-days: 1
build-windows:
needs: prepare-ui
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Download UI build artifacts
uses: actions/download-artifact@v4
with:
name: ui-build
path: ./ui/dist
- name: Build Windows binaries
env:
CGO_ENABLED: 0
GOOS: windows
run: |
mkdir -p dist/windows
for ARCH in amd64 arm64; do
go env -w GOARCH=$ARCH
go build -ldflags="-s -w -X github.com/usual2970/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/windows/certimate_${GITHUB_REF#refs/tags/}_windows_$ARCH.exe
done
- name: Upload Windows binaries
uses: actions/upload-artifact@v4
with:
name: windows-binaries
path: dist/windows/
retention-days: 1
create-release:
needs: [build-linux, build-macos, build-windows]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all binaries
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Prepare release assets
run: |
mkdir -p dist
cp -r artifacts/linux-binaries/* dist/
cp -r artifacts/macos-binaries/* dist/
cp -r artifacts/windows-binaries/* dist/
find dist -type f -not -name "*.exe" -exec chmod +x {} \;
# 为每个二进制文件创建 zip 包
cd dist
for bin in certimate_*; do
if [[ "$bin" == *".exe" ]]; then
entrypoint="certimate.exe"
else
entrypoint="certimate"
fi
tmpdir=$(mktemp -d)
cp "$bin" "${tmpdir}/${entrypoint}"
cp ../README.md ../LICENSE.md ../CHANGELOG.md "$tmpdir"
if [[ "$bin" == *".exe" ]]; then
zip -j "${bin%.exe}.zip" "$tmpdir"/*
else
zip -j -X "${bin}.zip" "$tmpdir"/*
fi
rm -rf "$tmpdir"
done
# 创建校验和文件
sha256sum *.zip > checksums.txt
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.zip
dist/checksums.txt
draft: true
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

6
.gitignore vendored
View File

@@ -15,8 +15,6 @@ vendor
pb_data
build
main
/ui/dist/*
!/ui/dist/.gitkeep
./dist
./certimate
/dist
/docker/data
/certimate

View File

@@ -30,13 +30,16 @@ builds:
- goos: darwin
goarch: arm
# upx:
# - enabled: true
release:
draft: true
archives:
- id: archive_noncgo
builds: [build_noncgo]
format: zip
format: "zip"
files:
- CHANGELOG.md
- LICENSE.md

View File

@@ -11,10 +11,11 @@
"gopls": {
"formatting.gofumpt": true,
},
"typescript.tsdk": "ui/node_modules/typescript/lib",
"[go]": {
"editor.defaultFormatter": "golang.go"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}

View File

@@ -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.

View File

@@ -9,7 +9,7 @@
## 前提条件
- Go 1.22+ (用于修改 Go 代码)
- Go 1.24+ (用于修改 Go 代码)
- Node 20+ (用于修改 UI)
如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改:
@@ -35,6 +35,8 @@ git clone https://github.com/your_username/certimate.git
这将启动一个 Web 服务器,默认运行在 `http://localhost:8090`,并使用来自 `ui/dist` 的预构建管理页面。
> 如果你遇到报错 `ui/embed.go:10:12: pattern all:dist: no matching files found` 请先参考 [构建 Admin UI](#修改管理页面-admin-ui)
**在向主仓库提交 PR 之前,建议你:**
- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 对你的代码进行格式化。

View File

@@ -9,7 +9,7 @@ Thank you for taking the time to improve Certimate! Below is a guide for submitt
## Prerequisites
- Go 1.22+ (for Go code changes)
- Go 1.24+ (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:
@@ -36,6 +36,8 @@ Once you have made changes to the Go code in Certimate, follow these steps to ru
This will start a web server at `http://localhost:8090` using the prebuilt Admin UI located in `ui/dist`.
> if you encounter an error `ui/embed.go:10:12: pattern all:dist: no matching files found`, please refer to [build Admin UI](#making-changes-in-the-admin-ui)
**Before submitting a PR to the main repository, consider:**
- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt).

View File

@@ -1,33 +1,24 @@
FROM node:20-alpine3.19 AS front-builder
FROM node:20-alpine3.19 AS webui-builder
WORKDIR /app
COPY . /app/
RUN \
cd /app/ui && \
npm install && \
npm run build
FROM golang:1.22-alpine AS builder
FROM golang:1.24-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
COPY --from=webui-builder /app/ui/dist /app/ui/dist
ENV CGO_ENABLED=0
RUN go build -ldflags="-s -w" -o certimate
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/certimate .
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]

View File

@@ -21,7 +21,8 @@ $(OS_ARCH):
@mkdir -p $(BUILD_DIR)
GOOS=$(word 1,$(subst /, ,$@)) \
GOARCH=$(word 2,$(subst /, ,$@)) \
go build -o $(BUILD_DIR)/$(BINARY_NAME)_$(word 1,$(subst /, ,$@))_$(word 2,$(subst /, ,$@)) -ldflags="-X main.version=$(VERSION)" .
CGO_ENABLED=0 \
go build -o $(BUILD_DIR)/$(BINARY_NAME)_$(word 1,$(subst /, ,$@))_$(word 2,$(subst /, ,$@)) -ldflags="-X main.version=$(VERSION) -s -w" .
# 清理构建文件
clean:

210
README.md
View File

@@ -1,191 +1,111 @@
[中文](README.md) | [English](README_EN.md)
<h1 align="center">🔒 Certimate</h1>
# 🔒Certimate
<div align="center">
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
[![Stars](https://img.shields.io/github/stars/usual2970/certimate?style=flat)](https://github.com/usual2970/certimate)
[![Forks](https://img.shields.io/github/forks/usual2970/certimate?style=flat)](https://github.com/usual2970/certimate)
[![Docker Pulls](https://img.shields.io/docker/pulls/usual2970/certimate?style=flat)](https://hub.docker.com/r/usual2970/certimate)
[![Release](https://img.shields.io/github/v/release/usual2970/certimate?sort=semver)](https://github.com/usual2970/certimate/releases)
[![License](https://img.shields.io/github/license/usual2970/certimate)](https://mit-license.org/)
1. 😱 麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
2. 😭 易忘:当前免费证书有效期仅 90 天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
</div>
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
<div align="center">
1. 操作简单:自动申请、部署、续期 SSL 证书,全程无需人工干预。
2. 支持私有部署部署方法简单只需下载二进制文件执行即可。二进制文件、Docker 镜像全部用 Github Actions 生成,过程透明,可自行审计。
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
中文 [English](README_EN.md)
相关文章:
</div>
- [V0.2.0-第一个不向后兼容的版本](https://docs.certimate.me/blog/v0.2.0)
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
- [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
---
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问 [https://docs.certimate.me](https://docs.certimate.me)
## 🚩 项目简介
## 一、安装
做个人产品或者在中小企业里负责运维的同学,会遇到要管理多个域名的情况,需要给域名申请证书。但是手动申请证书有以下缺点:
安装 Certimate 非常简单,你可以选择以下方式之一进行安装:
- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,尤其是你有多个域名需要维护的时候。
- 😭 易忘:另外当前免费证书的有效期只有 90 天,这就要求你定期的操作,增加了工作量的同时,你也很容易忘掉续期,从而导致网站访问不了。
### 1. 二进制文件
Certimate 就是为了解决上述问题而产生的,它具有以下优势:
你可以直接从[Releases 页](https://github.com/usual2970/certimate/releases)下载预先编译好的二进制文件,解压后执行:
- **本地部署**:一键安装,只需要下载二进制文件,然后直接运行即可。同时也支持 Docker 部署、源代码部署等方式。​
- **数据安全**:由于是私有部署,所有数据均存储在自己的服务器上,不会经过第三方,确保数据的隐私和安全。​
- **操作简单**:简单配置即可轻松申请 SSL 证书并部署到指定的目标上,在证书即将过期前自动续期,从申请证书到使用证书完全自动化,无需人工操作。​
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。
## 💡 功能特性
- 灵活的工作流编排方式,证书从申请到部署完全自动化;
- 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法;
- 支持 PEM、PFX、JKS 等多种格式输出证书;
- 支持 30+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 100+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-hosting-providers)
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
- 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构;
- 更多特性等待探索。
## ⏱️ 快速启动
**5 分钟部署 Certimate**
以二进制部署为例,从 [GitHub Releases](https://github.com/usual2970/certimate/releases) 页面下载预先编译好的二进制可执行文件压缩包,解压缩后在终端中执行:
```bash
./certimate serve
```
或运行以下命令自动给 Certimate 自身添加证书
浏览器中访问 `http://127.0.0.1:8090`
```bash
./certimate serve 你的域名
```
初始的管理员账号及密码:
> [!NOTE]
> MacOS 在执行二进制文件时会提示无法打开“Certimate”因为 Apple 无法检查其是否包含恶意软件。可在“系统设置 > 隐私与安全性 > 安全性”中点击“仍然允许”,然后再次尝试执行二进制文件。
- 账号:`admin@certimate.fun`
- 密码:`1234567890`
### 2. Docker 安装
即刻使用 Certimate。
```bash
如何使用 Docker 或其他部署方式请参考文档。
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
## 📄 使用手册
```
请访问文档站 [docs.certimate.me](https://docs.certimate.me/) 以阅读使用手册。
### 3. 源代码安装
相关文章:
```bash
git clone EMAIL:usual2970/certimate.git
cd certimate
make local.run
```
- [使用 CNAME 完成 ACME DNS-01 质询](https://docs.certimate.me/blog/cname)
- [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 管理页面。
[![Screenshot](https://i.imgur.com/4DAUKEE.gif)](https://www.bilibili.com/video/BV1xockeZEm2)
```bash
用户名admin@certimate.fun
密码1234567890
```
## 🤝 参与贡献
![usage.gif](https://i.imgur.com/zpCoLVM.gif)
## 三、支持的服务商列表
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
| :--------: | :----------: | :----------: | ----------------------------------------------------------------- |
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、SLB |
| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO |
| 百度智能云 | | √ | 可部署到百度智能云 CDN |
| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
| 七牛云 | | √ | 可部署到七牛云 CDN |
| 多吉云 | | √ | 可部署到多吉云 CDN |
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
| Namesilo | √ | | 可签发在 Namesilo 注册的域名 |
| PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 |
| HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 |
| 本地部署 | | √ | 可部署到本地服务器 |
| SSH | | √ | 可部署到 SSH 服务器 |
| Webhook | | √ | 可部署时回调到 Webhook |
| Kubernetes | | √ | 可部署到 Kubernetes Secret |
## 四、系统截图
<div align="center">
<img src="https://i.imgur.com/SYjjbql.jpeg" title="Login page" width="95%"/>
<img src="https://i.imgur.com/WMVbBId.jpeg" title="Dashboard page" width="47%"/>
<img src="https://i.imgur.com/8wit3ZA.jpeg" title="Domains page" width="47%"/>
<img src="https://i.imgur.com/EWtOoJ0.jpeg" title="Accesses page" width="47%"/>
<img src="https://i.imgur.com/aaPtSW7.jpeg" title="History page" width="47%"/>
</div>
## 五、概念
Certimate 的工作流程如下:
- 用户通过 Certimate 管理页面填写申请证书的信息包括域名、DNS 服务商的授权信息、以及要部署到的服务商的授权信息。
- Certimate 向证书厂商的 API 发起申请请求,获取 SSL 证书。
- Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
- Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
这就涉及域名、DNS 服务商的授权信息、部署服务商的授权信息等。
### 1. 域名
就是要申请证书的域名。
### 2. DNS 服务商授权信息
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 域名解析记录。
Certimate 会自动添加一个 TXT 域名解析记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
比如你在阿里云购买的域名,授权信息如下:
```bash
accessKeyId: your-access-key-id
accessKeySecret: your-access-key-secret
```
在腾讯云购买的域名,授权信息如下:
```bash
secretId: your-secret-id
secretKey: your-secret-key
```
注意,此授权信息需具有访问域名及 DNS 解析的管理权限,具体的权限清单请参阅各服务商自己的技术文档。
### 3. 部署服务商授权信息
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDNCertimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
部署服务商授权信息和 DNS 服务商授权信息基本一致,区别在于 DNS 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
注意,此授权信息需具有访问部署目标服务的相关管理权限,具体的权限清单请参阅各服务商自己的技术文档。
## 六、常见问题
Q: 提供 SaaS 服务吗?
> A: 不提供,目前仅支持 self-hosted私有部署
Q: 数据安全?
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外 Certimate 源码也开源,二进制包及 Docker 镜像打包过程全部使用 Github Actions 进行,过程透明可见,可自行审计。
Q: 自动续期证书?
> A: 已经申请的证书会在**过期前 10 天**自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
## 七、贡献
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
Certimate 是一个免费且开源的项目,采用 [MIT License](./LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
你可以通过以下方式来支持 Certimate 的开发:
- 提交代码:如果你发现了 Bug 或有新的功能需求,而你又有相关经验,可以[提交代码](CONTRIBUTING.md)给我们。
- 提交 Issue功能建议或者 Bug 可以[提交 Issue](https://github.com/usual2970/certimate/issues) 给我们。
支持更多服务商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家提交 PR
支持更多提供商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家参与贡献
## 八、免责声明
## 免责声明
本软件依据 MIT 许可证MIT License发布,免费提供,旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任,包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
Certimate 基于 [MIT License](https://opensource.org/licenses/MIT) 发布,完全免费提供,旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任,包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
无任何保证:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
**无任何保证**:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
用户责任:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
**用户责任**:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
## 九、加入社
## 🌐 加入社
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
- [Telegram](https://t.me/+ZXphsppxUg41YmVl)
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
<img src="https://i.imgur.com/8xwsLTA.png" width="240"/>
## 十、Star 趋势图
## 🚀 Star 趋势图
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@@ -1,189 +1,109 @@
[中文](README.md) | [English](README_EN.md)
<h1 align="center">🔒 Certimate</h1>
# 🔒Certimate
<div align="center">
[![Stars](https://img.shields.io/github/stars/usual2970/certimate?style=flat)](https://github.com/usual2970/certimate)
[![Forks](https://img.shields.io/github/forks/usual2970/certimate?style=flat)](https://github.com/usual2970/certimate)
[![Docker Pulls](https://img.shields.io/docker/pulls/usual2970/certimate?style=flat)](https://hub.docker.com/r/usual2970/certimate)
[![Release](https://img.shields.io/github/v/release/usual2970/certimate?style=flat&sort=semver)](https://github.com/usual2970/certimate/releases)
[![License](https://img.shields.io/github/license/usual2970/certimate?style=flat)](https://mit-license.org/)
</div>
<div align="center">
[中文](README.md) English
</div>
---
## 🚩 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:
1. 😱Troublesome: Applying for and deploying certificates isnt difficult, but it can be quite a hassle, especially when managing multiple domains.
2. 😭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.
- 😱 Troublesome: Applying for and deploying certificates isnt 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 features:
Certimate was created to solve the above-mentioned issues and has the following advantages:
1. Simple operation: Automatically apply, deploy, and renew SSL certificates without any manual intervention.
2. Support for self-hosted deployment: The deployment method is simple; you only need to download the binary file and execute it. Both the binary files and Docker images are generated using GitHub Actions, ensuring a transparent process that can be audited independently.
3. Data security: Since it is a self-hosted deployment, all data is stored locally and will not be saved on the service providers servers, ensuring the security of the data.
- **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.
Related articles:
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution.
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
- [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
## 💡 Features
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution. For usage documentation, please visit [https://docs.certimate.me](https://docs.certimate.me).
- Flexible workflow orchestration, fully automation from certificate application to deployment;
- Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC.
- Supports various certificate formats such as PEM, PFX, JKS.
- Supports more than 30+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
- Supports more than 100+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-hosting-providers));
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
- Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust ServicesSSL.com, ZeroSSL, and more;
- More features waiting to be discovered.
## Installation
## ⏱️ Fast Track
Installing Certimate is very simple, you can choose one of the following methods for installation:
**Deploy Certimate in 5 minutes!**
### 1. Binary File
You can download the precompiled binary files directly from the [Releases page](https://github.com/usual2970/certimate/releases), and after extracting them, execute:
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
```
Or run the following command to automatically add a certificate to Certimate itself.
Visit `http://127.0.0.1:8090` in your browser.
```bash
./certimate serve yourDomain
```
Default administrator account:
> [!NOTE]
> When executing the binary file on macOS, you may see a prompt saying: “Cannot open certimate because Apple cannot check it for malicious software.” You can go to System Preferences > Security & Privacy > General, then click “Allow Anyway,” and try executing the binary file again.
- Username: `admin@certimate.fun`
- Password: `1234567890`
### 2. Docker Installation
Work with Certimate right now. Or read other content in the documentation to learn more.
```bash
## 📄 Documentation
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
Please visit the documentation site [docs.certimate.me](https://docs.certimate.me/en/).
```
Related articles:
### 3. Source Code Installation
- [使用 CNAME 完成 ACME DNS-01 质询](https://docs.certimate.me/blog/cname)
- [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)
```bash
git clone EMAIL:usual2970/certimate.git
cd certimate
make local.run
```
## ⭐ Screenshot
## Usage
[![Screenshot](https://i.imgur.com/4DAUKEE.gif)](https://www.youtube.com/watch?v=am_yzdfyNOE)
After completing the installation steps above, you can access the Certimate management page by visiting <http://127.0.0.1:8090> in your browser.
## 🤝 Contributing
```bash
usernameadmin@certimate.fun
password1234567890
```
![usage.gif](https://i.imgur.com/zpCoLVM.gif)
## List of Supported Providers
| Provider | Registration | Deployment | Remarks |
| :-----------: | :----------: | :--------: | ----------------------------------------------------------------------------------------------------------- |
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN,SLB |
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO |
| Baidu Cloud | | √ | Supports deployment to Baidu Cloud CDN |
| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
| Doge Cloud | | √ | Supports deployment to Doge Cloud CDN |
| AWS | √ | | Supports domains managed on AWS Route53 |
| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
| GoDaddy | √ | | Supports domains registered on GoDaddy |
| Namesilo | √ | | Supports domains registered on Namesilo |
| PowerDNS | √ | | Supports domains managed on PowerDNS |
| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request |
| Local Deploy | | √ | Supports deployment to local servers |
| SSH | | √ | Supports deployment to SSH servers |
| Webhook | | √ | Supports callback to Webhook |
| Kubernetes | | √ | Supports deployment to Kubernetes Secret |
## Screenshots
<div align="center">
<img src="https://i.imgur.com/SYjjbql.jpeg" title="Login page" width="95%"/>
<img src="https://i.imgur.com/WMVbBId.jpeg" title="Dashboard page" width="47%"/>
<img src="https://i.imgur.com/8wit3ZA.jpeg" title="Domains page" width="47%"/>
<img src="https://i.imgur.com/EWtOoJ0.jpeg" title="Accesses page" width="47%"/>
<img src="https://i.imgur.com/aaPtSW7.jpeg" title="History page" width="47%"/>
</div>
## Concepts
The workflow of Certimate is as follows:
- Users fill in the certificate application information on the Certimate management page, including domain name, authorization information for the DNS provider, and authorization information for the service provider to deploy to.
- Certimate sends a request to the certificate vendor's API to apply for an SSL certificate.
- Certimate stores the certificate information, including the certificate content, private key, validity period, etc., and automatically renews the certificate when it is about to expire.
- Certimate sends a deployment request to the service provider's API to deploy the certificate to the service provider's servers.
This involves authorization information for the domain, DNS provider, and deployment service provider.
### 1. Domain
It involves the domain name for which the certificate is being requested.
### 2. Authorization Information for the DNS Provider
To apply for a certificate for a domain, you need to prove that the domain belongs to you. Therefore, when manually applying for a certificate, you typically need to add a TXT record to the DNS records in the domain provider's control panel.
Certimate will automatically add a TXT record for you; you only need to fill in the authorization information for your DNS provider in the Certimate backend.
For example, if you purchased the domain from Alibaba Cloud, the authorization information would be as follows:
```bash
accessKeyId: your-access-key-id
accessKeySecret: your-access-key-secret
```
If you purchased the domain from Tencent Cloud, the authorization information would be as follows:
```bash
secretId: your-secret-id
secretKey: your-secret-key
```
Notes: This authorization information requires relevant administration permissions for accessing the DNS services. Please refer to the documentations of each service provider for the specific permissions list.
### 3. Authorization Information for the Deployment Service Provider
After Certimate applies for the certificate, it will automatically deploy the certificate to your specified target, such as Alibaba Cloud CDN. At this point, you need to fill in the authorization information for Alibaba Cloud. Certimate will use the authorization information and domain name you provided to locate the corresponding CDN service and deploy the certificate to that service.
The authorization information for the deployment service provider is the same as that for the DNS provider, with the distinction that the DNS provider's authorization information is used to prove that the domain belongs to you, while the deployment service provider's authorization information is used to provide authorization for the certificate deployment.
Notes: This authorization information requires relevant administration permissions to access the target deployment services. Please refer to the documentations of each service provider for the specific permissions list.
## FAQ
Q: Do you provide SaaS services?
> A: No, we do not provide that. Currently, we only support self-hosted.
Q: Data Security?
> A: Since only self-hosted is supported, all data is stored on the users server. Additionally, the source code of Certimate is open-source, and the packaging process for binary files and Docker images is entirely done using GitHub Actions. This process is transparent and visible, allowing for independent auditing.
Q: Automatic Certificate Renewal?
> A: Certificates that have already been issued will be automatically renewed **10 days before expiration**. The system checks once a day to see if any certificates are nearing expiration, and if so, it will automatically reapply for the certificate and deploy it to the target service.
## 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.
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 submit pull requests (PRs).
Support for more service providers, UI enhancements, bug fixes, and documentation improvements are all welcome. We encourage everyone to contribute.
## Disclaimer
## Disclaimer
This software is provided under the MIT License 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.
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.
**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 Responsibility: By using this software, you agree to take full responsibility for any outcomes resulting from its use.
**User Responsibilities**: By using this software, you agree to take full responsibility for any outcomes resulting from its use.
## Join the Community
## 🌐 Join the Community
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
- [Telegram](https://t.me/+ZXphsppxUg41YmVl)
- Wechat Group
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
<img src="https://i.imgur.com/zSHEoIm.png" width="240"/>
## Star History
## 🚀 Star History
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@@ -6,5 +6,7 @@ services:
ports:
- 8090:8090
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- ./data:/app/pb_data
restart: unless-stopped

301
go.mod
View File

@@ -1,183 +1,226 @@
module github.com/usual2970/certimate
go 1.22.0
go 1.24.0
toolchain go1.23.2
toolchain go1.24.3
require (
github.com/alibabacloud-go/alb-20200616/v2 v2.2.1
github.com/alibabacloud-go/cas-20200407/v3 v3.0.1
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1
github.com/Edgio/edgio-api v0.0.0-workspace
github.com/G-Core/gcorelabscdn-go v1.0.31
github.com/alibabacloud-go/alb-20200616/v2 v2.2.8
github.com/alibabacloud-go/apig-20240327/v3 v3.2.2
github.com/alibabacloud-go/cas-20200407/v3 v3.0.4
github.com/alibabacloud-go/cdn-20180510/v5 v5.2.2
github.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.4
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7
github.com/alibabacloud-go/ddoscoo-20200101/v4 v4.0.0
github.com/alibabacloud-go/esa-20240910/v2 v2.33.0
github.com/alibabacloud-go/fc-20230330/v4 v4.3.5
github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12
github.com/alibabacloud-go/ga-20191120/v3 v3.1.8
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.9
github.com/alibabacloud-go/tea v1.2.2
github.com/alibabacloud-go/slb-20140515/v4 v4.0.10
github.com/alibabacloud-go/tea v1.3.9
github.com/alibabacloud-go/vod-20170321/v4 v4.8.4
github.com/alibabacloud-go/waf-openapi-20211001/v5 v5.1.3
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/baidubce/bce-sdk-go v0.9.197
github.com/go-acme/lego/v4 v4.19.2
github.com/gojek/heimdall/v7 v7.0.3
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/nikoksr/notify v1.0.0
github.com/aws/aws-sdk-go-v2/service/acm v1.32.0
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.46.1
github.com/aws/aws-sdk-go-v2/service/iam v1.42.0
github.com/baidubce/bce-sdk-go v0.9.228
github.com/blinkbean/dingtalk v1.1.3
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.46
github.com/go-acme/lego/v4 v4.23.1
github.com/go-lark/lark v1.16.0
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.150
github.com/jdcloud-api/jdcloud-sdk-go v1.64.0
github.com/libdns/dynv6 v1.0.0
github.com/libdns/libdns v0.2.3
github.com/luthermonson/go-proxmox v0.2.2
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
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/cdn v1.0.1017
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030
golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
k8s.io/api v0.31.1
k8s.io/apimachinery v0.31.1
k8s.io/client-go v0.31.1
github.com/pkg/sftp v1.13.9
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.28.2
github.com/povsister/scp v0.0.0-20250504051308-e467f71ea63c
github.com/qiniu/go-sdk/v7 v7.25.3
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1155
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1166
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1150
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1169
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1166
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.0.1164
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.0.1170
github.com/ucloud/ucloud-sdk-go v0.22.41
github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.12
github.com/volcengine/volc-sdk-golang v1.0.208
github.com/volcengine/volcengine-go-sdk v1.1.8
gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0
golang.org/x/crypto v0.38.0
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
k8s.io/client-go v0.33.1
software.sslmate.com/src/go-pkcs12 v0.5.0
)
require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.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/internal v1.1.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // 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-sdk v1.1.5 // indirect
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/diskfs/go-diskfs v1.5.0 // indirect
github.com/djherbis/times v1.6.0 // 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.14.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // 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-sql-driver/mysql v1.8.1 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/jinzhu/copier v0.3.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magefile/mage v1.14.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/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.10.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/peterhellberg/link v1.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/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/x448/float16 v0.8.4 // indirect
go.mongodb.org/mongo-driver v1.12.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-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.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/BurntSushi/toml v1.5.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2
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.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // 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.63.15 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.100 // indirect
github.com/aliyun/credentials-go v1.4.6 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // 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.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3
github.com/aws/aws-sdk-go-v2/config v1.29.9
github.com/aws/aws-sdk-go-v2/credentials v1.17.62
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // 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.6 // indirect
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/cloudflare-go v0.115.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.6 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // 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.5 // 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/golang-lru/v2 v2.0.7 // indirect
github.com/google/uuid v1.6.0
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.62 // indirect
github.com/miekg/dns v1.1.64 // 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
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // 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.1002 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // 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.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.25.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
google.golang.org/api v0.202.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
golang.org/x/image v0.27.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0
golang.org/x/tools v0.33.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.65.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.37.1 // indirect
)
replace github.com/Edgio/edgio-api v0.0.0-workspace => ./internal/pkg/sdk3rd/edgio/edgio-api@v0.0.0-workspace
replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/sdk3rd/cmcc/ecloudsdkcore@v1.0.0
replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/sdk3rd/cmcc/ecloudsdkclouddns@v1.0.1

1325
go.sum

File diff suppressed because it is too large Load Diff

32
internal/app/app.go Normal file
View 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()
}

View File

@@ -2,17 +2,25 @@ package app
import (
"sync"
"time"
_ "time/tzdata"
"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

View File

@@ -0,0 +1,31 @@
package applicant
import "github.com/usual2970/certimate/internal/domain"
const (
caLetsEncrypt = string(domain.CAProviderTypeLetsEncrypt)
caLetsEncryptStaging = string(domain.CAProviderTypeLetsEncryptStaging)
caBuypass = string(domain.CAProviderTypeBuypass)
caGoogleTrustServices = string(domain.CAProviderTypeGoogleTrustServices)
caSSLCom = string(domain.CAProviderTypeSSLCom)
caZeroSSL = string(domain.CAProviderTypeZeroSSL)
caCustom = string(domain.CAProviderTypeACMECA)
caDefault = caLetsEncrypt
)
var caDirUrls = map[string]string{
caLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
caLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
caBuypass: "https://api.buypass.com/acme/directory",
caGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
caSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
caSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
caSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
caZeroSSL: "https://acme.zerossl.com/v2/DV90",
}
type acmeSSLProviderConfig struct {
Config map[domain.CAProviderType]map[string]any `json:"config"`
Provider string `json:"provider"`
}

View File

@@ -0,0 +1,206 @@
package applicant
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"strings"
"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"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
"github.com/usual2970/certimate/internal/repository"
)
type acmeUser struct {
// 证书颁发机构标识。
// 通常等同于 [CAProviderType] 的值。
// 对于自定义 ACME CA值为 "custom#{access_id}"。
CA string
// 邮箱。
Email string
// 注册信息。
Registration *registration.Resource
// CSR 私钥。
privkey string
}
func newAcmeUser(ca, caAccessId, email string) (*acmeUser, error) {
repo := repository.NewAcmeAccountRepository()
applyUser := &acmeUser{
CA: ca,
Email: email,
}
if ca == caCustom {
applyUser.CA = fmt.Sprintf("%s#%s", ca, caAccessId)
}
acmeAccount, err := repo.GetByCAAndEmail(applyUser.CA, applyUser.Email)
if err != nil {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
keyPEM, err := certutil.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, _ := certutil.ParseECPrivateKeyFromPEM(u.privkey)
return rs
}
func (u *acmeUser) hasRegistration() bool {
return u.Registration != nil
}
func (u *acmeUser) getCAProvider() string {
return strings.Split(u.CA, "#")[0]
}
func (u *acmeUser) getPrivateKeyPEM() string {
return u.privkey
}
var registerGroup singleflight.Group
func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", user.CA, user.Email), func() (interface{}, error) {
return registerAcmeUser(client, user, userRegisterOptions)
})
if err != nil {
return nil, err
}
return resp.(*registration.Resource), nil
}
func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
var reg *registration.Resource
var err error
switch user.getCAProvider() {
case caLetsEncrypt, caLetsEncryptStaging:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
case caBuypass:
{
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
case caGoogleTrustServices:
{
access := domain.AccessConfigForGoogleTrustServices{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
case caSSLCom:
{
access := domain.AccessConfigForSSLCom{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
case caZeroSSL:
{
access := domain.AccessConfigForZeroSSL{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
case caCustom:
{
access := domain.AccessConfigForACMECA{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
if access.EabKid == "" && access.EabHmacKey == "" {
reg, err = client.Registration.Register(registration.RegisterOptions{
TermsOfServiceAgreed: true,
})
} else {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
}
default:
err = fmt.Errorf("unsupported ca provider '%s'", user.CA)
}
if err != nil {
return nil, err
}
repo := repository.NewAcmeAccountRepository()
resp, err := repo.GetByCAAndEmail(user.CA, user.Email)
if err == nil {
user.privkey = resp.Key
return resp.Resource, nil
}
if _, err := repo.Save(context.Background(), &domain.AcmeAccount{
CA: user.CA,
Email: user.Email,
Key: user.getPrivateKeyPEM(),
Resource: reg,
}); err != nil {
return nil, fmt.Errorf("failed to save acme account registration: %w", err)
}
return reg, nil
}

View File

@@ -1,36 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/usual2970/certimate/internal/domain"
)
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)
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := alidns.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -1,368 +1,276 @@
package applicant
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"strconv"
"strings"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/usual2970/certimate/internal/repository"
"github.com/usual2970/certimate/internal/utils/app"
"sync"
"time"
"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"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice"
"github.com/usual2970/certimate/internal/repository"
)
const (
configTypeAliyun = "aliyun"
configTypeTencent = "tencent"
configTypeHuaweiCloud = "huaweicloud"
configTypeAws = "aws"
configTypeCloudflare = "cloudflare"
configTypeNamesilo = "namesilo"
configTypeGodaddy = "godaddy"
configTypePdns = "pdns"
configTypeHttpreq = "httpreq"
)
const defaultSSLProvider = "letsencrypt"
const (
sslProviderLetsencrypt = "letsencrypt"
sslProviderZeroSSL = "zerossl"
sslProviderGts = "gts"
)
const (
zerosslUrl = "https://acme.zerossl.com/v2/DV90"
letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory"
gtsUrl = "https://dv.acme-v02.api.pki.goog/directory"
)
var sslProviderUrls = map[string]string{
sslProviderLetsencrypt: letsencryptUrl,
sslProviderZeroSSL: zerosslUrl,
sslProviderGts: gtsUrl,
}
const defaultEmail = "536464346@qq.com"
const defaultTimeout = 60
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"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
}
type ApplyUser struct {
Ca string
Email string
Registration *registration.Resource
key string
}
func newApplyUser(ca, email string) (*ApplyUser, error) {
repo := getAcmeAccountRepository()
rs := &ApplyUser{
Ca: ca,
Email: email,
}
resp, err := repo.GetByCAAndEmail(ca, email)
if err != nil {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
keyStr, err := x509.ConvertECPrivateKeyToPEM(privateKey)
if err != nil {
return nil, err
}
rs.key = keyStr
return rs, nil
}
rs.Registration = resp.Resource
rs.key = resp.Key
return rs, nil
}
func (u *ApplyUser) GetEmail() string {
return u.Email
}
func (u ApplyUser) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *ApplyUser) GetPrivateKey() crypto.PrivateKey {
rs, _ := x509.ParseECPrivateKeyFromPEM(u.key)
return rs
}
func (u *ApplyUser) hasRegistration() bool {
return u.Registration != nil
}
func (u *ApplyUser) getPrivateKeyString() string {
return u.key
type ApplyResult struct {
CSR string
FullChainCertificate string
IssuerCertificate string
PrivateKey string
ACMEAccountUrl string
ACMECertUrl string
ACMECertStableUrl string
ARIReplaced bool
}
type Applicant interface {
Apply() (*Certificate, error)
Apply(ctx context.Context) (*ApplyResult, error)
}
func Get(record *models.Record) (Applicant, error) {
if record.GetString("applyConfig") == "" {
return nil, errors.New("applyConfig is empty")
}
applyConfig := &domain.ApplyConfig{}
record.UnmarshalJSONField("applyConfig", applyConfig)
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
if err != nil {
return nil, fmt.Errorf("access record not found: %w", err)
}
if applyConfig.Email == "" {
applyConfig.Email = defaultEmail
}
if applyConfig.Timeout == 0 {
applyConfig.Timeout = defaultTimeout
}
option := &ApplyOption{
Email: applyConfig.Email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
KeyAlgorithm: applyConfig.KeyAlgorithm,
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
}
switch access.GetString("configType") {
case configTypeAliyun:
return NewAliyun(option), nil
case configTypeTencent:
return NewTencent(option), nil
case configTypeHuaweiCloud:
return NewHuaweiCloud(option), nil
case configTypeAws:
return NewAws(option), nil
case configTypeCloudflare:
return NewCloudflare(option), nil
case configTypeNamesilo:
return NewNamesilo(option), nil
case configTypeGodaddy:
return NewGodaddy(option), nil
case configTypePdns:
return NewPdns(option), nil
case configTypeHttpreq:
return NewHttpreq(option), nil
default:
return nil, errors.New("unknown config type")
}
type ApplicantWithWorkflowNodeConfig struct {
Node *domain.WorkflowNode
Logger *slog.Logger
}
type SSLProviderConfig struct {
Config SSLProviderConfigContent `json:"config"`
Provider string `json:"provider"`
}
type SSLProviderConfigContent struct {
Zerossl SSLProviderEab `json:"zerossl"`
Gts SSLProviderEab `json:"gts"`
}
type SSLProviderEab struct {
EabHmacKey string `json:"eabHmacKey"`
EabKid string `json:"eabKid"`
}
func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, error) {
record, _ := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='ssl-provider'")
sslProvider := &SSLProviderConfig{
Config: SSLProviderConfigContent{},
Provider: defaultSSLProvider,
func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, error) {
if config.Node == nil {
return nil, fmt.Errorf("node is nil")
}
if record != nil {
if err := record.UnmarshalJSONField("content", sslProvider); err != nil {
return nil, err
if config.Node.Type != domain.WorkflowNodeTypeApply {
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply))
}
nodeCfg := config.Node.GetConfigForApply()
options := &applicantProviderOptions{
Domains: sliceutil.Filter(strings.Split(nodeCfg.Domains, ";"), func(s string) bool { return s != "" }),
ContactEmail: nodeCfg.ContactEmail,
Provider: domain.ACMEDns01ProviderType(nodeCfg.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderServiceConfig: nodeCfg.ProviderConfig,
CAProvider: domain.CAProviderType(nodeCfg.CAProvider),
CAProviderAccessConfig: make(map[string]any),
CAProviderServiceConfig: nodeCfg.CAProviderConfig,
KeyAlgorithm: nodeCfg.KeyAlgorithm,
Nameservers: sliceutil.Filter(strings.Split(nodeCfg.Nameservers, ";"), func(s string) bool { return s != "" }),
DnsPropagationWait: nodeCfg.DnsPropagationWait,
DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout,
DnsTTL: nodeCfg.DnsTTL,
DisableFollowCNAME: nodeCfg.DisableFollowCNAME,
}
accessRepo := repository.NewAccessRepository()
if nodeCfg.ProviderAccessId != "" {
if access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err)
} else {
options.ProviderAccessConfig = access.Config
}
}
if nodeCfg.CAProviderAccessId != "" {
if access, err := accessRepo.GetById(context.Background(), nodeCfg.CAProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err)
} else {
options.CAProviderAccessId = access.Id
options.CAProviderAccessConfig = access.Config
}
}
// 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(option.DisableFollowCNAME))
settingsRepo := repository.NewSettingsRepository()
if string(options.CAProvider) == "" {
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
myUser, err := newApplyUser(sslProvider.Provider, option.Email)
sslProviderConfig := &acmeSSLProviderConfig{
Config: make(map[domain.CAProviderType]map[string]any),
Provider: caDefault,
}
if settings != nil {
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
return nil, err
} else if sslProviderConfig.Provider == "" {
sslProviderConfig.Provider = caDefault
}
}
options.CAProvider = domain.CAProviderType(sslProviderConfig.Provider)
options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider]
}
certRepo := repository.NewCertificateRepository()
lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), config.Node.Id)
if lastCertificate != nil && !lastCertificate.ACMERenewed {
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.ARIReplaceAcct = lastCertificate.ACMEAccountUrl
options.ARIReplaceCert = replacedARICertId
}
}
}
applicant, err := createApplicantProvider(options)
if err != nil {
return nil, err
}
config := lego.NewConfig(myUser)
return &applicantImpl{
applicant: applicant,
options: options,
}, nil
}
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
config.CADirURL = sslProviderUrls[sslProvider.Provider]
config.Certificate.KeyType = parseKeyAlgorithm(option.KeyAlgorithm)
type applicantImpl struct {
applicant challenge.Provider
options *applicantProviderOptions
}
// A client facilitates communication with the CA server.
var _ Applicant = (*applicantImpl)(nil)
func (d *applicantImpl) Apply(ctx context.Context) (*ApplyResult, error) {
limiter := getLimiter(fmt.Sprintf("apply_%s", d.options.ContactEmail))
if err := limiter.Wait(ctx); err != nil {
return nil, err
}
return applyUseLego(d.applicant, d.options)
}
const (
limitBurst = 300
limitRate float64 = float64(1) / float64(36)
)
var limiters sync.Map
func getLimiter(key string) *rate.Limiter {
limiter, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Limit(limitRate), 300))
return limiter.(*rate.Limiter)
}
func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOptions) (*ApplyResult, error) {
user, err := newAcmeUser(string(options.CAProvider), options.CAProviderAccessId, options.ContactEmail)
if err != nil {
return nil, err
}
// 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(user)
config.Certificate.KeyType = parseLegoKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
switch user.getCAProvider() {
case caSSLCom:
if strings.HasPrefix(options.KeyAlgorithm, "RSA") {
config.CADirURL = caDirUrls[caSSLCom+"RSA"]
} else if strings.HasPrefix(options.KeyAlgorithm, "EC") {
config.CADirURL = caDirUrls[caSSLCom+"ECC"]
} else {
config.CADirURL = caDirUrls[caSSLCom]
}
case caCustom:
caDirURL := maputil.GetString(options.CAProviderAccessConfig, "endpoint")
if caDirURL != "" {
config.CADirURL = caDirURL
} else {
return nil, fmt.Errorf("invalid ca provider endpoint")
}
default:
config.CADirURL = caDirUrls[user.CA]
}
// Create an ACME client
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
challengeOptions := make([]dns01.ChallengeOption, 0)
nameservers := parseNameservers(option.Nameservers)
if len(nameservers) > 0 {
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(nameservers))
}
// Set the DNS01 challenge provider
client.Challenge.SetDNS01Provider(legoProvider,
dns01.CondOption(
len(options.Nameservers) > 0,
dns01.AddRecursiveNameservers(dns01.ParseNameservers(options.Nameservers)),
),
dns01.CondOption(
options.DnsPropagationWait > 0,
dns01.PropagationWait(time.Duration(options.DnsPropagationWait)*time.Second, true),
),
dns01.CondOption(
len(options.Nameservers) > 0 || options.DnsPropagationWait > 0,
dns01.DisableAuthoritativeNssPropagationRequirement(),
),
)
client.Challenge.SetDNS01Provider(provider, challengeOptions...)
// New users will need to register
if !myUser.hasRegistration() {
reg, err := getReg(client, sslProvider, myUser)
// New users need to register first
if !user.hasRegistration() {
reg, err := registerAcmeUserWithSingleFlight(client, user, options.CAProviderAccessConfig)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
return nil, fmt.Errorf("failed to register acme user: %w", err)
}
myUser.Registration = reg
user.Registration = reg
}
domains := strings.Split(option.Domain, ";")
request := certificate.ObtainRequest{
Domains: domains,
// Obtain a certificate
certRequest := certificate.ObtainRequest{
Domains: options.Domains,
Bundle: true,
}
certificates, err := client.Certificate.Obtain(request)
if options.ARIReplaceAcct == user.Registration.URI {
certRequest.ReplacesCertID = options.ARIReplaceCert
}
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 &ApplyResult{
CSR: strings.TrimSpace(string(certResource.CSR)),
FullChainCertificate: strings.TrimSpace(string(certResource.Certificate)),
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMEAccountUrl: user.Registration.URI,
ACMECertUrl: certResource.CertURL,
ACMECertStableUrl: certResource.CertStableURL,
ARIReplaced: certRequest.ReplacesCertID != "",
}, nil
}
type AcmeAccountRepository interface {
GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error)
Save(ca, email, key string, resource *registration.Resource) error
}
func getAcmeAccountRepository() AcmeAccountRepository {
return repository.NewAcmeAccountRepository()
}
func getReg(client *lego.Client, sslProvider *SSLProviderConfig, user *ApplyUser) (*registration.Resource, error) {
var reg *registration.Resource
var err error
switch sslProvider.Provider {
case sslProviderZeroSSL:
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: sslProvider.Config.Zerossl.EabKid,
HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey,
})
case sslProviderGts:
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: sslProvider.Config.Gts.EabKid,
HmacEncoded: sslProvider.Config.Gts.EabHmacKey,
})
case sslProviderLetsencrypt:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
default:
err = errors.New("unknown ssl provider")
}
if err != nil {
return nil, err
}
repo := getAcmeAccountRepository()
resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail())
if err == nil {
user.key = resp.Key
return resp.Resource, nil
}
if err := repo.Save(sslProvider.Provider, user.GetEmail(), user.getPrivateKeyString(), reg); err != nil {
return nil, fmt.Errorf("failed to save registration: %w", err)
}
return reg, nil
}
func parseNameservers(ns string) []string {
nameservers := make([]string, 0)
lines := strings.Split(ns, ";")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
nameservers = append(nameservers, line)
}
return nameservers
}
func parseKeyAlgorithm(algo string) certcrypto.KeyType {
switch algo {
case "RSA2048":
return certcrypto.RSA2048
case "RSA3072":
return certcrypto.RSA3072
case "RSA4096":
return certcrypto.RSA4096
case "RSA8192":
return certcrypto.RSA8192
case "EC256":
return certcrypto.EC256
case "EC384":
return certcrypto.EC384
default:
return certcrypto.RSA2048
}
func parseLegoKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyType {
alogMap := map[domain.CertificateKeyAlgorithmType]certcrypto.KeyType{
domain.CertificateKeyAlgorithmTypeRSA2048: certcrypto.RSA2048,
domain.CertificateKeyAlgorithmTypeRSA3072: certcrypto.RSA3072,
domain.CertificateKeyAlgorithmTypeRSA4096: certcrypto.RSA4096,
domain.CertificateKeyAlgorithmTypeRSA8192: certcrypto.RSA8192,
domain.CertificateKeyAlgorithmTypeEC256: certcrypto.EC256,
domain.CertificateKeyAlgorithmTypeEC384: certcrypto.EC384,
domain.CertificateKeyAlgorithmTypeEC512: certcrypto.KeyType("P512"),
}
if keyType, ok := alogMap[algo]; ok {
return keyType
}
return certcrypto.RSA2048
}

View 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())
})
}
}

View File

@@ -1,39 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/route53"
"github.com/usual2970/certimate/internal/domain"
)
type aws struct {
option *ApplyOption
}
func NewAws(option *ApplyOption) Applicant {
return &aws{
option: option,
}
}
func (t *aws) Apply() (*Certificate, error) {
access := &domain.AwsAccess{}
json.Unmarshal([]byte(t.option.Access), access)
os.Setenv("AWS_REGION", access.Region)
os.Setenv("AWS_ACCESS_KEY_ID", access.AccessKeyId)
os.Setenv("AWS_SECRET_ACCESS_KEY", access.SecretAccessKey)
os.Setenv("AWS_HOSTED_ZONE_ID", access.HostedZoneId)
os.Setenv("AWS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := route53.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(t.option, dnsProvider)
}

View File

@@ -1,36 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/usual2970/certimate/internal/domain"
)
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)
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
provider, err := cf.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(c.option, provider)
}

View File

@@ -1,37 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/usual2970/certimate/internal/domain"
)
type godaddy struct {
option *ApplyOption
}
func NewGodaddy(option *ApplyOption) Applicant {
return &godaddy{
option: option,
}
}
func (a *godaddy) Apply() (*Certificate, error) {
access := &domain.GodaddyAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("GODADDY_API_KEY", access.ApiKey)
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := godaddyProvider.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -1,38 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/httpreq"
"github.com/usual2970/certimate/internal/domain"
)
type httpReq struct {
option *ApplyOption
}
func NewHttpreq(option *ApplyOption) Applicant {
return &httpReq{
option: option,
}
}
func (a *httpReq) Apply() (*Certificate, error) {
access := &domain.HttpreqAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("HTTPREQ_ENDPOINT", access.Endpoint)
os.Setenv("HTTPREQ_MODE", access.Mode)
os.Setenv("HTTPREQ_USERNAME", access.Username)
os.Setenv("HTTPREQ_PASSWORD", access.Password)
os.Setenv("HTTPREQ_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := httpreq.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -1,43 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
"github.com/usual2970/certimate/internal/domain"
)
type huaweicloud struct {
option *ApplyOption
}
func NewHuaweiCloud(option *ApplyOption) Applicant {
return &huaweicloud{
option: option,
}
}
func (t *huaweicloud) Apply() (*Certificate, error) {
access := &domain.HuaweiCloudAccess{}
json.Unmarshal([]byte(t.option.Access), access)
region := access.Region
if region == "" {
region = "cn-north-1"
}
os.Setenv("HUAWEICLOUD_REGION", region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(t.option, dnsProvider)
}

View File

@@ -1,36 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
"github.com/usual2970/certimate/internal/domain"
)
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)
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := namesiloProvider.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -1,36 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/pdns"
"github.com/usual2970/certimate/internal/domain"
)
type powerdns struct {
option *ApplyOption
}
func NewPdns(option *ApplyOption) Applicant {
return &powerdns{
option: option,
}
}
func (a *powerdns) Apply() (*Certificate, error) {
access := &domain.PdnsAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("PDNS_API_URL", access.ApiUrl)
os.Setenv("PDNS_API_KEY", access.ApiKey)
os.Setenv("PDNS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := pdns.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -0,0 +1,683 @@
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"
pAliyunESA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa"
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"
pBunny "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/bunny"
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"
pConstellix "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/constellix"
pCTCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud"
pDeSEC "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/desec"
pDigitalOcean "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean"
pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla"
pDuckDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/duckdns"
pDynv6 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6"
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"
pHetzner "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/hetzner"
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"
pNetcup "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netcup"
pNetlify "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netlify"
pNS1 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ns1"
pPorkbun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun"
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"
pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo"
pUCloudUDNR "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ucloud-udnr"
pVercel "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/vercel"
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"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
type applicantProviderOptions struct {
Domains []string
ContactEmail string
Provider domain.ACMEDns01ProviderType
ProviderAccessConfig map[string]any
ProviderServiceConfig map[string]any
CAProvider domain.CAProviderType
CAProviderAccessId string
CAProviderAccessConfig map[string]any
CAProviderServiceConfig map[string]any
KeyAlgorithm string
Nameservers []string
DnsPropagationWait int32
DnsPropagationTimeout int32
DnsTTL int32
DisableFollowCNAME bool
ARIReplaceAcct string
ARIReplaceCert string
}
func createApplicantProvider(options *applicantProviderOptions) (challenge.Provider, error) {
/*
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
switch options.Provider {
case domain.ACMEDns01ProviderTypeACMEHttpReq:
{
access := domain.AccessConfigForACMEHttpReq{}
if err := maputil.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.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS, domain.ACMEDns01ProviderTypeAliyunESA:
{
access := domain.AccessConfigForAliyun{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
case domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS:
applicant, err := pAliyun.NewChallengeProvider(&pAliyun.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
case domain.ACMEDns01ProviderTypeAliyunESA:
applicant, err := pAliyunESA.NewChallengeProvider(&pAliyunESA.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: maputil.GetString(options.ProviderServiceConfig, "region"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
default:
break
}
}
case domain.ACMEDns01ProviderTypeAWS, domain.ACMEDns01ProviderTypeAWSRoute53:
{
access := domain.AccessConfigForAWS{}
if err := maputil.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: maputil.GetString(options.ProviderServiceConfig, "region"),
HostedZoneId: maputil.GetString(options.ProviderServiceConfig, "hostedZoneId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeAzure, domain.ACMEDns01ProviderTypeAzureDNS:
{
access := domain.AccessConfigForAzure{}
if err := maputil.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.ACMEDns01ProviderTypeBaiduCloud, domain.ACMEDns01ProviderTypeBaiduCloudDNS:
{
access := domain.AccessConfigForBaiduCloud{}
if err := maputil.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.ACMEDns01ProviderTypeBunny:
{
access := domain.AccessConfigForBunny{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pBunny.NewChallengeProvider(&pBunny.ChallengeProviderConfig{
ApiKey: access.ApiKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeCloudflare:
{
access := domain.AccessConfigForCloudflare{}
if err := maputil.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,
ZoneApiToken: access.ZoneApiToken,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeClouDNS:
{
access := domain.AccessConfigForClouDNS{}
if err := maputil.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.ACMEDns01ProviderTypeCMCCCloud, domain.ACMEDns01ProviderTypeCMCCCloudDNS:
{
access := domain.AccessConfigForCMCCCloud{}
if err := maputil.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.ACMEDns01ProviderTypeConstellix:
{
access := domain.AccessConfigForConstellix{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pConstellix.NewChallengeProvider(&pConstellix.ChallengeProviderConfig{
ApiKey: access.ApiKey,
SecretKey: access.SecretKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeCTCCCloud, domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS:
{
access := domain.AccessConfigForCTCCCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pCTCCCloud.NewChallengeProvider(&pCTCCCloud.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeDeSEC:
{
access := domain.AccessConfigForDeSEC{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pDeSEC.NewChallengeProvider(&pDeSEC.ChallengeProviderConfig{
Token: access.Token,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeDigitalOcean:
{
access := domain.AccessConfigForDigitalOcean{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pDigitalOcean.NewChallengeProvider(&pDigitalOcean.ChallengeProviderConfig{
AccessToken: access.AccessToken,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeDNSLA:
{
access := domain.AccessConfigForDNSLA{}
if err := maputil.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.ACMEDns01ProviderTypeDuckDNS:
{
access := domain.AccessConfigForDuckDNS{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pDuckDNS.NewChallengeProvider(&pDuckDNS.ChallengeProviderConfig{
Token: access.Token,
DnsPropagationTimeout: options.DnsPropagationTimeout,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeDynv6:
{
access := domain.AccessConfigForDynv6{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pDynv6.NewChallengeProvider(&pDynv6.ChallengeProviderConfig{
HttpToken: access.HttpToken,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeGcore:
{
access := domain.AccessConfigForGcore{}
if err := maputil.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.ACMEDns01ProviderTypeGname:
{
access := domain.AccessConfigForGname{}
if err := maputil.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.ACMEDns01ProviderTypeGoDaddy:
{
access := domain.AccessConfigForGoDaddy{}
if err := maputil.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.ACMEDns01ProviderTypeHetzner:
{
access := domain.AccessConfigForHetzner{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pHetzner.NewChallengeProvider(&pHetzner.ChallengeProviderConfig{
ApiToken: access.ApiToken,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeHuaweiCloud, domain.ACMEDns01ProviderTypeHuaweiCloudDNS:
{
access := domain.AccessConfigForHuaweiCloud{}
if err := maputil.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: maputil.GetString(options.ProviderServiceConfig, "region"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeJDCloud, domain.ACMEDns01ProviderTypeJDCloudDNS:
{
access := domain.AccessConfigForJDCloud{}
if err := maputil.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: maputil.GetString(options.ProviderServiceConfig, "regionId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeNamecheap:
{
access := domain.AccessConfigForNamecheap{}
if err := maputil.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.ACMEDns01ProviderTypeNameDotCom:
{
access := domain.AccessConfigForNameDotCom{}
if err := maputil.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.ACMEDns01ProviderTypeNameSilo:
{
access := domain.AccessConfigForNameSilo{}
if err := maputil.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.ACMEDns01ProviderTypeNetcup:
{
access := domain.AccessConfigForNetcup{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pNetcup.NewChallengeProvider(&pNetcup.ChallengeProviderConfig{
CustomerNumber: access.CustomerNumber,
ApiKey: access.ApiKey,
ApiPassword: access.ApiPassword,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeNetlify:
{
access := domain.AccessConfigForNetlify{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pNetlify.NewChallengeProvider(&pNetlify.ChallengeProviderConfig{
ApiToken: access.ApiToken,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeNS1:
{
access := domain.AccessConfigForNS1{}
if err := maputil.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.ACMEDns01ProviderTypePorkbun:
{
access := domain.AccessConfigForPorkbun{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pPorkbun.NewChallengeProvider(&pPorkbun.ChallengeProviderConfig{
ApiKey: access.ApiKey,
SecretApiKey: access.SecretApiKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypePowerDNS:
{
access := domain.AccessConfigForPowerDNS{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pPowerDNS.NewChallengeProvider(&pPowerDNS.ChallengeProviderConfig{
ServerUrl: access.ServerUrl,
ApiKey: access.ApiKey,
AllowInsecureConnections: access.AllowInsecureConnections,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeRainYun:
{
access := domain.AccessConfigForRainYun{}
if err := maputil.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.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS, domain.ACMEDns01ProviderTypeTencentCloudEO:
{
access := domain.AccessConfigForTencentCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
case domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS:
applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
case domain.ACMEDns01ProviderTypeTencentCloudEO:
applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
ZoneId: maputil.GetString(options.ProviderServiceConfig, "zoneId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
default:
break
}
}
case domain.ACMEDns01ProviderTypeUCloudUDNR:
{
access := domain.AccessConfigForUCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pUCloudUDNR.NewChallengeProvider(&pUCloudUDNR.ChallengeProviderConfig{
PrivateKey: access.PrivateKey,
PublicKey: access.PublicKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeVercel:
{
access := domain.AccessConfigForVercel{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pVercel.NewChallengeProvider(&pVercel.ChallengeProviderConfig{
ApiAccessToken: access.ApiAccessToken,
TeamId: access.TeamId,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeVolcEngine, domain.ACMEDns01ProviderTypeVolcEngineDNS:
{
access := domain.AccessConfigForVolcEngine{}
if err := maputil.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.ACMEDns01ProviderTypeWestcn:
{
access := domain.AccessConfigForWestcn{}
if err := maputil.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))
}

View File

@@ -1,37 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/usual2970/certimate/internal/domain"
)
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)
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := tencentcloud.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(t.option, dnsProvider)
}

View File

@@ -0,0 +1,292 @@
package certificate
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/pocketbase/dbx"
"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"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
"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)
DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error)
}
type settingsRepository interface {
GetByName(ctx context.Context, name string) (*domain.Settings, error)
}
type CertificateService struct {
certificateRepo certificateRepository
settingsRepo settingsRepository
}
func NewCertificateService(certificateRepo certificateRepository, settingsRepo settingsRepository) *CertificateService {
return &CertificateService{
certificateRepo: certificateRepo,
settingsRepo: settingsRepo,
}
}
func (s *CertificateService) InitSchedule(ctx context.Context) error {
// 每日发送过期证书提醒
app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() {
certificates, err := s.certificateRepo.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)
}
})
// 每日清理过期证书
app.GetScheduler().MustAdd("certificateExpiredCleanup", "0 0 * * *", func() {
settings, err := s.settingsRepo.GetByName(ctx, "persistence")
if err != nil {
app.GetLogger().Error("failed to get persistence settings", "err", err)
return
}
var settingsContent *domain.PersistenceSettingsContent
json.Unmarshal([]byte(settings.Content), &settingsContent)
if settingsContent != nil && settingsContent.ExpiredCertificatesMaxDaysRetention != 0 {
ret, err := s.certificateRepo.DeleteWhere(
context.Background(),
dbx.NewExp(fmt.Sprintf("expireAt<DATETIME('now', '-%d days')", settingsContent.ExpiredCertificatesMaxDaysRetention)),
)
if err != nil {
app.GetLogger().Error("failed to delete expired certificates", "err", err)
}
if ret > 0 {
app.GetLogger().Info(fmt.Sprintf("cleanup %d expired certificates", ret))
}
}
})
return nil
}
func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) (*dtos.CertificateArchiveFileResp, error) {
certificate, err := s.certificateRepo.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 := certutil.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 := certutil.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 := certutil.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}
}

View File

@@ -1,281 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
)
type AliyunALBDeployer struct {
option *DeployerOption
infos []string
sdkClient *aliyunAlb.Client
sslUploader uploader.Uploader
}
func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunALBDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
aliCasRegion := option.DeployConfig.GetConfigAsString("region")
if aliCasRegion != "" {
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
// 国内版接入点:华东一杭州
// 国际版接入点:亚太东南一新加坡
if !strings.HasPrefix(aliCasRegion, "cn-") {
aliCasRegion = "ap-southeast-1"
} else {
aliCasRegion = "cn-hangzhou"
}
}
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: aliCasRegion,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunALBDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *AliyunALBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunALBDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunALBDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "loadbalancer":
if err := d.deployToLoadbalancer(ctx); err != nil {
return err
}
case "listener":
if err := d.deployToListener(ctx); err != nil {
return err
}
default:
return errors.New("unsupported resource type")
}
return nil
}
func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州
}
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
var endpoint string
switch region {
case "cn-hangzhou-finance":
endpoint = "alb.cn-hangzhou.aliyuncs.com"
default:
endpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region)
}
aConfig.Endpoint = tea.String(endpoint)
client, err := aliyunAlb.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}
func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
if aliLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
aliListenerIds := make([]string, 0)
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(aliLoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp))
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
listListenersPage := 1
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
listListenersReq := &aliyunAlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
ListenerProtocol: tea.String("HTTPS"),
}
listListenersResp, err := d.sdkClient.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 {
aliListenerIds = append(aliListenerIds, *listener.ListenerId)
}
}
if listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", aliListenerIds))
// 查询 QUIC 监听列表
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
listListenersPage = 1
listListenersToken = nil
for {
listListenersReq := &aliyunAlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
ListenerProtocol: tea.String("QUIC"),
}
listListenersResp, err := d.sdkClient.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 {
aliListenerIds = append(aliListenerIds, *listener.ListenerId)
}
}
if listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", aliListenerIds))
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听证书
var errs []error
for _, aliListenerId := range aliListenerIds {
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error {
aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
if aliListenerId == "" {
return errors.New("`listenerId` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId 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(aliListenerId),
}
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 ALB 监听配置", getListenerAttributeResp))
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{
ListenerId: tea.String(aliListenerId),
Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{
CertificateId: tea.String(aliCertId),
}},
}
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'")
}
d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp))
return nil
}

View File

@@ -1,88 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"time"
aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
)
type AliyunCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *aliyunCdn.Client
}
func NewAliyunCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
func (d *AliyunCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunCDNDeployer) Deploy(ctx context.Context) error {
// 设置 CDN 域名域名证书
// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate
setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(d.option.DeployConfig.GetConfigAsString("domain")),
CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
}
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'")
}
d.infos = append(d.infos, toStr("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp))
return nil
}
func (d *AliyunCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) {
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("cdn.aliyuncs.com"),
}
client, err := aliyunCdn.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -1,285 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderAliyunSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb"
)
type AliyunCLBDeployer struct {
option *DeployerOption
infos []string
sdkClient *aliyunSlb.Client
sslUploader uploader.Uploader
}
func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunCLBDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploaderAliyunSlb.New(&uploaderAliyunSlb.AliyunSLBUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: option.DeployConfig.GetConfigAsString("region"),
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunCLBDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *AliyunCLBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunCLBDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunCLBDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "loadbalancer":
if err := d.deployToLoadbalancer(ctx); err != nil {
return err
}
case "listener":
if err := d.deployToListener(ctx); err != nil {
return err
}
default:
return errors.New("unsupported resource type")
}
return nil
}
func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
}
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
var endpoint string
switch region {
case "cn-hangzhou":
case "cn-hangzhou-finance":
case "cn-shanghai-finance-1":
case "cn-shenzhen-finance-1":
endpoint = "slb.aliyuncs.com"
default:
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
}
aConfig.Endpoint = tea.String(endpoint)
client, err := aliyunSlb.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}
func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
aliListenerPorts := make([]int32, 0)
if aliLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
LoadBalancerId: tea.String(aliLoadbalancerId),
}
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp))
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners
listListenersPage := 1
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerId: []*string{tea.String(aliLoadbalancerId)},
ListenerProtocol: tea.String("https"),
}
describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'")
}
if describeLoadBalancerListenersResp.Body.Listeners != nil {
for _, listener := range describeLoadBalancerListenersResp.Body.Listeners {
aliListenerPorts = append(aliListenerPorts, *listener.ListenerPort)
}
}
if describeLoadBalancerListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = describeLoadBalancerListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", aliListenerPorts))
// 上传证书到 SLB
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听证书
var errs []error
for _, aliListenerPort := range aliListenerPorts {
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error {
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
if aliLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
aliListenerPort := d.option.DeployConfig.GetConfigAsInt32("listenerPort")
if aliListenerPort == 0 {
return errors.New("`listenerPort` is required")
}
// 上传证书到 SLB
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil {
return err
}
return nil
}
func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLoadbalancerId string, aliListenerPort int32, aliCertId string) error {
// 查询监听配置
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute
describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{
LoadBalancerId: tea.String(aliLoadbalancerId),
ListenerPort: tea.Int32(aliListenerPort),
}
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp))
// 查询扩展域名
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions
describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
LoadBalancerId: tea.String(aliLoadbalancerId),
ListenerPort: tea.Int32(aliListenerPort),
}
describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'")
}
d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp))
// 遍历修改扩展域名
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
//
// 这里仅修改跟被替换证书一致的扩展域名
if describeDomainExtensionsResp.Body.DomainExtensions == nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension == nil {
for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
if *domainExtension.ServerCertificateId == *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
break
}
setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
DomainExtensionId: tea.String(*domainExtension.DomainExtensionId),
ServerCertificateId: tea.String(aliCertId),
}
_, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'")
}
}
}
// 修改监听配置
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
//
// 注意修改监听配置要放在修改扩展域名之后
setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
LoadBalancerId: tea.String(aliLoadbalancerId),
ListenerPort: tea.Int32(aliListenerPort),
ServerCertificateId: tea.String(aliCertId),
}
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'")
}
d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp))
return nil
}

View File

@@ -1,95 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunDcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
)
type AliyunDCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *aliyunDcdn.Client
}
func NewAliyunDCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunDCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunDCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
func (d *AliyunDCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunDCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunDCDNDeployer) Deploy(ctx context.Context) error {
// 支持泛解析域名,在 Aliyun DCDN 中泛解析域名表示为 .example.com
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domain = strings.TrimPrefix(domain, "*")
}
// 配置域名证书
// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate
setDcdnDomainSSLCertificateReq := &aliyunDcdn.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(domain),
CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
}
setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'")
}
d.infos = append(d.infos, toStr("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp))
return nil
}
func (d *AliyunDCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) {
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("dcdn.aliyuncs.com"),
}
client, err := aliyunDcdn.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -1,245 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
)
type AliyunNLBDeployer struct {
option *DeployerOption
infos []string
sdkClient *aliyunNlb.Client
sslUploader uploader.Uploader
}
func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunNLBDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
aliCasRegion := option.DeployConfig.GetConfigAsString("region")
if aliCasRegion != "" {
// 阿里云 CAS 服务接入点是独立于 NLB 服务的
// 国内版接入点:华东一杭州
// 国际版接入点:亚太东南一新加坡
if !strings.HasPrefix(aliCasRegion, "cn-") {
aliCasRegion = "ap-southeast-1"
} else {
aliCasRegion = "cn-hangzhou"
}
}
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: aliCasRegion,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunNLBDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *AliyunNLBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunNLBDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunNLBDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "loadbalancer":
if err := d.deployToLoadbalancer(ctx); err != nil {
return err
}
case "listener":
if err := d.deployToListener(ctx); err != nil {
return err
}
default:
return errors.New("unsupported resource type")
}
return nil
}
func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
}
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
var endpoint string
switch region {
default:
endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region)
}
aConfig.Endpoint = tea.String(endpoint)
client, err := aliyunNlb.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}
func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error {
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
if aliLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
aliListenerIds := make([]string, 0)
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute
getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(aliLoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp))
// 查询 TCPSSL 监听列表
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners
listListenersPage := 1
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
listListenersReq := &aliyunNlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
ListenerProtocol: tea.String("TCPSSL"),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'")
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
aliListenerIds = append(aliListenerIds, *listener.ListenerId)
}
}
if listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", aliListenerIds))
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听证书
var errs []error
for _, aliListenerId := range aliListenerIds {
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error {
aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
if aliListenerId == "" {
return errors.New("`listenerId` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error {
// 查询监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute
getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{
ListenerId: tea.String(aliListenerId),
}
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 NLB 监听配置", getListenerAttributeResp))
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute
updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{
ListenerId: tea.String(aliListenerId),
CertificateIds: []*string{tea.String(aliCertId)},
}
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'")
}
d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp))
return nil
}

View File

@@ -1,86 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
)
type AliyunOSSDeployer struct {
option *DeployerOption
infos []string
sdkClient *oss.Client
}
func NewAliyunOSSDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunOSSDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
option.DeployConfig.GetConfigAsString("endpoint"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunOSSDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
func (d *AliyunOSSDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunOSSDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error {
aliBucket := d.option.DeployConfig.GetConfigAsString("bucket")
if aliBucket == "" {
return errors.New("`bucket` is required")
}
// 为存储空间绑定自定义域名
// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname
err := d.sdkClient.PutBucketCnameWithCertificate(aliBucket, oss.PutBucketCname{
Cname: d.option.DeployConfig.GetConfigAsString("domain"),
CertificateConfiguration: &oss.CertificateConfiguration{
Certificate: d.option.Certificate.Certificate,
PrivateKey: d.option.Certificate.PrivateKey,
Force: true,
},
})
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'")
}
return nil
}
func (d *AliyunOSSDeployer) createSdkClient(accessKeyId, accessKeySecret, endpoint string) (*oss.Client, error) {
if endpoint == "" {
endpoint = "oss.aliyuncs.com"
}
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -1,80 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"time"
bceCdn "github.com/baidubce/bce-sdk-go/services/cdn"
bceCdnApi "github.com/baidubce/bce-sdk-go/services/cdn/api"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
)
type BaiduCloudCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *bceCdn.Client
}
func NewBaiduCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.BaiduCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&BaiduCloudCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.SecretAccessKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &BaiduCloudCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
func (d *BaiduCloudCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *BaiduCloudCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *BaiduCloudCDNDeployer) Deploy(ctx context.Context) error {
// 修改域名证书
// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8
putCertResp, err := d.sdkClient.PutCert(
d.option.DeployConfig.GetConfigAsString("domain"),
&bceCdnApi.UserCertificate{
CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
ServerData: d.option.Certificate.Certificate,
PrivateData: d.option.Certificate.PrivateKey,
},
"ON",
)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.PutCert'")
}
d.infos = append(d.infos, toStr("已修改域名证书", putCertResp))
return nil
}
func (d *BaiduCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey string) (*bceCdn.Client, error) {
client, err := bceCdn.NewClient(accessKeyId, secretAccessKey, "")
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -1,217 +1,72 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"time"
"log/slog"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"github.com/pocketbase/pocketbase/models"
"software.sslmate.com/src/go-pkcs12"
"github.com/usual2970/certimate/internal/applicant"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/usual2970/certimate/internal/utils/app"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/repository"
)
const (
targetAliyunOSS = "aliyun-oss"
targetAliyunCDN = "aliyun-cdn"
targetAliyunDCDN = "aliyun-dcdn"
targetAliyunCLB = "aliyun-clb"
targetAliyunALB = "aliyun-alb"
targetAliyunNLB = "aliyun-nlb"
targetTencentCDN = "tencent-cdn"
targetTencentECDN = "tencent-ecdn"
targetTencentCLB = "tencent-clb"
targetTencentCOS = "tencent-cos"
targetTencentTEO = "tencent-teo"
targetHuaweiCloudCDN = "huaweicloud-cdn"
targetHuaweiCloudELB = "huaweicloud-elb"
targetBaiduCloudCDN = "baiducloud-cdn"
targetQiniuCdn = "qiniu-cdn"
targetDogeCloudCdn = "dogecloud-cdn"
targetLocal = "local"
targetSSH = "ssh"
targetWebhook = "webhook"
targetK8sSecret = "k8s-secret"
)
type DeployerOption struct {
DomainId string `json:"domainId"`
Domain string `json:"domain"`
Access string `json:"access"`
AccessRecord *models.Record `json:"-"`
DeployConfig domain.DeployConfig `json:"deployConfig"`
Certificate applicant.Certificate `json:"certificate"`
Variables map[string]string `json:"variables"`
}
type Deployer interface {
Deploy(ctx context.Context) error
GetInfos() []string
GetID() string
}
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
rs := make([]Deployer, 0)
if record.GetString("deployConfig") == "" {
return rs, nil
type DeployerWithWorkflowNodeConfig struct {
Node *domain.WorkflowNode
Logger *slog.Logger
CertificatePEM string
PrivateKeyPEM string
}
func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error) {
if config.Node == nil {
return nil, fmt.Errorf("node is nil")
}
if config.Node.Type != domain.WorkflowNodeTypeDeploy {
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy))
}
deployConfigs := make([]domain.DeployConfig, 0)
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
if err != nil {
return nil, fmt.Errorf("解析部署配置失败: %w", err)
nodeCfg := config.Node.GetConfigForDeploy()
options := &deployerProviderOptions{
Provider: domain.DeploymentProviderType(nodeCfg.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderServiceConfig: nodeCfg.ProviderConfig,
}
if len(deployConfigs) == 0 {
return rs, nil
}
for _, deployConfig := range deployConfigs {
deployer, err := getWithDeployConfig(record, cert, deployConfig)
accessRepo := repository.NewAccessRepository()
if nodeCfg.ProviderAccessId != "" {
access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId)
if err != nil {
return nil, err
}
rs = append(rs, deployer)
}
return rs, nil
}
func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) {
access, err := app.GetApp().Dao().FindRecordById("access", deployConfig.Access)
if err != nil {
return nil, fmt.Errorf("access record not found: %w", err)
}
option := &DeployerOption{
DomainId: record.Id,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
AccessRecord: access,
DeployConfig: deployConfig,
}
if cert != nil {
option.Certificate = *cert
} else {
option.Certificate = applicant.Certificate{
Certificate: record.GetString("certificate"),
PrivateKey: record.GetString("privateKey"),
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err)
} else {
options.ProviderAccessConfig = access.Config
}
}
switch deployConfig.Type {
case targetAliyunOSS:
return NewAliyunOSSDeployer(option)
case targetAliyunCDN:
return NewAliyunCDNDeployer(option)
case targetAliyunDCDN:
return NewAliyunDCDNDeployer(option)
case targetAliyunCLB:
return NewAliyunCLBDeployer(option)
case targetAliyunALB:
return NewAliyunALBDeployer(option)
case targetAliyunNLB:
return NewAliyunNLBDeployer(option)
case targetTencentCDN:
return NewTencentCDNDeployer(option)
case targetTencentECDN:
return NewTencentECDNDeployer(option)
case targetTencentCLB:
return NewTencentCLBDeployer(option)
case targetTencentCOS:
return NewTencentCOSDeployer(option)
case targetTencentTEO:
return NewTencentTEODeployer(option)
case targetHuaweiCloudCDN:
return NewHuaweiCloudCDNDeployer(option)
case targetHuaweiCloudELB:
return NewHuaweiCloudELBDeployer(option)
case targetBaiduCloudCDN:
return NewBaiduCloudCDNDeployer(option)
case targetQiniuCdn:
return NewQiniuCDNDeployer(option)
case targetDogeCloudCdn:
return NewDogeCloudCDNDeployer(option)
case targetLocal:
return NewLocalDeployer(option)
case targetSSH:
return NewSSHDeployer(option)
case targetWebhook:
return NewWebhookDeployer(option)
case targetK8sSecret:
return NewK8sSecretDeployer(option)
}
return nil, errors.New("unsupported deploy target")
}
func toStr(tag string, data any) string {
if data == nil {
return tag
}
byts, _ := json.Marshal(data)
return tag + "" + string(byts)
}
func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
cert, err := x509.ParseCertificateFromPEM(certificate)
deployerProvider, err := createDeployerProvider(options)
if err != nil {
return nil, err
}
privkey, err := x509.ParsePKCS1PrivateKeyFromPEM(privateKey)
if err != nil {
return nil, err
}
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, password)
if err != nil {
return nil, err
}
return pfxData, nil
return &deployerImpl{
provider: deployerProvider.WithLogger(config.Logger),
certPEM: config.CertificatePEM,
privkeyPEM: config.PrivateKeyPEM,
}, nil
}
func convertPEMToJKS(certificate string, privateKey string, alias string, keypass string, storepass string) ([]byte, error) {
certBlock, _ := pem.Decode([]byte(certificate))
if certBlock == nil {
return nil, errors.New("failed to decode certificate PEM")
}
privkeyBlock, _ := pem.Decode([]byte(privateKey))
if privkeyBlock == nil {
return nil, errors.New("failed to decode private key PEM")
}
ks := keystore.New()
entry := keystore.PrivateKeyEntry{
CreationTime: time.Now(),
PrivateKey: privkeyBlock.Bytes,
CertificateChain: []keystore.Certificate{
{
Type: "X509",
Content: certBlock.Bytes,
},
},
}
if err := ks.SetPrivateKeyEntry(alias, entry, []byte(keypass)); err != nil {
return nil, err
}
var buf bytes.Buffer
if err := ks.Store(&buf, []byte(storepass)); err != nil {
return nil, err
}
return buf.Bytes(), nil
type deployerImpl struct {
provider deployer.Deployer
certPEM string
privkeyPEM string
}
var _ Deployer = (*deployerImpl)(nil)
func (d *deployerImpl) Deploy(ctx context.Context) error {
_, err := d.provider.Deploy(ctx, d.certPEM, d.privkeyPEM)
return err
}

View File

@@ -1,88 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strconv"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderDoge "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/dogecloud"
doge "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
)
type DogeCloudCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *doge.Client
sslUploader uploader.Uploader
}
func NewDogeCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.DogeCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&DogeCloudCDNDeployer{}).createSdkClient(
access.AccessKey,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploaderDoge.New(&uploaderDoge.DogeCloudUploaderConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DogeCloudCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DogeCloudCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *DogeCloudCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *DogeCloudCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书到 CDN
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 绑定证书
// REF: https://docs.dogecloud.com/cdn/api-cert-bind
bindCdnCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
bindCdnCertResp, err := d.sdkClient.BindCdnCertWithDomain(bindCdnCertId, d.option.DeployConfig.GetConfigAsString("domain"))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BindCdnCert'")
}
d.infos = append(d.infos, toStr("已绑定证书", bindCdnCertResp))
return nil
}
func (d *DogeCloudCDNDeployer) createSdkClient(accessKey, secretKey string) (*doge.Client, error) {
client := doge.NewClient(accessKey, secretKey)
return client, nil
}

View File

@@ -1,143 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderHcScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
hcCdnEx "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk"
)
type HuaweiCloudCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *hcCdnEx.Client
sslUploader uploader.Uploader
}
func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.HuaweiCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.SecretAccessKey,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploaderHcScm.New(&uploaderHcScm.HuaweiCloudSCMUploaderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: "",
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &HuaweiCloudCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *HuaweiCloudCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *HuaweiCloudCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 查询加速域名配置
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
DomainName: d.option.DeployConfig.GetConfigAsString("domain"),
}
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'")
}
d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp))
// 更新加速域名配置
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html
updateDomainMultiCertificatesReqBodyContent := &hcCdnEx.UpdateDomainMultiCertificatesExRequestBodyContent{}
updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain")
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId)
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName)
updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs)
updateDomainMultiCertificatesReq := &hcCdnEx.UpdateDomainMultiCertificatesExRequest{
Body: &hcCdnEx.UpdateDomainMultiCertificatesExRequestBody{
Https: updateDomainMultiCertificatesReqBodyContent,
},
}
updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'")
}
d.infos = append(d.infos, toStr("已更新加速域名配置", updateDomainMultiCertificatesResp))
return nil
}
func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdnEx.Client, error) {
if region == "" {
region = "cn-north-1" // CDN 服务默认区域:华北一北京
}
auth, err := global.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return nil, err
}
hcRegion, err := hcCdnRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := hcCdn.CdnClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := hcCdnEx.NewClient(hcClient)
return client, nil
}

View File

@@ -1,386 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderHcElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
type HuaweiCloudELBDeployer struct {
option *DeployerOption
infos []string
sdkClient *hcElb.ElbClient
sslUploader uploader.Uploader
}
func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.HuaweiCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&HuaweiCloudELBDeployer{}).createSdkClient(
access.AccessKeyId,
access.SecretAccessKey,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploaderHcElb.New(&uploaderHcElb.HuaweiCloudELBUploaderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: option.DeployConfig.GetConfigAsString("region"),
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &HuaweiCloudELBDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *HuaweiCloudELBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *HuaweiCloudELBDeployer) GetInfos() []string {
return d.infos
}
func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "certificate":
// 部署到指定证书
if err := d.deployToCertificate(ctx); err != nil {
return err
}
case "loadbalancer":
// 部署到指定负载均衡器
if err := d.deployToLoadbalancer(ctx); err != nil {
return err
}
case "listener":
// 部署到指定监听器
if err := d.deployToListener(ctx); err != nil {
return err
}
default:
return errors.New("unsupported resource type")
}
return nil
}
func (d *HuaweiCloudELBDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
if region == "" {
region = "cn-north-4" // ELB 服务默认区域:华北四北京
}
projectId, err := (&HuaweiCloudELBDeployer{}).getSdkProjectId(
accessKeyId,
secretAccessKey,
region,
)
if err != nil {
return nil, err
}
auth, err := basic.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
WithProjectId(projectId).
SafeBuild()
if err != nil {
return nil, err
}
hcRegion, err := hcElbRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := hcElb.ElbClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := hcElb.NewElbClient(hcClient)
return client, nil
}
func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
if region == "" {
region = "cn-north-4" // IAM 服务默认区域:华北四北京
}
auth, err := global.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return "", err
}
hcRegion, err := hcIamRegion.SafeValueOf(region)
if err != nil {
return "", err
}
hcClient, err := hcIam.IamClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return "", err
}
client := hcIam.NewIamClient(hcClient)
if err != nil {
return "", err
}
request := &hcIamModel.KeystoneListProjectsRequest{
Name: &region,
}
response, err := client.KeystoneListProjects(request)
if err != nil {
return "", err
} else if response.Projects == nil || len(*response.Projects) == 0 {
return "", errors.New("no project found")
}
return (*response.Projects)[0].Id, nil
}
func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error {
hcCertId := d.option.DeployConfig.GetConfigAsString("certificateId")
if hcCertId == "" {
return errors.New("`certificateId` is required")
}
// 更新证书
// REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html
updateCertificateReq := &hcElbModel.UpdateCertificateRequest{
CertificateId: hcCertId,
Body: &hcElbModel.UpdateCertificateRequestBody{
Certificate: &hcElbModel.UpdateCertificateOption{
Certificate: cast.StringPtr(d.option.Certificate.Certificate),
PrivateKey: cast.StringPtr(d.option.Certificate.PrivateKey),
},
},
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'")
}
d.infos = append(d.infos, toStr("已更新 ELB 证书", updateCertificateResp))
return nil
}
func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error {
hcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
if hcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
hcListenerIds := make([]string, 0)
// 查询负载均衡器详情
// REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html
showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{
LoadbalancerId: hcLoadbalancerId,
}
showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'")
}
d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp))
// 查询监听器列表
// REF: https://support.huaweicloud.com/api-elb/ListListeners.html
listListenersLimit := int32(2000)
var listListenersMarker *string = nil
for {
listListenersReq := &hcElbModel.ListListenersRequest{
Limit: cast.Int32Ptr(listListenersLimit),
Marker: listListenersMarker,
Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"},
LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'")
}
if listListenersResp.Listeners != nil {
for _, listener := range *listListenersResp.Listeners {
hcListenerIds = append(hcListenerIds, listener.Id)
}
}
if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) {
break
} else {
listListenersMarker = listListenersResp.PageInfo.NextMarker
}
}
d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds))
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听器证书
var errs []error
for _, hcListenerId := range hcListenerIds {
if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error {
hcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
if hcListenerId == "" {
return errors.New("`listenerId` is required")
}
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听器证书
if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error {
// 查询监听器详情
// REF: https://support.huaweicloud.com/api-elb/ShowListener.html
showListenerReq := &hcElbModel.ShowListenerRequest{
ListenerId: hcListenerId,
}
showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'")
}
d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp))
// 更新监听器
// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
updateListenerReq := &hcElbModel.UpdateListenerRequest{
ListenerId: hcListenerId,
Body: &hcElbModel.UpdateListenerRequestBody{
Listener: &hcElbModel.UpdateListenerOption{
DefaultTlsContainerRef: cast.StringPtr(hcCertId),
},
},
}
if showListenerResp.Listener.SniContainerRefs != nil {
if len(showListenerResp.Listener.SniContainerRefs) > 0 {
// 如果开启 SNI需替换同 SAN 的证书
sniCertIds := make([]string, 0)
sniCertIds = append(sniCertIds, hcCertId)
listOldCertificateReq := &hcElbModel.ListCertificatesRequest{
Id: &showListenerResp.Listener.SniContainerRefs,
}
listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
}
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
CertificateId: hcCertId,
}
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'")
}
for _, certificate := range *listOldCertificateResp.Certificates {
oldCertificate := certificate
newCertificate := showNewCertificateResp.Certificate
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
oldCertificateSans := oldCertificate.SubjectAlternativeNames
newCertificateSans := newCertificate.SubjectAlternativeNames
sort.Strings(*oldCertificateSans)
sort.Strings(*newCertificateSans)
if strings.Join(*oldCertificateSans, ";") == strings.Join(*newCertificateSans, ";") {
continue
}
} else {
if oldCertificate.Domain == newCertificate.Domain {
continue
}
}
sniCertIds = append(sniCertIds, certificate.Id)
}
updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds
}
if showListenerResp.Listener.SniMatchAlgo != "" {
updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo)
}
}
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'")
}
d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp))
return nil
}

View File

@@ -1,136 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
k8sCore "k8s.io/api/core/v1"
k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type K8sSecretDeployer struct {
option *DeployerOption
infos []string
k8sClient *kubernetes.Clientset
}
func NewK8sSecretDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.KubernetesAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&K8sSecretDeployer{}).createK8sClient(access)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create k8s client")
}
return &K8sSecretDeployer{
option: option,
infos: make([]string, 0),
k8sClient: client,
}, nil
}
func (d *K8sSecretDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *K8sSecretDeployer) GetInfos() []string {
return d.infos
}
func (d *K8sSecretDeployer) Deploy(ctx context.Context) error {
namespace := d.option.DeployConfig.GetConfigAsString("namespace")
secretName := d.option.DeployConfig.GetConfigAsString("secretName")
secretDataKeyForCrt := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForCrt", "tls.crt")
secretDataKeyForKey := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForKey", "tls.key")
if namespace == "" {
namespace = "default"
}
if secretName == "" {
return errors.New("`secretName` is required")
}
certX509, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate)
if err != nil {
return err
}
secretPayload := k8sCore.Secret{
TypeMeta: k8sMeta.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: k8sMeta.ObjectMeta{
Name: secretName,
Annotations: map[string]string{
"certimate/domains": d.option.Domain,
"certimate/alt-names": strings.Join(certX509.DNSNames, ","),
"certimate/common-name": certX509.Subject.CommonName,
"certimate/issuer-organization": strings.Join(certX509.Issuer.Organization, ","),
},
},
Type: k8sCore.SecretType("kubernetes.io/tls"),
}
secretPayload.Data = make(map[string][]byte)
secretPayload.Data[secretDataKeyForCrt] = []byte(d.option.Certificate.Certificate)
secretPayload.Data[secretDataKeyForKey] = []byte(d.option.Certificate.PrivateKey)
// 获取 Secret 实例
_, err = d.k8sClient.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMeta.GetOptions{})
if err != nil {
_, err = d.k8sClient.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMeta.CreateOptions{})
if err != nil {
return xerrors.Wrap(err, "failed to create k8s secret")
} else {
d.infos = append(d.infos, toStr("Certificate has been created in K8s Secret", nil))
return nil
}
}
// 更新 Secret 实例
_, err = d.k8sClient.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMeta.UpdateOptions{})
if err != nil {
return xerrors.Wrap(err, "failed to update k8s secret")
}
d.infos = append(d.infos, toStr("Certificate has been updated to K8s Secret", nil))
return nil
}
func (d *K8sSecretDeployer) createK8sClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) {
var config *rest.Config
var err error
if access.KubeConfig == "" {
config, err = rest.InClusterConfig()
} else {
kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(access.KubeConfig))
if err != nil {
return nil, err
}
config, err = kubeConfig.ClientConfig()
}
if err != nil {
return nil, err
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -1,162 +0,0 @@
package deployer
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"runtime"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
)
type LocalDeployer struct {
option *DeployerOption
infos []string
}
const (
certFormatPEM = "pem"
certFormatPFX = "pfx"
certFormatJKS = "jks"
)
const (
shellEnvSh = "sh"
shellEnvCmd = "cmd"
shellEnvPowershell = "powershell"
)
func NewLocalDeployer(option *DeployerOption) (Deployer, error) {
return &LocalDeployer{
option: option,
infos: make([]string, 0),
}, nil
}
func (d *LocalDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *LocalDeployer) GetInfos() []string {
return []string{}
}
func (d *LocalDeployer) Deploy(ctx context.Context) error {
// 执行前置命令
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
if preCommand != "" {
stdout, stderr, err := d.execCommand(preCommand)
if err != nil {
return xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("执行前置命令成功", stdout))
}
// 写入证书和私钥文件
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case certFormatPEM:
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存私钥成功", nil))
case certFormatPFX:
pfxData, err := convertPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil {
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
case certFormatJKS:
jksData, err := convertPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
default:
return errors.New("unsupported format")
}
// 执行命令
command := d.option.DeployConfig.GetConfigAsString("command")
if command != "" {
stdout, stderr, err := d.execCommand(command)
if err != nil {
return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("执行命令成功", stdout))
}
return nil
}
func (d *LocalDeployer) execCommand(command string) (string, string, error) {
var cmd *exec.Cmd
switch d.option.DeployConfig.GetConfigAsString("shell") {
case shellEnvSh:
cmd = exec.Command("sh", "-c", command)
case shellEnvCmd:
cmd = exec.Command("cmd", "/C", command)
case shellEnvPowershell:
cmd = exec.Command("powershell", "-Command", command)
case "":
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", command)
} else {
cmd = exec.Command("sh", "-c", command)
}
default:
return "", "", errors.New("unsupported shell")
}
var stdoutBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
err := cmd.Run()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute shell script")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,107 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert"
qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
)
type QiniuCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *qiniuEx.Client
sslUploader uploader.Uploader
}
func NewQiniuCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.QiniuAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&QiniuCDNDeployer{}).createSdkClient(
access.AccessKey,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploaderQiniu.New(&uploaderQiniu.QiniuSSLCertUploaderConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &QiniuCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *QiniuCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *QiniuCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *QiniuCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 获取域名信息
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
domain := d.option.DeployConfig.GetConfigAsString("domain")
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")
}
d.infos = append(d.infos, toStr("已获取域名信息", getDomainInfoResp))
// 判断域名是否已启用 HTTPS。如果已启用修改域名证书否则启用 HTTPS
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" {
modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
}
d.infos = append(d.infos, toStr("已修改域名证书", modifyDomainHttpsConfResp))
} else {
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
}
d.infos = append(d.infos, toStr("已将域名升级为 HTTPS", enableDomainHttpsResp))
}
return nil
}
func (u *QiniuCDNDeployer) createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) {
credential := auth.New(accessKey, secretKey)
client := qiniuEx.NewClient(credential)
return client, nil
}

View File

@@ -1,208 +0,0 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/domain"
)
type SSHDeployer struct {
option *DeployerOption
infos []string
}
func NewSSHDeployer(option *DeployerOption) (Deployer, error) {
return &SSHDeployer{
option: option,
infos: make([]string, 0),
}, nil
}
func (d *SSHDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *SSHDeployer) GetInfos() []string {
return d.infos
}
func (d *SSHDeployer) Deploy(ctx context.Context) error {
access := &domain.SSHAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return err
}
// 连接
client, err := d.createSshClient(access)
if err != nil {
return err
}
defer client.Close()
d.infos = append(d.infos, toStr("SSH 连接成功", nil))
// 执行前置命令
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
if preCommand != "" {
stdout, stderr, err := d.sshExecCommand(client, preCommand)
if err != nil {
return xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout))
}
// 上传证书和私钥文件
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case certFormatPEM:
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
case certFormatPFX:
pfxData, err := convertPEMToPFX(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil {
return err
}
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
case certFormatJKS:
jksData, err := convertPEMToJKS(
d.option.Certificate.Certificate,
d.option.Certificate.PrivateKey,
d.option.DeployConfig.GetConfigAsString("jksAlias"),
d.option.DeployConfig.GetConfigAsString("jksKeypass"),
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return err
}
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
default:
return errors.New("unsupported format")
}
// 执行命令
command := d.option.DeployConfig.GetConfigAsString("command")
if command != "" {
stdout, stderr, err := d.sshExecCommand(client, command)
if err != nil {
return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout))
}
return nil
}
func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, error) {
var authMethod ssh.AuthMethod
if access.Key != "" {
var signer ssh.Signer
var err error
if access.KeyPassphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(access.Key))
}
if err != nil {
return nil, err
}
authMethod = ssh.PublicKeys(signer)
} else {
authMethod = ssh.Password(access.Password)
}
return ssh.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &ssh.ClientConfig{
User: access.Username,
Auth: []ssh.AuthMethod{
authMethod,
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
}
func (d *SSHDeployer) sshExecCommand(sshCli *ssh.Client, command string) (string, string, error) {
session, err := sshCli.NewSession()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to create ssh session")
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
session.Stderr = &stderrBuf
err = session.Run(command)
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute ssh script")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}
func (d *SSHDeployer) writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
return d.writeSftpFile(sshCli, path, []byte(content))
}
func (d *SSHDeployer) writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return xerrors.Wrap(err, "failed to create sftp client")
}
defer sftpCli.Close()
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
return xerrors.Wrap(err, "failed to create remote directory")
}
file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
if err != nil {
return xerrors.Wrap(err, "failed to open remote file")
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return xerrors.Wrap(err, "failed to write to remote file")
}
return nil
}

View File

@@ -1,12 +0,0 @@
package deployer
import (
"os"
"path"
"testing"
)
func TestPath(t *testing.T) {
dir := path.Dir("./a/b/c")
os.MkdirAll(dir, 0o755)
}

View File

@@ -1,194 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCDNDeployer struct {
option *DeployerOption
infos []string
sdkClients *tencentCDNDeployerSdkClients
sslUploader uploader.Uploader
}
type tencentCDNDeployerSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func NewTencentCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
clients, err := (&TencentCDNDeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 获取待部署的 CDN 实例
// 如果是泛域名,根据证书匹配 CDN 实例
tcInstanceIds := make([]string, 0)
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return err
}
tcInstanceIds = domains
} else {
tcInstanceIds = append(tcInstanceIds, domain)
}
// 跳过已部署的 CDN 实例
if len(tcInstanceIds) > 0 {
deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId)
if err != nil {
return err
}
temp := make([]string, 0)
for _, aliInstanceId := range tcInstanceIds {
if !slices.Contains(deployedDomains, aliInstanceId) {
temp = append(temp, aliInstanceId)
}
}
tcInstanceIds = temp
}
if len(tcInstanceIds) == 0 {
d.infos = append(d.infos, "已部署过或没有要部署的 CDN 实例")
return nil
}
// 证书部署到 CDN 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(tcInstanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentCDNDeployerSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}
func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(tcCertId)
describeCertDomainsReq.Product = common.StringPtr("cdn")
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
}
domains := make([]string, 0)
if describeCertDomainsResp.Response.Domains == nil {
for _, domain := range describeCertDomainsResp.Response.Domains {
domains = append(domains, *domain)
}
}
return domains, nil
}
func (d *TencentCDNDeployer) getDeployedDomainsByCertificateId(tcCertId string) ([]string, error) {
// 根据证书查询关联 CDN 域名
// REF: https://cloud.tencent.com/document/product/400/62674
describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest()
describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{tcCertId})
describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn")
describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'")
}
domains := make([]string, 0)
if describeDeployedResourcesResp.Response.DeployedResources != nil {
for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources {
for _, resource := range deployedResource.Resources {
domains = append(domains, *resource)
}
}
}
return domains, nil
}

View File

@@ -1,328 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
xerrors "github.com/pkg/errors"
tcClb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCLBDeployer struct {
option *DeployerOption
infos []string
sdkClients *tencentCLBDeployerSdkClients
sslUploader uploader.Uploader
}
type tencentCLBDeployerSdkClients struct {
ssl *tcSsl.Client
clb *tcClb.Client
}
func NewTencentCLBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
clients, err := (&TencentCLBDeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCLBDeployer{
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentCLBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentCLBDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentCLBDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "ssl-deploy":
// 通过 SSL 服务部署到云资源实例
err := d.deployToInstanceUseSsl(ctx)
if err != nil {
return err
}
case "loadbalancer":
// 部署到指定负载均衡器
if err := d.deployToLoadbalancer(ctx); err != nil {
return err
}
case "listener":
// 部署到指定监听器
if err := d.deployToListener(ctx); err != nil {
return err
}
case "ruledomain":
// 部署到指定七层监听转发规则域名
if err := d.deployToRuleDomain(ctx); err != nil {
return err
}
default:
return errors.New("unsupported resource type")
}
return nil
}
func (d *TencentCLBDeployer) createSdkClients(secretId, secretKey, region string) (*tencentCLBDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
clbClient, err := tcClb.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentCLBDeployerSdkClients{
ssl: sslClient,
clb: clbClient,
}, nil
}
func (d *TencentCLBDeployer) deployToInstanceUseSsl(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
if tcListenerId == "" {
return errors.New("`listenerId` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 证书部署到 CLB 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("clb")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
if tcDomain == "" {
// 未开启 SNI只需指定到监听器
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", tcLoadbalancerId, tcListenerId)})
} else {
// 开启 SNI需指定到域名支持泛域名
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", tcLoadbalancerId, tcListenerId, tcDomain)})
}
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
func (d *TencentCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerIds := make([]string, 0)
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
// 查询负载均衡器详细信息
// REF: https://cloud.tencent.com/document/api/214/46916
describeLoadBalancersDetailReq := tcClb.NewDescribeLoadBalancersDetailRequest()
describeLoadBalancersDetailResp, err := d.sdkClients.clb.DescribeLoadBalancersDetail(describeLoadBalancersDetailReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeLoadBalancersDetail'")
}
d.infos = append(d.infos, toStr("已查询到负载均衡详细信息", describeLoadBalancersDetailResp))
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
} else {
if describeListenersResp.Response.Listeners != nil {
for _, listener := range describeListenersResp.Response.Listeners {
if listener.Protocol == nil || (*listener.Protocol != "HTTPS" && *listener.Protocol != "TCP_SSL" && *listener.Protocol != "QUIC") {
continue
}
tcListenerIds = append(tcListenerIds, *listener.ListenerId)
}
}
}
d.infos = append(d.infos, toStr("已查询到负载均衡器下的监听器", tcListenerIds))
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听器证书
var errs []error
for _, tcListenerId := range tcListenerIds {
if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *TencentCLBDeployer) deployToListener(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
if tcListenerId == "" {
return errors.New("`listenerId` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听器证书
if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *TencentCLBDeployer) deployToRuleDomain(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
if tcListenerId == "" {
return errors.New("`listenerId` is required")
}
if tcDomain == "" {
return errors.New("`domain` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 修改负载均衡七层监听器转发规则的域名级别属性
// REF: https://cloud.tencent.com/document/api/214/38092
modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest()
modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
modifyDomainAttributesReq.ListenerId = common.StringPtr(tcListenerId)
modifyDomainAttributesReq.Domain = common.StringPtr(tcDomain)
modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{
SSLMode: common.StringPtr("UNIDIRECTIONAL"),
CertId: common.StringPtr(upres.CertId),
}
modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'")
}
d.infos = append(d.infos, toStr("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response))
return nil
}
func (d *TencentCLBDeployer) modifyListenerCertificate(ctx context.Context, tcLoadbalancerId, tcListenerId, tcCertId string) error {
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
describeListenersReq.ListenerIds = common.StringPtrs([]string{tcListenerId})
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
}
if len(describeListenersResp.Response.Listeners) == 0 {
d.infos = append(d.infos, toStr("未找到监听器", nil))
return errors.New("listener not found")
}
d.infos = append(d.infos, toStr("已查询到监听器属性", describeListenersResp.Response))
// 修改监听器属性
// REF: https://cloud.tencent.com/document/product/214/30681
modifyListenerReq := tcClb.NewModifyListenerRequest()
modifyListenerReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
modifyListenerReq.ListenerId = common.StringPtr(tcListenerId)
modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(tcCertId)}
if describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil {
modifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode
modifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId
} else {
modifyListenerReq.Certificate.SSLMode = common.StringPtr("UNIDIRECTIONAL")
}
modifyListenerResp, err := d.sdkClients.clb.ModifyListener(modifyListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyListener'")
}
d.infos = append(d.infos, toStr("已修改监听器属性", modifyListenerResp.Response))
return nil
}

View File

@@ -1,106 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCOSDeployer struct {
option *DeployerOption
infos []string
sdkClient *tcSsl.Client
sslUploader uploader.Uploader
}
func NewTencentCOSDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&TencentCOSDeployer{}).createSdkClient(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCOSDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *TencentCOSDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentCOSDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentCOSDeployer) Deploy(ctx context.Context) error {
tcRegion := d.option.DeployConfig.GetConfigAsString("region")
tcBucket := d.option.DeployConfig.GetConfigAsString("bucket")
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
if tcBucket == "" {
return errors.New("`bucket` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 证书部署到 COS 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("cos")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", tcRegion, tcBucket, tcDomain)})
deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey string) (*tcSsl.Client, error) {
credential := common.NewCredential(secretId, secretKey)
client, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -1,154 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentECDNDeployer struct {
option *DeployerOption
infos []string
sdkClients *tencentECDNDeployerSdkClients
sslUploader uploader.Uploader
}
type tencentECDNDeployerSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
clients, err := (&TencentECDNDeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentECDNDeployer{
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentECDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentECDNDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentECDNDeployer) Deploy(ctx context.Context) error {
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 获取待部署的 ECDN 实例
// 如果是泛域名,根据证书匹配 ECDN 实例
aliInstanceIds := make([]string, 0)
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return err
}
aliInstanceIds = domains
} else {
aliInstanceIds = append(aliInstanceIds, domain)
}
if len(aliInstanceIds) == 0 {
d.infos = append(d.infos, "没有要部署的 ECDN 实例")
return nil
}
// 证书部署到 ECDN 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(aliInstanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
func (d *TencentECDNDeployer) createSdkClients(secretId, secretKey string) (*tencentECDNDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentECDNDeployerSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}
func (d *TencentECDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(tcCertId)
describeCertDomainsReq.Product = common.StringPtr("ecdn")
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
}
domains := make([]string, 0)
if describeCertDomainsResp.Response.Domains == nil {
for _, domain := range describeCertDomainsResp.Response.Domains {
domains = append(domains, *domain)
}
}
return domains, nil
}

View File

@@ -1,119 +0,0 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcTeo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentTEODeployer struct {
option *DeployerOption
infos []string
sdkClients *tencentTEODeployerSdkClients
sslUploader uploader.Uploader
}
type tencentTEODeployerSdkClients struct {
ssl *tcSsl.Client
teo *tcTeo.Client
}
func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
clients, err := (&TencentTEODeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentTEODeployer{
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
func (d *TencentTEODeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentTEODeployer) GetInfos() []string {
return d.infos
}
func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
tcZoneId := d.option.DeployConfig.GetConfigAsString("zoneId")
if tcZoneId == "" {
return xerrors.New("`zoneId` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 配置域名证书
// REF: https://cloud.tencent.com/document/product/1552/80764
modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(tcZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
modifyHostsCertificateReq.Hosts = common.StringPtrs(strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"), "\n"))
modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'")
}
d.infos = append(d.infos, toStr("已配置域名证书", modifyHostsCertificateResp.Response))
return nil
}
func (d *TencentTEODeployer) createSdkClients(secretId, secretKey string) (*tencentTEODeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentTEODeployerSdkClients{
ssl: sslClient,
teo: teoClient,
}, nil
}

View File

@@ -1,66 +0,0 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
xhttp "github.com/usual2970/certimate/internal/utils/http"
)
type WebhookDeployer struct {
option *DeployerOption
infos []string
}
func NewWebhookDeployer(option *DeployerOption) (Deployer, error) {
return &WebhookDeployer{
option: option,
infos: make([]string, 0),
}, nil
}
func (d *WebhookDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *WebhookDeployer) GetInfos() []string {
return d.infos
}
type webhookData struct {
Domain string `json:"domain"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
Variables map[string]string `json:"variables"`
}
func (d *WebhookDeployer) Deploy(ctx context.Context) error {
access := &domain.WebhookAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return xerrors.Wrap(err, "failed to get access")
}
data := &webhookData{
Domain: d.option.Domain,
Certificate: d.option.Certificate.Certificate,
PrivateKey: d.option.Certificate.PrivateKey,
Variables: d.option.DeployConfig.GetConfigAsVariables(),
}
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 xerrors.Wrap(err, "failed to send webhook request")
}
d.infos = append(d.infos, toStr("Webhook Response", string(resp)))
return nil
}

View File

@@ -1,83 +1,414 @@
package domain
type AliyunAccess struct {
import (
"time"
)
const CollectionNameAccess = "access"
type Access struct {
Meta
Name string `json:"name" db:"name"`
Provider string `json:"provider" db:"provider"`
Config map[string]any `json:"config" db:"config"`
Reserve string `json:"reserve,omitempty" db:"reserve"`
DeletedAt *time.Time `json:"deleted" db:"deleted"`
}
type AccessConfigFor1Panel struct {
ServerUrl string `json:"serverUrl"`
ApiVersion string `json:"apiVersion"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForACMECA struct {
Endpoint string `json:"endpoint"`
EabKid string `json:"eabKid,omitempty"`
EabHmacKey string `json:"eabHmacKey,omitempty"`
}
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"`
ResourceGroupId string `json:"resourceGroupId,omitempty"`
}
type AccessConfigForAPISIX struct {
ServerUrl string `json:"serverUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForAWS struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type AccessConfigForAzure struct {
TenantId string `json:"tenantId"`
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
CloudName string `json:"cloudName,omitempty"`
}
type AccessConfigForBaiduCloud struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type AccessConfigForBaishan struct {
ApiToken string `json:"apiToken"`
}
type AccessConfigForBaotaPanel struct {
ServerUrl string `json:"serverUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForBaotaWAF struct {
ServerUrl string `json:"serverUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForBytePlus struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type AccessConfigForBunny struct {
ApiKey string `json:"apiKey"`
}
type AccessConfigForCacheFly struct {
ApiToken string `json:"apiToken"`
}
type AccessConfigForCdnfly struct {
ServerUrl string `json:"serverUrl"`
ApiKey string `json:"apiKey"`
ApiSecret string `json:"apiSecret"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForCloudflare struct {
DnsApiToken string `json:"dnsApiToken"`
ZoneApiToken string `json:"zoneApiToken,omitempty"`
}
type AccessConfigForClouDNS struct {
AuthId string `json:"authId"`
AuthPassword string `json:"authPassword"`
}
type AccessConfigForCMCCCloud struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type TencentAccess struct {
SecretId string `json:"secretId"`
type AccessConfigForConstellix struct {
ApiKey string `json:"apiKey"`
SecretKey string `json:"secretKey"`
}
type HuaweiCloudAccess struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region"`
}
type BaiduCloudAccess struct {
type AccessConfigForCTCCCloud struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type AwsAccess struct {
Region string `json:"region"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
HostedZoneId string `json:"hostedZoneId"`
type AccessConfigForDeSEC struct {
Token string `json:"token"`
}
type CloudflareAccess struct {
DnsApiToken string `json:"dnsApiToken"`
type AccessConfigForDigitalOcean struct {
AccessToken string `json:"accessToken"`
}
type QiniuAccess struct {
type AccessConfigForDingTalkBot struct {
WebhookUrl string `json:"webhookUrl"`
Secret string `json:"secret"`
}
type AccessConfigForDiscordBot struct {
BotToken string `json:"botToken"`
DefaultChannelId string `json:"defaultChannelId,omitempty"`
}
type AccessConfigForDNSLA struct {
ApiId string `json:"apiId"`
ApiSecret string `json:"apiSecret"`
}
type AccessConfigForDogeCloud struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type DogeCloudAccess struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
type AccessConfigForDuckDNS struct {
Token string `json:"token"`
}
type NameSiloAccess struct {
ApiKey string `json:"apiKey"`
type AccessConfigForDynv6 struct {
HttpToken string `json:"httpToken"`
}
type GodaddyAccess struct {
type AccessConfigForEdgio struct {
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}
type AccessConfigForEmail struct {
SmtpHost string `json:"smtpHost"`
SmtpPort int32 `json:"smtpPort"`
SmtpTls bool `json:"smtpTls"`
Username string `json:"username"`
Password string `json:"password"`
DefaultSenderAddress string `json:"defaultSenderAddress,omitempty"`
DefaultSenderName string `json:"defaultSenderName,omitempty"`
DefaultReceiverAddress string `json:"defaultReceiverAddress,omitempty"`
}
type AccessConfigForFlexCDN struct {
ServerUrl string `json:"serverUrl"`
ApiRole string `json:"apiRole"`
AccessKeyId string `json:"accessKeyId"`
AccessKey string `json:"accessKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
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 PdnsAccess struct {
ApiUrl string `json:"apiUrl"`
type AccessConfigForGoEdge struct {
ServerUrl string `json:"serverUrl"`
ApiRole string `json:"apiRole"`
AccessKeyId string `json:"accessKeyId"`
AccessKey string `json:"accessKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForGoogleTrustServices struct {
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
}
type AccessConfigForHetzner struct {
ApiToken string `json:"apiToken"`
}
type AccessConfigForHuaweiCloud struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"`
}
type AccessConfigForJDCloud struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type AccessConfigForKubernetes struct {
KubeConfig string `json:"kubeConfig,omitempty"`
}
type AccessConfigForLarkBot struct {
WebhookUrl string `json:"webhookUrl"`
}
type AccessConfigForLeCDN struct {
ServerUrl string `json:"serverUrl"`
ApiVersion string `json:"apiVersion"`
ApiRole string `json:"apiRole"`
Username string `json:"username"`
Password string `json:"password"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForMattermost struct {
ServerUrl string `json:"serverUrl"`
Username string `json:"username"`
Password string `json:"password"`
DefaultChannelId string `json:"defaultChannelId,omitempty"`
}
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 HttpreqAccess struct {
Endpoint string `json:"endpoint"`
Mode string `json:"mode"`
type AccessConfigForNetcup struct {
CustomerNumber string `json:"customerNumber"`
ApiKey string `json:"apiKey"`
ApiPassword string `json:"apiPassword"`
}
type AccessConfigForNetlify struct {
ApiToken string `json:"apiToken"`
}
type AccessConfigForNS1 struct {
ApiKey string `json:"apiKey"`
}
type AccessConfigForPorkbun struct {
ApiKey string `json:"apiKey"`
SecretApiKey string `json:"secretApiKey"`
}
type AccessConfigForPowerDNS struct {
ServerUrl string `json:"serverUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForProxmoxVE struct {
ServerUrl string `json:"serverUrl"`
ApiToken string `json:"apiToken"`
ApiTokenSecret string `json:"apiTokenSecret,omitempty"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForQiniu struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type AccessConfigForRainYun struct {
ApiKey string `json:"apiKey"`
}
type AccessConfigForRatPanel struct {
ServerUrl string `json:"serverUrl"`
AccessTokenId int32 `json:"accessTokenId"`
AccessToken string `json:"accessToken"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForSafeLine struct {
ServerUrl string `json:"serverUrl"`
ApiToken string `json:"apiToken"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForSlackBot struct {
BotToken string `json:"botToken"`
DefaultChannelId string `json:"defaultChannelId,omitempty"`
}
type AccessConfigForSSH struct {
Host string `json:"host"`
Port int32 `json:"port"`
AuthMethod string `json:"authMethod,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Key string `json:"key,omitempty"`
KeyPassphrase string `json:"keyPassphrase,omitempty"`
JumpServers []struct {
Host string `json:"host"`
Port int32 `json:"port"`
AuthMethod string `json:"authMethod,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Key string `json:"key,omitempty"`
KeyPassphrase string `json:"keyPassphrase,omitempty"`
} `json:"jumpServers,omitempty"`
}
type AccessConfigForSSLCom struct {
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
}
type AccessConfigForTelegramBot struct {
BotToken string `json:"botToken"`
DefaultChatId int64 `json:"defaultChatId,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 AccessConfigForUniCloud struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LocalAccess struct{}
type SSHAccess struct {
Host string `json:"host"`
Port string `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Key string `json:"key"`
KeyPassphrase string `json:"keyPassphrase"`
type AccessConfigForUpyun struct {
Username string `json:"username"`
Password string `json:"password"`
}
type WebhookAccess struct {
Url string `json:"url"`
type AccessConfigForVercel struct {
ApiAccessToken string `json:"apiAccessToken"`
TeamId string `json:"teamId,omitempty"`
}
type KubernetesAccess struct {
KubeConfig string `json:"kubeConfig"`
type AccessConfigForVolcEngine struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type AccessConfigForWangsu struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
ApiKey string `json:"apiKey"`
}
type AccessConfigForWebhook struct {
Url string `json:"url"`
Method string `json:"method,omitempty"`
HeadersString string `json:"headers,omitempty"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
DefaultDataForDeployment string `json:"defaultDataForDeployment,omitempty"`
DefaultDataForNotification string `json:"defaultDataForNotification,omitempty"`
}
type AccessConfigForWeComBot struct {
WebhookUrl string `json:"webhookUrl"`
}
type AccessConfigForWestcn struct {
Username string `json:"username"`
ApiPassword string `json:"apiPassword"`
}
type AccessConfigForZeroSSL struct {
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
}

View 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"`
}

View File

@@ -1,17 +0,0 @@
package domain
import (
"time"
"github.com/go-acme/lego/v4/registration"
)
type AcmeAccount struct {
Id string
Ca string
Email string
Resource *registration.Resource
Key string
Created time.Time
Updated time.Time
}

View File

@@ -0,0 +1,137 @@
package domain
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"fmt"
"strings"
"time"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
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"`
IssuerOrg string `json:"issuerOrg" db:"issuerOrg"`
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"`
ACMERenewed bool `json:"acmeRenewed" db:"acmeRenewed"`
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.IssuerOrg = strings.Join(certX509.Issuer.Organization, ";")
c.EffectAt = certX509.NotBefore
c.ExpireAt = certX509.NotAfter
switch certX509.PublicKeyAlgorithm {
case x509.RSA:
{
len := 0
if pubkey, ok := certX509.PublicKey.(*rsa.PublicKey); ok {
len = pubkey.N.BitLen()
}
switch len {
case 0:
c.KeyAlgorithm = CertificateKeyAlgorithmType("RSA")
case 2048:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA2048
case 3072:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA3072
case 4096:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA4096
case 8192:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA8192
default:
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("RSA%d", len))
}
}
case x509.ECDSA:
{
len := 0
if pubkey, ok := certX509.PublicKey.(*ecdsa.PublicKey); ok {
if pubkey.Curve != nil && pubkey.Curve.Params() != nil {
len = pubkey.Curve.Params().BitSize
}
}
switch len {
case 0:
c.KeyAlgorithm = CertificateKeyAlgorithmType("EC")
case 256:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC256
case 384:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC384
case 521:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC512
default:
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("EC%d", len))
}
}
case x509.Ed25519:
{
c.KeyAlgorithm = CertificateKeyAlgorithmType("ED25519")
}
default:
c.KeyAlgorithm = CertificateKeyAlgorithmType("")
}
return c
}
func (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate {
c.Certificate = certPEM
c.PrivateKey = privkeyPEM
_, issuerCertPEM, _ := certutil.ExtractCertificatesFromPEM(certPEM)
c.IssuerCertificate = issuerCertPEM
certX509, _ := certutil.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")
)

View File

@@ -1,172 +0,0 @@
package domain
import (
"encoding/json"
"strings"
)
type ApplyConfig struct {
Email string `json:"email"`
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
}
type DeployConfig struct {
Id string `json:"id"`
Access string `json:"access"`
Type string `json:"type"`
Config map[string]any `json:"config"`
}
// 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
func (dc *DeployConfig) GetConfigAsString(key string) string {
return dc.GetConfigOrDefaultAsString(key, "")
}
// 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
if dc.Config == nil {
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(string); ok {
return result
}
}
return defaultValue
}
// 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
return dc.GetConfigOrDefaultAsInt32(key, 0)
}
// 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
if dc.Config == nil {
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(int32); ok {
return result
}
}
return defaultValue
}
// 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
return dc.GetConfigOrDefaultAsBool(key, false)
}
// 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
if dc.Config == nil {
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(bool); ok {
return result
}
}
return defaultValue
}
// 以变量字典形式获取配置项。
//
// 出参:
// - 变量字典。
func (dc *DeployConfig) GetConfigAsVariables() map[string]string {
rs := make(map[string]string)
if dc.Config != nil {
value, ok := dc.Config["variables"]
if !ok {
return rs
}
kvs := make([]KV, 0)
bts, _ := json.Marshal(value)
if err := json.Unmarshal(bts, &kvs); err != nil {
return rs
}
for _, kv := range kvs {
rs[kv.Key] = kv.Value
}
}
return rs
}
// GetDomain returns the domain from the deploy config
// if the domain is a wildcard domain, and wildcard is true, return the wildcard domain
func (dc *DeployConfig) GetDomain(wildcard ...bool) string {
val := dc.GetConfigAsString("domain")
if val == "" {
return ""
}
if !strings.HasPrefix(val, "*") {
return val
}
if len(wildcard) > 0 && wildcard[0] {
return val
}
return strings.TrimPrefix(val, "*")
}
type KV struct {
Key string `json:"key"`
Value string `json:"value"`
}

View 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"`
}

View File

@@ -0,0 +1,7 @@
package dtos
import "github.com/usual2970/certimate/internal/domain"
type NotifyTestPushReq struct {
Channel domain.NotifyChannelType `json:"channel"`
}

View 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:"-"`
}

View File

@@ -1,23 +0,0 @@
package domain
var ErrAuthFailed = NewXError(4999, "auth failed")
type XError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func NewXError(code int, msg string) *XError {
return &XError{code, msg}
}
func (e *XError) Error() string {
return e.Msg
}
func (e *XError) GetCode() int {
if e.Code == 0 {
return 100
}
return e.Code
}

30
internal/domain/error.go Normal file
View 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
}

View File

@@ -0,0 +1,630 @@
package expr
import (
"encoding/json"
"fmt"
"strconv"
)
type (
ExprType string
ExprComparisonOperator string
ExprLogicalOperator string
ExprValueType string
)
const (
GreaterThan ExprComparisonOperator = "gt"
GreaterOrEqual ExprComparisonOperator = "gte"
LessThan ExprComparisonOperator = "lt"
LessOrEqual ExprComparisonOperator = "lte"
Equal ExprComparisonOperator = "eq"
NotEqual ExprComparisonOperator = "neq"
And ExprLogicalOperator = "and"
Or ExprLogicalOperator = "or"
Not ExprLogicalOperator = "not"
Number ExprValueType = "number"
String ExprValueType = "string"
Boolean ExprValueType = "boolean"
ConstantExprType ExprType = "const"
VariantExprType ExprType = "var"
ComparisonExprType ExprType = "comparison"
LogicalExprType ExprType = "logical"
NotExprType ExprType = "not"
)
type EvalResult struct {
Type ExprValueType
Value any
}
func (e *EvalResult) GetFloat64() (float64, error) {
if e.Type != Number {
return 0, fmt.Errorf("type mismatch: %s", e.Type)
}
stringValue, ok := e.Value.(string)
if !ok {
return 0, fmt.Errorf("value is not a string: %v", e.Value)
}
floatValue, err := strconv.ParseFloat(stringValue, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse float64: %v", err)
}
return floatValue, nil
}
func (e *EvalResult) GetBool() (bool, error) {
if e.Type != Boolean {
return false, fmt.Errorf("type mismatch: %s", e.Type)
}
strValue, ok := e.Value.(string)
if ok {
if strValue == "true" {
return true, nil
} else if strValue == "false" {
return false, nil
}
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
}
boolValue, ok := e.Value.(bool)
if !ok {
return false, fmt.Errorf("value is not a boolean: %v", e.Value)
}
return boolValue, nil
}
func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case String:
return &EvalResult{
Type: Boolean,
Value: e.Value.(string) > other.Value.(string),
}, nil
case Number:
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left > right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case String:
return &EvalResult{
Type: Boolean,
Value: e.Value.(string) >= other.Value.(string),
}, nil
case Number:
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left >= right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case String:
return &EvalResult{
Type: Boolean,
Value: e.Value.(string) < other.Value.(string),
}, nil
case Number:
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left < right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case String:
return &EvalResult{
Type: Boolean,
Value: e.Value.(string) <= other.Value.(string),
}, nil
case Number:
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left <= right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case String:
return &EvalResult{
Type: Boolean,
Value: e.Value.(string) == other.Value.(string),
}, nil
case Number:
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left == right,
}, nil
case Boolean:
left, err := e.GetBool()
if err != nil {
return nil, err
}
right, err := other.GetBool()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left == right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case String:
return &EvalResult{
Type: Boolean,
Value: e.Value.(string) != other.Value.(string),
}, nil
case Number:
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left != right,
}, nil
case Boolean:
left, err := e.GetBool()
if err != nil {
return nil, err
}
right, err := other.GetBool()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left != right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case Boolean:
left, err := e.GetBool()
if err != nil {
return nil, err
}
right, err := other.GetBool()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left && right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case Boolean:
left, err := e.GetBool()
if err != nil {
return nil, err
}
right, err := other.GetBool()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: left || right,
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s", e.Type)
}
}
func (e *EvalResult) Not() (*EvalResult, error) {
if e.Type != Boolean {
return nil, fmt.Errorf("type mismatch: %s", e.Type)
}
boolValue, err := e.GetBool()
if err != nil {
return nil, err
}
return &EvalResult{
Type: Boolean,
Value: !boolValue,
}, nil
}
type Expr interface {
GetType() ExprType
Eval(variables map[string]map[string]any) (*EvalResult, error)
}
type ExprValueSelector struct {
Id string `json:"id"`
Name string `json:"name"`
Type ExprValueType `json:"type"`
}
type ConstantExpr struct {
Type ExprType `json:"type"`
Value string `json:"value"`
ValueType ExprValueType `json:"valueType"`
}
func (c ConstantExpr) GetType() ExprType { return c.Type }
func (c ConstantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
return &EvalResult{
Type: c.ValueType,
Value: c.Value,
}, nil
}
type VariantExpr struct {
Type ExprType `json:"type"`
Selector ExprValueSelector `json:"selector"`
}
func (v VariantExpr) GetType() ExprType { return v.Type }
func (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
if v.Selector.Id == "" {
return nil, fmt.Errorf("node id is empty")
}
if v.Selector.Name == "" {
return nil, fmt.Errorf("name is empty")
}
if _, ok := variables[v.Selector.Id]; !ok {
return nil, fmt.Errorf("node %s not found", v.Selector.Id)
}
if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok {
return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id)
}
return &EvalResult{
Type: v.Selector.Type,
Value: variables[v.Selector.Id][v.Selector.Name],
}, nil
}
type ComparisonExpr struct {
Type ExprType `json:"type"` // compare
Operator ExprComparisonOperator `json:"operator"`
Left Expr `json:"left"`
Right Expr `json:"right"`
}
func (c ComparisonExpr) GetType() ExprType { return c.Type }
func (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
left, err := c.Left.Eval(variables)
if err != nil {
return nil, err
}
right, err := c.Right.Eval(variables)
if err != nil {
return nil, err
}
switch c.Operator {
case GreaterThan:
return left.GreaterThan(right)
case LessThan:
return left.LessThan(right)
case GreaterOrEqual:
return left.GreaterOrEqual(right)
case LessOrEqual:
return left.LessOrEqual(right)
case Equal:
return left.Equal(right)
case NotEqual:
return left.NotEqual(right)
default:
return nil, fmt.Errorf("unknown expression operator: %s", c.Operator)
}
}
type LogicalExpr struct {
Type ExprType `json:"type"` // logical
Operator ExprLogicalOperator `json:"operator"`
Left Expr `json:"left"`
Right Expr `json:"right"`
}
func (l LogicalExpr) GetType() ExprType { return l.Type }
func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
left, err := l.Left.Eval(variables)
if err != nil {
return nil, err
}
right, err := l.Right.Eval(variables)
if err != nil {
return nil, err
}
switch l.Operator {
case And:
return left.And(right)
case Or:
return left.Or(right)
default:
return nil, fmt.Errorf("unknown expression operator: %s", l.Operator)
}
}
type NotExpr struct {
Type ExprType `json:"type"` // not
Expr Expr `json:"expr"`
}
func (n NotExpr) GetType() ExprType { return n.Type }
func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
inner, err := n.Expr.Eval(variables)
if err != nil {
return nil, err
}
return inner.Not()
}
type rawExpr struct {
Type ExprType `json:"type"`
}
func MarshalExpr(e Expr) ([]byte, error) {
return json.Marshal(e)
}
func UnmarshalExpr(data []byte) (Expr, error) {
var typ rawExpr
if err := json.Unmarshal(data, &typ); err != nil {
return nil, err
}
switch typ.Type {
case ConstantExprType:
var e ConstantExpr
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e, nil
case VariantExprType:
var e VariantExpr
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e, nil
case ComparisonExprType:
var e ComparisonExprRaw
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e.ToComparisonExpr()
case LogicalExprType:
var e LogicalExprRaw
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e.ToLogicalExpr()
case NotExprType:
var e NotExprRaw
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e.ToNotExpr()
default:
return nil, fmt.Errorf("unknown expression type: %s", typ.Type)
}
}
type ComparisonExprRaw struct {
Type ExprType `json:"type"`
Operator ExprComparisonOperator `json:"operator"`
Left json.RawMessage `json:"left"`
Right json.RawMessage `json:"right"`
}
func (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) {
leftExpr, err := UnmarshalExpr(r.Left)
if err != nil {
return ComparisonExpr{}, err
}
rightExpr, err := UnmarshalExpr(r.Right)
if err != nil {
return ComparisonExpr{}, err
}
return ComparisonExpr{
Type: r.Type,
Operator: r.Operator,
Left: leftExpr,
Right: rightExpr,
}, nil
}
type LogicalExprRaw struct {
Type ExprType `json:"type"`
Operator ExprLogicalOperator `json:"operator"`
Left json.RawMessage `json:"left"`
Right json.RawMessage `json:"right"`
}
func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) {
left, err := UnmarshalExpr(r.Left)
if err != nil {
return LogicalExpr{}, err
}
right, err := UnmarshalExpr(r.Right)
if err != nil {
return LogicalExpr{}, err
}
return LogicalExpr{
Type: r.Type,
Operator: r.Operator,
Left: left,
Right: right,
}, nil
}
type NotExprRaw struct {
Type ExprType `json:"type"`
Expr json.RawMessage `json:"expr"`
}
func (r NotExprRaw) ToNotExpr() (NotExpr, error) {
inner, err := UnmarshalExpr(r.Expr)
if err != nil {
return NotExpr{}, err
}
return NotExpr{
Type: r.Type,
Expr: inner,
}, nil
}

View File

@@ -0,0 +1,127 @@
package expr
import (
"testing"
)
func TestLogicalEval(t *testing.T) {
// 测试逻辑表达式 and
logicalExpr := LogicalExpr{
Left: ConstantExpr{
Type: "const",
Value: "true",
ValueType: "boolean",
},
Operator: And,
Right: ConstantExpr{
Type: "const",
Value: "true",
ValueType: "boolean",
},
}
result, err := logicalExpr.Eval(nil)
if err != nil {
t.Errorf("failed to evaluate logical expression: %v", err)
}
if result.Value != true {
t.Errorf("expected true, got %v", result)
}
// 测试逻辑表达式 or
orExpr := LogicalExpr{
Left: ConstantExpr{
Type: "const",
Value: "true",
ValueType: "boolean",
},
Operator: Or,
Right: ConstantExpr{
Type: "const",
Value: "true",
ValueType: "boolean",
},
}
result, err = orExpr.Eval(nil)
if err != nil {
t.Errorf("failed to evaluate logical expression: %v", err)
}
if result.Value != true {
t.Errorf("expected true, got %v", result)
}
}
func TestUnmarshalExpr(t *testing.T) {
type args struct {
data []byte
}
tests := []struct {
name string
args args
want Expr
wantErr bool
}{
{
name: "test1",
args: args{
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := UnmarshalExpr(tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got == nil {
t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want)
return
}
})
}
}
func TestExpr_Eval(t *testing.T) {
type args struct {
variables map[string]map[string]any
data []byte
}
tests := []struct {
name string
args args
want *EvalResult
wantErr bool
}{
{
name: "test1",
args: args{
variables: map[string]map[string]any{
"ODnYSOXB6HQP2_vz6JcZE": {
"certificate.validity": true,
"certificate.daysLeft": 2,
},
},
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := UnmarshalExpr(tt.args.data)
if err != nil {
t.Errorf("UnmarshalExpr() error = %v", err)
return
}
got, err := c.Eval(tt.args.variables)
t.Log("got:", got)
if (err != nil) != tt.wantErr {
t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got.Value != true {
t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true)
}
})
}
}

9
internal/domain/meta.go Normal file
View 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"`
}

View File

@@ -1,15 +1,25 @@
package domain
const (
NotifyChannelDingtalk = "dingtalk"
NotifyChannelWebhook = "webhook"
NotifyChannelTelegram = "telegram"
NotifyChannelLark = "lark"
NotifyChannelServerChan = "serverchan"
NotifyChannelMail = "mail"
NotifyChannelBark = "bark"
)
type NotifyChannelType string
type NotifyTestPushReq struct {
Channel string `json:"channel"`
}
/*
消息通知渠道常量值。
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
// Deprecated: v0.4.x 将废弃
const (
NotifyChannelTypeBark = NotifyChannelType("bark")
NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk")
NotifyChannelTypeEmail = NotifyChannelType("email")
NotifyChannelTypeGotify = NotifyChannelType("gotify")
NotifyChannelTypeLark = NotifyChannelType("lark")
NotifyChannelTypeMattermost = NotifyChannelType("mattermost")
NotifyChannelTypePushover = NotifyChannelType("pushover")
NotifyChannelTypePushPlus = NotifyChannelType("pushplus")
NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
NotifyChannelTypeTelegram = NotifyChannelType("telegram")
NotifyChannelTypeWebhook = NotifyChannelType("webhook")
NotifyChannelTypeWeCom = NotifyChannelType("wecom")
)

300
internal/domain/provider.go Normal file
View File

@@ -0,0 +1,300 @@
package domain
type AccessProviderType string
/*
授权提供商类型常量值。
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
AccessProviderType1Panel = AccessProviderType("1panel")
AccessProviderTypeACMECA = AccessProviderType("acmeca")
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai预留
AccessProviderTypeAliyun = AccessProviderType("aliyun")
AccessProviderTypeAPISIX = AccessProviderType("apisix")
AccessProviderTypeAWS = AccessProviderType("aws")
AccessProviderTypeAzure = AccessProviderType("azure")
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
AccessProviderTypeBaishan = AccessProviderType("baishan")
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
AccessProviderTypeBaotaWAF = AccessProviderType("baotawaf")
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
AccessProviderTypeBunny = AccessProviderType("bunny")
AccessProviderTypeBuypass = AccessProviderType("buypass")
AccessProviderTypeCacheFly = AccessProviderType("cachefly")
AccessProviderTypeCdnfly = AccessProviderType("cdnfly")
AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
AccessProviderTypeConstellix = AccessProviderType("constellix")
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud")
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留)
AccessProviderTypeDeSEC = AccessProviderType("desec")
AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean")
AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot")
AccessProviderTypeDiscordBot = AccessProviderType("discordbot")
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
AccessProviderTypeDuckDNS = AccessProviderType("duckdns")
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
AccessProviderTypeEdgio = AccessProviderType("edgio")
AccessProviderTypeEmail = AccessProviderType("email")
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly预留
AccessProviderTypeFlexCDN = AccessProviderType("flexcdn")
AccessProviderTypeGname = AccessProviderType("gname")
AccessProviderTypeGcore = AccessProviderType("gcore")
AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
AccessProviderTypeGoEdge = AccessProviderType("goedge")
AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices")
AccessProviderTypeHetzner = AccessProviderType("hetzner")
AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud")
AccessProviderTypeJDCloud = AccessProviderType("jdcloud")
AccessProviderTypeKubernetes = AccessProviderType("k8s")
AccessProviderTypeLarkBot = AccessProviderType("larkbot")
AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt")
AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging")
AccessProviderTypeLeCDN = AccessProviderType("lecdn")
AccessProviderTypeLocal = AccessProviderType("local")
AccessProviderTypeMattermost = AccessProviderType("mattermost")
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
AccessProviderTypeNetcup = AccessProviderType("netcup")
AccessProviderTypeNetlify = AccessProviderType("netlify")
AccessProviderTypeNS1 = AccessProviderType("ns1")
AccessProviderTypePorkbun = AccessProviderType("porkbun")
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
AccessProviderTypeProxmoxVE = AccessProviderType("proxmoxve")
AccessProviderTypeQiniu = AccessProviderType("qiniu")
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
AccessProviderTypeRainYun = AccessProviderType("rainyun")
AccessProviderTypeRatPanel = AccessProviderType("ratpanel")
AccessProviderTypeSafeLine = AccessProviderType("safeline")
AccessProviderTypeSlackBot = AccessProviderType("slackbot")
AccessProviderTypeSSH = AccessProviderType("ssh")
AccessProviderTypeSSLCOM = AccessProviderType("sslcom")
AccessProviderTypeTelegramBot = AccessProviderType("telegrambot")
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
AccessProviderTypeUCloud = AccessProviderType("ucloud")
AccessProviderTypeUniCloud = AccessProviderType("unicloud")
AccessProviderTypeUpyun = AccessProviderType("upyun")
AccessProviderTypeVercel = AccessProviderType("vercel")
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
AccessProviderTypeWangsu = AccessProviderType("wangsu")
AccessProviderTypeWebhook = AccessProviderType("webhook")
AccessProviderTypeWeComBot = AccessProviderType("wecombot")
AccessProviderTypeWestcn = AccessProviderType("westcn")
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
)
type CAProviderType string
/*
证书颁发机构提供商常量值。
短横线前的部分始终等于授权提供商类型。
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
CAProviderTypeACMECA = CAProviderType(AccessProviderTypeACMECA)
CAProviderTypeBuypass = CAProviderType(AccessProviderTypeBuypass)
CAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices)
CAProviderTypeLetsEncrypt = CAProviderType(AccessProviderTypeLetsEncrypt)
CAProviderTypeLetsEncryptStaging = CAProviderType(AccessProviderTypeLetsEncryptStaging)
CAProviderTypeSSLCom = CAProviderType(AccessProviderTypeSSLCOM)
CAProviderTypeZeroSSL = CAProviderType(AccessProviderTypeZeroSSL)
)
type ACMEDns01ProviderType string
/*
ACME DNS-01 提供商常量值。
短横线前的部分始终等于授权提供商类型。
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa")
ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53]
ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53")
ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure]
ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns")
ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]
ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns")
ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny)
ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare)
ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS]
ACMEDns01ProviderTypeCMCCCloudDNS = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + "-dns")
ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix)
ACMEDns01ProviderTypeCTCCCloud = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS]
ACMEDns01ProviderTypeCTCCCloudSmartDNS = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud + "-smartdns")
ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname)
ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)
ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner)
ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]
ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns")
ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS]
ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns")
ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr")
ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
)
type DeploymentProviderType string
/*
部署证书主机提供商常量值。
短横线前的部分始终等于授权提供商类型。
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
DeploymentProviderType1PanelConsole = DeploymentProviderType(AccessProviderType1Panel + "-console")
DeploymentProviderType1PanelSite = DeploymentProviderType(AccessProviderType1Panel + "-site")
DeploymentProviderTypeAliyunALB = DeploymentProviderType(AccessProviderTypeAliyun + "-alb")
DeploymentProviderTypeAliyunAPIGW = DeploymentProviderType(AccessProviderTypeAliyun + "-apigw")
DeploymentProviderTypeAliyunCAS = DeploymentProviderType(AccessProviderTypeAliyun + "-cas")
DeploymentProviderTypeAliyunCASDeploy = DeploymentProviderType(AccessProviderTypeAliyun + "-casdeploy")
DeploymentProviderTypeAliyunCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-cdn")
DeploymentProviderTypeAliyunCLB = DeploymentProviderType(AccessProviderTypeAliyun + "-clb")
DeploymentProviderTypeAliyunDCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-dcdn")
DeploymentProviderTypeAliyunDDoS = DeploymentProviderType(AccessProviderTypeAliyun + "-ddos")
DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa")
DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc")
DeploymentProviderTypeAliyunGA = DeploymentProviderType(AccessProviderTypeAliyun + "-ga")
DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live")
DeploymentProviderTypeAliyunNLB = DeploymentProviderType(AccessProviderTypeAliyun + "-nlb")
DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss")
DeploymentProviderTypeAliyunVOD = DeploymentProviderType(AccessProviderTypeAliyun + "-vod")
DeploymentProviderTypeAliyunWAF = DeploymentProviderType(AccessProviderTypeAliyun + "-waf")
DeploymentProviderTypeAPISIX = DeploymentProviderType(AccessProviderTypeAWS + "-apisix")
DeploymentProviderTypeAWSACM = DeploymentProviderType(AccessProviderTypeAWS + "-acm")
DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront")
DeploymentProviderTypeAWSIAM = DeploymentProviderType(AccessProviderTypeAWS + "-iam")
DeploymentProviderTypeAzureKeyVault = DeploymentProviderType(AccessProviderTypeAzure + "-keyvault")
DeploymentProviderTypeBaiduCloudAppBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-appblb")
DeploymentProviderTypeBaiduCloudBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-blb")
DeploymentProviderTypeBaiduCloudCDN = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cdn")
DeploymentProviderTypeBaiduCloudCert = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cert")
DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn")
DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console")
DeploymentProviderTypeBaotaPanelSite = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-site")
DeploymentProviderTypeBaotaWAFConsole = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-console")
DeploymentProviderTypeBaotaWAFSite = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-site")
DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn")
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)
DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly)
DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao")
DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn")
DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms")
DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb")
DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn")
DeploymentProviderTypeCTCCCloudLVDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ldvn")
DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn")
DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications")
DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN)
DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn")
DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge)
DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn")
DeploymentProviderTypeHuaweiCloudELB = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-elb")
DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm")
DeploymentProviderTypeHuaweiCloudWAF = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-waf")
DeploymentProviderTypeJDCloudALB = DeploymentProviderType(AccessProviderTypeJDCloud + "-alb")
DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn")
DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live")
DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod")
DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret")
DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN)
DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal)
DeploymentProviderTypeNetlifySite = DeploymentProviderType(AccessProviderTypeNetlify + "-site")
DeploymentProviderTypeProxmoxVE = DeploymentProviderType(AccessProviderTypeProxmoxVE)
DeploymentProviderTypeQiniuCDN = DeploymentProviderType(AccessProviderTypeQiniu + "-cdn")
DeploymentProviderTypeQiniuKodo = DeploymentProviderType(AccessProviderTypeQiniu + "-kodo")
DeploymentProviderTypeQiniuPili = DeploymentProviderType(AccessProviderTypeQiniu + "-pili")
DeploymentProviderTypeRainYunRCDN = DeploymentProviderType(AccessProviderTypeRainYun + "-rcdn")
DeploymentProviderTypeRatPanelConsole = DeploymentProviderType(AccessProviderTypeRatPanel + "-console")
DeploymentProviderTypeRatPanelSite = DeploymentProviderType(AccessProviderTypeRatPanel + "-site")
DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine)
DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH)
DeploymentProviderTypeTencentCloudCDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cdn")
DeploymentProviderTypeTencentCloudCLB = DeploymentProviderType(AccessProviderTypeTencentCloud + "-clb")
DeploymentProviderTypeTencentCloudCOS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cos")
DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css")
DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn")
DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo")
DeploymentProviderTypeTencentCloudGAAP = DeploymentProviderType(AccessProviderTypeTencentCloud + "-gaap")
DeploymentProviderTypeTencentCloudSCF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-scf")
DeploymentProviderTypeTencentCloudSSL = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssl")
DeploymentProviderTypeTencentCloudSSLDeploy = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssldeploy")
DeploymentProviderTypeTencentCloudVOD = DeploymentProviderType(AccessProviderTypeTencentCloud + "-vod")
DeploymentProviderTypeTencentCloudWAF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-waf")
DeploymentProviderTypeUCloudUCDN = DeploymentProviderType(AccessProviderTypeUCloud + "-ucdn")
DeploymentProviderTypeUCloudUS3 = DeploymentProviderType(AccessProviderTypeUCloud + "-us3")
DeploymentProviderTypeUniCloudWebHost = DeploymentProviderType(AccessProviderTypeUniCloud + "-webhost")
DeploymentProviderTypeUpyunCDN = DeploymentProviderType(AccessProviderTypeUpyun + "-cdn")
DeploymentProviderTypeUpyunFile = DeploymentProviderType(AccessProviderTypeUpyun + "-file")
DeploymentProviderTypeVolcEngineALB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-alb")
DeploymentProviderTypeVolcEngineCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-cdn")
DeploymentProviderTypeVolcEngineCertCenter = DeploymentProviderType(AccessProviderTypeVolcEngine + "-certcenter")
DeploymentProviderTypeVolcEngineCLB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-clb")
DeploymentProviderTypeVolcEngineDCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-dcdn")
DeploymentProviderTypeVolcEngineImageX = DeploymentProviderType(AccessProviderTypeVolcEngine + "-imagex")
DeploymentProviderTypeVolcEngineLive = DeploymentProviderType(AccessProviderTypeVolcEngine + "-live")
DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos")
DeploymentProviderTypeWangsuCDN = DeploymentProviderType(AccessProviderTypeWangsu + "-cdn")
DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro")
DeploymentProviderTypeWangsuCertificate = DeploymentProviderType(AccessProviderTypeWangsu + "-certificate")
DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook)
)
type NotificationProviderType string
/*
消息通知提供商常量值。
短横线前的部分始终等于授权提供商类型。
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
NotificationProviderTypeDingTalkBot = NotificationProviderType(AccessProviderTypeDingTalkBot)
NotificationProviderTypeDiscordBot = NotificationProviderType(AccessProviderTypeDiscordBot)
NotificationProviderTypeEmail = NotificationProviderType(AccessProviderTypeEmail)
NotificationProviderTypeLarkBot = NotificationProviderType(AccessProviderTypeLarkBot)
NotificationProviderTypeMattermost = NotificationProviderType(AccessProviderTypeMattermost)
NotificationProviderTypeSlackBot = NotificationProviderType(AccessProviderTypeSlackBot)
NotificationProviderTypeTelegramBot = NotificationProviderType(AccessProviderTypeTelegramBot)
NotificationProviderTypeWebhook = NotificationProviderType(AccessProviderTypeWebhook)
NotificationProviderTypeWeComBot = NotificationProviderType(AccessProviderTypeWeComBot)
)

View File

@@ -1,31 +0,0 @@
package domain
import (
"encoding/json"
"fmt"
"time"
)
type Setting struct {
ID string `json:"id"`
Name string `json:"name"`
Content string `json:"content"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
type ChannelsConfig map[string]map[string]any
func (s *Setting) GetChannelContent(channel string) (map[string]any, error) {
conf := &ChannelsConfig{}
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
}

View File

@@ -0,0 +1,45 @@
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"`
}
// Deprecated: v0.4.x 将废弃
type NotifyTemplatesSettingsContent struct {
NotifyTemplates []struct {
Subject string `json:"subject"`
Message string `json:"message"`
} `json:"notifyTemplates"`
}
// Deprecated: v0.4.x 将废弃
type NotifyChannelsSettingsContent map[string]map[string]any
// Deprecated: v0.4.x 将废弃
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
}
type PersistenceSettingsContent struct {
WorkflowRunsMaxDaysRetention int `json:"workflowRunsMaxDaysRetention"`
ExpiredCertificatesMaxDaysRetention int `json:"expiredCertificatesMaxDaysRetention"`
}

View 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"`
}

211
internal/domain/workflow.go Normal file
View File

@@ -0,0 +1,211 @@
package domain
import (
"encoding/json"
"time"
"github.com/usual2970/certimate/internal/domain/expr"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
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")
WorkflowNodeTypeMonitor = WorkflowNodeType("monitor")
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 提供商额外配置
CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置)
CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID
CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置
KeyAlgorithm string `json:"keyAlgorithm"` // 证书算法
Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播检查超时时间(零值时使用提供商的默认值)
DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL零值时使用提供商的默认值
DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随
DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值时默认值 30
}
type WorkflowNodeConfigForUpload struct {
Certificate string `json:"certificate"` // 证书 PEM 内容
PrivateKey string `json:"privateKey"` // 私钥 PEM 内容
Domains string `json:"domains,omitempty"`
}
type WorkflowNodeConfigForMonitor struct {
Host string `json:"host"` // 主机地址
Port int32 `json:"port,omitempty"` // 端口(零值时默认值 443
Domain string `json:"domain,omitempty"` // 域名(零值时默认值 [Host]
RequestPath string `json:"requestPath,omitempty"` // 请求路径
}
type WorkflowNodeConfigForDeploy struct {
Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate”
Provider string `json:"provider"` // 主机提供商
ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置
SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过
}
type WorkflowNodeConfigForNotify struct {
Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃
Provider string `json:"provider"` // 通知提供商
ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
Subject string `json:"subject"` // 通知主题
Message string `json:"message"` // 通知内容
SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过
}
type WorkflowNodeConfigForCondition struct {
Expression expr.Expr `json:"expression"` // 条件表达式
}
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
return WorkflowNodeConfigForApply{
Domains: maputil.GetString(n.Config, "domains"),
ContactEmail: maputil.GetString(n.Config, "contactEmail"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
CAProvider: maputil.GetString(n.Config, "caProvider"),
CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
CAProviderConfig: maputil.GetKVMapAny(n.Config, "caProviderConfig"),
KeyAlgorithm: maputil.GetOrDefaultString(n.Config, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)),
Nameservers: maputil.GetString(n.Config, "nameservers"),
DnsPropagationWait: maputil.GetInt32(n.Config, "dnsPropagationWait"),
DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
DnsTTL: maputil.GetInt32(n.Config, "dnsTTL"),
DisableFollowCNAME: maputil.GetBool(n.Config, "disableFollowCNAME"),
DisableARI: maputil.GetBool(n.Config, "disableARI"),
SkipBeforeExpiryDays: maputil.GetOrDefaultInt32(n.Config, "skipBeforeExpiryDays", 30),
}
}
func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload {
return WorkflowNodeConfigForUpload{
Certificate: maputil.GetString(n.Config, "certificate"),
PrivateKey: maputil.GetString(n.Config, "privateKey"),
Domains: maputil.GetString(n.Config, "domains"),
}
}
func (n *WorkflowNode) GetConfigForMonitor() WorkflowNodeConfigForMonitor {
host := maputil.GetString(n.Config, "host")
return WorkflowNodeConfigForMonitor{
Host: host,
Port: maputil.GetOrDefaultInt32(n.Config, "port", 443),
Domain: maputil.GetOrDefaultString(n.Config, "domain", host),
RequestPath: maputil.GetString(n.Config, "path"),
}
}
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
return WorkflowNodeConfigForDeploy{
Certificate: maputil.GetString(n.Config, "certificate"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
SkipOnLastSucceeded: maputil.GetBool(n.Config, "skipOnLastSucceeded"),
}
}
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
return WorkflowNodeConfigForNotify{
Channel: maputil.GetString(n.Config, "channel"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
Subject: maputil.GetString(n.Config, "subject"),
Message: maputil.GetString(n.Config, "message"),
SkipOnAllPrevSkipped: maputil.GetBool(n.Config, "skipOnAllPrevSkipped"),
}
}
func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
expression := n.Config["expression"]
if expression == nil {
return WorkflowNodeConfigForCondition{}
}
exprRaw, _ := json.Marshal(expression)
expr, err := expr.UnmarshalExpr([]byte(exprRaw))
if err != nil {
return WorkflowNodeConfigForCondition{}
}
return WorkflowNodeConfigForCondition{
Expression: expr,
}
}
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 = expr.ExprValueSelector
const WorkflowNodeIONameCertificate string = "certificate"

View File

@@ -0,0 +1,30 @@
package domain
import "strings"
const CollectionNameWorkflowLog = "workflow_logs"
type WorkflowLog struct {
Meta
WorkflowId string `json:"workflowId" db:"workflowId"`
RunId string `json:"workflorunIdwId" db:"runId"`
NodeId string `json:"nodeId" db:"nodeId"`
NodeName string `json:"nodeName" db:"nodeName"`
Timestamp int64 `json:"timestamp" db:"timestamp"` // 毫秒级时间戳
Level string `json:"level" db:"level"`
Message string `json:"message" db:"message"`
Data map[string]any `json:"data" db:"data"`
}
type WorkflowLogs []WorkflowLog
func (r WorkflowLogs) ErrorString() string {
var builder strings.Builder
for _, log := range r {
if log.Level == "ERROR" {
builder.WriteString(log.Message)
builder.WriteString("\n")
}
}
return strings.TrimSpace(builder.String())
}

View 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"`
}

View File

@@ -0,0 +1,28 @@
package domain
import (
"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"`
Detail *WorkflowNode `json:"detail" db:"detail"`
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"
)

View File

@@ -1,123 +0,0 @@
package domains
import (
"context"
"fmt"
"time"
"github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/applicant"
"github.com/usual2970/certimate/internal/deployer"
"github.com/usual2970/certimate/internal/utils/app"
)
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)
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)
// 跳过的情况也算成功
history.setWholeSuccess(true)
return nil
}
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)
deployers, err := deployer.Gets(currRecord, certificate)
if err != nil {
history.record(deployPhase, "获取deployer失败", &RecordInfo{Err: err})
app.GetApp().Logger().Error("获取deployer失败", "err", err)
return err
}
// 没有部署配置,也算成功
if len(deployers) == 0 {
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
history.setWholeSuccess(true)
return nil
}
for _, deployer := range deployers {
if err = deployer.Deploy(ctx); err != nil {
app.GetApp().Logger().Error("部署失败", "err", err)
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfos()})
return err
}
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
Info: deployer.GetInfos(),
}, false)
}
app.GetApp().Logger().Info("部署成功")
history.record(deployPhase, "部署成功", nil, true)
history.setWholeSuccess(true)
return nil
}

View File

@@ -1,79 +0,0 @@
package domains
import (
"context"
"fmt"
"github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/utils/app"
)
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)
}

View File

@@ -1,27 +0,0 @@
package domains
import (
"github.com/pocketbase/pocketbase/core"
"github.com/usual2970/certimate/internal/utils/app"
)
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
}

View File

@@ -1,122 +0,0 @@
package domains
import (
"time"
"github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/applicant"
"github.com/usual2970/certimate/internal/utils/app"
"github.com/usual2970/certimate/internal/utils/xtime"
)
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"`
WholeSuccess bool `json:"wholeSuccess"`
}
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) setWholeSuccess(success bool) {
a.WholeSuccess = success
}
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)
record.Set("wholeSuccess", a.WholeSuccess)
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
}

View File

@@ -1,38 +0,0 @@
package domains
import (
"context"
"github.com/usual2970/certimate/internal/notify"
"github.com/usual2970/certimate/internal/utils/app"
)
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().Add("expire", "0 0 * * *", func() {
notify.PushExpireMsg()
})
// 启动定时任务
app.GetScheduler().Start()
app.GetApp().Logger().Info("定时任务启动成功", "total", app.GetScheduler().Total())
}

View File

@@ -1,97 +0,0 @@
package notify
import (
"strconv"
"strings"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/utils/app"
"github.com/usual2970/certimate/internal/utils/xtime"
)
type msg struct {
subject string
message string
}
const (
defaultExpireSubject = "您有{COUNT}张证书即将过期"
defaultExpireMsg = "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!"
)
func PushExpireMsg() {
// 查询即将过期的证书
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
if err != nil {
app.GetApp().Logger().Error("find expired domains by filter", "error", err)
return
}
// 组装消息
msg := buildMsg(records)
if msg == nil {
return
}
if err := Send(msg.subject, msg.message); err != nil {
app.GetApp().Logger().Error("send expire msg", "error", err)
}
}
type notifyTemplates struct {
NotifyTemplates []notifyTemplate `json:"notifyTemplates"`
}
type notifyTemplate struct {
Title string `json:"title"`
Content string `json:"content"`
}
func buildMsg(records []*models.Record) *msg {
if len(records) == 0 {
return nil
}
// 查询模板信息
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
title := defaultExpireSubject
content := defaultExpireMsg
if err == nil {
var templates *notifyTemplates
templateRecord.UnmarshalJSONField("content", templates)
if templates != nil && len(templates.NotifyTemplates) > 0 {
title = templates.NotifyTemplates[0].Title
content = templates.NotifyTemplates[0].Content
}
}
// 替换变量
count := len(records)
domains := make([]string, count)
for i, record := range records {
domains[i] = record.GetString("domain")
}
countStr := strconv.Itoa(count)
domainStr := strings.Join(domains, ",")
title = strings.ReplaceAll(title, "{COUNT}", countStr)
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr)
content = strings.ReplaceAll(content, "{COUNT}", countStr)
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr)
// 返回消息
return &msg{
subject: title,
message: content,
}
}

View File

@@ -1,56 +0,0 @@
package notify
import (
"context"
"fmt"
"net/mail"
"strconv"
"github.com/pocketbase/pocketbase/tools/mailer"
)
const defaultSmtpHostPort = "25"
type Mail struct {
username string
to string
client *mailer.SmtpClient
}
func NewMail(senderAddress, receiverAddresses, smtpHostAddr, smtpHostPort, password string) (*Mail, error) {
if smtpHostPort == "" {
smtpHostPort = defaultSmtpHostPort
}
port, err := strconv.Atoi(smtpHostPort)
if err != nil {
return nil, fmt.Errorf("invalid smtp port: %w", err)
}
client := mailer.SmtpClient{
Host: smtpHostAddr,
Port: port,
Username: senderAddress,
Password: password,
Tls: true,
}
return &Mail{
username: senderAddress,
client: &client,
to: receiverAddresses,
}, nil
}
func (m *Mail) Send(ctx context.Context, subject, content string) error {
message := &mailer.Message{
From: mail.Address{
Address: m.username,
},
To: []mail.Address{{Address: m.to}},
Subject: subject,
Text: content,
}
return m.client.Send(message)
}

View File

@@ -0,0 +1,72 @@
package notify
import (
"context"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
"github.com/usual2970/certimate/internal/repository"
)
type Notifier interface {
Notify(ctx context.Context) error
}
type NotifierWithWorkflowNodeConfig struct {
Node *domain.WorkflowNode
Logger *slog.Logger
Subject string
Message string
}
func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error) {
if config.Node == nil {
return nil, fmt.Errorf("node is nil")
}
if config.Node.Type != domain.WorkflowNodeTypeNotify {
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify))
}
nodeCfg := config.Node.GetConfigForNotify()
options := &notifierProviderOptions{
Provider: domain.NotificationProviderType(nodeCfg.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderServiceConfig: nodeCfg.ProviderConfig,
}
accessRepo := repository.NewAccessRepository()
if nodeCfg.ProviderAccessId != "" {
access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId)
if err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err)
} else {
options.ProviderAccessConfig = access.Config
}
}
notifierProvider, err := createNotifierProvider(options)
if err != nil {
return nil, err
}
return &notifierImpl{
provider: notifierProvider.WithLogger(config.Logger),
subject: config.Subject,
message: config.Message,
}, nil
}
type notifierImpl struct {
provider notifier.Notifier
subject string
message string
}
var _ Notifier = (*notifierImpl)(nil)
func (n *notifierImpl) Notify(ctx context.Context) error {
_, err := n.provider.Notify(ctx, n.subject, n.message)
return err
}

View File

@@ -2,25 +2,20 @@ package notify
import (
"context"
"encoding/json"
"fmt"
"strconv"
stdhttp "net/http"
"golang.org/x/sync/errgroup"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/app"
notifyPackage "github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark"
"github.com/nikoksr/notify/service/dingding"
"github.com/nikoksr/notify/service/http"
"github.com/nikoksr/notify/service/lark"
"github.com/nikoksr/notify/service/telegram"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
"github.com/usual2970/certimate/internal/repository"
)
func Send(title, content string) error {
// 获取所有的推送渠道
notifiers, err := getNotifiers()
// Deprecated: v0.4.x 将废弃
func SendToAllChannels(subject, message string) error {
notifiers, err := getEnabledNotifiers()
if err != nil {
return err
}
@@ -28,184 +23,59 @@ func Send(title, content string) error {
return nil
}
n := notifyPackage.New()
// 添加推送渠道
n.UseServices(notifiers...)
var eg errgroup.Group
for _, n := range notifiers {
if n == nil {
continue
}
// 发送消息
return n.Send(context.Background(), title, content)
eg.Go(func() error {
_, err := n.Notify(context.Background(), subject, message)
return err
})
}
err = eg.Wait()
return err
}
type sendTestParam struct {
Title string `json:"title"`
Content string `json:"content"`
Channel string `json:"channel"`
Conf map[string]any `json:"conf"`
}
func SendTest(param *sendTestParam) error {
notifier, err := getNotifier(param.Channel, param.Conf)
// Deprecated: v0.4.x 将废弃
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
notifier, err := createNotifierProviderUseGlobalSettings(domain.NotifyChannelType(channel), channelConfig)
if err != nil {
return err
}
n := notifyPackage.New()
// 添加推送渠道
n.UseServices(notifier)
// 发送消息
return n.Send(context.Background(), param.Title, param.Content)
_, err = notifier.Notify(context.Background(), subject, message)
return err
}
func getNotifiers() ([]notifyPackage.Notifier, error) {
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
// Deprecated: v0.4.x 将废弃
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)
}
notifiers := make([]notifyPackage.Notifier, 0)
rs := make(map[string]map[string]any)
if err := resp.UnmarshalJSONField("content", &rs); err != nil {
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 !getBool(v, "enabled") {
if !maputil.GetBool(v, "enabled") {
continue
}
notifier, err := getNotifier(k, v)
notifier, err := createNotifierProviderUseGlobalSettings(domain.NotifyChannelType(k), v)
if err != nil {
continue
}
notifiers = append(notifiers, notifier)
}
return notifiers, nil
}
func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, error) {
switch channel {
case domain.NotifyChannelTelegram:
temp := getTelegramNotifier(conf)
if temp == nil {
return nil, fmt.Errorf("telegram notifier config error")
}
return temp, nil
case domain.NotifyChannelDingtalk:
return getDingTalkNotifier(conf), nil
case domain.NotifyChannelLark:
return getLarkNotifier(conf), nil
case domain.NotifyChannelWebhook:
return getWebhookNotifier(conf), nil
case domain.NotifyChannelServerChan:
return getServerChanNotifier(conf), nil
case domain.NotifyChannelMail:
return getMailNotifier(conf)
case domain.NotifyChannelBark:
return getBarkNotifier(conf), nil
}
return nil, fmt.Errorf("notifier not found")
}
func getWebhookNotifier(conf map[string]any) notifyPackage.Notifier {
rs := http.New()
rs.AddReceiversURLs(getString(conf, "url"))
return rs
}
func getTelegramNotifier(conf map[string]any) notifyPackage.Notifier {
rs, err := telegram.New(getString(conf, "apiToken"))
if err != nil {
return nil
}
chatId := getString(conf, "chatId")
id, err := strconv.ParseInt(chatId, 10, 64)
if err != nil {
return nil
}
rs.AddReceivers(id)
return rs
}
func getServerChanNotifier(conf map[string]any) notifyPackage.Notifier {
rs := http.New()
rs.AddReceivers(&http.Webhook{
URL: getString(conf, "url"),
Header: stdhttp.Header{},
ContentType: "application/json",
Method: stdhttp.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]string{
"text": subject,
"desp": message,
}
},
})
return rs
}
func getBarkNotifier(conf map[string]any) notifyPackage.Notifier {
deviceKey := getString(conf, "deviceKey")
serverURL := getString(conf, "serverUrl")
if serverURL == "" {
return bark.New(deviceKey)
}
return bark.NewWithServers(deviceKey, serverURL)
}
func getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier {
return dingding.New(&dingding.Config{
Token: getString(conf, "accessToken"),
Secret: getString(conf, "secret"),
})
}
func getLarkNotifier(conf map[string]any) notifyPackage.Notifier {
return lark.NewWebhookService(getString(conf, "webhookUrl"))
}
func getMailNotifier(conf map[string]any) (notifyPackage.Notifier, error) {
rs, err := NewMail(getString(conf, "senderAddress"),
getString(conf, "receiverAddresses"),
getString(conf, "smtpHostAddr"),
getString(conf, "smtpHostPort"),
getString(conf, "password"),
)
if err != nil {
return nil, err
}
return rs, nil
}
func getString(conf map[string]any, key string) string {
if _, ok := conf[key]; !ok {
return ""
}
return conf[key].(string)
}
func getBool(conf map[string]any, key string) bool {
if _, ok := conf[key]; !ok {
return false
}
return conf[key].(bool)
}

View File

@@ -0,0 +1,182 @@
package notify
import (
"fmt"
"net/http"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
pDingTalkBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalkbot"
pDiscordBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/discordbot"
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pLarkBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/larkbot"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pSlackBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/slackbot"
pTelegramBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegrambot"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
pWeComBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecombot"
httputil "github.com/usual2970/certimate/internal/pkg/utils/http"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
type notifierProviderOptions struct {
Provider domain.NotificationProviderType
ProviderAccessConfig map[string]any
ProviderServiceConfig map[string]any
}
func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier, error) {
/*
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
switch options.Provider {
case domain.NotificationProviderTypeDingTalkBot:
{
access := domain.AccessConfigForDingTalkBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pDingTalkBot.NewNotifier(&pDingTalkBot.NotifierConfig{
WebhookUrl: access.WebhookUrl,
Secret: access.Secret,
})
}
case domain.NotificationProviderTypeDiscordBot:
{
access := domain.AccessConfigForDiscordBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pDiscordBot.NewNotifier(&pDiscordBot.NotifierConfig{
BotToken: access.BotToken,
ChannelId: maputil.GetOrDefaultString(options.ProviderServiceConfig, "channelId", access.DefaultChannelId),
})
}
case domain.NotificationProviderTypeEmail:
{
access := domain.AccessConfigForEmail{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pEmail.NewNotifier(&pEmail.NotifierConfig{
SmtpHost: access.SmtpHost,
SmtpPort: access.SmtpPort,
SmtpTls: access.SmtpTls,
Username: access.Username,
Password: access.Password,
SenderAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "senderAddress", access.DefaultSenderAddress),
SenderName: maputil.GetOrDefaultString(options.ProviderServiceConfig, "senderName", access.DefaultSenderName),
ReceiverAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "receiverAddress", access.DefaultReceiverAddress),
})
}
case domain.NotificationProviderTypeLarkBot:
{
access := domain.AccessConfigForLarkBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pLarkBot.NewNotifier(&pLarkBot.NotifierConfig{
WebhookUrl: access.WebhookUrl,
})
}
case domain.NotificationProviderTypeMattermost:
{
access := domain.AccessConfigForMattermost{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
ServerUrl: access.ServerUrl,
Username: access.Username,
Password: access.Password,
ChannelId: maputil.GetOrDefaultString(options.ProviderServiceConfig, "channelId", access.DefaultChannelId),
})
}
case domain.NotificationProviderTypeSlackBot:
{
access := domain.AccessConfigForSlackBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pSlackBot.NewNotifier(&pSlackBot.NotifierConfig{
BotToken: access.BotToken,
ChannelId: maputil.GetOrDefaultString(options.ProviderServiceConfig, "channelId", access.DefaultChannelId),
})
}
case domain.NotificationProviderTypeTelegramBot:
{
access := domain.AccessConfigForTelegramBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pTelegramBot.NewNotifier(&pTelegramBot.NotifierConfig{
BotToken: access.BotToken,
ChatId: maputil.GetOrDefaultInt64(options.ProviderServiceConfig, "chatId", access.DefaultChatId),
})
}
case domain.NotificationProviderTypeWebhook:
{
access := domain.AccessConfigForWebhook{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
mergedHeaders := make(map[string]string)
if defaultHeadersString := access.HeadersString; defaultHeadersString != "" {
h, err := httputil.ParseHeaders(defaultHeadersString)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook headers: %w", err)
}
for key := range h {
mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)
}
}
if extendedHeadersString := maputil.GetString(options.ProviderServiceConfig, "headers"); extendedHeadersString != "" {
h, err := httputil.ParseHeaders(extendedHeadersString)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook headers: %w", err)
}
for key := range h {
mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)
}
}
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
WebhookUrl: access.Url,
WebhookData: maputil.GetOrDefaultString(options.ProviderServiceConfig, "webhookData", access.DefaultDataForNotification),
Method: access.Method,
Headers: mergedHeaders,
AllowInsecureConnections: access.AllowInsecureConnections,
})
}
case domain.NotificationProviderTypeWeComBot:
{
access := domain.AccessConfigForWeComBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pWeComBot.NewNotifier(&pWeComBot.NotifierConfig{
WebhookUrl: access.WebhookUrl,
})
}
}
return nil, fmt.Errorf("unsupported notifier provider '%s'", options.Provider)
}

View File

@@ -0,0 +1,108 @@
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/dingtalkbot"
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/larkbot"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegrambot"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
pWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecombot"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
// Deprecated: v0.4.x 将废弃
func createNotifierProviderUseGlobalSettings(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: maputil.GetString(channelConfig, "deviceKey"),
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
})
case domain.NotifyChannelTypeDingTalk:
return pDingTalk.NewNotifier(&pDingTalk.NotifierConfig{
WebhookUrl: "https://oapi.dingtalk.com/robot/send?access_token=" + maputil.GetString(channelConfig, "accessToken"),
Secret: maputil.GetString(channelConfig, "secret"),
})
case domain.NotifyChannelTypeEmail:
return pEmail.NewNotifier(&pEmail.NotifierConfig{
SmtpHost: maputil.GetString(channelConfig, "smtpHost"),
SmtpPort: maputil.GetInt32(channelConfig, "smtpPort"),
SmtpTls: maputil.GetOrDefaultBool(channelConfig, "smtpTLS", true),
Username: maputil.GetOrDefaultString(channelConfig, "username", maputil.GetString(channelConfig, "senderAddress")),
Password: maputil.GetString(channelConfig, "password"),
SenderAddress: maputil.GetString(channelConfig, "senderAddress"),
ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"),
})
case domain.NotifyChannelTypeGotify:
return pGotify.NewNotifier(&pGotify.NotifierConfig{
ServerUrl: maputil.GetString(channelConfig, "url"),
Token: maputil.GetString(channelConfig, "token"),
Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
})
case domain.NotifyChannelTypeLark:
return pLark.NewNotifier(&pLark.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
case domain.NotifyChannelTypeMattermost:
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
ChannelId: maputil.GetString(channelConfig, "channelId"),
Username: maputil.GetString(channelConfig, "username"),
Password: maputil.GetString(channelConfig, "password"),
})
case domain.NotifyChannelTypePushover:
return pPushover.NewNotifier(&pPushover.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
User: maputil.GetString(channelConfig, "user"),
})
case domain.NotifyChannelTypePushPlus:
return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
})
case domain.NotifyChannelTypeServerChan:
return pServerChan.NewNotifier(&pServerChan.NotifierConfig{
ServerUrl: maputil.GetString(channelConfig, "url"),
})
case domain.NotifyChannelTypeTelegram:
return pTelegram.NewNotifier(&pTelegram.NotifierConfig{
BotToken: maputil.GetString(channelConfig, "apiToken"),
ChatId: maputil.GetInt64(channelConfig, "chatId"),
})
case domain.NotifyChannelTypeWebhook:
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "url"),
AllowInsecureConnections: maputil.GetBool(channelConfig, "allowInsecureConnections"),
})
case domain.NotifyChannelTypeWeCom:
return pWeCom.NewNotifier(&pWeCom.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
}
return nil, fmt.Errorf("unsupported notifier channel '%s'", channelConfig)
}

View File

@@ -5,42 +5,43 @@ import (
"fmt"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/domain/dtos"
)
// Deprecated: v0.4.x 将废弃
const (
notifyTestTitle = "测试通知"
notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。"
)
type SettingRepository interface {
GetByName(ctx context.Context, name string) (*domain.Setting, error)
// Deprecated: v0.4.x 将废弃
type settingsRepository interface {
GetByName(ctx context.Context, name string) (*domain.Settings, error)
}
// Deprecated: v0.4.x 将废弃
type NotifyService struct {
settingRepo SettingRepository
settingsRepo settingsRepository
}
func NewNotifyService(settingRepo SettingRepository) *NotifyService {
// Deprecated: v0.4.x 将废弃
func NewNotifyService(settingsRepo settingsRepository) *NotifyService {
return &NotifyService{
settingRepo: settingRepo,
settingsRepo: settingsRepo,
}
}
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
// Deprecated: v0.4.x 将废弃
func (n *NotifyService) Test(ctx context.Context, req *dtos.NotifyTestPushReq) error {
settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels")
if err != nil {
return fmt.Errorf("get notify channels setting failed: %w", err)
return fmt.Errorf("failed to get notify channels settings: %w", err)
}
conf, err := setting.GetChannelContent(req.Channel)
channelConfig, err := settings.GetNotifyChannelConfig(string(req.Channel))
if err != nil {
return fmt.Errorf("get notify channel %s config failed: %w", req.Channel, err)
return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
}
return SendTest(&sendTestParam{
Title: notifyTestTitle,
Content: notifyTestBody,
Channel: req.Channel,
Conf: conf,
})
return SendToChannel(notifyTestTitle, notifyTestBody, string(req.Channel), channelConfig)
}

View File

@@ -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
}

View File

@@ -0,0 +1,41 @@
package aliyunesa
import (
"time"
"github.com/go-acme/lego/v4/challenge"
internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/internal"
)
type ChallengeProviderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
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")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.SecretID = config.AccessKeyId
providerConfig.SecretKey = config.AccessKeySecret
providerConfig.RegionID = config.Region
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
}

Some files were not shown because too many files have changed in this diff Show More