Compare commits

...

326 Commits

Author SHA1 Message Date
yoan
c853f2976f v0.2.20 2024-11-15 08:07:37 +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
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
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
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
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
usual2970
c36db3545f Merge pull request #321 from fudiwei/feat/notifier
feat: notifiers
2024-11-11 18:16:30 +08:00
yoan
a367585ab4 v0.2.18 2024-11-11 07:58:13 +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
usual2970
5362371bda Merge pull request #319 from fudiwei/bugfix/aliyun-api-error
bugfix #318
2024-11-10 19:40:40 +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
yoan
35e1bfcd7f Update readme 2024-11-09 11:37:34 +08:00
Fu Diwei
24df7913fe feat: support aliyun global ALB/NLB 2024-11-09 09:54:49 +08:00
Fu Diwei
83674e4b35 refactor: ensure compile-time check for Uploader implementations 2024-11-09 09:47:14 +08:00
Fu Diwei
22d3aeb7b5 fix: #318 2024-11-09 09:41:05 +08:00
yoan
cf005711c0 v0.2.17 2024-11-08 08:11:04 +08:00
usual2970
0a00d0c52f Merge pull request #314 from fudiwei/bugfix/dogecloud-api-error
bugfix #313
2024-11-08 08:10:18 +08:00
usual2970
9aa17a0395 Merge pull request #315 from fudiwei/bugfix/qiniu-panic
bugfix #304
2024-11-08 08:09:41 +08:00
Fu Diwei
65ecdf7dc2 update README 2024-11-07 17:36:41 +08:00
Fu Diwei
0dfa5994cc fix: #304 2024-11-07 17:35:43 +08:00
Fu Diwei
5d2844fdb6 fix: #313 2024-11-07 15:01:46 +08:00
yoan
44332b9d07 v0.2.16 2024-11-07 08:09:25 +08:00
usual2970
20a23e148c Merge pull request #309 from fudiwei/bugfix/dogecloud-api-error
bugfix #308
2024-11-07 08:06:55 +08:00
RHQYZ
0bcb6206f4 fix #308 2024-11-06 11:07:24 +08:00
yoan
943b9827ee v0.2.15 2024-11-06 07:12:48 +08:00
usual2970
741f3ec212 Merge pull request #306 from fudiwei/bugfix/dogecloud-api-error
bugfix #303
2024-11-06 07:08:12 +08:00
Fu Diwei
8549a17675 fix: #303 2024-11-05 18:16:21 +08:00
yoan
718cfccbea resolve new sftp client failure 2024-11-05 08:35:37 +08:00
yoan
2458fa26d8 v0.2.14 2024-11-05 08:30:28 +08:00
yoan
ac24684d2b Merge branch 'main' of github.com:usual2970/certimate 2024-11-05 08:29:58 +08:00
yoan
106dbd9538 Merge branch 'fudiwei-feat/cloud-cdn' 2024-11-05 08:29:30 +08:00
yoan
f9efb2b800 migration 2024-11-05 08:28:35 +08:00
usual2970
897d124d5b Merge pull request #299 from fudiwei/bugfix/ssh-jks
bugfix #298
2024-11-05 08:15:13 +08:00
Fu Diwei
34daf9ccac refactor: clean code 2024-11-04 12:54:23 +08:00
Fu Diwei
269a97e81e feat: add baiducloud cdn deployer 2024-11-04 12:44:53 +08:00
Fu Diwei
2fd57621d8 fix: #298 2024-11-04 11:20:35 +08:00
Fu Diwei
76de837214 feat: add baiducloud provider 2024-11-04 11:11:00 +08:00
Fu Diwei
1e41020728 feat: add dogecloud cdn deployer 2024-11-04 10:34:05 +08:00
Fu Diwei
8a78e49bf0 feat: add dogecloud provider 2024-11-04 10:30:18 +08:00
yoan
e6726e4c02 v0.2.13 2024-11-04 08:07:05 +08:00
yoan
76330a4a1a v0.2.12 2024-11-04 07:49:00 +08:00
usual2970
7e5f0097e4 Merge pull request #296 from usual2970/hotfix/email
fix: resolve email notification delivery failure
2024-11-02 13:09:37 +08:00
yoan
18e1c02d1c fix: resolve email notification delivery failure 2024-11-02 10:17:16 +08:00
usual2970
28992f178e Merge pull request #294 from funnyzak/bark_notify
feat: add Bark notifier
2024-11-02 09:52:13 +08:00
usual2970
c41f34c352 Merge pull request #276 from fudiwei/feat/cloud-load-balance
feat: tencent clb deployer
2024-11-02 09:46:42 +08:00
Fu Diwei
6b5580a30c refactor: clean code 2024-11-01 15:56:22 +08:00
Fu Diwei
1dee14e32d refactor: adjust project structure 2024-11-01 15:54:05 +08:00
Fu Diwei
1e3c4881d0 refactor: remove unused certificate name in TencentCloudSSLUploader 2024-11-01 15:33:02 +08:00
Leon
657964cda4 feat: add Bark notification channel and related settings 2024-11-01 11:35:09 +08:00
Fu Diwei
893aac916c feat(ui): show deploy provider name rather than access provider name in DeployList 2024-10-31 20:25:06 +08:00
Fu Diwei
68da6cf3ae fix: fix import cycle 2024-10-31 20:03:04 +08:00
Fu Diwei
0d96ea9eef refactor: deprecate internal/deployer/deployer.getDeployVariables 2024-10-31 19:59:21 +08:00
Fu Diwei
0ceb44a7cd refactor: deprecate internal/utils/rand.RandStr 2024-10-31 19:53:48 +08:00
Fu Diwei
4fec0036cb refactor: fix typo 2024-10-31 18:25:22 +08:00
Fu Diwei
f82eee4636 refactor: clean code 2024-10-31 14:30:16 +08:00
Fu Diwei
260cfb96ec refactor(ui): declare deploy config params 2024-10-31 14:27:11 +08:00
Fu Diwei
f71a519674 refactor: clean code 2024-10-31 13:41:21 +08:00
Fu Diwei
369c146eca feat: support tencent clb deployment in multiple ways 2024-10-31 13:24:43 +08:00
Fu Diwei
83264a6946 refactor: clean code 2024-10-31 11:37:16 +08:00
Fu Diwei
3c3d4e9109 refactor: extend qiniu sdk 2024-10-31 11:37:03 +08:00
Fu Diwei
ce55365292 refactor: extend huaweicloud cdn sdk 2024-10-31 10:14:27 +08:00
Fu Diwei
be495839b6 Merge branch 'main' into feat/cloud-load-balance 2024-10-31 09:14:57 +08:00
usual2970
a27a9f55a7 Merge pull request #284 from usual2970/feat/ui-1030
Fix the issue where long domain names or titles overlap the next column.
2024-10-31 08:15:22 +08:00
usual2970
10e14caf35 Merge pull request #285 from LeoChen98/fix-tencent-cos-locales-loss
fix: tencent cos ui locales loss
2024-10-31 08:15:04 +08:00
Fu Diwei
59af246479 refactor: clean code 2024-10-30 19:37:44 +08:00
Leo Chen
1f52eaca01 fix: tencent cos ui locales loss 2024-10-30 17:09:11 +08:00
yoan
d833f4b5ff fix cos region validate 2024-10-30 16:08:32 +08:00
yoan
bfee39049d Merge branch 'LeoChen98-feat-add-netsh-preset' 2024-10-30 12:29:07 +08:00
yoan
b4599df6c6 code format 2024-10-30 12:28:59 +08:00
yoan
261c6f6956 Merge branch 'feat-add-netsh-preset' of github.com:LeoChen98/certimate into LeoChen98-feat-add-netsh-preset 2024-10-30 12:26:06 +08:00
yoan
b97d77c848 Fix the issue where long domain names or titles overlap the next column. 2024-10-30 11:57:16 +08:00
yoan
c1cefe0e7f v0.2.11 2024-10-30 11:07:59 +08:00
yoan
55b77fdf5c Fix the issue where the deployment type could not be selected 2024-10-30 11:03:41 +08:00
yoan
16967c4ab1 fix tencent cdn deploy 2024-10-30 09:31:51 +08:00
yoan
61a4fd8657 v0.2.10 2024-10-30 07:04:05 +08:00
Leo Chen
67ca7e3097 feat: add netsh preset
新增本地Windows下使用netsh绑定证书的预设
2024-10-29 21:43:20 +08:00
Fu Diwei
26fa8e75bd refactor: clean code 2024-10-29 21:32:48 +08:00
Fu Diwei
aeaa45b713 Merge branch 'main' into feat/cloud-load-balance 2024-10-29 09:12:39 +08:00
yoan
edeac86f06 Merge branch 'fudiwei-feat/multiple-certificate-formats' 2024-10-29 08:46:06 +08:00
yoan
4e0c23165f fix conflict 2024-10-29 08:45:51 +08:00
usual2970
feb851a3fc Merge pull request #273 from LeoChen98/enhance-tencent-cdn-dupe-deploy
enhance: resolve error on tencent cdn dupe deployment
2024-10-29 08:39:57 +08:00
usual2970
3103d60508 Merge pull request #274 from PittyXu/feat/k8s
fix: k8s部署更新报错
2024-10-29 08:39:15 +08:00
usual2970
53be6b5f5b Merge pull request #272 from LeoChen98/feat-add-mail-push
feat: add mail push
2024-10-29 08:38:10 +08:00
usual2970
9d3e0d1090 Merge pull request #278 from usual2970/feat/searchable_select
feat: Searchable when selecting authorization type
2024-10-29 08:37:53 +08:00
yoan
f8aef129cf Searchable when selecting authorization type 2024-10-28 22:52:25 +08:00
Leo Chen
c419b2c8b4 use slice pkg 2024-10-28 20:28:13 +08:00
Fu Diwei
e1a3a3e7c7 refactor: clean code 2024-10-28 14:15:33 +08:00
Fu Diwei
b47a1a13cb feat: support jks format 2024-10-28 11:49:44 +08:00
徐雪君
3397f424bc fix: k8s部署更新报错 #266 2024-10-28 11:15:08 +08:00
yoan
48672d1a44 v0.2.9 2024-10-28 08:48:30 +08:00
Leo Chen
38dc8a63d9 enhance: resolve error on tencent cdn dupe deployment
优化:腾讯云cdn重复部署报错的问题
2024-10-27 23:48:52 +08:00
Fu Diwei
009e8fb976 feat: preset scripts on deployment to local 2024-10-27 21:10:19 +08:00
Fu Diwei
6d7a91f49b refactor: clean code 2024-10-27 20:44:38 +08:00
yoan
9d4d14db06 Update README.md 2024-10-27 20:42:47 +08:00
Leo Chen
c9f347f77a fix mail push onchange 2024-10-27 20:27:46 +08:00
Leo Chen
0396d8222e feat: add mail push
新增电子邮箱推送
2024-10-27 20:21:34 +08:00
Fu Diwei
305f3de50f Merge branch 'main' into feat/multiple-certificate-formats 2024-10-27 20:17:04 +08:00
yoan
ffacfe0f42 Merge branch 'LeoChen98-feat-serverchan-push-tube' 2024-10-27 09:18:46 +08:00
yoan
be9e66c7d3 Merge branch 'feat-serverchan-push-tube' of github.com:LeoChen98/certimate into LeoChen98-feat-serverchan-push-tube 2024-10-27 09:15:12 +08:00
yoan
1238508bdb Merge branch 'fudiwei-feat/cloud-load-balance' 2024-10-27 09:12:05 +08:00
yoan
1ab5c4035a fix conflict 2024-10-27 09:10:12 +08:00
yoan
67fa9d91bf Merge branch 'PittyXu-feat/k8s' 2024-10-27 08:38:44 +08:00
yoan
dc5f9abf20 detail ajustments 2024-10-27 08:37:42 +08:00
yoan
7240a42fbc Merge branch 'feat/k8s' of github.com:PittyXu/certimate into PittyXu-feat/k8s 2024-10-27 08:35:36 +08:00
yoan
6fbb6d4992 Merge branch 'LeoChen98-feat-tecent-ecdn-teo-deploy' 2024-10-27 08:33:00 +08:00
yoan
86838f305b detail ajustments 2024-10-27 08:32:48 +08:00
yoan
1b1b5939c5 Merge branch 'feat-tecent-ecdn-teo-deploy' of github.com:LeoChen98/certimate into LeoChen98-feat-tecent-ecdn-teo-deploy 2024-10-27 08:07:48 +08:00
Leo Chen
ffdd61b5ee feat: add ServerChan notifier
新增Server酱通知
2024-10-27 04:01:42 +08:00
Fu Diwei
adad5d86ba feat: support specified format on deployment to local/ssh 2024-10-27 00:19:34 +08:00
Fu Diwei
e7870e2b05 feat: support specified shell on deployment to local 2024-10-26 22:22:28 +08:00
徐雪君
548cbbfdd4 feat: k8s部署支持ServiceAccount权限 2024-10-26 22:15:16 +08:00
Fu Diwei
da4715e6dc fix: fix aliyun nlb endpoint 2024-10-26 13:18:15 +08:00
Fu Diwei
506ab4f18e feat: support quic listener in deployment to aliyun alb 2024-10-26 13:15:01 +08:00
Fu Diwei
d87026d5be feat: add aliyun nlb deployer 2024-10-26 12:52:55 +08:00
Fu Diwei
1690963aaf feat: add aliyun alb deployer 2024-10-26 12:40:45 +08:00
Fu Diwei
20d2c5699c feat: add aliyun clb deployer 2024-10-26 00:31:38 +08:00
Fu Diwei
e660e9cad1 feat: add aliyun slb uploader 2024-10-25 23:13:33 +08:00
Fu Diwei
26d7b0ba03 refactor: clean code 2024-10-25 23:03:52 +08:00
Leo Chen
ee097b3135 update README for tencent TEO support 2024-10-25 22:21:30 +08:00
Leo Chen
f5052e9a58 fix the missing parentheses 2024-10-25 22:18:40 +08:00
Leo Chen
3b3376899c add feat: tencent TEO deploy support
新增腾讯TEO(Edge One)部署方式
2024-10-25 22:16:27 +08:00
Leo Chen
a24a3595fa feat: add tencent ECDN deploy 2024-10-25 18:47:41 +08:00
Leo Chen
6a14d801f1 fix type incompatible error 2024-10-25 18:32:45 +08:00
yoan
332c5c5127 fix error type 2024-10-25 18:32:32 +08:00
usual2970
f9568f1a4a Merge pull request #254 from fudiwei/feat/cloud-load-balance
feat: huaweicloud elb deployer
2024-10-25 17:43:11 +08:00
usual2970
b458720dca Merge pull request #257 from belier-cn/main
feat: keep qiniu cdn https configuration
2024-10-25 16:16:20 +08:00
belier
935a320100 feat: keep qiniu cdn https configuration 2024-10-25 14:45:48 +08:00
yoan
361d0de17c v0.2.8 2024-10-25 08:10:05 +08:00
Fu Diwei
024b3c936e Merge branch 'main' into feat/cloud-load-balance 2024-10-24 22:45:25 +08:00
Fu Diwei
dc720a5d99 feat: add huaweicloud elb deployer 2024-10-24 22:37:55 +08:00
Fu Diwei
af3e20709d refactor: clean code 2024-10-24 21:42:39 +08:00
yoan
ea9e9165b6 Fix the issue where log information is not displayed. 2024-10-24 21:03:57 +08:00
Fu Diwei
ee531dd186 fix: aliyun oss deploy config validation error 2024-10-24 20:49:51 +08:00
yoan
51abe8de56 Merge branch 'zzci-main' 2024-10-24 20:47:00 +08:00
yoan
e2254faf15 Reuse the x509 package 2024-10-24 20:44:41 +08:00
Fu Diwei
cea6be37dc feat: allow set a different region on deployment to huaweicloud cdn 2024-10-24 20:16:23 +08:00
Roy
46dccb176e fix typo, get annotations from cert. 2024-10-24 18:39:18 +08:00
Roy
5411b9cb92 change annotations to certimage. 2024-10-24 17:06:57 +07:00
Roy
9f6ea410af Update k8s_secret.go 2024-10-24 17:05:05 +07:00
Roy
528a3d9da8 support create secret, add cert annotations. 2024-10-24 17:56:36 +08:00
yoan
564eb48ebe update dark mod stype 2024-10-24 08:59:17 +08:00
usual2970
92a6b179d4 Merge pull request #247 from LeoChen98/feat-tencent-clb
feat: add support for tencent CLB
2024-10-24 08:03:28 +08:00
Leo Chen
83393a4ee1 update readme for tencent clb support 2024-10-24 00:00:24 +08:00
Leo Chen
6875151717 fix tencent clb deploy failed
- 新增region参数
- 新增配置说明
2024-10-23 23:56:22 +08:00
usual2970
2a8c6cf033 Merge pull request #244 from usual2970/feat/gts
Support Google Trust Services
2024-10-23 21:13:50 +08:00
Leo Chen
7544286b0f add support for tencent CLB
新增腾讯云CLB负载均衡配置支持
2024-10-23 18:57:12 +08:00
Leo Chen
7c685646da fix tencent cos ui placeholder 2024-10-23 18:48:01 +08:00
Leo Chen
d82a9c9253 fix tencent cos ui onload verify 2024-10-23 18:45:36 +08:00
Leo Chen
59584a2961 fix tencent cos input verify 2024-10-23 18:40:52 +08:00
Leo Chen
195aa54cdc add wildcase domain supported ui label 2024-10-23 18:21:18 +08:00
Leo Chen
4b324e6a22 fix tencent COS ui 2024-10-23 18:19:35 +08:00
Leo Chen
0e575a0ce7 rename tencent_cos.go 2024-10-23 17:40:32 +08:00
yoan
7ab8517a93 Handle concurrency issues in a simple way. 2024-10-23 17:32:35 +08:00
yoan
1dca6ecf8d An account for many customers 2024-10-23 16:25:21 +08:00
yoan
8bec234fe8 gts support 2024-10-23 13:22:17 +08:00
yoan
bff18a7be7 update vite plugin to preserve special file 2024-10-23 08:36:25 +08:00
yoan
bac00491fe v0.2.7 2024-10-23 07:56:29 +08:00
usual2970
f8da3ded0d Merge pull request #242 from LeoChen98/feat-tencent-cos-support
feat: add tencent cos deploy support
2024-10-23 07:55:03 +08:00
Leo Chen
b01849eb0c update readme for new feat. 2024-10-22 22:32:35 +08:00
Leo Chen
c9eb487953 fix ui-deployer route 2024-10-22 22:18:52 +08:00
yoan
dc383644d6 update make file 2024-10-22 22:13:01 +08:00
Leo Chen
a8f718afa0 add tencent-cos ui
表单主要从OSS表单修改
2024-10-22 21:56:26 +08:00
Leo Chen
cd76d170b2 fix region of cos 2024-10-22 21:55:49 +08:00
Leo Chen
7b129c11e9 Merge branch 'usual2970:main' into feat-tencent-cos-support 2024-10-22 21:22:45 +08:00
Leo Chen
f7972d5b68 remove unused imports 2024-10-22 21:19:20 +08:00
usual2970
b1a0d84033 Merge pull request #227 from fudiwei/feat/cloud-load-balance
feat: cloud load balance pre-works
2024-10-22 21:18:46 +08:00
usual2970
969fba8a57 Merge branch 'main' into feat/cloud-load-balance 2024-10-22 21:13:59 +08:00
Leo Chen
63865b5fbd fix ResourceType 2024-10-22 21:11:49 +08:00
Leo Chen
46c32f15e3 feat: add tencent cos deploy support
新增腾讯云COS配置支持
2024-10-22 21:07:58 +08:00
usual2970
5f62c887c0 Merge pull request #236 from liburdi/docs/upd_readme
feat: reamdme里面的源码启动命令已经失效,因为ui/dist不在代码仓库中管理
2024-10-22 20:11:29 +08:00
usual2970
c85beaa52b Merge pull request #238 from usual2970/feat/qiniu_wildcard
Qiniu cdn supports wildcard domain deployment
2024-10-22 20:08:40 +08:00
Fu Diwei
885cdfaec9 fix: fix repeat certificates judgement logical in tencentcloud ssl uploader 2024-10-22 18:39:42 +08:00
Fu Diwei
011130432c refactor: clean code 2024-10-22 18:06:56 +08:00
Fu Diwei
062d66222a refactor: divide DeployList 2024-10-22 17:44:39 +08:00
Fu Diwei
e53749e16e refactor: clean code 2024-10-22 15:58:58 +08:00
yoan
dbfb84ec6d qiniu cdn supports wildcard domain deployment 2024-10-22 15:41:44 +08:00
liburdi
265842feeb feat: 根据输入的参数,显示不同的内容 2024-10-22 13:52:33 +08:00
liburdi
0c35928eee feat: 启动时终端打印 2024-10-22 12:26:08 +08:00
liburdi
ea4bcb4aaf feat: reamdme里面的源码启动命令已经失效,因为ui/dist不在代码仓库中管理 2024-10-22 12:17:43 +08:00
Fu Diwei
716f5f1426 refactor: clean code 2024-10-22 11:38:55 +08:00
yoan
4e86c1eb45 v0.2.6 2024-10-22 08:26:27 +08:00
yoan
0576a8bec3 Merge branch 'main' of github.com:usual2970/certimate 2024-10-22 07:32:13 +08:00
yoan
97f334b5ab v0.2.6 2024-10-22 07:31:47 +08:00
Fu Diwei
18a7bf0d66 feat: set default region when applying certificates by huaweicloud 2024-10-21 15:10:14 +08:00
Fu Diwei
908d33f186 refactor: clean code 2024-10-21 15:07:09 +08:00
Fu Diwei
68b9171390 Merge branch 'main' into feat/cloud-load-balance 2024-10-21 14:55:19 +08:00
Fu Diwei
45005a5073 refactor: clean code 2024-10-21 14:53:43 +08:00
usual2970
028eb088a5 Merge pull request #229 from PBK-B/feat_disable_cname
feat: 支持配置 CNAME 认证跟随禁用
2024-10-21 13:53:21 +08:00
PBK-B
8f98664665 fix: handle i18n label naming conventions 2024-10-21 09:18:41 +08:00
PBK-B
699385a8c4 fix: adjust the string conversion syntax 2024-10-21 09:18:41 +08:00
PBK-B
64b7ed00f5 feat: docking disableFollowCNAME config item function #228 2024-10-21 09:18:41 +08:00
PBK-B
2c75d2bfde feat: domain config add disable follow CNAME #228 2024-10-21 09:18:41 +08:00
Fu Diwei
9c41b0e357 refactor: clean code 2024-10-21 09:15:36 +08:00
yoan
ec6f10053a Merge branch 'main' of github.com:usual2970/certimate 2024-10-21 07:34:47 +08:00
yoan
0095600615 v0.2.5 2024-10-21 07:34:33 +08:00
Fu Diwei
b031f00764 feat: add aliyun cas uploader 2024-10-21 00:35:16 +08:00
Fu Diwei
a4fc8dfc56 feat: add tencentcloud ssl uploader 2024-10-20 23:53:10 +08:00
Fu Diwei
f168bd903d feat: add huaweicloud elb uploader 2024-10-20 21:33:08 +08:00
Fu Diwei
fc55e37454 chore: git keep /ui/dist 2024-10-20 21:09:28 +08:00
usual2970
84e2fd4f5c Merge pull request #225 from PBK-B/fix_dcdn_wildcard
fix: handle aliyun dcdn does not support wildcard domain #223
2024-10-20 21:04:08 +08:00
usual2970
0037659462 Merge pull request #224 from usual2970/feat/version-improve
Document location adjustment
2024-10-20 21:00:23 +08:00
usual2970
364289894e Merge pull request #222 from usual2970/feat/cert-leftday
Add remaining days
2024-10-20 21:00:03 +08:00
Fu Diwei
f6a3f4edfa refactor: optimize code 2024-10-20 20:42:13 +08:00
Fu Diwei
560d21c854 refactor: optimize code 2024-10-20 20:10:07 +08:00
yoan
4719f99155 Document location adjustment 2024-10-20 17:56:23 +08:00
Fu Diwei
3a213dc9c3 feat: do not use region from access when deploy to huaweicloud cdn 2024-10-20 17:51:36 +08:00
PBK-B
0d07c7c234 fix: handle aliyun dcdn does not support wildcard domain #223 2024-10-20 17:49:44 +08:00
yoan
21670f64d1 Document location adjustment 2024-10-20 17:44:54 +08:00
Fu Diwei
f0e7fe695d clean code 2024-10-20 17:24:23 +08:00
yoan
2bab727569 add remaining days 2024-10-20 17:15:20 +08:00
Fu Diwei
8d41a9aae7 Merge branch 'main' into feat/cloud-load-balance 2024-10-20 16:48:18 +08:00
Fu Diwei
896b5d3a13 Merge branch 'main' into feat/cloud-load-balance 2024-10-20 16:48:06 +08:00
Fu Diwei
88e64717cd feat: support using scm service on deployment to huaweicloud cdn 2024-10-20 16:42:05 +08:00
yoan
2d275a14ab update ISSUE template 2024-10-20 13:43:36 +08:00
yoan
1b796cffd1 fix httpreq and powerdns timeout 2024-10-20 13:37:28 +08:00
usual2970
f53e54c8de Merge pull request #220 from usual2970/feat/update_gomod
update package name
2024-10-20 13:26:53 +08:00
yoan
a9b9be96cb fix conflict 2024-10-20 13:26:25 +08:00
yoan
1033885c99 Merge branch 'zzci-main' 2024-10-20 13:01:53 +08:00
yoan
c45ad3c901 update actions,fix something 2024-10-20 13:00:27 +08:00
Roy
24192b61c1 Merge branch 'main' into main 2024-10-20 08:33:33 +07:00
Roy
1562e92e74 merge source 2024-10-20 09:31:20 +08:00
usual2970
17f72eb9cb Merge pull request #218 from fudiwei/feat/huaweicloud-cdn
feat: huaweicloud cdn
2024-10-20 06:30:48 +08:00
Roy
57ae6d5b40 add ui/dist to .gitignore. change dockerfile to build front. 2024-10-20 04:51:06 +08:00
Roy
467e4c4634 add powerdns,http request apply. 2024-10-19 22:46:37 +08:00
Roy
d6d296b546 add powerdns,http request apply. 2024-10-19 22:46:15 +08:00
yoan
499bbe4fa7 fix deployment dialog title 2024-10-19 22:23:56 +08:00
Fu Diwei
ae814766f3 Merge branch 'main' into feat/huaweicloud-cdn 2024-10-19 21:15:32 +08:00
yoan
17cfeee374 update package name 2024-10-19 21:15:01 +08:00
RHQYZ
efa394e9bd Merge branch 'usual2970:main' into main 2024-10-19 21:14:05 +08:00
RHQYZ
94ca0f27bf Merge branch 'main' into feat/huaweicloud-cdn 2024-10-19 18:49:48 +08:00
Fu Diwei
e08df5e6d8 refactor: fix typo 2024-10-19 18:46:27 +08:00
Fu Diwei
952c6ef73d feat: add huaweicloud cdn deployer 2024-10-19 18:46:02 +08:00
usual2970
b9902c926f Merge pull request #217 from usual2970/feat/push_test_msg
Notify push test
2024-10-19 18:15:16 +08:00
yoan
7fa6ea1797 Notify push test 2024-10-19 18:12:45 +08:00
Fu Diwei
be3cdbf585 refactor 2024-10-19 17:10:42 +08:00
Fu Diwei
6225969d4c refactor 2024-10-19 10:02:31 +08:00
yoan
4382474449 v0.2.4 2024-10-19 08:35:24 +08:00
yoan
678ef9c232 Merge branch 'fudiwei-feat/k8s' 2024-10-19 08:34:09 +08:00
yoan
3d535320b9 fix dependency 2024-10-19 08:31:03 +08:00
Fu Diwei
77d3e40ffb Merge branch 'feat/k8s' of https://github.com/fudiwei/certimate into feat/k8s 2024-10-18 19:58:58 +08:00
Fu Diwei
5dca64d3d3 refactor: clean code 2024-10-18 19:58:15 +08:00
RHQYZ
02d582b564 Merge branch 'main' into feat/k8s 2024-10-18 18:03:54 +08:00
Fu Diwei
8e906cbf23 feat: add k8s secret deployer 2024-10-18 17:56:27 +08:00
Fu Diwei
411b7bbfe2 feat: add k8s provider
commit
2024-10-18 17:54:53 +08:00
Fu Diwei
3093fc6b02 refactor
refactor

refactor
2024-10-18 17:54:53 +08:00
yoan
3c4b7d251a v0.2.3 2024-10-18 17:54:52 +08:00
Fu Diwei
f87a1be192 feat: add aws route53 provider 2024-10-18 17:54:01 +08:00
yoan
9b91cbd67e v0.2.3 2024-10-18 06:49:16 +08:00
usual2970
9b5e1052a1 Merge pull request #210 from fudiwei/feat/aws
feat: add aws provider
2024-10-18 06:47:52 +08:00
Fu Diwei
0d47d7cfd0 Merge branch 'main' into feat/aws 2024-10-17 18:27:01 +08:00
Fu Diwei
ef87975c80 feat: add aws route53 provider 2024-10-17 18:22:23 +08:00
yoan
9db757fbbb mod tidy 2024-10-17 17:57:04 +08:00
usual2970
f6ef305441 Merge pull request #208 from g1335333249/main
通知方式支持飞书
2024-10-17 17:55:52 +08:00
g1335333249
004c6a8506 Notify Add Lark 2024-10-17 12:39:18 +08:00
yoan
a035ba192a v0.2.2 2024-10-17 07:33:37 +08:00
usual2970
d90319bb53 Merge pull request #207 from fudiwei/feat/ecc-certs
feat: certificate key algorithm
2024-10-17 07:32:39 +08:00
Fu Diwei
51db5e1ed0 update README 2024-10-16 22:08:14 +08:00
Fu Diwei
1ce2a52d70 feat: certificate key algorithm 2024-10-16 20:20:27 +08:00
usual2970
fbbea22eee Merge pull request #206 from fudiwei/feat/ssh-key-passphrase
feat: support ssh key passphrase
2024-10-16 16:36:48 +08:00
Fu Diwei
71f43c5bd4 feat: support ssh key passphrase 2024-10-16 12:30:15 +08:00
usual2970
79dce124a7 Merge pull request #203 from fudiwei/main
style: format
2024-10-16 11:48:06 +08:00
RHQYZ
0bc042ae31 Merge branch 'main' into main 2024-10-16 11:31:56 +08:00
yoan
704d2eed32 v0.2.1 2024-10-16 08:16:43 +08:00
usual2970
3348301493 Merge pull request #204 from LeoChen98/change-tencent-ssl-upload-repeatable
change: tencent ssl upload repeatable to false
2024-10-16 08:13:38 +08:00
Leo Chen
afeae4269c change: tencent ssl upload repeatable to false
腾讯云ssl证书上传接口可重复选项设置为`false`,以避免重复上传导致的列表污染。
2024-10-16 00:19:57 +08:00
RHQYZ
a4d1c9a7c0 Merge branch 'main' into main 2024-10-15 21:30:16 +08:00
Fu Diwei
26be47d072 style: format 2024-10-15 21:16:43 +08:00
Fu Diwei
cf3de10eff chore: config gofumpt 2024-10-15 21:16:09 +08:00
Fu Diwei
7ef885319e style: format 2024-10-15 21:15:21 +08:00
Fu Diwei
b0923d54ee chore: config vscode eslint 2024-10-15 21:15:16 +08:00
yoan
be15f2b6a6 Delete the mistakenly added files 2024-10-15 18:28:56 +08:00
yoan
0c1d3341f4 update tencent cdn deploy 2024-10-15 17:53:38 +08:00
usual2970
1a9cad355c Merge pull request #198 from fudiwei/main
chore: improve i18n
2024-10-15 16:58:36 +08:00
Fu Diwei
2c97fa929a chore: improve i18n 2024-10-14 21:43:05 +08:00
Fu Diwei
e397793153 chore: improve i18n 2024-10-14 21:00:50 +08:00
usual2970
9bd279a8a0 Update README.md 2024-10-13 19:24:52 +08:00
yoan
dd7897feff Update readme 2024-10-13 18:37:52 +08:00
yoan
d851a86a9b fix dark mod style 2024-10-13 18:36:02 +08:00
usual2970
62133a91a6 Merge pull request #190 from usual2970/feat/oss
Replace the OSS deployment api
2024-10-13 11:16:18 +08:00
yoan
5b30fc8aba Replace the OSS deployment api 2024-10-13 11:15:35 +08:00
usual2970
2ed94bf509 Merge pull request #188 from usual2970/feat/v0.2
Feat/v0.2
2024-10-13 08:16:34 +08:00
yoan
1928a47961 v0.2 2024-10-13 08:15:21 +08:00
yoan
19f5348802 Merge branch 'main' into feat/v0.2 2024-10-12 08:43:32 +08:00
yoan
f148240bcf v0.1.19 2024-10-12 08:04:34 +08:00
yoan
f914931bc9 go mod tidy 2024-10-11 22:28:26 +08:00
yoan
8c1033634d update Dockerfile name 2024-10-11 22:23:41 +08:00
usual2970
781b79f529 Merge pull request #184 from fudiwei/feat/huaweicloud
feat: add huaweicloud provider
2024-10-11 22:08:18 +08:00
yoan
7d74e1d41e init 2024-10-11 21:53:54 +08:00
RHQYZ
ad91703492 Merge branch 'main' into feat/huaweicloud 2024-10-11 09:43:01 +08:00
Fu Diwei
a007c81e9a feat: add huaweicloud provider 2024-10-11 09:30:14 +08:00
yoan
39bffe3389 v0.1.18 2024-10-11 07:53:32 +08:00
yoan
3b06c7b0a6 init 2024-10-11 07:52:16 +08:00
usual2970
3f2767b28b Merge pull request #183 from LeoChen98/feat-tencent-cdn-extensive-support
add feat: support for tencent cdn extensive domain
2024-10-11 07:02:24 +08:00
Leo Chen
312c6e685a change var name style 2024-10-10 22:45:19 +08:00
Leo Chen
d2b6ab75b7 add feat: support for tencent cdn extensive domain 2024-10-10 19:01:32 +08:00
yoan
9f1b00f04c init 2024-10-10 11:13:45 +08:00
usual2970
dc16294b3d Update README_EN.md 2024-10-09 12:33:42 +08:00
usual2970
77dfcef168 Update README.md 2024-10-09 12:31:53 +08:00
yoan
30ef5841d6 dark mode style fix 2024-10-09 09:26:39 +08:00
yoan
217ba85ff8 v0.1.16 2024-10-09 09:04:23 +08:00
yoan
71e2555391 multiple domain support 2024-10-08 22:02:00 +08:00
331 changed files with 22099 additions and 9184 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = crlf
indent_size = 2
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -3,15 +3,15 @@ name: Bug report
about: 创建一个报告来帮助我们改进
title: "[Bug] 标题简要描述问题"
labels: bug
assignees: ''
assignees: ""
---
**描述问题**
简要描述问题是什么
简要描述问题是什么1 个 ISSUE 只描述一个问题。
**复现步骤**
复现该问题的步骤:
1. 去到 '...'
2. 点击 '...'
3. 滚动到 '...'
@@ -24,9 +24,10 @@ assignees: ''
如有可能,请添加截图以帮助解释问题。
**环境**
- 操作系统: [e.g. Windows, macOS]
- 浏览器: [e.g. Chrome, Safari]
- 仓库版本: [e.g. v1.0.0]
**其他信息**
在此处添加关于该问题的任何其他信息。
在此处添加关于该问题的任何其他信息。

View File

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

View File

@@ -30,7 +30,7 @@ jobs:
uses: docker/metadata-action@v5
with:
images: |
usual2970/certimate
usual2970/certimate
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
- name: Log in to DOCKERHUB
@@ -52,7 +52,8 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile_build
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -14,18 +14,18 @@ jobs:
with:
fetch-depth: 0
# - name: Set up Node.js
# uses: actions/setup-node@v4
# with:
# node-version: 20.11.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.11.0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ">=1.22.5"
# - name: Build Admin dashboard UI
# run: npm --prefix=./ui ci && npm --prefix=./ui run build
- name: Build Admin dashboard UI
run: npm --prefix=./ui ci && npm --prefix=./ui run build
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3

20
.gitignore vendored
View File

@@ -1,14 +1,7 @@
__debug_bin*
vendor
pb_data
main
./certimate
build
/docker/data
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/settings.tailwind.json
.idea
.DS_Store
*.suo
@@ -16,5 +9,14 @@ build
*.njsproj
*.sln
*.sw?
__debug_bin*
vendor
pb_data
build
main
/ui/dist/*
!/ui/dist/.gitkeep
./dist
./certimate
/docker/data

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
]
}

20
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"css.customData": [
".vscode/settings.tailwind.json"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"go.useLanguageServer": true,
"gopls": {
"formatting.gofumpt": true,
},
"[go]": {
"editor.defaultFormatter": "golang.go"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

55
.vscode/settings.tailwind.json vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that youd like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}

View File

@@ -19,8 +19,8 @@ git clone https://github.com/your_username/certimate.git
```
> **重要提示:**
> 建议为每个 bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR请保持不同的改动在独立分支中以便更容易进行代码审查并最终合并。
> 保持一个 pr 只实现一个功能。
> 建议为每个 Bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR请保持不同的改动在独立分支中以便更容易进行代码审查并最终合并。
> 保持一个 PR 只实现一个功能。
## 修改 Go 代码
@@ -28,6 +28,7 @@ git clone https://github.com/your_username/certimate.git
1. 进入根目录
2. 运行以下命令启动服务:
```bash
go run main.go serve
```
@@ -36,11 +37,12 @@ git clone https://github.com/your_username/certimate.git
**在向主仓库提交 PR 之前,建议你:**
- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 对你的代码进行格式化。
- 为你的改动添加单元测试或集成测试Certimate 使用 Go 的标准 `testing` 包)。你可以通过以下命令运行测试(在项目根目录下):
```bash
go test ./...
```
## 修改管理页面 (Admin UI)
@@ -49,17 +51,19 @@ Certimate 的管理页面是一个基于 React 和 Vite 构建的单页应用(
要启动 Admin UI
1. 进入 `ui` 项目目录
2. 运行 `npm install` 安装依赖
1. 进入 `ui` 项目目录
2. 运行 `npm install` 安装依赖。
3. 启动 Vite 开发服务器:
```bash
npm run dev
```
你可以通过浏览器访问 `http://localhost:5173` 来查看运行中的管理页面。
由于 Admin UI 只是一个客户端应用,运行时需要 Certimate 的后端服务作为支撑。你可以手动运行Certimate或者使用预构建的可执行文件。
由于 Admin UI 只是一个客户端应用,运行时需要 Certimate 的后端服务作为支撑。你可以手动运行 Certimate或者使用预构建的可执行文件。
所有对 Admin UI 的修改将会自动反映在浏览器中,无需手动刷新页面。
@@ -69,4 +73,4 @@ Certimate 的管理页面是一个基于 React 和 Vite 构建的单页应用(
npm run build
```
完成所有步骤后,你可以将改动提交 PR 到 Certimate 主仓库。
完成所有步骤后,你可以将改动提交 PR 到 Certimate 主仓库。

View File

@@ -27,7 +27,9 @@ git clone https://github.com/your_username/certimate.git
Once you have made changes to the Go code in Certimate, follow these steps to run the project:
1. Navigate to the root directory.
2. Start the service by running:
```bash
go run main.go serve
```
@@ -36,6 +38,8 @@ This will start a web server at `http://localhost:8090` using the prebuilt Admin
**Before submitting a PR to the main repository, consider:**
- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt).
- Adding unit or integration tests for your changes. Certimate uses Gos standard `testing` package. You can run tests using the following command (while in the root project directory):
```bash
@@ -49,11 +53,15 @@ Certimates Admin UI is a single-page application (SPA) built using React and
To start the Admin UI:
1. Navigate to the `ui` project directory.
2. Install the necessary dependencies by running:
```bash
npm install
```
3. Start the Vite development server:
```bash
npm run dev
```
@@ -70,4 +78,4 @@ After completing your changes, build the Admin UI so it can be embedded into the
npm run build
```
Once all steps are completed, you are ready to submit a PR to the main Certimate repository.
Once all steps are completed, you are ready to submit a PR to the main Certimate repository.

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM node:20-alpine3.19 AS front-builder
WORKDIR /app
COPY . /app/
RUN \
cd /app/ui && \
npm install && \
npm run build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY ../. /app/
RUN rm -rf /app/ui/dist
COPY --from=front-builder /app/ui/dist /app/ui/dist
RUN go build -o certimate
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/certimate .
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]

View File

@@ -1,16 +0,0 @@
FROM golang:1.22-alpine as builder
WORKDIR /app
COPY ../. /app/
RUN go build -o certimate
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/certimate .
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]

View File

@@ -34,4 +34,7 @@ help:
@echo " make clean - 清理构建文件"
@echo " make help - 显示此帮助信息"
.PHONY: all build clean help
.PHONY: all build clean help
local.run:
go mod vendor&& npm --prefix=./ui install && npm --prefix=./ui run build && go run main.go serve --http 127.0.0.1:8090

131
README.md
View File

@@ -4,21 +4,22 @@
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
1. 😱麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
2. 😭易忘当前免费证书有效期仅90天这就要求定期操作增加工作量的同时也很容易忘掉导致网站无法访问。
1. 😱 麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
2. 😭 易忘:当前免费证书有效期仅 90 天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
1. 操作简单:自动申请、部署、续期 SSL 证书,全程无需人工干预。
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行审计。
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、Docker 镜像全部用 Github Actions 生成,过程透明,可自行审计。
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
相关文章:
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
* [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
- [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 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问 [https://docs.certimate.me](https://docs.certimate.me)
## 一、安装
@@ -32,8 +33,14 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
./certimate serve
```
或运行以下命令自动给 Certimate 自身添加证书
```bash
./certimate serve 你的域名
```
> [!NOTE]
> MacOS 在执行二进制文件时会提示:无法打开“certimate”因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
> MacOS 在执行二进制文件时会提示:无法打开“Certimate”因为 Apple 无法检查其是否包含恶意软件。可在系统设置 > 隐私与安全性 > 安全性中点击仍然允许,然后再次尝试执行二进制文件。
### 2. Docker 安装
@@ -48,8 +55,7 @@ mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubuserconten
```bash
git clone EMAIL:usual2970/certimate.git
cd certimate
go mod vendor
go run main.go serve
make local.run
```
## 二、使用
@@ -65,81 +71,94 @@ go run main.go serve
## 三、支持的服务商列表
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
| ---------- | -------------- | ------------ | ------------------------------------------------------ |
| 阿里云 | | | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
| 腾讯云 | | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
| 七牛云 | | | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
| CloudFlare | 是 | | 支持 CloudFlare 注册的域名CloudFlare 服务自带SSL证书 |
| SSH | | | 支持部署到 SSH 服务器 |
| WEBHOOK | | | 支持回调到 WEBHOOK |
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
| :--------: | :----------: | :----------: | ----------------------------------------------------------------- |
| 阿里云 | | | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、SLB |
| 腾讯云 | | | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO |
| 百度智能云 | | | 可部署到百度智能云 CDN |
| 华为云 | | | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
| 七牛云 | | | 部署到七牛云 CDN |
| 多吉云 | | | 可部署到多吉云 CDN |
| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN |
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
| Namesilo | √ | | 可签发在 Namesilo 注册的域名 |
| PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 |
| HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 |
| 本地部署 | | √ | 可部署到本地服务器 |
| SSH | | √ | 可部署到 SSH 服务器 |
| Webhook | | √ | 可部署时回调到 Webhook |
| Kubernetes | | √ | 可部署到 Kubernetes Secret |
## 四、系统截图
![login](https://i.imgur.com/SYjjbql.jpeg)
![dashboard](https://i.imgur.com/WMVbBId.jpeg)
![domains](https://i.imgur.com/8wit3ZA.jpeg)
![accesses](https://i.imgur.com/EWtOoJ0.jpeg)
![history](https://i.imgur.com/aaPtSW7.jpeg)
<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 发起部署请求,将证书部署到服务商的服务器上。
- 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、DNS 服务商的授权信息、以及要部署到的服务商的授权信息。
- Certimate 向证书厂商的 API 发起申请请求,获取 SSL 证书。
- Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
- Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
这就涉及域名、dns 服务商的授权信息、部署服务商的授权信息等。
这就涉及域名、DNS 服务商的授权信息、部署服务商的授权信息等。
### 1. 域名
就是要申请证书的域名。
### 2. dns 服务商授权信息
### 2. DNS 服务商授权信息
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 记录。
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 域名解析记录。
Certimate 会自动添加一个 TXT 记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
Certimate 会自动添加一个 TXT 域名解析记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
比如你在阿里云购买的域名,授权信息如下:
```bash
accessKeyId: xxx
accessKeySecret: TOKEN
accessKeyId: your-access-key-id
accessKeySecret: your-access-key-secret
```
在腾讯云购买的域名,授权信息如下:
```bash
secretId: xxx
secretKey: TOKEN
secretId: your-secret-id
secretKey: your-secret-key
```
注意,此授权信息需具有访问域名及 DNS 解析的管理权限,具体的权限清单请参阅各服务商自己的技术文档。
### 3. 部署服务商授权信息
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN 这时你需要填写阿里云的授权信息。Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDNCertimate 会根据你填写的授权信息及域名找到对应的 CDN 服务并将证书部署到对应的 CDN 服务上。
部署服务商授权信息和 dns 服务商授权信息一致,区别在于 dns 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
部署服务商授权信息和 DNS 服务商授权信息基本一致,区别在于 DNS 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
注意,此授权信息需具有访问部署目标服务的相关管理权限,具体的权限清单请参阅各服务商自己的技术文档。
## 六、常见问题
Q: 提供saas服务吗?
Q: 提供 SaaS 服务吗?
> A: 不提供目前仅支持self-hosted私有部署
> A: 不提供,目前仅支持 self-hosted私有部署
Q: 数据安全?
> A: 由于仅支持私有部署各种数据都保存在用户的服务器上。另外Certimate源码也开源二进制包及Docker镜像打包过程全部使用Github actions进行过程透明可见可自行审计。
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外 Certimate 源码也开源,二进制包及 Docker 镜像打包过程全部使用 Github Actions 进行,过程透明可见,可自行审计。
Q: 自动续期证书?
> A: 已经申请的证书会在过期前10自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
> A: 已经申请的证书会在**过期前 10 天**自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
## 七、贡献
@@ -147,17 +166,27 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
你可以通过以下方式来支持 Certimate 的开发:
* [提交代码:如果你发现了 bug 或有新的功能需求,而你又有相关经验,可以提交代码给我们](CONTRIBUTING.md)。
* 提交 issue功能建议或者 bug 可以[提交 issue](https://github.com/usual2970/certimate/issues) 给我们。
- 提交代码:如果你发现了 Bug 或有新的功能需求,而你又有相关经验,可以[提交代码](CONTRIBUTING.md)给我们
- 提交 Issue功能建议或者 Bug 可以[提交 Issue](https://github.com/usual2970/certimate/issues) 给我们。
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。
支持更多服务商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家提交 PR。
## 八、加入社区
## 八、免责声明
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
* 微信群聊(需要邀请入群,可先加作者好友)
本软件依据 MIT 许可证MIT License发布免费提供旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。
无任何保证:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。
用户责任:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。
## 九、加入社区
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
## 、Star History
## 、Star 趋势图
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@@ -15,10 +15,10 @@ Certimate was created to solve the above-mentioned issues and has the following
Related articles:
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
* [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
- [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
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)
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).
## Installation
@@ -32,6 +32,12 @@ You can download the precompiled binary files directly from the [Releases page](
./certimate serve
```
Or run the following command to automatically add a certificate to Certimate itself.
```bash
./certimate serve yourDomain
```
> [!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.
@@ -48,13 +54,12 @@ mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubuserconten
```bash
git clone EMAIL:usual2970/certimate.git
cd certimate
go mod vendor
go run main.go serve
make local.run
```
## Usage
After completing the installation steps above, you can access the Certimate management page by visiting http://127.0.0.1:8090 in your browser.
After completing the installation steps above, you can access the Certimate management page by visiting <http://127.0.0.1:8090> in your browser.
```bash
usernameadmin@certimate.fun
@@ -65,35 +70,44 @@ password1234567890
## List of Supported Providers
| Provider | Domain Registrar | Deployment Service | Remarks |
| ------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------- |
| Alibaba Cloud | Yes | Yes | Supports domains registered with Alibaba Cloud; supports deployment to Alibaba Cloud CDN and OSS. |
| Tencent Cloud | Yes | Yes | Supports domains registered with Tencent Cloud; supports deployment to Tencent Cloud CDN. |
| Qiniu Cloud | No | Yes | Qiniu Cloud does not offer domain registration services; supports deployment to Qiniu Cloud CDN. |
| Cloudflare | Yes | No | Supports domains registered with Cloudflare; Cloudflare services come with SSL certificates. |
| SSH | No | Yes | Supports deployment to SSH servers. |
| WEBHOOK | No | Yes | Supports callbacks to WEBHOOK. |
| 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 |
| Volcengine | √ | √ | Supports domains registered on Volcengine; supports deployment to Volcengine Live, 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
![login](https://i.imgur.com/SYjjbql.jpeg)
![dashboard](https://i.imgur.com/WMVbBId.jpeg)
![domains](https://i.imgur.com/8wit3ZA.jpeg)
![accesses](https://i.imgur.com/EWtOoJ0.jpeg)
![history](https://i.imgur.com/aaPtSW7.jpeg)
<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.
- 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.
@@ -110,23 +124,27 @@ Certimate will automatically add a TXT record for you; you only need to fill in
For example, if you purchased the domain from Alibaba Cloud, the authorization information would be as follows:
```bash
accessKeyId: xxx
accessKeySecret: TOKEN
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: xxx
secretKey: TOKEN
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?
@@ -139,7 +157,7 @@ Q: Data Security?
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.
> 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
@@ -147,14 +165,26 @@ Certimate is a free and open-source project, licensed under the [MIT License](LI
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.
- **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).
## 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.
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.
## Join the Community
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
* Wechat Group
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
- Wechat Group
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
## Star History
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@@ -1,10 +1,10 @@
version: '3.0'
version: "3.0"
services:
certimate:
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
container_name: certimate_server
ports:
certimate:
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
container_name: certimate_server
ports:
- 8090:8090
volumes:
volumes:
- ./data:/app/pb_data
restart: unless-stopped
restart: unless-stopped

155
go.mod
View File

@@ -1,26 +1,41 @@
module certimate
module github.com/usual2970/certimate
go 1.22
go 1.22.0
toolchain go1.22.5
toolchain go1.23.2
require (
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0
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.9
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
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/tea-utils/v2 v2.0.6
github.com/go-acme/lego/v4 v4.17.4
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.20.2
github.com/gojek/heimdall/v7 v7.0.3
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.120
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/nikoksr/notify v1.0.0
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/common v1.0.992
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.1034
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
golang.org/x/crypto v0.26.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030
github.com/volcengine/volc-sdk-golang v1.0.184
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
software.sslmate.com/src/go-pkcs12 v0.5.0
)
require (
@@ -28,65 +43,91 @@ require (
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.0 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // 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/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/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // 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
sigs.k8s.io/yaml v1.4.0 // indirect
)
require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2
github.com/alibabacloud-go/debug v1.0.0 // indirect
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/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 // indirect
github.com/aliyun/credentials-go v1.3.1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.47 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.28.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.42 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // 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.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.3 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect
github.com/aws/smithy-go v1.22.0 // 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.97.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cloudflare/cloudflare-go v0.108.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -97,40 +138,40 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/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.898 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1034 // 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.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/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.189.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/api v0.204.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // 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

962
go.sum
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/usual2970/certimate/internal/domain"
)
type aliyun struct {
@@ -19,12 +21,12 @@ func NewAliyun(option *ApplyOption) Applicant {
}
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

View File

@@ -1,15 +1,21 @@
package applicant
import (
"certimate/internal/utils/app"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"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"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
@@ -20,31 +26,41 @@ import (
)
const (
configTypeTencent = "tencent"
configTypeAliyun = "aliyun"
configTypeCloudflare = "cloudflare"
configTypeNamesilo = "namesilo"
configTypeGodaddy = "godaddy"
configTypeAliyun = "aliyun"
configTypeTencent = "tencent"
configTypeHuaweiCloud = "huaweicloud"
configTypeAws = "aws"
configTypeCloudflare = "cloudflare"
configTypeNamesilo = "namesilo"
configTypeGodaddy = "godaddy"
configTypePdns = "pdns"
configTypeHttpreq = "httpreq"
configTypeVolcengine = "volcengine"
)
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"`
@@ -55,25 +71,67 @@ type Certificate struct {
}
type ApplyOption struct {
Email string `json:"email"`
Domain string `json:"domain"`
Access string `json:"access"`
Nameservers string `json:"nameservers"`
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 MyUser struct {
type ApplyUser struct {
Ca string
Email string
Registration *registration.Resource
key crypto.PrivateKey
key string
}
func (u *MyUser) GetEmail() 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 MyUser) GetRegistration() *registration.Resource {
func (u ApplyUser) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
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
}
@@ -82,32 +140,60 @@ type Applicant interface {
}
func Get(record *models.Record) (Applicant, error) {
access := record.ExpandedOne("access")
email := record.GetString("email")
if email == "" {
email = defaultEmail
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: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
Nameservers: record.GetString("nameservers"),
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 configTypeTencent:
return NewTencent(option), nil
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
case configTypeVolcengine:
return NewVolcengine(option), nil
default:
return nil, errors.New("unknown config type")
}
}
type SSLProviderConfig struct {
@@ -116,10 +202,13 @@ type SSLProviderConfig struct {
}
type SSLProviderConfigContent struct {
Zerossl struct {
EabHmacKey string `json:"eabHmacKey"`
EabKid string `json:"eabKid"`
}
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) {
@@ -135,21 +224,20 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
}
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// 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))
myUser, err := newApplyUser(sslProvider.Provider, option.Email)
if err != nil {
return nil, err
}
myUser := MyUser{
Email: option.Email,
key: privateKey,
}
config := lego.NewConfig(&myUser)
config := lego.NewConfig(myUser)
// 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 = certcrypto.RSA2048
config.Certificate.KeyType = parseKeyAlgorithm(option.KeyAlgorithm)
// A client facilitates communication with the CA server.
client, err := lego.NewClient(config)
@@ -158,7 +246,7 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
challengeOptions := make([]dns01.ChallengeOption, 0)
nameservers := ParseNameservers(option.Nameservers)
nameservers := parseNameservers(option.Nameservers)
if len(nameservers) > 0 {
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(nameservers))
}
@@ -166,20 +254,15 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
client.Challenge.SetDNS01Provider(provider, challengeOptions...)
// New users will need to register
reg, err := getReg(client, sslProvider)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
}
myUser.Registration = reg
domains := []string{option.Domain}
// 如果是通配置符域名,把根域名也加入
if strings.HasPrefix(option.Domain, "*.") && len(strings.Split(option.Domain, ".")) == 3 {
rootDomain := strings.TrimPrefix(option.Domain, "*.")
domains = append(domains, rootDomain)
if !myUser.hasRegistration() {
reg, err := getReg(client, sslProvider, myUser)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
}
myUser.Registration = reg
}
domains := strings.Split(option.Domain, ";")
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
@@ -197,10 +280,18 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
IssuerCertificate: string(certificates.IssuerCertificate),
Csr: string(certificates.CSR),
}, nil
}
func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.Resource, error) {
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 {
@@ -210,31 +301,46 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
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 {
func parseNameservers(ns string) []string {
nameservers := make([]string, 0)
lines := strings.Split(ns, ";")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
@@ -244,3 +350,22 @@ func ParseNameservers(ns string) []string {
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
}
}

39
internal/applicant/aws.go Normal file
View File

@@ -0,0 +1,39 @@
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,11 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/usual2970/certimate/internal/domain"
)
type cloudflare struct {
@@ -23,6 +25,7 @@ func (c *cloudflare) Apply() (*Certificate, error) {
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 {

View File

@@ -1,11 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/usual2970/certimate/internal/domain"
)
type godaddy struct {
@@ -19,12 +21,12 @@ func NewGodaddy(option *ApplyOption) Applicant {
}
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 {

View File

@@ -0,0 +1,38 @@
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

@@ -0,0 +1,43 @@
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,11 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
"github.com/usual2970/certimate/internal/domain"
)
type namesilo struct {
@@ -19,11 +21,11 @@ func NewNamesilo(option *ApplyOption) Applicant {
}
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 {

View File

@@ -0,0 +1,36 @@
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

@@ -1,11 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/usual2970/certimate/internal/domain"
)
type tencent struct {
@@ -19,12 +21,13 @@ func NewTencent(option *ApplyOption) Applicant {
}
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

View File

@@ -0,0 +1,35 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
volcengineDns "github.com/go-acme/lego/v4/providers/dns/volcengine"
"github.com/usual2970/certimate/internal/domain"
)
type volcengine struct {
option *ApplyOption
}
func NewVolcengine(option *ApplyOption) Applicant {
return &volcengine{
option: option,
}
}
func (a *volcengine) Apply() (*Certificate, error) {
access := &domain.VolcengineAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyID)
os.Setenv("VOLC_SECRETKEY", access.SecretAccessKey)
os.Setenv("VOLC_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := volcengineDns.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -1,206 +0,0 @@
package deployer
import (
"certimate/internal/applicant"
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
cas20200407 "github.com/alibabacloud-go/cas-20200407/v2/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type aliyun struct {
client *cas20200407.Client
option *DeployerOption
infos []string
}
func NewAliyun(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &aliyun{
option: option,
infos: make([]string, 0),
}
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
if err != nil {
return nil, err
}
a.client = client
return a, nil
}
func (a *aliyun) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (a *aliyun) GetInfo() []string {
return a.infos
}
func (a *aliyun) Deploy(ctx context.Context) error {
// 查询有没有对应的资源
resource, err := a.resource()
if err != nil {
return err
}
a.infos = append(a.infos, toStr("查询对应的资源", resource))
// 查询有没有对应的联系人
contacts, err := a.contacts()
if err != nil {
return err
}
a.infos = append(a.infos, toStr("查询联系人", contacts))
// 上传证书
certId, err := a.uploadCert(&a.option.Certificate)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("上传证书", certId))
// 部署证书
jobId, err := a.deploy(resource, certId, contacts)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("创建部署证书任务", jobId))
// 等待部署成功
err = a.updateDeployStatus(*jobId)
if err != nil {
return err
}
// 部署成功后删除旧的证书
a.deleteCert(resource)
return nil
}
func (a *aliyun) updateDeployStatus(jobId int64) error {
// 查询部署状态
req := &cas20200407.UpdateDeploymentJobStatusRequest{
JobId: tea.Int64(jobId),
}
resp, err := a.client.UpdateDeploymentJobStatus(req)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("查询对应的资源", resp))
return nil
}
func (a *aliyun) deleteCert(resource *cas20200407.ListCloudResourcesResponseBodyData) error {
// 查询有没有对应的资源
if resource.CertId == nil {
return nil
}
// 删除证书
_, err := a.client.DeleteUserCertificate(&cas20200407.DeleteUserCertificateRequest{
CertId: resource.CertId,
})
if err != nil {
return err
}
return nil
}
func (a *aliyun) contacts() ([]*cas20200407.ListContactResponseBodyContactList, error) {
listContactRequest := &cas20200407.ListContactRequest{}
runtime := &util.RuntimeOptions{}
resp, err := a.client.ListContactWithOptions(listContactRequest, runtime)
if err != nil {
return nil, err
}
if resp.Body.TotalCount == nil {
return nil, errors.New("no contact found")
}
return resp.Body.ContactList, nil
}
func (a *aliyun) deploy(resource *cas20200407.ListCloudResourcesResponseBodyData, certId int64, contacts []*cas20200407.ListContactResponseBodyContactList) (*int64, error) {
contactIds := make([]string, 0, len(contacts))
for _, contact := range contacts {
contactIds = append(contactIds, fmt.Sprintf("%d", *contact.ContactId))
}
// 部署证书
createCloudResourceRequest := &cas20200407.CreateDeploymentJobRequest{
CertIds: tea.String(fmt.Sprintf("%d", certId)),
Name: tea.String(a.option.Domain + rand.RandStr(6)),
JobType: tea.String("user"),
ResourceIds: tea.String(fmt.Sprintf("%d", *resource.Id)),
ContactIds: tea.String(strings.Join(contactIds, ",")),
}
runtime := &util.RuntimeOptions{}
resp, err := a.client.CreateDeploymentJobWithOptions(createCloudResourceRequest, runtime)
if err != nil {
return nil, err
}
return resp.Body.JobId, nil
}
func (a *aliyun) uploadCert(cert *applicant.Certificate) (int64, error) {
uploadUserCertificateRequest := &cas20200407.UploadUserCertificateRequest{
Cert: &cert.Certificate,
Key: &cert.PrivateKey,
Name: tea.String(a.option.Domain + rand.RandStr(6)),
}
runtime := &util.RuntimeOptions{}
resp, err := a.client.UploadUserCertificateWithOptions(uploadUserCertificateRequest, runtime)
if err != nil {
return 0, err
}
return *resp.Body.CertId, nil
}
func (a *aliyun) createClient(accessKeyId, accessKeySecret string) (_result *cas20200407.Client, _err error) {
config := &openapi.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
config.Endpoint = tea.String("cas.aliyuncs.com")
_result = &cas20200407.Client{}
_result, _err = cas20200407.NewClient(config)
return _result, _err
}
func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, error) {
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
CloudProduct: tea.String(a.option.Product),
Keyword: tea.String(a.option.Domain),
}
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
if err != nil {
return nil, err
}
if *resp.Body.Total == 0 {
return nil, errors.New("no resource found")
}
return resp.Body.Data[0], nil
}

View File

@@ -0,0 +1,281 @@
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,81 +1,88 @@
package deployer
import (
"certimate/internal/domain"
"context"
"encoding/json"
"fmt"
"time"
cdn20180510 "github.com/alibabacloud-go/cdn-20180510/v5/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
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 AliyunCdn struct {
client *cdn20180510.Client
type AliyunCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *aliyunCdn.Client
}
func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
func NewAliyunCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &AliyunCdn{
option: option,
}
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
if err != nil {
return nil, err
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
return &AliyunCdn{
client: client,
option: option,
infos: make([]string, 0),
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 (a *AliyunCdn) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
func (d *AliyunCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (a *AliyunCdn) GetInfo() []string {
return a.infos
func (d *AliyunCDNDeployer) GetInfos() []string {
return d.infos
}
func (a *AliyunCdn) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(a.option.Domain),
CertName: tea.String(certName),
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(a.option.Certificate.Certificate),
SSLPri: tea.String(a.option.Certificate.PrivateKey),
CertRegion: tea.String("cn-hangzhou"),
SSLPub: tea.String(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
}
runtime := &util.RuntimeOptions{}
resp, err := a.client.SetCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime)
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
if err != nil {
return err
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'")
}
a.infos = append(a.infos, toStr("cdn设置证书", resp))
d.infos = append(d.infos, toStr("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp))
return nil
}
func (a *AliyunCdn) createClient(accessKeyId, accessKeySecret string) (_result *cdn20180510.Client, _err error) {
config := &openapi.Config{
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"),
}
config.Endpoint = tea.String("cdn.aliyuncs.com")
_result = &cdn20180510.Client{}
_result, _err = cdn20180510.NewClient(config)
return _result, _err
client, err := aliyunCdn.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,286 @@
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",
"cn-hangzhou-finance",
"cn-shanghai-finance-1",
"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 {
continue
}
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

@@ -0,0 +1,95 @@
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,86 +0,0 @@
/*
* @Author: Bin
* @Date: 2024-09-17
* @FilePath: /certimate/internal/deployer/aliyun_esa.go
*/
package deployer
import (
"certimate/internal/domain"
"context"
"encoding/json"
"fmt"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dcdn20180115 "github.com/alibabacloud-go/dcdn-20180115/v3/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type AliyunEsa struct {
client *dcdn20180115.Client
option *DeployerOption
infos []string
}
func NewAliyunEsa(option *DeployerOption) (*AliyunEsa, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &AliyunEsa{
option: option,
}
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
if err != nil {
return nil, err
}
return &AliyunEsa{
client: client,
option: option,
infos: make([]string, 0),
}, nil
}
func (a *AliyunEsa) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (a *AliyunEsa) GetInfo() []string {
return a.infos
}
func (a *AliyunEsa) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(a.option.Domain),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(a.option.Certificate.Certificate),
SSLPri: tea.String(a.option.Certificate.PrivateKey),
CertRegion: tea.String("cn-hangzhou"),
}
runtime := &util.RuntimeOptions{}
resp, err := a.client.SetDcdnDomainSSLCertificateWithOptions(setDcdnDomainSSLCertificateRequest, runtime)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("dcdn设置证书", resp))
return nil
}
func (a *AliyunEsa) createClient(accessKeyId, accessKeySecret string) (_result *dcdn20180115.Client, _err error) {
config := &openapi.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
config.Endpoint = tea.String("dcdn.aliyuncs.com")
_result = &dcdn20180115.Client{}
_result, _err = dcdn20180115.NewClient(config)
return _result, _err
}

View File

@@ -0,0 +1,245 @@
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

@@ -0,0 +1,86 @@
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

@@ -0,0 +1,80 @@
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,109 +1,106 @@
package deployer
import (
"certimate/internal/applicant"
"certimate/internal/utils/app"
"certimate/internal/utils/variables"
"bytes"
"context"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"strings"
"time"
"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"
)
const (
targetAliyunOss = "aliyun-oss"
targetAliyunCdn = "aliyun-cdn"
targetAliyunEsa = "aliyun-dcdn"
targetSSH = "ssh"
targetWebhook = "webhook"
targetTencentCdn = "tencent-cdn"
targetQiniuCdn = "qiniu-cdn"
targetLocal = "local"
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"
targetVolcengineLive = "volcengine-live"
targetVolcengineCDN = "volcengine-cdn"
)
type DeployerOption struct {
DomainId string `json:"domainId"`
Domain string `json:"domain"`
Product string `json:"product"`
Access string `json:"access"`
AceessRecord *models.Record `json:"-"`
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
GetInfo() []string
GetInfos() []string
GetID() string
}
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
rs := make([]Deployer, 0)
if record.GetString("targetAccess") != "" {
singleDeployer, err := Get(record, cert)
if err != nil {
return nil, err
}
rs = append(rs, singleDeployer)
if record.GetString("deployConfig") == "" {
return rs, nil
}
if record.GetString("group") != "" {
group := record.ExpandedOne("group")
if errs := app.GetApp().Dao().ExpandRecord(group, []string{"access"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
}
err := errors.Join(errList...)
return nil, err
}
records := group.ExpandedAll("access")
deployers, err := getByGroup(record, cert, records...)
if err != nil {
return nil, err
}
rs = append(rs, deployers...)
deployConfigs := make([]domain.DeployConfig, 0)
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
if err != nil {
return nil, fmt.Errorf("解析部署配置失败: %w", err)
}
return rs, nil
if len(deployConfigs) == 0 {
return rs, nil
}
}
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
rs := make([]Deployer, 0)
for _, access := range accesses {
deployer, err := getWithAccess(record, cert, access)
for _, deployConfig := range deployConfigs {
deployer, err := getWithDeployConfig(record, cert, deployConfig)
if err != nil {
return nil, err
}
rs = append(rs, deployer)
}
return rs, nil
}
func getWithAccess(record *models.Record, cert *applicant.Certificate, access *models.Record) (Deployer, error) {
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"),
Product: getProduct(record),
Access: access.GetString("config"),
AceessRecord: access,
Variables: variables.Parse2Map(record.GetString("variables")),
AccessRecord: access,
DeployConfig: deployConfig,
}
if cert != nil {
option.Certificate = *cert
@@ -114,42 +111,53 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
}
}
switch record.GetString("targetType") {
case targetAliyunOss:
return NewAliyun(option)
case targetAliyunCdn:
return NewAliyunCdn(option)
case targetAliyunEsa:
return NewAliyunEsa(option)
case targetSSH:
return NewSSH(option)
case targetWebhook:
return NewWebhook(option)
case targetTencentCdn:
return NewTencentCdn(option)
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 NewQiNiu(option)
return NewQiniuCDNDeployer(option)
case targetDogeCloudCdn:
return NewDogeCloudCDNDeployer(option)
case targetLocal:
return NewLocal(option), nil
return NewLocalDeployer(option)
case targetSSH:
return NewSSHDeployer(option)
case targetWebhook:
return NewWebhookDeployer(option)
case targetK8sSecret:
return NewK8sSecretDeployer(option)
case targetVolcengineLive:
return NewVolcengineLiveDeployer(option)
case targetVolcengineCDN:
return NewVolcengineCDNDeployer(option)
}
return nil, errors.New("not implemented")
}
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
access := record.ExpandedOne("targetAccess")
return getWithAccess(record, cert, access)
}
func getProduct(record *models.Record) string {
targetType := record.GetString("targetType")
rs := strings.Split(targetType, "-")
if len(rs) < 2 {
return ""
}
return rs[1]
return nil, errors.New("unsupported deploy target")
}
func toStr(tag string, data any) string {
@@ -159,3 +167,57 @@ func toStr(tag string, data any) string {
byts, _ := json.Marshal(data)
return tag + "" + string(byts)
}
func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
cert, err := x509.ParseCertificateFromPEM(certificate)
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
}
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
}

View File

@@ -0,0 +1,88 @@
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

@@ -0,0 +1,143 @@
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

@@ -0,0 +1,386 @@
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

@@ -0,0 +1,136 @@
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,106 +1,162 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
)
type localAccess struct {
Command string `json:"command"`
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
}
type local struct {
type LocalDeployer struct {
option *DeployerOption
infos []string
}
func NewLocal(option *DeployerOption) *local {
return &local{
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 (l *local) GetID() string {
return fmt.Sprintf("%s-%s", l.option.AceessRecord.GetString("name"), l.option.AceessRecord.Id)
func (d *LocalDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (l *local) GetInfo() []string {
func (d *LocalDeployer) GetInfos() []string {
return []string{}
}
func (l *local) Deploy(ctx context.Context) error {
access := &localAccess{}
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
return err
}
// 复制文件
if err := copyFile(l.option.Certificate.Certificate, access.CertPath); err != nil {
return fmt.Errorf("复制证书失败: %w", err)
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))
}
if err := copyFile(l.option.Certificate.PrivateKey, access.KeyPath); err != nil {
return fmt.Errorf("复制私钥失败: %w", err)
// 写入证书和私钥文件
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)
}
if err := execCmd(access.Command); err != nil {
return fmt.Errorf("执行命令失败: %w", err)
d.infos = append(d.infos, toStr("执行命令成功", stdout))
}
return nil
}
func execCmd(command string) error {
// 执行命令
func (d *LocalDeployer) execCommand(command string) (string, string, error) {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", command)
} else {
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")
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
var stdoutBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
err := cmd.Run()
if err != nil {
return fmt.Errorf("执行命令失败: %w", err)
return "", "", xerrors.Wrap(err, "failed to execute shell script")
}
return nil
}
func copyFile(content string, path string) error {
dir := filepath.Dir(path)
// 如果目录不存在,创建目录
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return fmt.Errorf("创建目录失败: %w", err)
}
// 创建或打开文件
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("创建文件失败: %w", err)
}
defer file.Close()
// 写入内容到文件
_, err = file.Write([]byte(content))
if err != nil {
return fmt.Errorf("写入文件失败: %w", err)
}
return nil
return stdoutBuf.String(), stderrBuf.String(), nil
}

View File

@@ -1,212 +0,0 @@
package deployer
import (
"bytes"
"certimate/internal/domain"
xhttp "certimate/internal/utils/http"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/qiniu/go-sdk/v7/auth"
)
const qiniuGateway = "http://api.qiniu.com"
type qiuniu struct {
option *DeployerOption
info []string
credentials *auth.Credentials
}
func NewQiNiu(option *DeployerOption) (*qiuniu, error) {
access := &domain.QiniuAccess{}
json.Unmarshal([]byte(option.Access), access)
return &qiuniu{
option: option,
info: make([]string, 0),
credentials: auth.New(access.AccessKey, access.SecretKey),
}, nil
}
func (a *qiuniu) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (q *qiuniu) GetInfo() []string {
return q.info
}
func (q *qiuniu) Deploy(ctx context.Context) error {
// 上传证书
certId, err := q.uploadCert()
if err != nil {
return fmt.Errorf("uploadCert failed: %w", err)
}
// 获取域名信息
domainInfo, err := q.getDomainInfo()
if err != nil {
return fmt.Errorf("getDomainInfo failed: %w", err)
}
// 判断域名是否启用 https
if domainInfo.Https != nil && domainInfo.Https.CertID != "" {
// 启用了 https
// 修改域名证书
err = q.modifyDomainCert(certId)
if err != nil {
return fmt.Errorf("modifyDomainCert failed: %w", err)
}
} else {
// 没启用 https
// 启用 https
err = q.enableHttps(certId)
if err != nil {
return fmt.Errorf("enableHttps failed: %w", err)
}
}
return nil
}
func (q *qiuniu) enableHttps(certId string) error {
path := fmt.Sprintf("/domain/%s/sslize", q.option.Domain)
body := &modifyDomainCertReq{
CertID: certId,
ForceHttps: true,
Http2Enable: true,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
return nil
}
type domainInfo struct {
Https *modifyDomainCertReq `json:"https"`
}
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
path := fmt.Sprintf("/domain/%s", q.option.Domain)
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
if err != nil {
return nil, fmt.Errorf("req failed: %w", err)
}
resp := &domainInfo{}
err = json.Unmarshal(res, resp)
if err != nil {
return nil, fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp, nil
}
type uploadCertReq struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
Pri string `json:"pri"`
Ca string `json:"ca"`
}
type uploadCertResp struct {
CertID string `json:"certID"`
}
func (q *qiuniu) uploadCert() (string, error) {
path := "/sslcert"
body := &uploadCertReq{
Name: q.option.Domain,
CommonName: q.option.Domain,
Pri: q.option.Certificate.PrivateKey,
Ca: q.option.Certificate.Certificate,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", fmt.Errorf("json.Marshal failed: %w", err)
}
res, err := q.req(qiniuGateway+path, http.MethodPost, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("req failed: %w", err)
}
resp := &uploadCertResp{}
err = json.Unmarshal(res, resp)
if err != nil {
return "", fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp.CertID, nil
}
type modifyDomainCertReq struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
}
func (q *qiuniu) modifyDomainCert(certId string) error {
path := fmt.Sprintf("/domain/%s/httpsconf", q.option.Domain)
body := &modifyDomainCertReq{
CertID: certId,
ForceHttps: true,
Http2Enable: true,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("json.Marshal failed: %w", err)
}
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("req failed: %w", err)
}
return nil
}
func (q *qiuniu) req(url, method string, body io.Reader) ([]byte, error) {
req := xhttp.BuildReq(url, method, body, map[string]string{
"Content-Type": "application/json",
})
if err := q.credentials.AddToken(auth.TokenQBox, req); err != nil {
return nil, fmt.Errorf("credentials.AddToken failed: %w", err)
}
respBody, err := xhttp.ToRequest(req)
if err != nil {
return nil, fmt.Errorf("ToRequest failed: %w", err)
}
defer respBody.Close()
res, err := io.ReadAll(respBody)
if err != nil {
return nil, fmt.Errorf("io.ReadAll failed: %w", err)
}
return res, nil
}

View File

@@ -0,0 +1,113 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
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))
// 在七牛 CDN 中泛域名表示为 .example.com需去除前缀星号
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domain = strings.TrimPrefix(domain, "*")
}
// 获取域名信息
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
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,86 +0,0 @@
package deployer
import (
"certimate/internal/applicant"
"testing"
"github.com/qiniu/go-sdk/v7/auth"
)
func Test_qiuniu_uploadCert(t *testing.T) {
type fields struct {
option *DeployerOption
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "test",
fields: fields{
option: &DeployerOption{
DomainId: "1",
Domain: "example.com",
Product: "test",
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
Certificate: applicant.Certificate{
Certificate: "",
PrivateKey: "",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, _ := NewQiNiu(tt.fields.option)
got, err := q.uploadCert()
if (err != nil) != tt.wantErr {
t.Errorf("qiuniu.uploadCert() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("qiuniu.uploadCert() = %v, want %v", got, tt.want)
}
})
}
}
func Test_qiuniu_modifyDomainCert(t *testing.T) {
type fields struct {
option *DeployerOption
info []string
credentials *auth.Credentials
}
type args struct {
certId string
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "test",
fields: fields{
option: &DeployerOption{
DomainId: "1",
Domain: "jt1.ikit.fun",
Product: "test",
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, _ := NewQiNiu(tt.fields.option)
if err := q.modifyDomainCert(tt.args.certId); (err != nil) != tt.wantErr {
t.Errorf("qiuniu.modifyDomainCert() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -4,163 +4,205 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
xpath "path"
"strings"
"path/filepath"
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
sshPkg "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/domain"
)
type ssh struct {
type SSHDeployer struct {
option *DeployerOption
infos []string
}
type sshAccess struct {
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
Key string `json:"key"`
Port string `json:"port"`
PreCommand string `json:"preCommand"`
Command string `json:"command"`
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
}
func NewSSH(option *DeployerOption) (Deployer, error) {
return &ssh{
func NewSSHDeployer(option *DeployerOption) (Deployer, error) {
return &SSHDeployer{
option: option,
infos: make([]string, 0),
}, nil
}
func (a *ssh) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
func (d *SSHDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (s *ssh) GetInfo() []string {
return s.infos
func (d *SSHDeployer) GetInfos() []string {
return d.infos
}
func (s *ssh) Deploy(ctx context.Context) error {
access := &sshAccess{}
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
func (d *SSHDeployer) Deploy(ctx context.Context) error {
access := &domain.SSHAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return err
}
// 将证书路径和命令中的变量替换为实际值
for k, v := range s.option.Variables {
key := fmt.Sprintf("${%s}", k)
access.CertPath = strings.ReplaceAll(access.CertPath, key, v)
access.KeyPath = strings.ReplaceAll(access.KeyPath, key, v)
access.Command = strings.ReplaceAll(access.Command, key, v)
access.PreCommand = strings.ReplaceAll(access.PreCommand, key, v)
}
// 连接
client, err := s.getClient(access)
client, err := d.createSshClient(access)
if err != nil {
return err
}
defer client.Close()
s.infos = append(s.infos, toStr("ssh连接成功", nil))
d.infos = append(d.infos, toStr("SSH 连接成功", nil))
// 执行前置命令
if access.PreCommand != "" {
err, stdout, stderr := s.sshExecCommand(client, access.PreCommand)
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
if preCommand != "" {
stdout, stderr, err := d.sshExecCommand(client, preCommand)
if err != nil {
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
return xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout))
}
// 上传证书
if err := s.upload(client, s.option.Certificate.Certificate, access.CertPath); err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
// 上传证书和私钥文件
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")
}
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
// 上传私钥
if err := s.upload(client, s.option.Certificate.PrivateKey, access.KeyPath); err != nil {
return fmt.Errorf("failed to upload private key: %w", err)
}
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
// 执行命令
err, stdout, stderr := s.sshExecCommand(client, access.Command)
if err != nil {
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}
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)
}
s.infos = append(s.infos, toStr("ssh执行命令成功", stdout))
d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout))
}
return nil
}
func (s *ssh) sshExecCommand(client *sshPkg.Client, command string) (error, string, string) {
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("failed to create ssh session: %w", err), "", ""
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)
return err, stdoutBuf.String(), stderrBuf.String()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute ssh script")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}
func (s *ssh) upload(client *sshPkg.Client, content, path string) error {
func (d *SSHDeployer) writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
return d.writeSftpFile(sshCli, path, []byte(content))
}
sftpCli, err := sftp.NewClient(client)
func (d *SSHDeployer) writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return fmt.Errorf("failed to create sftp client: %w", err)
return xerrors.Wrap(err, "failed to create sftp client")
}
defer sftpCli.Close()
if err := sftpCli.MkdirAll(xpath.Dir(path)); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
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 fmt.Errorf("failed to open remote file: %w", err)
return xerrors.Wrap(err, "failed to open remote file")
}
defer file.Close()
_, err = file.Write([]byte(content))
_, err = file.Write(data)
if err != nil {
return fmt.Errorf("failed to write to remote file: %w", err)
return xerrors.Wrap(err, "failed to write to remote file")
}
return nil
}
func (s *ssh) getClient(access *sshAccess) (*sshPkg.Client, error) {
var authMethod sshPkg.AuthMethod
if access.Key != "" {
signer, err := sshPkg.ParsePrivateKey([]byte(access.Key))
if err != nil {
return nil, err
}
authMethod = sshPkg.PublicKeys(signer)
} else {
authMethod = sshPkg.Password(access.Password)
}
return sshPkg.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &sshPkg.ClientConfig{
User: access.Username,
Auth: []sshPkg.AuthMethod{
authMethod,
},
HostKeyCallback: sshPkg.InsecureIgnoreHostKey(),
})
}

View File

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

View File

@@ -1,108 +1,194 @@
package deployer
import (
"certimate/internal/domain"
"certimate/internal/utils/rand"
"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"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
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 tencentCdn struct {
option *DeployerOption
credential *common.Credential
infos []string
type TencentCDNDeployer struct {
option *DeployerOption
infos []string
sdkClients *tencentCDNDeployerSdkClients
sslUploader uploader.Uploader
}
func NewTencentCdn(option *DeployerOption) (Deployer, error) {
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, fmt.Errorf("failed to unmarshal tencent access: %w", err)
return nil, xerrors.Wrap(err, "failed to get access")
}
credential := common.NewCredential(
clients, err := (&TencentCDNDeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
return &tencentCdn{
option: option,
credential: credential,
infos: make([]string, 0),
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 (a *tencentCdn) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
func (d *TencentCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (t *tencentCdn) GetInfo() []string {
return t.infos
func (d *TencentCDNDeployer) GetInfos() []string {
return d.infos
}
func (t *tencentCdn) Deploy(ctx context.Context) error {
// 上传证书
certId, err := t.uploadCert()
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 fmt.Errorf("failed to upload certificate: %w", err)
return err
}
t.infos = append(t.infos, toStr("上传证书", certId))
if err := t.deploy(certId); err != nil {
return fmt.Errorf("failed to deploy: %w", 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 (t *tencentCdn) uploadCert() (string, error) {
func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
client, _ := ssl.NewClient(t.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(t.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(t.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(t.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(true)
response, err := client.UploadCertificate(request)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
return nil, err
}
return *response.Response.CertificateId, nil
}
func (t *tencentCdn) deploy(certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(t.credential, "", cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.InstanceIdList = common.StringPtrs([]string{t.option.Domain})
request.ResourceType = common.StringPtr("cdn")
request.Status = common.Int64Ptr(1)
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
return nil, err
}
t.infos = append(t.infos, toStr("部署证书", resp.Response))
return nil
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

@@ -0,0 +1,328 @@
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, region, 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

@@ -0,0 +1,107 @@
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,
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 &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, region string) (*tcSsl.Client, error) {
credential := common.NewCredential(secretId, secretKey)
client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,154 @@
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

@@ -0,0 +1,119 @@
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

@@ -0,0 +1,116 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
volcenginecdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/volcengine/volc-sdk-golang/service/cdn"
)
type VolcengineCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *cdn.CDN
sslUploader uploader.Uploader
}
func NewVolcengineCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.VolcengineAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := cdn.NewInstance()
client.Client.SetAccessKey(access.AccessKeyID)
client.Client.SetSecretKey(access.SecretAccessKey)
uploader, err := volcenginecdn.New(&volcenginecdn.VolcengineCDNUploaderConfig{
AccessKeyId: access.AccessKeyID,
AccessKeySecret: access.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcengineCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcengineCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *VolcengineCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *VolcengineCDNDeployer) Deploy(ctx context.Context) error {
apiCtx := context.Background()
// 上传证书
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
domains := make([]string, 0)
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(configDomain, "*.") {
// 获取证书可以部署的域名
// REF: https://www.volcengine.com/docs/6454/125711
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
}
for i := range describeCertConfigResp.Result.CertNotConfig {
// 当前未启用 HTTPS 的加速域名列表。
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.OtherCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有匹配的域名都配置了该证书,跳过部署
return nil
} else {
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
}
}
} else {
domains = append(domains, configDomain)
}
// 部署证书
// REF: https://www.volcengine.com/docs/6454/125712
for i := range domains {
batchDeployCertReq := &cdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domains[i],
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
} else {
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp))
}
}
return nil
}

View File

@@ -0,0 +1,148 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
volcenginelive "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-live"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/volcengine/volc-sdk-golang/base"
live "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
)
type VolcengineLiveDeployer struct {
option *DeployerOption
infos []string
sdkClient *live.Live
sslUploader uploader.Uploader
}
func NewVolcengineLiveDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.VolcengineAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := live.NewInstance()
client.SetCredential(base.Credentials{
AccessKeyID: access.AccessKeyID,
SecretAccessKey: access.SecretAccessKey,
})
uploader, err := volcenginelive.New(&volcenginelive.VolcengineLiveUploaderConfig{
AccessKeyId: access.AccessKeyID,
AccessKeySecret: access.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcengineLiveDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcengineLiveDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *VolcengineLiveDeployer) GetInfos() []string {
return d.infos
}
func (d *VolcengineLiveDeployer) Deploy(ctx context.Context) error {
apiCtx := context.Background()
// 上传证书
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
domains := make([]string, 0)
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(configDomain, "*.") {
// 如果是泛域名,获取所有的域名并匹配
matchDomains, err := d.getDomainsByWildcardDomain(apiCtx, configDomain)
if err != nil {
d.infos = append(d.infos, toStr("获取域名列表失败", upres))
return xerrors.Wrap(err, "failed to execute sdk request 'live.ListDomainDetail'")
}
if len(matchDomains) == 0 {
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
}
domains = matchDomains
} else {
domains = append(domains, configDomain)
}
// 部署证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6d
for i := range domains {
bindCertReq := &live.BindCertBody{
ChainID: upres.CertId,
Domain: domains[i],
HTTPS: cast.BoolPtr(true),
}
bindCertResp, err := d.sdkClient.BindCert(apiCtx, bindCertReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'live.BindCert'")
} else {
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), bindCertResp))
}
}
return nil
}
func (d *VolcengineLiveDeployer) getDomainsByWildcardDomain(ctx context.Context, wildcardDomain string) ([]string, error) {
pageNum := int32(1)
searchTotal := 0
domains := make([]string, 0)
for {
listDomainDetailReq := &live.ListDomainDetailBody{
PageNum: pageNum,
PageSize: 1000,
}
// 查询域名列表
// REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8
listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq)
if err != nil {
return domains, err
}
if listDomainDetailResp.Result.DomainList != nil {
for _, item := range listDomainDetailResp.Result.DomainList {
if matchWildcardDomain(item.Domain, wildcardDomain) {
domains = append(domains, item.Domain)
}
}
}
searchTotal += len(listDomainDetailResp.Result.DomainList)
if int(listDomainDetailResp.Result.Total) > searchTotal {
pageNum++
} else {
break
}
}
return domains, nil
}
func matchWildcardDomain(domain, wildcardDomain string) bool {
if strings.HasPrefix(wildcardDomain, "*.") {
if "*."+domain == wildcardDomain {
return true
}
regexPattern := "^([a-zA-Z0-9_-]+)\\." + regexp.QuoteMeta(wildcardDomain[2:]) + "$"
regex := regexp.MustCompile(regexPattern)
return regex.MatchString(domain)
}
return domain == wildcardDomain
}

View File

@@ -2,66 +2,65 @@ package deployer
import (
"bytes"
xhttp "certimate/internal/utils/http"
"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 webhookAccess struct {
Url string `json:"url"`
}
type hookData struct {
Domain string `json:"domain"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
}
type webhook struct {
type WebhookDeployer struct {
option *DeployerOption
infos []string
}
func NewWebhook(option *DeployerOption) (Deployer, error) {
return &webhook{
func NewWebhookDeployer(option *DeployerOption) (Deployer, error) {
return &WebhookDeployer{
option: option,
infos: make([]string, 0),
}, nil
}
func (a *webhook) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
func (d *WebhookDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (w *webhook) GetInfo() []string {
return w.infos
func (d *WebhookDeployer) GetInfos() []string {
return d.infos
}
func (w *webhook) Deploy(ctx context.Context) error {
access := &webhookAccess{}
if err := json.Unmarshal([]byte(w.option.Access), access); err != nil {
return fmt.Errorf("failed to parse hook access config: %w", err)
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 := &hookData{
Domain: w.option.Domain,
Certificate: w.option.Certificate.Certificate,
PrivateKey: w.option.Certificate.PrivateKey,
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 fmt.Errorf("failed to send hook request: %w", err)
return xerrors.Wrap(err, "failed to send webhook request")
}
w.infos = append(w.infos, toStr("webhook response", string(resp)))
d.infos = append(d.infos, toStr("Webhook Response", string(resp)))
return nil
}

View File

@@ -10,6 +10,24 @@ type TencentAccess struct {
SecretKey string `json:"secretKey"`
}
type HuaweiCloudAccess struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region"`
}
type BaiduCloudAccess 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 CloudflareAccess struct {
DnsApiToken string `json:"dnsApiToken"`
}
@@ -19,6 +37,11 @@ type QiniuAccess struct {
SecretKey string `json:"secretKey"`
}
type DogeCloudAccess struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type NameSiloAccess struct {
ApiKey string `json:"apiKey"`
}
@@ -28,3 +51,38 @@ type GodaddyAccess struct {
ApiSecret string `json:"apiSecret"`
}
type PdnsAccess struct {
ApiUrl string `json:"apiUrl"`
ApiKey string `json:"apiKey"`
}
type VolcengineAccess struct {
AccessKeyID string
SecretAccessKey string
}
type HttpreqAccess struct {
Endpoint string `json:"endpoint"`
Mode string `json:"mode"`
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 WebhookAccess struct {
Url string `json:"url"`
}
type KubernetesAccess struct {
KubeConfig string `json:"kubeConfig"`
}

View File

@@ -0,0 +1,17 @@
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
}

144
internal/domain/domains.go Normal file
View File

@@ -0,0 +1,144 @@
package domain
import (
"encoding/json"
"strings"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
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 maps.GetValueAsString(dc.Config, key)
}
// 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
}
// 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
return maps.GetValueAsInt32(dc.Config, key)
}
// 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
}
// 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
return maps.GetValueAsBool(dc.Config, key)
}
// 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
// - defaultValue: 默认值。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
return maps.GetValueOrDefaultAsBool(dc.Config, key, 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"`
}

23
internal/domain/err.go Normal file
View File

@@ -0,0 +1,23 @@
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
}

15
internal/domain/notify.go Normal file
View File

@@ -0,0 +1,15 @@
package domain
const (
NotifyChannelEmail = "email"
NotifyChannelWebhook = "webhook"
NotifyChannelDingtalk = "dingtalk"
NotifyChannelLark = "lark"
NotifyChannelTelegram = "telegram"
NotifyChannelServerChan = "serverchan"
NotifyChannelBark = "bark"
)
type NotifyTestPushReq struct {
Channel string `json:"channel"`
}

View File

@@ -0,0 +1,31 @@
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

@@ -1,15 +1,15 @@
package domains
import (
"certimate/internal/applicant"
"certimate/internal/deployer"
"certimate/internal/utils/app"
"context"
"errors"
"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
@@ -41,18 +41,6 @@ func deploy(ctx context.Context, record *models.Record) error {
return err
}
history.record(checkPhase, "获取记录成功", nil)
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess", "group"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
}
err = errors.Join(errList...)
app.GetApp().Logger().Error("展开记录失败", "err", err)
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
return err
}
history.record(checkPhase, "获取授权信息成功", nil)
cert := currRecord.GetString("certificate")
expiredAt := currRecord.GetDateTime("expiredAt").Time()
@@ -106,15 +94,22 @@ func deploy(ctx context.Context, record *models.Record) error {
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.GetInfo()})
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfos()})
return err
}
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
Info: deployer.GetInfo(),
Info: deployer.GetInfos(),
}, false)
}

View File

@@ -1,11 +1,12 @@
package domains
import (
"certimate/internal/utils/app"
"context"
"fmt"
"github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/utils/app"
)
func create(ctx context.Context, record *models.Record) error {
@@ -19,7 +20,6 @@ func create(ctx context.Context, record *models.Record) error {
app.GetApp().Logger().Error("deploy failed", "err", err)
}
}()
}
scheduler := app.GetScheduler()
@@ -27,7 +27,6 @@ func create(ctx context.Context, record *models.Record) error {
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)
@@ -46,7 +45,6 @@ func update(ctx context.Context, record *models.Record) error {
}
if record.GetBool("rightnow") {
go func() {
if err := deploy(ctx, record); err != nil {
app.GetApp().Logger().Error("deploy failed", "err", err)
@@ -57,7 +55,6 @@ func update(ctx context.Context, record *models.Record) error {
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)

View File

@@ -1,9 +1,9 @@
package domains
import (
"certimate/internal/utils/app"
"github.com/pocketbase/pocketbase/core"
"github.com/usual2970/certimate/internal/utils/app"
)
const tableName = "domains"

View File

@@ -1,12 +1,13 @@
package domains
import (
"certimate/internal/applicant"
"certimate/internal/utils/app"
"certimate/internal/utils/xtime"
"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 {
@@ -62,7 +63,6 @@ func (a *history) record(phase Phase, msg string, info *RecordInfo, pass ...bool
Info: info.Info,
Time: xtime.BeijingTimeStr(),
})
}
func (a *history) setCert(cert *applicant.Certificate) {

View File

@@ -1,9 +1,10 @@
package domains
import (
"certimate/internal/notify"
"certimate/internal/utils/app"
"context"
"github.com/usual2970/certimate/internal/notify"
"github.com/usual2970/certimate/internal/utils/app"
)
func InitSchedule() {
@@ -34,5 +35,4 @@ func InitSchedule() {
// 启动定时任务
app.GetScheduler().Start()
app.GetApp().Logger().Info("定时任务启动成功", "total", app.GetScheduler().Total())
}

View File

@@ -1,29 +1,24 @@
package notify
import (
"certimate/internal/utils/app"
"certimate/internal/utils/xtime"
"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},请保持关注!"
defaultExpireSubject = "您有 {COUNT} 张证书即将过期"
defaultExpireMessage = "有 {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 {
@@ -33,15 +28,14 @@ func PushExpireMsg() {
// 组装消息
msg := buildMsg(records)
if msg == nil {
return
}
if err := Send(msg.subject, msg.message); err != nil {
// 发送通知
if err := SendToAllChannels(msg.Subject, msg.Message); err != nil {
app.GetApp().Logger().Error("send expire msg", "error", err)
}
}
type notifyTemplates struct {
@@ -53,22 +47,27 @@ type notifyTemplate struct {
Content string `json:"content"`
}
func buildMsg(records []*models.Record) *msg {
type notifyMessage struct {
Subject string
Message string
}
func buildMsg(records []*models.Record) *notifyMessage {
if len(records) == 0 {
return nil
}
// 查询模板信息
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
title := defaultExpireSubject
content := defaultExpireMsg
subject := defaultExpireSubject
message := defaultExpireMessage
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
subject = templates.NotifyTemplates[0].Title
message = templates.NotifyTemplates[0].Content
}
}
@@ -81,18 +80,17 @@ func buildMsg(records []*models.Record) *msg {
}
countStr := strconv.Itoa(count)
domainStr := strings.Join(domains, ",")
domainStr := strings.Join(domains, ";")
title = strings.ReplaceAll(title, "{COUNT}", countStr)
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr)
subject = strings.ReplaceAll(subject, "{COUNT}", countStr)
subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr)
content = strings.ReplaceAll(content, "{COUNT}", countStr)
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr)
message = strings.ReplaceAll(message, "{COUNT}", countStr)
message = strings.ReplaceAll(message, "{DOMAINS}", domainStr)
// 返回消息
return &msg{
subject: title,
message: content,
return &notifyMessage{
Subject: subject,
Message: message,
}
}

View File

@@ -0,0 +1,66 @@
package notify
import (
"errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
notifierBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
notifierDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
notifierLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
notifierServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
notifierTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
notifierWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) {
switch channel {
case domain.NotifyChannelEmail:
return notifierEmail.New(&notifierEmail.EmailNotifierConfig{
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
Username: maps.GetValueOrDefaultAsString(channelConfig, "username", maps.GetValueAsString(channelConfig, "senderAddress")),
Password: maps.GetValueAsString(channelConfig, "password"),
SenderAddress: maps.GetValueAsString(channelConfig, "senderAddress"),
ReceiverAddress: maps.GetValueAsString(channelConfig, "receiverAddress"),
})
case domain.NotifyChannelWebhook:
return notifierWebhook.New(&notifierWebhook.WebhookNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelDingtalk:
return notifierDingTalk.New(&notifierDingTalk.DingTalkNotifierConfig{
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
Secret: maps.GetValueAsString(channelConfig, "secret"),
})
case domain.NotifyChannelLark:
return notifierLark.New(&notifierLark.LarkNotifierConfig{
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
})
case domain.NotifyChannelTelegram:
return notifierTelegram.New(&notifierTelegram.TelegramNotifierConfig{
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
})
case domain.NotifyChannelServerChan:
return notifierServerChan.New(&notifierServerChan.ServerChanNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelBark:
return notifierBark.New(&notifierBark.BarkNotifierConfig{
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
})
}
return nil, errors.New("unsupported notifier channel")
}

View File

@@ -1,29 +1,18 @@
package notify
import (
"certimate/internal/utils/app"
"context"
"fmt"
"strconv"
notifyPackage "github.com/nikoksr/notify"
"golang.org/x/sync/errgroup"
"github.com/nikoksr/notify/service/dingding"
"github.com/nikoksr/notify/service/telegram"
"github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
"github.com/usual2970/certimate/internal/utils/app"
)
const (
notifyChannelDingtalk = "dingtalk"
notifyChannelWebhook = "webhook"
notifyChannelTelegram = "telegram"
)
func Send(title, content string) error {
// 获取所有的推送渠道
notifiers, err := getNotifiers()
func SendToAllChannels(subject, message string) error {
notifiers, err := getEnabledNotifiers()
if err != nil {
return err
}
@@ -31,100 +20,56 @@ 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
}
func getNotifiers() ([]notifyPackage.Notifier, error) {
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
notifier, err := createNotifier(channel, channelConfig)
if err != nil {
return err
}
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
_, err = notifier.Notify(context.Background(), subject, message)
return err
}
func getEnabledNotifiers() ([]notifier.Notifier, error) {
settings, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='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 := settings.UnmarshalJSONField("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 !maps.GetValueAsBool(v, "enabled") {
continue
}
switch k {
case notifyChannelTelegram:
temp := getTelegramNotifier(v)
if temp == nil {
continue
}
notifiers = append(notifiers, temp)
case notifyChannelDingtalk:
notifiers = append(notifiers, getDingTalkNotifier(v))
case notifyChannelWebhook:
notifiers = append(notifiers, getWebhookNotifier(v))
notifier, err := createNotifier(k, v)
if err != nil {
continue
}
notifiers = append(notifiers, notifier)
}
return notifiers, nil
}
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 getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier {
return dingding.New(&dingding.Config{
Token: getString(conf, "accessToken"),
Secret: getString(conf, "secret"),
})
}
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,41 @@
package notify
import (
"context"
"fmt"
"github.com/usual2970/certimate/internal/domain"
)
const (
notifyTestTitle = "测试通知"
notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。"
)
type SettingRepository interface {
GetByName(ctx context.Context, name string) (*domain.Setting, error)
}
type NotifyService struct {
settingRepo SettingRepository
}
func NewNotifyService(settingRepo SettingRepository) *NotifyService {
return &NotifyService{
settingRepo: settingRepo,
}
}
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
if err != nil {
return fmt.Errorf("failed to get notify channels settings: %w", err)
}
channelConfig, err := setting.GetChannelContent(req.Channel)
if err != nil {
return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
}
return SendToChannel(notifyTestTitle, notifyTestBody, req.Channel, channelConfig)
}

View File

@@ -0,0 +1,23 @@
package notifier
import "context"
// 表示定义消息通知器的抽象类型接口。
type Notifier interface {
// 发送通知。
//
// 入参:
// - ctx上下文。
// - subject通知主题。
// - message通知内容。
//
// 出参:
// - res发送结果。
// - err: 错误。
Notify(ctx context.Context, subject string, message string) (res *NotifyResult, err error)
}
// 表示通知发送结果的数据结构。
type NotifyResult struct {
NotificationData map[string]any `json:"notificationData,omitempty"`
}

View File

@@ -0,0 +1,48 @@
package bark
import (
"context"
"errors"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type BarkNotifierConfig struct {
ServerUrl string `json:"serverUrl"`
DeviceKey string `json:"deviceKey"`
}
type BarkNotifier struct {
config *BarkNotifierConfig
}
var _ notifier.Notifier = (*BarkNotifier)(nil)
func New(config *BarkNotifierConfig) (*BarkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &BarkNotifier{
config: config,
}, nil
}
func (n *BarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
var srv notify.Notifier
if n.config.ServerUrl == "" {
srv = bark.New(n.config.DeviceKey)
} else {
srv = bark.NewWithServers(n.config.DeviceKey, n.config.ServerUrl)
}
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,45 @@
package dingtalk
import (
"context"
"errors"
"github.com/nikoksr/notify/service/dingding"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type DingTalkNotifierConfig struct {
AccessToken string `json:"accessToken"`
Secret string `json:"secret"`
}
type DingTalkNotifier struct {
config *DingTalkNotifierConfig
}
var _ notifier.Notifier = (*DingTalkNotifier)(nil)
func New(config *DingTalkNotifierConfig) (*DingTalkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &DingTalkNotifier{
config: config,
}, nil
}
func (n *DingTalkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := dingding.New(&dingding.Config{
Token: n.config.AccessToken,
Secret: n.config.Secret,
})
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,95 @@
package email
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/smtp"
"github.com/domodwyer/mailyak/v3"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type EmailNotifierConfig struct {
SmtpHost string `json:"smtpHost"`
SmtpPort int32 `json:"smtpPort"`
SmtpTLS bool `json:"smtpTLS"`
Username string `json:"username"`
Password string `json:"password"`
SenderAddress string `json:"senderAddress"`
ReceiverAddress string `json:"receiverAddress"`
}
type EmailNotifier struct {
config *EmailNotifierConfig
}
var _ notifier.Notifier = (*EmailNotifier)(nil)
func New(config *EmailNotifierConfig) (*EmailNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &EmailNotifier{
config: config,
}, nil
}
func (n *EmailNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
var smtpAuth smtp.Auth
if n.config.Username != "" || n.config.Password != "" {
smtpAuth = smtp.PlainAuth("", n.config.Username, n.config.Password, n.config.SmtpHost)
}
var smtpAddr string
if n.config.SmtpPort == 0 {
if n.config.SmtpTLS {
smtpAddr = fmt.Sprintf("%s:465", n.config.SmtpHost)
} else {
smtpAddr = fmt.Sprintf("%s:25", n.config.SmtpHost)
}
} else {
smtpAddr = fmt.Sprintf("%s:%d", n.config.SmtpHost, n.config.SmtpPort)
}
var yak *mailyak.MailYak
if n.config.SmtpTLS {
yak, err = mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig())
if err != nil {
return nil, err
}
} else {
yak = mailyak.New(smtpAddr, smtpAuth)
}
yak.From(n.config.SenderAddress)
yak.To(n.config.ReceiverAddress)
yak.Subject(subject)
yak.Plain().Set(message)
err = yak.Send()
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}
func newTlsConfig() *tls.Config {
var suiteIds []uint16
for _, suite := range tls.CipherSuites() {
suiteIds = append(suiteIds, suite.ID)
}
for _, suite := range tls.InsecureCipherSuites() {
suiteIds = append(suiteIds, suite.ID)
}
// 为兼容国内部分低版本 TLS 的 SMTP 服务商
return &tls.Config{
MinVersion: tls.VersionTLS10,
CipherSuites: suiteIds,
}
}

View File

@@ -0,0 +1,51 @@
package email_test
import (
"os"
"strconv"
"testing"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
)
/*
Shell command to run this test:
CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com" \
go test -v -run TestNotify email_test.go
*/
func TestNotify(t *testing.T) {
smtpPort, err := strconv.ParseInt(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPPORT"), 10, 32)
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
smtpTLS, err := strconv.ParseBool(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPTLS"))
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
res, err := notifierEmail.New(&notifierEmail.EmailNotifierConfig{
SmtpHost: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPHOST"),
SmtpPort: int32(smtpPort),
SmtpTLS: smtpTLS,
Username: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_USERNAME"),
Password: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_PASSWORD"),
SenderAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS"),
ReceiverAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS"),
})
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
t.Logf("notify result: %v", res)
}

View File

@@ -0,0 +1,41 @@
package lark
import (
"context"
"errors"
"github.com/nikoksr/notify/service/lark"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type LarkNotifierConfig struct {
WebhookUrl string `json:"webhookUrl"`
}
type LarkNotifier struct {
config *LarkNotifierConfig
}
var _ notifier.Notifier = (*LarkNotifier)(nil)
func New(config *LarkNotifierConfig) (*LarkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &LarkNotifier{
config: config,
}, nil
}
func (n *LarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := lark.NewWebhookService(n.config.WebhookUrl)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,55 @@
package serverchan
import (
"context"
"errors"
"net/http"
notifyHttp "github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type ServerChanNotifierConfig struct {
Url string `json:"url"`
}
type ServerChanNotifier struct {
config *ServerChanNotifierConfig
}
var _ notifier.Notifier = (*ServerChanNotifier)(nil)
func New(config *ServerChanNotifierConfig) (*ServerChanNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &ServerChanNotifier{
config: config,
}, nil
}
func (n *ServerChanNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := notifyHttp.New()
srv.AddReceivers(&notifyHttp.Webhook{
URL: n.config.Url,
Header: http.Header{},
ContentType: "application/json",
Method: http.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]string{
"text": subject,
"desp": message,
}
},
})
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,47 @@
package telegram
import (
"context"
"errors"
"github.com/nikoksr/notify/service/telegram"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type TelegramNotifierConfig struct {
ApiToken string `json:"apiToken"`
ChatId int64 `json:"chatId"`
}
type TelegramNotifier struct {
config *TelegramNotifierConfig
}
var _ notifier.Notifier = (*TelegramNotifier)(nil)
func New(config *TelegramNotifierConfig) (*TelegramNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &TelegramNotifier{
config: config,
}, nil
}
func (n *TelegramNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv, err := telegram.New(n.config.ApiToken)
if err != nil {
return nil, err
}
srv.AddReceivers(n.config.ChatId)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,43 @@
package webhook
import (
"context"
"errors"
"github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type WebhookNotifierConfig struct {
Url string `json:"url"`
}
type WebhookNotifier struct {
config *WebhookNotifierConfig
}
var _ notifier.Notifier = (*WebhookNotifier)(nil)
func New(config *WebhookNotifierConfig) (*WebhookNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &WebhookNotifier{
config: config,
}, nil
}
func (n *WebhookNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := http.New()
srv.AddReceiversURLs(n.config.Url)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,167 @@
package aliyuncas
import (
"context"
"errors"
"fmt"
"strings"
"time"
aliyunCas "github.com/alibabacloud-go/cas-20200407/v3/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type AliyunCASUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
Region string `json:"region"`
}
type AliyunCASUploader struct {
config *AliyunCASUploaderConfig
sdkClient *aliyunCas.Client
}
var _ uploader.Uploader = (*AliyunCASUploader)(nil)
func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.AccessKeySecret,
config.Region,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunCASUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listusercertificateorder
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail
listUserCertificateOrderPage := int64(1)
listUserCertificateOrderLimit := int64(50)
for {
listUserCertificateOrderReq := &aliyunCas.ListUserCertificateOrderRequest{
CurrentPage: tea.Int64(listUserCertificateOrderPage),
ShowSize: tea.Int64(listUserCertificateOrderLimit),
OrderType: tea.String("CERT"),
}
listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrder(listUserCertificateOrderReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.ListUserCertificateOrder'")
}
if listUserCertificateOrderResp.Body.CertificateOrderList != nil {
for _, certDetail := range listUserCertificateOrderResp.Body.CertificateOrderList {
if strings.EqualFold(certX509.SerialNumber.Text(16), *certDetail.SerialNo) {
getUserCertificateDetailReq := &aliyunCas.GetUserCertificateDetailRequest{
CertId: certDetail.CertificateId,
}
getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetail(getUserCertificateDetailReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.GetUserCertificateDetail'")
}
var isSameCert bool
if *getUserCertificateDetailResp.Body.Cert == certPem {
isSameCert = true
} else {
oldCertX509, err := x509.ParseCertificateFromPEM(*getUserCertificateDetailResp.Body.Cert)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)),
CertName: *certDetail.Name,
}, nil
}
}
}
}
if listUserCertificateOrderResp.Body.CertificateOrderList == nil || len(listUserCertificateOrderResp.Body.CertificateOrderList) < int(listUserCertificateOrderLimit) {
break
} else {
listUserCertificateOrderPage += 1
if listUserCertificateOrderPage > 99 { // 避免死循环
break
}
}
}
// 生成新证书名(需符合阿里云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate
uploadUserCertificateReq := &aliyunCas.UploadUserCertificateRequest{
Name: tea.String(certName),
Cert: tea.String(certPem),
Key: tea.String(privkeyPem),
}
uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificate(uploadUserCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.UploadUserCertificate'")
}
certId = fmt.Sprintf("%d", tea.Int64Value(uploadUserCertificateResp.Body.CertId))
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunCas.Client, error) {
if region == "" {
region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州
}
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
var endpoint string
switch region {
case "cn-hangzhou":
endpoint = "cas.aliyuncs.com"
default:
endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region)
}
aConfig.Endpoint = tea.String(endpoint)
client, err := aliyunCas.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,148 @@
package aliyunslb
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strings"
"time"
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/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type AliyunSLBUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
Region string `json:"region"`
}
type AliyunSLBUploader struct {
config *AliyunSLBUploaderConfig
sdkClient *aliyunSlb.Client
}
var _ uploader.Uploader = (*AliyunSLBUploader)(nil)
func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.AccessKeySecret,
config.Region,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunSLBUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates
describeServerCertificatesReq := &aliyunSlb.DescribeServerCertificatesRequest{
RegionId: tea.String(u.config.Region),
}
describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificates(describeServerCertificatesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeServerCertificates'")
}
if describeServerCertificatesResp.Body.ServerCertificates != nil && describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate != nil {
fingerprint := sha256.Sum256(certX509.Raw)
fingerprintHex := hex.EncodeToString(fingerprint[:])
for _, certDetail := range describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate {
isSameCert := *certDetail.IsAliCloudCertificate == 0 &&
strings.EqualFold(fingerprintHex, strings.ReplaceAll(*certDetail.Fingerprint, ":", "")) &&
strings.EqualFold(certX509.Subject.CommonName, *certDetail.CommonName)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: *certDetail.ServerCertificateId,
CertName: *certDetail.ServerCertificateName,
}, nil
}
}
}
// 生成新证书名(需符合阿里云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
// 去除证书和私钥内容中的空白行,以符合阿里云 API 要求
// REF: https://github.com/usual2970/certimate/issues/326
re := regexp.MustCompile(`(?m)^\s*$\n?`)
certPem = strings.TrimSpace(re.ReplaceAllString(certPem, ""))
privkeyPem = strings.TrimSpace(re.ReplaceAllString(privkeyPem, ""))
// 上传新证书
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate
uploadServerCertificateReq := &aliyunSlb.UploadServerCertificateRequest{
RegionId: tea.String(u.config.Region),
ServerCertificateName: tea.String(certName),
ServerCertificate: tea.String(certPem),
PrivateKey: tea.String(privkeyPem),
}
uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificate(uploadServerCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'slb.UploadServerCertificate'")
}
certId = *uploadServerCertificateResp.Body.ServerCertificateId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // SLB 服务默认区域:华东一杭州
}
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
var endpoint string
switch region {
case
"cn-hangzhou",
"cn-hangzhou-finance",
"cn-shanghai-finance-1",
"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
}

View File

@@ -0,0 +1,68 @@
package dogecloud
import (
"context"
"errors"
"fmt"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
doge "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
)
type DogeCloudUploaderConfig struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type DogeCloudUploader struct {
config *DogeCloudUploaderConfig
sdkClient *doge.Client
}
var _ uploader.Uploader = (*DogeCloudUploader)(nil)
func New(config *DogeCloudUploaderConfig) (*DogeCloudUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKey,
config.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &DogeCloudUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *DogeCloudUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 生成新证书名(需符合多吉云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://docs.dogecloud.com/cdn/api-cert-upload
uploadSslCertResp, err := u.sdkClient.UploadCdnCert(certName, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadCdnCert'")
}
certId = fmt.Sprintf("%d", uploadSslCertResp.Data.Id)
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKey, secretKey string) (*doge.Client, error) {
client := doge.NewClient(accessKey, secretKey)
return client, nil
}

View File

@@ -0,0 +1,223 @@
package huaweicloudelb
import (
"context"
"errors"
"fmt"
"time"
"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/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type HuaweiCloudELBUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region"`
}
type HuaweiCloudELBUploader struct {
config *HuaweiCloudELBUploaderConfig
sdkClient *hcElb.ElbClient
}
var _ uploader.Uploader = (*HuaweiCloudELBUploader)(nil)
func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.SecretAccessKey,
config.Region,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client: %w")
}
return &HuaweiCloudELBUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
newCert, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 遍历查询已有证书,避免重复上传
// REF: https://support.huaweicloud.com/api-elb/ListCertificates.html
listCertificatesPage := 1
listCertificatesLimit := int32(2000)
var listCertificatesMarker *string = nil
for {
listCertificatesReq := &hcElbModel.ListCertificatesRequest{
Limit: cast.Int32Ptr(listCertificatesLimit),
Marker: listCertificatesMarker,
Type: &[]string{"server"},
}
listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
}
if listCertificatesResp.Certificates != nil {
for _, certDetail := range *listCertificatesResp.Certificates {
var isSameCert bool
if certDetail.Certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(certDetail.Certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(cert, newCert)
}
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.Id,
CertName: certDetail.Name,
}, nil
}
}
}
if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) {
break
} else {
listCertificatesMarker = listCertificatesResp.PageInfo.NextMarker
listCertificatesPage++
if listCertificatesPage >= 9 { // 避免死循环
break
}
}
}
// 获取项目 ID
// REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html
projectId, err := getSdkProjectId(u.config.AccessKeyId, u.config.SecretAccessKey, u.config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to get SDK project id")
}
// 生成新证书名(需符合华为云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 创建新证书
// REF: https://support.huaweicloud.com/api-elb/CreateCertificate.html
createCertificateReq := &hcElbModel.CreateCertificateRequest{
Body: &hcElbModel.CreateCertificateRequestBody{
Certificate: &hcElbModel.CreateCertificateOption{
ProjectId: cast.StringPtr(projectId),
Name: cast.StringPtr(certName),
Certificate: cast.StringPtr(certPem),
PrivateKey: cast.StringPtr(privkeyPem),
},
},
}
createCertificateResp, err := u.sdkClient.CreateCertificate(createCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'elb.CreateCertificate'")
}
certId = createCertificateResp.Certificate.Id
certName = createCertificateResp.Certificate.Name
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
if region == "" {
region = "cn-north-4" // ELB 服务默认区域:华北四北京
}
auth, err := basic.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
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 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
}

View File

@@ -0,0 +1,177 @@
package huaweicloudscm
import (
"context"
"errors"
"fmt"
"time"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
hcScm "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3"
hcScmModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model"
hcScmRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type HuaweiCloudSCMUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region"`
}
type HuaweiCloudSCMUploader struct {
config *HuaweiCloudSCMUploaderConfig
sdkClient *hcScm.ScmClient
}
var _ uploader.Uploader = (*HuaweiCloudSCMUploader)(nil)
func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.SecretAccessKey,
config.Region,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &HuaweiCloudSCMUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 遍历查询已有证书,避免重复上传
// REF: https://support.huaweicloud.com/api-ccm/ListCertificates.html
// REF: https://support.huaweicloud.com/api-ccm/ExportCertificate_0.html
listCertificatesPage := 1
listCertificatesLimit := int32(50)
listCertificatesOffset := int32(0)
for {
listCertificatesReq := &hcScmModel.ListCertificatesRequest{
Limit: cast.Int32Ptr(listCertificatesLimit),
Offset: cast.Int32Ptr(listCertificatesOffset),
SortDir: cast.StringPtr("DESC"),
SortKey: cast.StringPtr("certExpiredTime"),
}
listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ListCertificates'")
}
if listCertificatesResp.Certificates != nil {
for _, certDetail := range *listCertificatesResp.Certificates {
exportCertificateReq := &hcScmModel.ExportCertificateRequest{
CertificateId: certDetail.Id,
}
exportCertificateResp, err := u.sdkClient.ExportCertificate(exportCertificateReq)
if err != nil {
if exportCertificateResp != nil && exportCertificateResp.HttpStatusCode == 404 {
continue
}
return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ExportCertificate'")
}
var isSameCert bool
if *exportCertificateResp.Certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(certX509, cert)
}
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.Id,
CertName: certDetail.Name,
}, nil
}
}
}
if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) {
break
} else {
listCertificatesOffset += listCertificatesLimit
listCertificatesPage += 1
if listCertificatesPage > 99 { // 避免死循环
break
}
}
}
// 生成新证书名(需符合华为云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://support.huaweicloud.com/api-ccm/ImportCertificate.html
importCertificateReq := &hcScmModel.ImportCertificateRequest{
Body: &hcScmModel.ImportCertificateRequestBody{
Name: certName,
Certificate: certPem,
PrivateKey: privkeyPem,
},
}
importCertificateResp, err := u.sdkClient.ImportCertificate(importCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ImportCertificate'")
}
certId = *importCertificateResp.CertificateId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) {
if region == "" {
region = "cn-north-4" // SCM 服务默认区域:华北四北京
}
auth, err := basic.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return nil, err
}
hcRegion, err := hcScmRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := hcScm.ScmClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := hcScm.NewScmClient(hcClient)
return client, nil
}

View File

@@ -0,0 +1,77 @@
package qiniusslcert
import (
"context"
"errors"
"fmt"
"time"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
)
type QiniuSSLCertUploaderConfig struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type QiniuSSLCertUploader struct {
config *QiniuSSLCertUploaderConfig
sdkClient *qiniuEx.Client
}
var _ uploader.Uploader = (*QiniuSSLCertUploader)(nil)
func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKey,
config.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &QiniuSSLCertUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *QiniuSSLCertUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 生成新证书名(需符合七牛云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate
uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'")
}
certId = uploadSslCertResp.CertID
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) {
credential := auth.New(accessKey, secretKey)
client := qiniuEx.NewClient(credential)
return client, nil
}

View File

@@ -0,0 +1,73 @@
package tencentcloudssl
import (
"context"
"errors"
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/pkg/core/uploader"
)
type TencentCloudSSLUploaderConfig struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
}
type TencentCloudSSLUploader struct {
config *TencentCloudSSLUploaderConfig
sdkClient *tcSsl.Client
}
var _ uploader.Uploader = (*TencentCloudSSLUploader)(nil)
func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.SecretId,
config.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &TencentCloudSSLUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *TencentCloudSSLUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 上传新证书
// REF: https://cloud.tencent.com/document/product/400/41665
uploadCertificateReq := tcSsl.NewUploadCertificateRequest()
uploadCertificateReq.CertificatePublicKey = common.StringPtr(certPem)
uploadCertificateReq.CertificatePrivateKey = common.StringPtr(privkeyPem)
uploadCertificateReq.Repeatable = common.BoolPtr(false)
uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.UploadCertificate'")
}
certId := *uploadCertificateResp.Response.CertificateId
return &uploader.UploadResult{
CertId: certId,
CertName: "",
}, nil
}
func 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

@@ -0,0 +1,112 @@
package volcenginecdn
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/volcengine/volc-sdk-golang/service/cdn"
)
type VolcengineCDNUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type VolcengineCDNUploader struct {
config *VolcengineCDNUploaderConfig
sdkClient *cdn.CDN
}
var _ uploader.Uploader = (*VolcengineCDNUploader)(nil)
func New(config *VolcengineCDNUploaderConfig) (*VolcengineCDNUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
instance := cdn.NewInstance()
client := instance.Client
client.SetAccessKey(config.AccessKeyId)
client.SetSecretKey(config.AccessKeySecret)
return &VolcengineCDNUploader{
config: config,
sdkClient: instance,
}, nil
}
func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6454/125709
pageNum := int64(1)
pageSize := int64(100)
certSource := "volc_cert_center"
listCertInfoReq := &cdn.ListCertInfoRequest{
PageNum: &pageNum,
PageSize: &pageSize,
Source: certSource,
}
searchTotal := 0
for {
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ListCertInfo'")
}
if listCertInfoResp.Result.CertInfo != nil {
for _, certDetail := range listCertInfoResp.Result.CertInfo {
hash := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(hash[:]), certDetail.CertFingerprint.Sha256)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.CertId,
CertName: certDetail.Desc,
}, nil
}
}
}
searchTotal += len(listCertInfoResp.Result.CertInfo)
if int(listCertInfoResp.Result.Total) > searchTotal {
pageNum++
} else {
break
}
}
// 生成新证书名(需符合火山引擎命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://www.volcengine.com/docs/6454/1245763
addCertificateReq := &cdn.AddCertificateRequest{
Certificate: certPem,
PrivateKey: privkeyPem,
Source: &certSource,
Desc: &certName,
}
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.AddCertificate'")
}
certId = addCertificateResp.Result.CertId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}

View File

@@ -0,0 +1,116 @@
package volcenginelive
import (
"context"
"errors"
"fmt"
"strings"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
live "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
)
type VolcengineLiveUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type VolcengineLiveUploader struct {
config *VolcengineLiveUploaderConfig
sdkClient *live.Live
}
var _ uploader.Uploader = (*VolcengineLiveUploader)(nil)
func New(config *VolcengineLiveUploaderConfig) (*VolcengineLiveUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client := live.NewInstance()
client.SetAccessKey(config.AccessKeyId)
client.SetSecretKey(config.AccessKeySecret)
return &VolcengineLiveUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *VolcengineLiveUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E8%AF%A2%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8
listCertReq := &live.ListCertV2Body{}
listCertResp, err := u.sdkClient.ListCertV2(ctx, listCertReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.ListCertV2'")
}
if listCertResp.Result.CertList != nil {
for _, certDetail := range listCertResp.Result.CertList {
describeCertDetailSecretReq := &live.DescribeCertDetailSecretV2Body{
ChainID: cast.StringPtr(certDetail.ChainID),
}
// 查询证书详细信息
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85
describeCertDetailSecretResp, detailErr := u.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq)
if detailErr != nil {
continue
}
var isSameCert bool
certificate := strings.Join(describeCertDetailSecretResp.Result.SSL.Chain, "\n\n")
if certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(cert, certX509)
}
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.ChainID,
CertName: certDetail.CertName,
}, nil
}
}
}
// 生成新证书名(需符合火山引擎命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%B7%BB%E5%8A%A0%E8%AF%81%E4%B9%A6
createCertReq := &live.CreateCertBody{
CertName: &certName,
UseWay: "https",
ProjectName: cast.StringPtr("default"),
Rsa: live.CreateCertBodyRsa{
Prikey: privkeyPem,
Pubkey: certPem,
},
}
createCertResp, err := u.sdkClient.CreateCert(ctx, createCertReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.CreateCert'")
}
certId = *createCertResp.Result.ChainID
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}

View File

@@ -0,0 +1,27 @@
package uploader
import "context"
// 表示定义证书上传器的抽象类型接口。
// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。
// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。
type Uploader interface {
// 上传证书。
//
// 入参:
// - ctx上下文。
// - certPem证书 PEM 内容。
// - privkeyPem私钥 PEM 内容。
//
// 出参:
// - res上传结果。
// - err: 错误。
Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error)
}
// 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。
type UploadResult struct {
CertId string `json:"certId"`
CertName string `json:"certName"`
CertData map[string]any `json:"certData,omitempty"`
}

View File

@@ -0,0 +1,25 @@
package cast
func Int32Ptr(i int32) *int32 {
return &i
}
func Int64Ptr(i int64) *int64 {
return &i
}
func UInt32Ptr(i uint32) *uint32 {
return &i
}
func UInt64Ptr(i uint64) *uint64 {
return &i
}
func StringPtr(s string) *string {
return &s
}
func BoolPtr(b bool) *bool {
return &b
}

View File

@@ -0,0 +1,52 @@
package fs
import (
"os"
"path/filepath"
xerrors "github.com/pkg/errors"
)
// 与 [WriteFile] 类似,但写入的是字符串内容。
//
// 入参:
// - path: 文件路径。
// - content: 文件内容。
//
// 出参:
// - 错误。
func WriteFileString(path string, content string) error {
return WriteFile(path, []byte(content))
}
// 将数据写入指定路径的文件。
// 如果目录不存在,将会递归创建目录。
// 如果文件不存在,将会创建该文件;如果文件已存在,将会覆盖原有内容。
//
// 入参:
// - path: 文件路径。
// - data: 文件数据字节数组。
//
// 出参:
// - 错误。
func WriteFile(path string, data []byte) error {
dir := filepath.Dir(path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return xerrors.Wrap(err, "failed to create directory")
}
file, err := os.Create(path)
if err != nil {
return xerrors.Wrap(err, "failed to create file")
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return xerrors.Wrap(err, "failed to write file")
}
return nil
}

View File

@@ -0,0 +1,164 @@
package maps
import "strconv"
// 以字符串形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回空字符串。
func GetValueAsString(dict map[string]any, key string) string {
return GetValueOrDefaultAsString(dict, key, "")
}
// 以字符串形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回默认值。
func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue string) string {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(string); ok {
return result
}
}
return defaultValue
}
// 以 32 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回 0。
func GetValueAsInt32(dict map[string]any, key string) int32 {
return GetValueOrDefaultAsInt32(dict, key, 0)
}
// 以 32 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回默认值。
func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int32) int32 {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(int32); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
return int32(result)
}
}
}
return defaultValue
}
// 以 64 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回 0。
func GetValueAsInt64(dict map[string]any, key string) int64 {
return GetValueOrDefaultAsInt64(dict, key, 0)
}
// 以 64 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回默认值。
func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int64) int64 {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(int64); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 64); err == nil {
return result
}
}
}
return defaultValue
}
// 以布尔形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回 false。
func GetValueAsBool(dict map[string]any, key string) bool {
return GetValueOrDefaultAsBool(dict, key, false)
}
// 以布尔形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回默认值。
func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) bool {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(bool); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseBool(str); err == nil {
return result
}
}
}
return defaultValue
}

View File

@@ -0,0 +1,22 @@
package x509
import (
"crypto/x509"
)
// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。
// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。
//
// 入参:
// - a: 待比较的第一个 x509.Certificate 对象。
// - b: 待比较的第二个 x509.Certificate 对象。
//
// 出参:
// - 是否相同。
func EqualCertificate(a, b *x509.Certificate) bool {
return string(a.Signature) == string(b.Signature) &&
a.SignatureAlgorithm == b.SignatureAlgorithm &&
a.SerialNumber.String() == b.SerialNumber.String() &&
a.Issuer.SerialNumber == b.Issuer.SerialNumber &&
a.Subject.SerialNumber == b.Subject.SerialNumber
}

View File

@@ -0,0 +1,31 @@
package x509
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
xerrors "github.com/pkg/errors"
)
// 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。
//
// 入参:
// - privkey: ecdsa.PrivateKey 对象。
//
// 出参:
// - privkeyPem: 私钥 PEM 内容。
// - err: 错误。
func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPem string, err error) {
data, err := x509.MarshalECPrivateKey(privkey)
if err != nil {
return "", xerrors.Wrap(err, "failed to marshal EC private key")
}
block := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: data,
}
return string(pem.EncodeToMemory(block)), nil
}

View File

@@ -0,0 +1,83 @@
package x509
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
xerrors "github.com/pkg/errors"
)
// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。
//
// 入参:
// - certPem: 证书 PEM 内容。
//
// 出参:
// - cert: x509.Certificate 对象。
// - err: 错误。
func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) {
pemData := []byte(certPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, xerrors.Wrap(err, "failed to parse certificate")
}
return cert, nil
}
// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: ecdsa.PrivateKey 对象。
// - err: 错误。
func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err error) {
pemData := []byte(privkeyPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
privkey, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, xerrors.Wrap(err, "failed to parse private key")
}
return privkey, nil
}
// 从 PEM 编码的私钥字符串解析并返回一个 rsa.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: rsa.PrivateKey 对象。
// - err: 错误。
func ParsePKCS1PrivateKeyFromPEM(privkeyPem string) (privkey *rsa.PrivateKey, err error) {
pemData := []byte(privkeyPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, xerrors.Wrap(err, "failed to parse private key")
}
return privkey, nil
}

View File

@@ -0,0 +1,183 @@
package dogecloudsdk
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
const dogeHost = "https://api.dogecloud.com"
type Client struct {
accessKey string
secretKey string
}
func NewClient(accessKey, secretKey string) *Client {
return &Client{accessKey: accessKey, secretKey: secretKey}
}
func (c *Client) UploadCdnCert(note, cert, private string) (*UploadCdnCertResponse, error) {
req := &UploadCdnCertRequest{
Note: note,
Certificate: cert,
PrivateKey: private,
}
reqBts, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMap := make(map[string]interface{})
err = json.Unmarshal(reqBts, &reqMap)
if err != nil {
return nil, err
}
respBts, err := c.sendReq(http.MethodPost, "cdn/cert/upload.json", reqMap, true)
if err != nil {
return nil, err
}
resp := &UploadCdnCertResponse{}
err = json.Unmarshal(respBts, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("dogecloud api error, code: %d, msg: %s", *resp.Code, *resp.Message)
}
return resp, nil
}
func (c *Client) BindCdnCertWithDomain(certId int64, domain string) (*BindCdnCertResponse, error) {
req := &BindCdnCertRequest{
CertId: certId,
Domain: &domain,
}
reqBts, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMap := make(map[string]interface{})
err = json.Unmarshal(reqBts, &reqMap)
if err != nil {
return nil, err
}
respBts, err := c.sendReq(http.MethodPost, "cdn/cert/bind.json", reqMap, true)
if err != nil {
return nil, err
}
resp := &BindCdnCertResponse{}
err = json.Unmarshal(respBts, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("dogecloud api error, code: %d, msg: %s", *resp.Code, *resp.Message)
}
return resp, nil
}
func (c *Client) BindCdnCertWithDomainId(certId int64, domainId int64) (*BindCdnCertResponse, error) {
req := &BindCdnCertRequest{
CertId: certId,
DomainId: &domainId,
}
reqBts, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMap := make(map[string]interface{})
err = json.Unmarshal(reqBts, &reqMap)
if err != nil {
return nil, err
}
respBts, err := c.sendReq(http.MethodPost, "cdn/cert/bind.json", reqMap, true)
if err != nil {
return nil, err
}
resp := &BindCdnCertResponse{}
err = json.Unmarshal(respBts, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("dogecloud api error, code: %d, msg: %s", *resp.Code, *resp.Message)
}
return resp, nil
}
// 调用多吉云的 API。
// https://docs.dogecloud.com/cdn/api-access-token?id=go
//
// 入参:
// - methodGET 或 POST
// - path是调用的 API 接口地址,包含 URL 请求参数 QueryString例如/console/vfetch/add.json?url=xxx&a=1&b=2
// - dataPOST 的数据,对象,例如 {a: 1, b: 2},传递此参数表示不是 GET 请求而是 POST 请求
// - jsonMode数据 data 是否以 JSON 格式请求,默认为 false 则使用表单形式a=1&b=2
func (c *Client) sendReq(method string, path string, data map[string]interface{}, jsonMode bool) ([]byte, error) {
body := ""
mime := ""
if jsonMode {
_body, err := json.Marshal(data)
if err != nil {
return nil, err
}
body = string(_body)
mime = "application/json"
} else {
values := url.Values{}
for k, v := range data {
values.Set(k, v.(string))
}
body = values.Encode()
mime = "application/x-www-form-urlencoded"
}
path = strings.TrimPrefix(path, "/")
signStr := "/" + path + "\n" + body
hmacObj := hmac.New(sha1.New, []byte(c.secretKey))
hmacObj.Write([]byte(signStr))
sign := hex.EncodeToString(hmacObj.Sum(nil))
auth := fmt.Sprintf("TOKEN %s:%s", c.accessKey, sign)
req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", dogeHost, path), strings.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", mime)
req.Header.Add("Authorization", auth)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
r, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return r, nil
}

View File

@@ -0,0 +1,31 @@
package dogecloudsdk
type BaseResponse struct {
Code *int `json:"code,omitempty"`
Message *string `json:"msg,omitempty"`
}
type UploadCdnCertRequest struct {
Note string `json:"note"`
Certificate string `json:"cert"`
PrivateKey string `json:"private"`
}
type UploadCdnCertResponseData struct {
Id int64 `json:"id"`
}
type UploadCdnCertResponse struct {
BaseResponse
Data *UploadCdnCertResponseData `json:"data,omitempty"`
}
type BindCdnCertRequest struct {
CertId int64 `json:"id"`
DomainId *int64 `json:"did,omitempty"`
Domain *string `json:"domain,omitempty"`
}
type BindCdnCertResponse struct {
BaseResponse
}

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