mirror of https://github.com/usual2970/certimate
				
				
				
			Merge branch 'main' into feat/multiple-certificate-formats
						commit
						305f3de50f
					
				
							
								
								
									
										33
									
								
								README.md
								
								
								
								
							
							
						
						
									
										33
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -71,22 +71,22 @@ make local.run
 | 
			
		|||
 | 
			
		||||
## 三、支持的服务商列表
 | 
			
		||||
 | 
			
		||||
|   服务商   | 支持申请证书 | 支持部署证书 | 备注                                                         |
 | 
			
		||||
| :--------: | :----------: | :----------: | ------------------------------------------------------------ |
 | 
			
		||||
|   阿里云   |      √       |      √       | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN            |
 | 
			
		||||
|   腾讯云   |      √       |      √       | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、CLB       |
 | 
			
		||||
|   华为云   |      √       |      √       | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB            |
 | 
			
		||||
|   七牛云   |              |      √       | 可部署到七牛云 CDN                                           |
 | 
			
		||||
|    AWS     |      √       |              | 可签发在 AWS Route53 托管的域名                              |
 | 
			
		||||
| CloudFlare |      √       |              | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 |
 | 
			
		||||
|  GoDaddy   |      √       |              | 可签发在 GoDaddy 注册的域名                                  |
 | 
			
		||||
|  Namesilo  |      √       |              | 可签发在 Namesilo 注册的域名                                 |
 | 
			
		||||
|  PowerDNS  |      √       |              | 可签发在 PowerDNS 托管的域名                                 |
 | 
			
		||||
| HTTP 请求  |      √       |              | 可签发允许通过 HTTP 请求修改 DNS 的域名                      |
 | 
			
		||||
|  本地部署  |              |      √       | 可部署到本地服务器                                           |
 | 
			
		||||
|    SSH     |              |      √       | 可部署到 SSH 服务器                                          |
 | 
			
		||||
|  Webhook   |              |      √       | 可部署时回调到 Webhook                                       |
 | 
			
		||||
| Kubernetes |              |      √       | 可部署到 Kubernetes Secret                                   |
 | 
			
		||||
|   服务商   | 支持申请证书 | 支持部署证书 | 备注                                                              |
 | 
			
		||||
| :--------: | :----------: | :----------: | ----------------------------------------------------------------- |
 | 
			
		||||
|   阿里云   |      √       |      √       | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、SLB            |
 | 
			
		||||
|   腾讯云   |      √       |      √       | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO |
 | 
			
		||||
|   华为云   |      √       |      √       | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB                 |
 | 
			
		||||
|   七牛云   |              |      √       | 可部署到七牛云 CDN                                                |
 | 
			
		||||
|    AWS     |      √       |              | 可签发在 AWS Route53 托管的域名                                   |
 | 
			
		||||
| CloudFlare |      √       |              | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书      |
 | 
			
		||||
|  GoDaddy   |      √       |              | 可签发在 GoDaddy 注册的域名                                       |
 | 
			
		||||
|  Namesilo  |      √       |              | 可签发在 Namesilo 注册的域名                                      |
 | 
			
		||||
|  PowerDNS  |      √       |              | 可签发在 PowerDNS 托管的域名                                      |
 | 
			
		||||
| HTTP 请求  |      √       |              | 可签发允许通过 HTTP 请求修改 DNS 的域名                           |
 | 
			
		||||
|  本地部署  |              |      √       | 可部署到本地服务器                                                |
 | 
			
		||||
|    SSH     |              |      √       | 可部署到 SSH 服务器                                               |
 | 
			
		||||
|  Webhook   |              |      √       | 可部署时回调到 Webhook                                            |
 | 
			
		||||
| Kubernetes |              |      √       | 可部署到 Kubernetes Secret                                        |
 | 
			
		||||
 | 
			
		||||
## 四、系统截图
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -180,3 +180,4 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
 | 
			
		|||
## 九、Star 趋势图
 | 
			
		||||
 | 
			
		||||
[](https://starchart.cc/usual2970/certimate)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										34
									
								
								README_EN.md
								
								
								
								
							
							
						
						
									
										34
									
								
								README_EN.md
								
								
								
								
							| 
						 | 
				
			
			@ -59,7 +59,7 @@ 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
 | 
			
		||||
username:admin@certimate.fun
 | 
			
		||||
| 
						 | 
				
			
			@ -70,22 +70,22 @@ password:1234567890
 | 
			
		|||
 | 
			
		||||
## List of Supported Providers
 | 
			
		||||
 | 
			
		||||
|   Provider    | Registration | Deployment | Remarks                                                                                          |
 | 
			
		||||
| :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------------ |
 | 
			
		||||
| Alibaba Cloud |      √       |     √      | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN      |
 | 
			
		||||
| Tencent Cloud |      √       |     √      | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, CLB |
 | 
			
		||||
| Huawei Cloud  |      √       |     √      | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB        |
 | 
			
		||||
|  Qiniu Cloud  |              |     √      | Supports deployment to Qiniu Cloud CDN                                                           |
 | 
			
		||||
|      AWS      |      √       |            | Supports domains managed on AWS Route53                                                          |
 | 
			
		||||
|  CloudFlare   |      √       |            | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates        |
 | 
			
		||||
|    GoDaddy    |      √       |            | Supports domains registered on GoDaddy                                                           |
 | 
			
		||||
|   Namesilo    |      √       |            | Supports domains registered on Namesilo                                                          |
 | 
			
		||||
|   PowerDNS    |      √       |            | Supports domains managed on PowerDNS                                                             |
 | 
			
		||||
| HTTP Request  |      √       |            | Supports domains which allow managing DNS by HTTP request                                        |
 | 
			
		||||
| Local Deploy  |              |     √      | Supports deployment to local servers                                                             |
 | 
			
		||||
|      SSH      |              |     √      | Supports deployment to SSH servers                                                               |
 | 
			
		||||
|    Webhook    |              |     √      | Supports callback to Webhook                                                                     |
 | 
			
		||||
|  Kubernetes   |              |     √      | Supports deployment to Kubernetes Secret                                                         |
 | 
			
		||||
|   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           |
 | 
			
		||||
| Huawei Cloud  |      √       |     √      | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB                             |
 | 
			
		||||
|  Qiniu Cloud  |              |     √      | Supports deployment to Qiniu Cloud CDN                                                                                |
 | 
			
		||||
|      AWS      |      √       |            | Supports domains managed on AWS Route53                                                                               |
 | 
			
		||||
|  CloudFlare   |      √       |            | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates                             |
 | 
			
		||||
|    GoDaddy    |      √       |            | Supports domains registered on GoDaddy                                                                                |
 | 
			
		||||
|   Namesilo    |      √       |            | Supports domains registered on Namesilo                                                                               |
 | 
			
		||||
|   PowerDNS    |      √       |            | Supports domains managed on PowerDNS                                                                                  |
 | 
			
		||||
| HTTP Request  |      √       |            | Supports domains which allow managing DNS by HTTP request                                                             |
 | 
			
		||||
| Local Deploy  |              |     √      | Supports deployment to local servers                                                                                  |
 | 
			
		||||
|      SSH      |              |     √      | Supports deployment to SSH servers                                                                                    |
 | 
			
		||||
|    Webhook    |              |     √      | Supports callback to Webhook                                                                                          |
 | 
			
		||||
|  Kubernetes   |              |     √      | Supports deployment to Kubernetes Secret                                                                              |
 | 
			
		||||
 | 
			
		||||
## Screenshots
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										6
									
								
								go.mod
								
								
								
								
							| 
						 | 
				
			
			@ -5,9 +5,12 @@ go 1.22.0
 | 
			
		|||
toolchain go1.23.2
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/alibabacloud-go/alb-20200616/v2 v2.2.1
 | 
			
		||||
	github.com/alibabacloud-go/cas-20200407/v3 v3.0.1
 | 
			
		||||
	github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0
 | 
			
		||||
	github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
 | 
			
		||||
	github.com/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/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +24,7 @@ require (
 | 
			
		|||
	github.com/pocketbase/pocketbase v0.22.18
 | 
			
		||||
	github.com/qiniu/go-sdk/v7 v7.22.0
 | 
			
		||||
	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
 | 
			
		||||
	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017
 | 
			
		||||
	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030
 | 
			
		||||
	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
 | 
			
		||||
	golang.org/x/crypto v0.28.0
 | 
			
		||||
	k8s.io/api v0.31.1
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +59,7 @@ require (
 | 
			
		|||
	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/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030 // 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										13
									
								
								go.sum
								
								
								
								
							| 
						 | 
				
			
			@ -29,6 +29,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe
 | 
			
		|||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
 | 
			
		||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 | 
			
		||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
 | 
			
		||||
github.com/alibabacloud-go/alb-20200616/v2 v2.2.1 h1:b8ixnrkFhWrmJQd+iEE1UWPD5vdyC3d9l7G0uvkfi2s=
 | 
			
		||||
github.com/alibabacloud-go/alb-20200616/v2 v2.2.1/go.mod h1:cPdZwovbqpv+5nM/HnMwZpG5q0/gBuX31hu2H1VoyrM=
 | 
			
		||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
 | 
			
		||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
 | 
			
		||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +47,8 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F
 | 
			
		|||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
 | 
			
		||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
 | 
			
		||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
 | 
			
		||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg=
 | 
			
		||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.7/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
 | 
			
		||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
 | 
			
		||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
 | 
			
		||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
 | 
			
		||||
| 
						 | 
				
			
			@ -61,11 +65,15 @@ github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA
 | 
			
		|||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
 | 
			
		||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
 | 
			
		||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
 | 
			
		||||
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3 h1:LtyUVlgBEKyzWgQJurzXM6MXCt84sQr9cE5OKqYymko=
 | 
			
		||||
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3/go.mod h1:4a/RcBYeAhYowHzX+LMgnouz7NradnSKPKl14KS3B1U=
 | 
			
		||||
github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
 | 
			
		||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
 | 
			
		||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
 | 
			
		||||
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 h1:L0TIjr9Qh/SLVc1yPhFkcB9+9SbCNK/jPq4ZKB5zmnc=
 | 
			
		||||
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1/go.mod h1:EKxBRDLcMzwl4VLF/1WJwlByZZECJawPXUvinKMsTTs=
 | 
			
		||||
github.com/alibabacloud-go/slb-20140515/v4 v4.0.9 h1:nrf9gQth7fONUj7V8i78Yb98eb9NdKl0VdeSjmeYugI=
 | 
			
		||||
github.com/alibabacloud-go/slb-20140515/v4 v4.0.9/go.mod h1:PEMEsQoxhkMvykMFP5ZXg6SWI9vmAiZ6lK3Pu4mTKB0=
 | 
			
		||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
 | 
			
		||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
 | 
			
		||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +97,7 @@ github.com/alibabacloud-go/tea-utils v1.3.6/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQ
 | 
			
		|||
github.com/alibabacloud-go/tea-utils v1.4.5 h1:h0/6Xd2f3bPE4XHTvkpjwxowIwRCJAJOqY6Eq8f3zfA=
 | 
			
		||||
github.com/alibabacloud-go/tea-utils v1.4.5/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
 | 
			
		||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
 | 
			
		||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.4/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
 | 
			
		||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
 | 
			
		||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLiSke5RsN5LcyQ0=
 | 
			
		||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
 | 
			
		||||
| 
						 | 
				
			
			@ -451,10 +460,14 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod
 | 
			
		|||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017 h1:SXrldOXwgomYuATVAuz5ofpTjB+99qVELgdy5R5kMgI=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992/go.mod h1:BcvC7ZPdSlhRggVq4J1ToJlgv8bmODIAuSo0naFZOLo=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030 h1:tlHbfQlAfL12J/5XF4indKl0cAA3vEn6TDiGZVsr050=
 | 
			
		||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030/go.mod h1:8dW6JByZKNDAPnjlXxBk9yDc+QGbldpa0tBRfi1kG+U=
 | 
			
		||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
 | 
			
		||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
 | 
			
		||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -98,7 +98,7 @@ func newApplyUser(ca, email string) (*ApplyUser, error) {
 | 
			
		|||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		keyStr, err := x509.PrivateKeyToPEM(privateKey)
 | 
			
		||||
		keyStr, err := x509.ConvertECPrivateKeyToPEM(privateKey)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +122,7 @@ func (u ApplyUser) GetRegistration() *registration.Resource {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (u *ApplyUser) GetPrivateKey() crypto.PrivateKey {
 | 
			
		||||
	rs, _ := x509.ParsePrivateKeyFromPEM(u.key)
 | 
			
		||||
	rs, _ := x509.ParseECPrivateKeyFromPEM(u.key)
 | 
			
		||||
	return rs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,265 @@
 | 
			
		|||
package deployer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	alb20200616 "github.com/alibabacloud-go/alb-20200616/v2/client"
 | 
			
		||||
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
 | 
			
		||||
	"github.com/alibabacloud-go/tea/tea"
 | 
			
		||||
 | 
			
		||||
	"github.com/usual2970/certimate/internal/domain"
 | 
			
		||||
	"github.com/usual2970/certimate/internal/pkg/core/uploader"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AliyunALBDeployer struct {
 | 
			
		||||
	option *DeployerOption
 | 
			
		||||
	infos  []string
 | 
			
		||||
 | 
			
		||||
	sdkClient   *alb20200616.Client
 | 
			
		||||
	sslUploader uploader.Uploader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) {
 | 
			
		||||
	access := &domain.AliyunAccess{}
 | 
			
		||||
	json.Unmarshal([]byte(option.Access), access)
 | 
			
		||||
 | 
			
		||||
	client, err := (&AliyunALBDeployer{}).createSdkClient(
 | 
			
		||||
		access.AccessKeyId,
 | 
			
		||||
		access.AccessKeySecret,
 | 
			
		||||
		option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{
 | 
			
		||||
		AccessKeyId:     access.AccessKeyId,
 | 
			
		||||
		AccessKeySecret: access.AccessKeySecret,
 | 
			
		||||
		Region:          option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) GetInfo() []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) (*alb20200616.Client, error) {
 | 
			
		||||
	if region == "" {
 | 
			
		||||
		region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	aConfig := &openapi.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 := alb20200616.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 := &alb20200616.GetLoadBalancerAttributeRequest{
 | 
			
		||||
		LoadBalancerId: tea.String(aliLoadbalancerId),
 | 
			
		||||
	}
 | 
			
		||||
	getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 := &alb20200616.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 fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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 := &alb20200616.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 fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已上传证书", uploadResult))
 | 
			
		||||
 | 
			
		||||
	// 批量更新监听证书
 | 
			
		||||
	var errs []error
 | 
			
		||||
	for _, aliListenerId := range aliListenerIds {
 | 
			
		||||
		if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已上传证书", uploadResult))
 | 
			
		||||
 | 
			
		||||
	// 更新监听
 | 
			
		||||
	if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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 := &alb20200616.GetListenerAttributeRequest{
 | 
			
		||||
		ListenerId: tea.String(aliListenerId),
 | 
			
		||||
	}
 | 
			
		||||
	getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'alb.GetListenerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 := &alb20200616.UpdateListenerAttributeRequest{
 | 
			
		||||
		ListenerId: tea.String(aliListenerId),
 | 
			
		||||
		Certificates: []*alb20200616.UpdateListenerAttributeRequestCertificates{{
 | 
			
		||||
			CertificateId: tea.String(aliCertId),
 | 
			
		||||
		}},
 | 
			
		||||
	}
 | 
			
		||||
	updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'alb.UpdateListenerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp))
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,282 @@
 | 
			
		|||
package deployer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
 | 
			
		||||
	slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client"
 | 
			
		||||
	"github.com/alibabacloud-go/tea/tea"
 | 
			
		||||
 | 
			
		||||
	"github.com/usual2970/certimate/internal/domain"
 | 
			
		||||
	"github.com/usual2970/certimate/internal/pkg/core/uploader"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AliyunCLBDeployer struct {
 | 
			
		||||
	option *DeployerOption
 | 
			
		||||
	infos  []string
 | 
			
		||||
 | 
			
		||||
	sdkClient   *slb20140515.Client
 | 
			
		||||
	sslUploader uploader.Uploader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) {
 | 
			
		||||
	access := &domain.AliyunAccess{}
 | 
			
		||||
	json.Unmarshal([]byte(option.Access), access)
 | 
			
		||||
 | 
			
		||||
	client, err := (&AliyunCLBDeployer{}).createSdkClient(
 | 
			
		||||
		access.AccessKeyId,
 | 
			
		||||
		access.AccessKeySecret,
 | 
			
		||||
		option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uploader, err := uploader.NewAliyunSLBUploader(&uploader.AliyunSLBUploaderConfig{
 | 
			
		||||
		AccessKeyId:     access.AccessKeyId,
 | 
			
		||||
		AccessKeySecret: access.AccessKeySecret,
 | 
			
		||||
		Region:          option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) GetInfo() []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) (*slb20140515.Client, error) {
 | 
			
		||||
	if region == "" {
 | 
			
		||||
		region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	aConfig := &openapi.Config{
 | 
			
		||||
		AccessKeyId:     tea.String(accessKeyId),
 | 
			
		||||
		AccessKeySecret: tea.String(accessKeySecret),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var endpoint string
 | 
			
		||||
	switch region {
 | 
			
		||||
	case "cn-hangzhou":
 | 
			
		||||
	case "cn-hangzhou-finance":
 | 
			
		||||
	case "cn-shanghai-finance-1":
 | 
			
		||||
	case "cn-shenzhen-finance-1":
 | 
			
		||||
		endpoint = "slb.aliyuncs.com"
 | 
			
		||||
	default:
 | 
			
		||||
		endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
 | 
			
		||||
	}
 | 
			
		||||
	aConfig.Endpoint = tea.String(endpoint)
 | 
			
		||||
 | 
			
		||||
	client, err := slb20140515.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")
 | 
			
		||||
	if aliLoadbalancerId == "" {
 | 
			
		||||
		return errors.New("`loadbalancerId` is required")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	aliListenerPorts := make([]int32, 0)
 | 
			
		||||
 | 
			
		||||
	// 查询负载均衡实例的详细信息
 | 
			
		||||
	// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
 | 
			
		||||
	describeLoadBalancerAttributeReq := &slb20140515.DescribeLoadBalancerAttributeRequest{
 | 
			
		||||
		RegionId:       tea.String(d.option.DeployConfig.GetConfigAsString("region")),
 | 
			
		||||
		LoadBalancerId: tea.String(aliLoadbalancerId),
 | 
			
		||||
	}
 | 
			
		||||
	describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已上传证书", uploadResult))
 | 
			
		||||
 | 
			
		||||
	// 批量更新监听证书
 | 
			
		||||
	var errs []error
 | 
			
		||||
	for _, aliListenerPort := range aliListenerPorts {
 | 
			
		||||
		if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.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
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已上传证书", uploadResult))
 | 
			
		||||
 | 
			
		||||
	// 更新监听
 | 
			
		||||
	if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.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 := &slb20140515.DescribeLoadBalancerHTTPSListenerAttributeRequest{
 | 
			
		||||
		LoadBalancerId: tea.String(aliLoadbalancerId),
 | 
			
		||||
		ListenerPort:   tea.Int32(aliListenerPort),
 | 
			
		||||
	}
 | 
			
		||||
	describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp))
 | 
			
		||||
 | 
			
		||||
	// 遍历修改扩展域名
 | 
			
		||||
	// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
 | 
			
		||||
	//
 | 
			
		||||
	// 这里仅修改跟被替换证书一致的扩展域名
 | 
			
		||||
	if describeDomainExtensionsResp.Body.DomainExtensions == nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension == nil {
 | 
			
		||||
		for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
 | 
			
		||||
			if *domainExtension.ServerCertificateId == *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			setDomainExtensionAttributeReq := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 修改监听配置
 | 
			
		||||
	// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
 | 
			
		||||
	//
 | 
			
		||||
	// 注意修改监听配置要放在修改扩展域名之后
 | 
			
		||||
	setLoadBalancerHTTPSListenerAttributeReq := &slb20140515.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 fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp))
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,229 @@
 | 
			
		|||
package deployer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
 | 
			
		||||
	nlb20220430 "github.com/alibabacloud-go/nlb-20220430/v2/client"
 | 
			
		||||
	"github.com/alibabacloud-go/tea/tea"
 | 
			
		||||
 | 
			
		||||
	"github.com/usual2970/certimate/internal/domain"
 | 
			
		||||
	"github.com/usual2970/certimate/internal/pkg/core/uploader"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AliyunNLBDeployer struct {
 | 
			
		||||
	option *DeployerOption
 | 
			
		||||
	infos  []string
 | 
			
		||||
 | 
			
		||||
	sdkClient   *nlb20220430.Client
 | 
			
		||||
	sslUploader uploader.Uploader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) {
 | 
			
		||||
	access := &domain.AliyunAccess{}
 | 
			
		||||
	json.Unmarshal([]byte(option.Access), access)
 | 
			
		||||
 | 
			
		||||
	client, err := (&AliyunNLBDeployer{}).createSdkClient(
 | 
			
		||||
		access.AccessKeyId,
 | 
			
		||||
		access.AccessKeySecret,
 | 
			
		||||
		option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{
 | 
			
		||||
		AccessKeyId:     access.AccessKeyId,
 | 
			
		||||
		AccessKeySecret: access.AccessKeySecret,
 | 
			
		||||
		Region:          option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) GetInfo() []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) (*nlb20220430.Client, error) {
 | 
			
		||||
	if region == "" {
 | 
			
		||||
		region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	aConfig := &openapi.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 := nlb20220430.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 := &nlb20220430.GetLoadBalancerAttributeRequest{
 | 
			
		||||
		LoadBalancerId: tea.String(aliLoadbalancerId),
 | 
			
		||||
	}
 | 
			
		||||
	getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 := &nlb20220430.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 fmt.Errorf("failed to execute sdk request 'nlb.ListListeners': %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已上传证书", uploadResult))
 | 
			
		||||
 | 
			
		||||
	// 批量更新监听证书
 | 
			
		||||
	var errs []error
 | 
			
		||||
	for _, aliListenerId := range aliListenerIds {
 | 
			
		||||
		if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已上传证书", uploadResult))
 | 
			
		||||
 | 
			
		||||
	// 更新监听
 | 
			
		||||
	if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.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 := &nlb20220430.GetListenerAttributeRequest{
 | 
			
		||||
		ListenerId: tea.String(aliListenerId),
 | 
			
		||||
	}
 | 
			
		||||
	getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'nlb.GetListenerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 := &nlb20220430.UpdateListenerAttributeRequest{
 | 
			
		||||
		ListenerId:     tea.String(aliListenerId),
 | 
			
		||||
		CertificateIds: []*string{tea.String(aliCertId)},
 | 
			
		||||
	}
 | 
			
		||||
	updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'nlb.UpdateListenerAttribute': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp))
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -21,9 +21,14 @@ const (
 | 
			
		|||
	targetAliyunOSS      = "aliyun-oss"
 | 
			
		||||
	targetAliyunCDN      = "aliyun-cdn"
 | 
			
		||||
	targetAliyunESA      = "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"
 | 
			
		||||
	targetQiniuCdn       = "qiniu-cdn"
 | 
			
		||||
| 
						 | 
				
			
			@ -109,12 +114,22 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
 | 
			
		|||
		return NewAliyunCDNDeployer(option)
 | 
			
		||||
	case targetAliyunESA:
 | 
			
		||||
		return NewAliyunESADeployer(option)
 | 
			
		||||
	case targetAliyunCLB:
 | 
			
		||||
		return NewAliyunCLBDeployer(option)
 | 
			
		||||
	case targetAliyunALB:
 | 
			
		||||
		return NewAliyunALBDeployer(option)
 | 
			
		||||
	case targetAliyunNLB:
 | 
			
		||||
		return NewAliyunNLBDeployer(option)
 | 
			
		||||
	case targetTencentCDN:
 | 
			
		||||
		return NewTencentCDNDeployer(option)
 | 
			
		||||
		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:
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +145,7 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
 | 
			
		|||
	case targetK8sSecret:
 | 
			
		||||
		return NewK8sSecretDeployer(option)
 | 
			
		||||
	}
 | 
			
		||||
	return nil, errors.New("not implemented")
 | 
			
		||||
	return nil, errors.New("unsupported deploy target")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getProduct(t string) string {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,9 +41,9 @@ func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
 | 
			
		|||
 | 
			
		||||
	// TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版
 | 
			
		||||
	uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{
 | 
			
		||||
		Region:          "",
 | 
			
		||||
		AccessKeyId:     access.AccessKeyId,
 | 
			
		||||
		SecretAccessKey: access.SecretAccessKey,
 | 
			
		||||
		Region:          "",
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,9 +46,9 @@ func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{
 | 
			
		||||
		Region:          option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
		AccessKeyId:     access.AccessKeyId,
 | 
			
		||||
		SecretAccessKey: access.SecretAccessKey,
 | 
			
		||||
		Region:          option.DeployConfig.GetConfigAsString("region"),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
| 
						 | 
				
			
			@ -176,10 +176,15 @@ func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, r
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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: d.option.DeployConfig.GetConfigAsString("certificateId"),
 | 
			
		||||
		CertificateId: hcCertId,
 | 
			
		||||
		Body: &hcElbModel.UpdateCertificateRequestBody{
 | 
			
		||||
			Certificate: &hcElbModel.UpdateCertificateOption{
 | 
			
		||||
				Certificate: cast.StringPtr(d.option.Certificate.Certificate),
 | 
			
		||||
| 
						 | 
				
			
			@ -198,21 +203,27 @@ func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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: d.option.DeployConfig.GetConfigAsString("loadbalancerId"),
 | 
			
		||||
		LoadbalancerId: hcLoadbalancerId,
 | 
			
		||||
	}
 | 
			
		||||
	showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器", showLoadBalancerResp))
 | 
			
		||||
	d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp))
 | 
			
		||||
 | 
			
		||||
	// 查询监听器列表
 | 
			
		||||
	// REF: https://support.huaweicloud.com/api-elb/ListListeners.html
 | 
			
		||||
	listenerIds := make([]string, 0)
 | 
			
		||||
	listListenersLimit := int32(2000)
 | 
			
		||||
	var listListenersMarker *string = nil
 | 
			
		||||
	for {
 | 
			
		||||
| 
						 | 
				
			
			@ -229,7 +240,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error
 | 
			
		|||
 | 
			
		||||
		if listListenersResp.Listeners != nil {
 | 
			
		||||
			for _, listener := range *listListenersResp.Listeners {
 | 
			
		||||
				listenerIds = append(listenerIds, listener.Id)
 | 
			
		||||
				hcListenerIds = append(hcListenerIds, listener.Id)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -240,7 +251,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器下的监听器", listenerIds))
 | 
			
		||||
	d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds))
 | 
			
		||||
 | 
			
		||||
	// 上传证书到 SCM
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
| 
						 | 
				
			
			@ -252,8 +263,8 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error
 | 
			
		|||
 | 
			
		||||
	// 批量更新监听器证书
 | 
			
		||||
	var errs []error
 | 
			
		||||
	for _, listenerId := range listenerIds {
 | 
			
		||||
		if err := d.updateListenerCertificate(ctx, listenerId, uploadResult.CertId); err != nil {
 | 
			
		||||
	for _, hcListenerId := range hcListenerIds {
 | 
			
		||||
		if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil {
 | 
			
		||||
			errs = append(errs, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -265,6 +276,11 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error {
 | 
			
		||||
	hcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
 | 
			
		||||
	if hcListenerId == "" {
 | 
			
		||||
		return errors.New("`listenerId` is required")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 上传证书到 SCM
 | 
			
		||||
	uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +290,7 @@ func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error {
 | 
			
		|||
	d.infos = append(d.infos, toStr("已上传证书", uploadResult))
 | 
			
		||||
 | 
			
		||||
	// 更新监听器证书
 | 
			
		||||
	if err := d.updateListenerCertificate(ctx, d.option.DeployConfig.GetConfigAsString("listenerId"), uploadResult.CertId); err != nil {
 | 
			
		||||
	if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -292,7 +308,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context,
 | 
			
		|||
		return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已查询到到 ELB 监听器", showListenerResp))
 | 
			
		||||
	d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp))
 | 
			
		||||
 | 
			
		||||
	// 更新监听器
 | 
			
		||||
	// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
 | 
			
		||||
| 
						 | 
				
			
			@ -359,7 +375,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context,
 | 
			
		|||
		return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("已更新监听器", updateListenerResp))
 | 
			
		||||
	d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp))
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import (
 | 
			
		|||
	corev1 "k8s.io/api/core/v1"
 | 
			
		||||
	k8sMetaV1 "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"
 | 
			
		||||
| 
						 | 
				
			
			@ -118,11 +119,18 @@ func (d *K8sSecretDeployer) Deploy(ctx context.Context) error {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) {
 | 
			
		||||
	kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(access.KubeConfig))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	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()
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	config, err := kubeConfig.ClientConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -131,6 +139,5 @@ func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kube
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return client, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,146 @@
 | 
			
		|||
package deployer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	cdn "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"
 | 
			
		||||
 | 
			
		||||
	"github.com/usual2970/certimate/internal/domain"
 | 
			
		||||
	"github.com/usual2970/certimate/internal/utils/rand"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TencentECDNDeployer struct {
 | 
			
		||||
	option     *DeployerOption
 | 
			
		||||
	credential *common.Credential
 | 
			
		||||
	infos      []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) {
 | 
			
		||||
	access := &domain.TencentAccess{}
 | 
			
		||||
	if err := json.Unmarshal([]byte(option.Access), access); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	credential := common.NewCredential(
 | 
			
		||||
		access.SecretId,
 | 
			
		||||
		access.SecretKey,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return &TencentECDNDeployer{
 | 
			
		||||
		option:     option,
 | 
			
		||||
		credential: credential,
 | 
			
		||||
		infos:      make([]string, 0),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentECDNDeployer) GetID() string {
 | 
			
		||||
	return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentECDNDeployer) GetInfo() []string {
 | 
			
		||||
	return d.infos
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentECDNDeployer) Deploy(ctx context.Context) error {
 | 
			
		||||
	// 上传证书
 | 
			
		||||
	certId, err := d.uploadCert()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to upload certificate: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	d.infos = append(d.infos, toStr("上传证书", certId))
 | 
			
		||||
 | 
			
		||||
	if err := d.deploy(certId); err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to deploy: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentECDNDeployer) uploadCert() (string, error) {
 | 
			
		||||
	cpf := profile.NewClientProfile()
 | 
			
		||||
	cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
 | 
			
		||||
 | 
			
		||||
	client, _ := ssl.NewClient(d.credential, "", cpf)
 | 
			
		||||
 | 
			
		||||
	request := ssl.NewUploadCertificateRequest()
 | 
			
		||||
 | 
			
		||||
	request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
 | 
			
		||||
	request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
 | 
			
		||||
	request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
 | 
			
		||||
	request.Repeatable = common.BoolPtr(false)
 | 
			
		||||
 | 
			
		||||
	response, err := client.UploadCertificate(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("failed to upload certificate: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return *response.Response.CertificateId, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentECDNDeployer) deploy(certId string) error {
 | 
			
		||||
	cpf := profile.NewClientProfile()
 | 
			
		||||
	cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
 | 
			
		||||
	// 实例化要请求产品的client对象,clientProfile是可选的
 | 
			
		||||
	client, _ := ssl.NewClient(d.credential, "", cpf)
 | 
			
		||||
 | 
			
		||||
	// 实例化一个请求对象,每个接口都会对应一个request对象
 | 
			
		||||
	request := ssl.NewDeployCertificateInstanceRequest()
 | 
			
		||||
 | 
			
		||||
	request.CertificateId = common.StringPtr(certId)
 | 
			
		||||
	request.ResourceType = common.StringPtr("ecdn")
 | 
			
		||||
	request.Status = common.Int64Ptr(1)
 | 
			
		||||
 | 
			
		||||
	// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
 | 
			
		||||
	domain := getDeployString(d.option.DeployConfig, "domain")
 | 
			
		||||
	if strings.Contains(domain, "*") {
 | 
			
		||||
		list, errGetList := d.getDomainList()
 | 
			
		||||
		if errGetList != nil {
 | 
			
		||||
			return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
 | 
			
		||||
		}
 | 
			
		||||
		if list == nil || len(list) == 0 {
 | 
			
		||||
			return fmt.Errorf("failed to get certificate domain list: empty list.")
 | 
			
		||||
		}
 | 
			
		||||
		request.InstanceIdList = common.StringPtrs(list)
 | 
			
		||||
	} else { // 否则直接使用传入的域名
 | 
			
		||||
		request.InstanceIdList = common.StringPtrs([]string{domain})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
 | 
			
		||||
	resp, err := client.DeployCertificateInstance(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to deploy certificate: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	d.infos = append(d.infos, toStr("部署证书", resp.Response))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentECDNDeployer) getDomainList() ([]string, error) {
 | 
			
		||||
	cpf := profile.NewClientProfile()
 | 
			
		||||
	cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
 | 
			
		||||
	client, _ := cdn.NewClient(d.credential, "", cpf)
 | 
			
		||||
 | 
			
		||||
	request := cdn.NewDescribeCertDomainsRequest()
 | 
			
		||||
 | 
			
		||||
	cert := base64.StdEncoding.EncodeToString([]byte(d.option.Certificate.Certificate))
 | 
			
		||||
	request.Cert = &cert
 | 
			
		||||
	request.Product = common.StringPtr("ecdn")
 | 
			
		||||
 | 
			
		||||
	response, err := client.DescribeCertDomains(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to get domain list: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	domains := make([]string, 0)
 | 
			
		||||
	for _, domain := range response.Response.Domains {
 | 
			
		||||
		domains = append(domains, *domain)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return domains, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
package deployer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
 | 
			
		||||
	"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"
 | 
			
		||||
 | 
			
		||||
	"github.com/usual2970/certimate/internal/domain"
 | 
			
		||||
	"github.com/usual2970/certimate/internal/utils/rand"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TencentTEODeployer struct {
 | 
			
		||||
	option     *DeployerOption
 | 
			
		||||
	credential *common.Credential
 | 
			
		||||
	infos      []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) {
 | 
			
		||||
	access := &domain.TencentAccess{}
 | 
			
		||||
	if err := json.Unmarshal([]byte(option.Access), access); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	credential := common.NewCredential(
 | 
			
		||||
		access.SecretId,
 | 
			
		||||
		access.SecretKey,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return &TencentTEODeployer{
 | 
			
		||||
		option:     option,
 | 
			
		||||
		credential: credential,
 | 
			
		||||
		infos:      make([]string, 0),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentTEODeployer) GetID() string {
 | 
			
		||||
	return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentTEODeployer) GetInfo() []string {
 | 
			
		||||
	return d.infos
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
 | 
			
		||||
	// 上传证书
 | 
			
		||||
	certId, err := d.uploadCert()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to upload certificate: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	d.infos = append(d.infos, toStr("上传证书", certId))
 | 
			
		||||
 | 
			
		||||
	if err := d.deploy(certId); err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to deploy: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentTEODeployer) uploadCert() (string, error) {
 | 
			
		||||
	cpf := profile.NewClientProfile()
 | 
			
		||||
	cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
 | 
			
		||||
 | 
			
		||||
	client, _ := ssl.NewClient(d.credential, "", cpf)
 | 
			
		||||
 | 
			
		||||
	request := ssl.NewUploadCertificateRequest()
 | 
			
		||||
 | 
			
		||||
	request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
 | 
			
		||||
	request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
 | 
			
		||||
	request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
 | 
			
		||||
	request.Repeatable = common.BoolPtr(false)
 | 
			
		||||
 | 
			
		||||
	response, err := client.UploadCertificate(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("failed to upload certificate: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return *response.Response.CertificateId, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *TencentTEODeployer) deploy(certId string) error {
 | 
			
		||||
	cpf := profile.NewClientProfile()
 | 
			
		||||
	cpf.HttpProfile.Endpoint = "teo.tencentcloudapi.com"
 | 
			
		||||
	// 实例化要请求产品的client对象,clientProfile是可选的
 | 
			
		||||
	client, _ := teo.NewClient(d.credential, "", cpf)
 | 
			
		||||
 | 
			
		||||
	// 实例化一个请求对象,每个接口都会对应一个request对象
 | 
			
		||||
	request := teo.NewModifyHostsCertificateRequest()
 | 
			
		||||
 | 
			
		||||
	request.ZoneId = common.StringPtr(getDeployString(d.option.DeployConfig, "zoneId"))
 | 
			
		||||
	request.Mode = common.StringPtr("sslcert")
 | 
			
		||||
	request.ServerCertInfo = []*teo.ServerCertInfo{{
 | 
			
		||||
		CertId: common.StringPtr(certId),
 | 
			
		||||
	}}
 | 
			
		||||
 | 
			
		||||
	domains := strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"),"\n")
 | 
			
		||||
	request.Hosts = common.StringPtrs(domains)
 | 
			
		||||
 | 
			
		||||
	// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
 | 
			
		||||
	resp, err := client.ModifyHostsCertificate(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to deploy certificate: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	d.infos = append(d.infos, toStr("部署证书", resp.Response))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +18,6 @@ type DeployConfig struct {
 | 
			
		|||
	Config map[string]any `json:"config"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 以字符串形式获取配置项。
 | 
			
		||||
//
 | 
			
		||||
// 入参:
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +51,39 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri
 | 
			
		|||
	return defaultValue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 以 32 位整数形式获取配置项。
 | 
			
		||||
//
 | 
			
		||||
// 入参:
 | 
			
		||||
//   - key: 配置项的键。
 | 
			
		||||
//
 | 
			
		||||
// 出参:
 | 
			
		||||
//   - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
 | 
			
		||||
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
 | 
			
		||||
	return dc.GetConfigOrDefaultAsInt32(key, 0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 以 32 位整数形式获取配置项。
 | 
			
		||||
//
 | 
			
		||||
// 入参:
 | 
			
		||||
//   - key: 配置项的键。
 | 
			
		||||
//   - defaultValue: 默认值。
 | 
			
		||||
//
 | 
			
		||||
// 出参:
 | 
			
		||||
//   - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
 | 
			
		||||
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
 | 
			
		||||
	if dc.Config == nil {
 | 
			
		||||
		return defaultValue
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if value, ok := dc.Config[key]; ok {
 | 
			
		||||
		if result, ok := value.(int32); ok {
 | 
			
		||||
			return result
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return defaultValue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 以布尔形式获取配置项。
 | 
			
		||||
//
 | 
			
		||||
// 入参:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ const (
 | 
			
		|||
	NotifyChannelWebhook  = "webhook"
 | 
			
		||||
	NotifyChannelTelegram = "telegram"
 | 
			
		||||
	NotifyChannelLark     = "lark"
 | 
			
		||||
	NotifyChannelServerChan = "serverchan"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type NotifyTestPushReq struct {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,8 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	stdhttp "net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/usual2970/certimate/internal/domain"
 | 
			
		||||
	"github.com/usual2970/certimate/internal/utils/app"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +104,8 @@ func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, e
 | 
			
		|||
		return getLarkNotifier(conf), nil
 | 
			
		||||
	case domain.NotifyChannelWebhook:
 | 
			
		||||
		return getWebhookNotifier(conf), nil
 | 
			
		||||
	case domain.NotifyChannelServerChan:
 | 
			
		||||
		return getServerChanNotifier(conf), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, fmt.Errorf("notifier not found")
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +136,25 @@ func getTelegramNotifier(conf map[string]any) notifyPackage.Notifier {
 | 
			
		|||
	return rs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getServerChanNotifier(conf map[string]any) notifyPackage.Notifier {
 | 
			
		||||
	rs := http.New()
 | 
			
		||||
 | 
			
		||||
	rs.AddReceivers(&http.Webhook{
 | 
			
		||||
		URL:         getString(conf, "url"),
 | 
			
		||||
		Header:      stdhttp.Header{},
 | 
			
		||||
		ContentType: "application/json",
 | 
			
		||||
		Method:      stdhttp.MethodPost,
 | 
			
		||||
		BuildPayload: func(subject, message string) (payload any) {
 | 
			
		||||
			return map[string]string{
 | 
			
		||||
				"text": subject,
 | 
			
		||||
				"desp": message,
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return rs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier {
 | 
			
		||||
	return dingding.New(&dingding.Config{
 | 
			
		||||
		Token:  getString(conf, "accessToken"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,9 +15,9 @@ import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
type AliyunCASUploaderConfig struct {
 | 
			
		||||
	Region          string `json:"region"`
 | 
			
		||||
	AccessKeyId     string `json:"accessKeyId"`
 | 
			
		||||
	AccessKeySecret string `json:"accessKeySecret"`
 | 
			
		||||
	Region          string `json:"region"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliyunCASUploader struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -28,9 +28,9 @@ type AliyunCASUploader struct {
 | 
			
		|||
 | 
			
		||||
func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (Uploader, error) {
 | 
			
		||||
	client, err := (&AliyunCASUploader{}).createSdkClient(
 | 
			
		||||
		config.Region,
 | 
			
		||||
		config.AccessKeyId,
 | 
			
		||||
		config.AccessKeySecret,
 | 
			
		||||
		config.Region,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create sdk client: %w", err)
 | 
			
		||||
| 
						 | 
				
			
			@ -81,12 +81,12 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP
 | 
			
		|||
					if *getUserCertificateDetailResp.Body.Cert == certPem {
 | 
			
		||||
						isSameCert = true
 | 
			
		||||
					} else {
 | 
			
		||||
						cert, err := x509.ParseCertificateFromPEM(*getUserCertificateDetailResp.Body.Cert)
 | 
			
		||||
						oldCertX509, err := x509.ParseCertificateFromPEM(*getUserCertificateDetailResp.Body.Cert)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							continue
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						isSameCert = x509.EqualCertificate(certX509, cert)
 | 
			
		||||
						isSameCert = x509.EqualCertificate(certX509, oldCertX509)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// 如果已存在相同证书,直接返回已有的证书信息
 | 
			
		||||
| 
						 | 
				
			
			@ -133,7 +133,7 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP
 | 
			
		|||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *AliyunCASUploader) createSdkClient(region, accessKeyId, accessKeySecret string) (*cas20200407.Client, error) {
 | 
			
		||||
func (u *AliyunCASUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*cas20200407.Client, error) {
 | 
			
		||||
	if region == "" {
 | 
			
		||||
		region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -147,10 +147,6 @@ func (u *AliyunCASUploader) createSdkClient(region, accessKeyId, accessKeySecret
 | 
			
		|||
	switch region {
 | 
			
		||||
	case "cn-hangzhou":
 | 
			
		||||
		endpoint = "cas.aliyuncs.com"
 | 
			
		||||
	case "ap-southeast-1":
 | 
			
		||||
		endpoint = "cas.ap-southeast-1.aliyuncs.com"
 | 
			
		||||
	case "eu-central-1":
 | 
			
		||||
		endpoint = "cas.eu-central-1.aliyuncs.com"
 | 
			
		||||
	default:
 | 
			
		||||
		endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,134 @@
 | 
			
		|||
package uploader
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
 | 
			
		||||
	slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client"
 | 
			
		||||
	util "github.com/alibabacloud-go/tea-utils/v2/service"
 | 
			
		||||
	"github.com/alibabacloud-go/tea/tea"
 | 
			
		||||
 | 
			
		||||
	"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  *slb20140515.Client
 | 
			
		||||
	sdkRuntime *util.RuntimeOptions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAliyunSLBUploader(config *AliyunSLBUploaderConfig) (Uploader, error) {
 | 
			
		||||
	client, err := (&AliyunSLBUploader{}).createSdkClient(
 | 
			
		||||
		config.AccessKeyId,
 | 
			
		||||
		config.AccessKeySecret,
 | 
			
		||||
		config.Region,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create sdk client: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AliyunSLBUploader{
 | 
			
		||||
		config:     config,
 | 
			
		||||
		sdkClient:  client,
 | 
			
		||||
		sdkRuntime: &util.RuntimeOptions{},
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *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 := &slb20140515.DescribeServerCertificatesRequest{
 | 
			
		||||
		RegionId: tea.String(u.config.Region),
 | 
			
		||||
	}
 | 
			
		||||
	describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificatesWithOptions(describeServerCertificatesReq, u.sdkRuntime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to execute sdk request 'slb.DescribeServerCertificates': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 &UploadResult{
 | 
			
		||||
					CertId:   *certDetail.ServerCertificateId,
 | 
			
		||||
					CertName: *certDetail.ServerCertificateName,
 | 
			
		||||
				}, nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 生成新证书名(需符合阿里云命名规则)
 | 
			
		||||
	var certId, certName string
 | 
			
		||||
	certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
 | 
			
		||||
 | 
			
		||||
	// 上传新证书
 | 
			
		||||
	// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate
 | 
			
		||||
	uploadServerCertificateReq := &slb20140515.UploadServerCertificateRequest{
 | 
			
		||||
		RegionId:              tea.String(u.config.Region),
 | 
			
		||||
		ServerCertificateName: tea.String(certName),
 | 
			
		||||
		ServerCertificate:     tea.String(certPem),
 | 
			
		||||
		PrivateKey:            tea.String(privkeyPem),
 | 
			
		||||
	}
 | 
			
		||||
	uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificateWithOptions(uploadServerCertificateReq, u.sdkRuntime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to execute sdk request 'slb.UploadServerCertificate': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	certId = *uploadServerCertificateResp.Body.ServerCertificateId
 | 
			
		||||
	return &UploadResult{
 | 
			
		||||
		CertId:   certId,
 | 
			
		||||
		CertName: certName,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *AliyunSLBUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) {
 | 
			
		||||
	if region == "" {
 | 
			
		||||
		region = "cn-hangzhou" // SLB 服务默认区域:华东一杭州
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	aConfig := &openapi.Config{
 | 
			
		||||
		AccessKeyId:     tea.String(accessKeyId),
 | 
			
		||||
		AccessKeySecret: tea.String(accessKeySecret),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var endpoint string
 | 
			
		||||
	switch region {
 | 
			
		||||
	case "cn-hangzhou":
 | 
			
		||||
	case "cn-hangzhou-finance":
 | 
			
		||||
	case "cn-shanghai-finance-1":
 | 
			
		||||
	case "cn-shenzhen-finance-1":
 | 
			
		||||
		endpoint = "slb.aliyuncs.com"
 | 
			
		||||
	default:
 | 
			
		||||
		endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
 | 
			
		||||
	}
 | 
			
		||||
	aConfig.Endpoint = tea.String(endpoint)
 | 
			
		||||
 | 
			
		||||
	client, err := slb20140515.NewClient(aConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return client, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,6 +7,23 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 比较两个 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。
 | 
			
		||||
//
 | 
			
		||||
// 入参:
 | 
			
		||||
| 
						 | 
				
			
			@ -31,26 +48,40 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error)
 | 
			
		|||
	return cert, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。
 | 
			
		||||
// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。
 | 
			
		||||
// 从 PEM 编码的私钥字符串解析并返回一个 ECDSA 私钥对象。
 | 
			
		||||
//
 | 
			
		||||
// 入参:
 | 
			
		||||
//   - a: 待比较的第一个 x509.Certificate 对象。
 | 
			
		||||
//   - b: 待比较的第二个 x509.Certificate 对象。
 | 
			
		||||
//   - privkeyPem: 私钥 PEM 内容。
 | 
			
		||||
//
 | 
			
		||||
// 出参:
 | 
			
		||||
//   - 是否相同。
 | 
			
		||||
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
 | 
			
		||||
//   - 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, fmt.Errorf("failed to decode PEM block")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	privkey, err = x509.ParseECPrivateKey(block.Bytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to parse private key: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return privkey, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 将 ECDSA 私钥转换为 PEM 格式的字符串。
 | 
			
		||||
func PrivateKeyToPEM(privateKey *ecdsa.PrivateKey) (string, error) {
 | 
			
		||||
	data, err := x509.MarshalECPrivateKey(privateKey)
 | 
			
		||||
// 将 ECDSA 私钥转换为 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 "", fmt.Errorf("failed to marshal EC private key: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -62,20 +93,3 @@ func PrivateKeyToPEM(privateKey *ecdsa.PrivateKey) (string, error) {
 | 
			
		|||
 | 
			
		||||
	return string(pem.EncodeToMemory(block)), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 从 PEM 编码的私钥字符串解析并返回一个 ECDSA 私钥对象。
 | 
			
		||||
func ParsePrivateKeyFromPEM(privateKeyPem string) (*ecdsa.PrivateKey, error) {
 | 
			
		||||
	pemData := []byte(privateKeyPem)
 | 
			
		||||
 | 
			
		||||
	block, _ := pem.Decode(pemData)
 | 
			
		||||
	if block == nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to decode PEM block")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	privateKey, err := x509.ParseECPrivateKey(block.Bytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to parse private key: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return privateKey, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProp
 | 
			
		|||
    configType: accessTypeFormSchema,
 | 
			
		||||
    kubeConfig: z
 | 
			
		||||
      .string()
 | 
			
		||||
      .min(1, "access.authorization.form.k8s_kubeconfig.placeholder")
 | 
			
		||||
      .min(0, "access.authorization.form.k8s_kubeconfig.placeholder")
 | 
			
		||||
      .max(20480, t("common.errmsg.string_max", { max: 20480 })),
 | 
			
		||||
    kubeConfigFile: z.any().optional(),
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -191,3 +191,4 @@ const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProp
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export default AccessKubernetesForm;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,13 @@ import AccessEditDialog from "./AccessEditDialog";
 | 
			
		|||
import { Context as DeployEditContext } from "./DeployEdit";
 | 
			
		||||
import DeployToAliyunOSS from "./DeployToAliyunOSS";
 | 
			
		||||
import DeployToAliyunCDN from "./DeployToAliyunCDN";
 | 
			
		||||
import DeployToAliyunCLB from "./DeployToAliyunCLB";
 | 
			
		||||
import DeployToAliyunALB from "./DeployToAliyunALB";
 | 
			
		||||
import DeployToAliyunNLB from "./DeployToAliyunNLB";
 | 
			
		||||
import DeployToTencentCDN from "./DeployToTencentCDN";
 | 
			
		||||
import DeployToTencentCLB from "./DeployToTencentCLB";
 | 
			
		||||
import DeployToTencentCOS from "./DeployToTencentCOS";
 | 
			
		||||
import DeployToTencentTEO from "./DeployToTencentTEO";
 | 
			
		||||
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
 | 
			
		||||
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
 | 
			
		||||
import DeployToQiniuCDN from "./DeployToQiniuCDN";
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +123,17 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
 | 
			
		|||
    case "aliyun-dcdn":
 | 
			
		||||
      childComponent = <DeployToAliyunCDN />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "aliyun-clb":
 | 
			
		||||
      childComponent = <DeployToAliyunCLB />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "aliyun-alb":
 | 
			
		||||
      childComponent = <DeployToAliyunALB />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "aliyun-nlb":
 | 
			
		||||
      childComponent = <DeployToAliyunNLB />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "tencent-cdn":
 | 
			
		||||
    case "tencent-ecdn":
 | 
			
		||||
      childComponent = <DeployToTencentCDN />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "tencent-clb":
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +142,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
 | 
			
		|||
    case "tencent-cos":
 | 
			
		||||
      childComponent = <DeployToTencentCOS />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "tencent-teo":
 | 
			
		||||
      childComponent = <DeployToTencentTEO />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "huaweicloud-cdn":
 | 
			
		||||
      childComponent = <DeployToHuaweiCloudCDN />;
 | 
			
		||||
      break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,162 @@
 | 
			
		|||
import { useEffect } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
import { produce } from "immer";
 | 
			
		||||
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
 | 
			
		||||
import { useDeployEditContext } from "./DeployEdit";
 | 
			
		||||
 | 
			
		||||
const DeployToAliyunALB = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!data.id) {
 | 
			
		||||
      setDeploy({
 | 
			
		||||
        ...data,
 | 
			
		||||
        config: {
 | 
			
		||||
          region: "cn-hangzhou",
 | 
			
		||||
          resourceType: "",
 | 
			
		||||
          loadbalancerId: "",
 | 
			
		||||
          listenerId: "",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setError({});
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const formSchema = z
 | 
			
		||||
    .object({
 | 
			
		||||
      region: z.string().min(1, t("domain.deployment.form.aliyun_alb_region.placeholder")),
 | 
			
		||||
      resourceType: z.union([z.literal("loadbalancer"), z.literal("listener")], {
 | 
			
		||||
        message: t("domain.deployment.form.aliyun_alb_resource_type.placeholder"),
 | 
			
		||||
      }),
 | 
			
		||||
      loadbalancerId: z.string().optional(),
 | 
			
		||||
      listenerId: z.string().optional(),
 | 
			
		||||
    })
 | 
			
		||||
    .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), {
 | 
			
		||||
      message: t("domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder"),
 | 
			
		||||
      path: ["loadbalancerId"],
 | 
			
		||||
    })
 | 
			
		||||
    .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), {
 | 
			
		||||
      message: t("domain.deployment.form.aliyun_alb_listener_id.placeholder"),
 | 
			
		||||
      path: ["listenerId"],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const res = formSchema.safeParse(data.config);
 | 
			
		||||
    if (!res.success) {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        region: res.error.errors.find((e) => e.path[0] === "region")?.message,
 | 
			
		||||
        resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message,
 | 
			
		||||
        loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message,
 | 
			
		||||
        listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        region: undefined,
 | 
			
		||||
        resourceType: undefined,
 | 
			
		||||
        loadbalancerId: undefined,
 | 
			
		||||
        listenerId: undefined,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col space-y-8">
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.aliyun_alb_region.label")}</Label>
 | 
			
		||||
        <Input
 | 
			
		||||
          placeholder={t("domain.deployment.form.aliyun_alb_region.placeholder")}
 | 
			
		||||
          className="w-full mt-1"
 | 
			
		||||
          value={data?.config?.region}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              draft.config ??= {};
 | 
			
		||||
              draft.config.region = e.target.value?.trim();
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.region}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.aliyun_alb_resource_type.label")}</Label>
 | 
			
		||||
        <Select
 | 
			
		||||
          value={data?.config?.resourceType}
 | 
			
		||||
          onValueChange={(value) => {
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              draft.config ??= {};
 | 
			
		||||
              draft.config.resourceType = value?.trim();
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <SelectTrigger>
 | 
			
		||||
            <SelectValue placeholder={t("domain.deployment.form.aliyun_alb_resource_type.placeholder")} />
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent>
 | 
			
		||||
            <SelectGroup>
 | 
			
		||||
              <SelectItem value="loadbalancer">{t("domain.deployment.form.aliyun_alb_resource_type.option.loadbalancer.label")}</SelectItem>
 | 
			
		||||
              <SelectItem value="listener">{t("domain.deployment.form.aliyun_alb_resource_type.option.listener.label")}</SelectItem>
 | 
			
		||||
            </SelectGroup>
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.resourceType}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {data?.config?.resourceType === "loadbalancer" ? (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.aliyun_alb_loadbalancer_id.label")}</Label>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder={t("domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder")}
 | 
			
		||||
            className="w-full mt-1"
 | 
			
		||||
            value={data?.config?.loadbalancerId}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newData = produce(data, (draft) => {
 | 
			
		||||
                draft.config ??= {};
 | 
			
		||||
                draft.config.loadbalancerId = e.target.value?.trim();
 | 
			
		||||
              });
 | 
			
		||||
              setDeploy(newData);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <div className="text-red-600 text-sm mt-1">{error?.loadbalancerId}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {data?.config?.resourceType === "listener" ? (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.aliyun_alb_listener_id.label")}</Label>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder={t("domain.deployment.form.aliyun_alb_listener_id.placeholder")}
 | 
			
		||||
            className="w-full mt-1"
 | 
			
		||||
            value={data?.config?.listenerId}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newData = produce(data, (draft) => {
 | 
			
		||||
                draft.config ??= {};
 | 
			
		||||
                draft.config.listenerId = e.target.value?.trim();
 | 
			
		||||
              });
 | 
			
		||||
              setDeploy(newData);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <div className="text-red-600 text-sm mt-1">{error?.listenerId}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DeployToAliyunALB;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,158 @@
 | 
			
		|||
import { useEffect } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
import { produce } from "immer";
 | 
			
		||||
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
 | 
			
		||||
import { useDeployEditContext } from "./DeployEdit";
 | 
			
		||||
 | 
			
		||||
const DeployToAliyunCLB = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!data.id) {
 | 
			
		||||
      setDeploy({
 | 
			
		||||
        ...data,
 | 
			
		||||
        config: {
 | 
			
		||||
          region: "cn-hangzhou",
 | 
			
		||||
          resourceType: "",
 | 
			
		||||
          loadbalancerId: "",
 | 
			
		||||
          listenerPort: "443",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setError({});
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const formSchema = z
 | 
			
		||||
    .object({
 | 
			
		||||
      region: z.string().min(1, t("domain.deployment.form.aliyun_clb_region.placeholder")),
 | 
			
		||||
      resourceType: z.union([z.literal("certificate"), z.literal("loadbalancer"), z.literal("listener")], {
 | 
			
		||||
        message: t("domain.deployment.form.aliyun_clb_resource_type.placeholder"),
 | 
			
		||||
      }),
 | 
			
		||||
      loadbalancerId: z.string().optional(),
 | 
			
		||||
      listenerPort: z.string().optional(),
 | 
			
		||||
    })
 | 
			
		||||
    .refine((data) => (data.resourceType === "loadbalancer" || data.resourceType === "listener" ? !!data.loadbalancerId?.trim() : true), {
 | 
			
		||||
      message: t("domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder"),
 | 
			
		||||
      path: ["loadbalancerId"],
 | 
			
		||||
    })
 | 
			
		||||
    .refine((data) => (data.resourceType === "listener" ? +data.listenerPort! > 0 && +data.listenerPort! < 65535 : true), {
 | 
			
		||||
      message: t("domain.deployment.form.aliyun_clb_listener_port.placeholder"),
 | 
			
		||||
      path: ["listenerPort"],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const res = formSchema.safeParse(data.config);
 | 
			
		||||
    if (!res.success) {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        region: res.error.errors.find((e) => e.path[0] === "region")?.message,
 | 
			
		||||
        resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message,
 | 
			
		||||
        loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message,
 | 
			
		||||
        listenerPort: res.error.errors.find((e) => e.path[0] === "listenerPort")?.message,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        region: undefined,
 | 
			
		||||
        resourceType: undefined,
 | 
			
		||||
        loadbalancerId: undefined,
 | 
			
		||||
        listenerPort: undefined,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col space-y-8">
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.aliyun_clb_region.label")}</Label>
 | 
			
		||||
        <Input
 | 
			
		||||
          placeholder={t("domain.deployment.form.aliyun_clb_region.placeholder")}
 | 
			
		||||
          className="w-full mt-1"
 | 
			
		||||
          value={data?.config?.region}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              draft.config ??= {};
 | 
			
		||||
              draft.config.region = e.target.value?.trim();
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.region}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.aliyun_clb_resource_type.label")}</Label>
 | 
			
		||||
        <Select
 | 
			
		||||
          value={data?.config?.resourceType}
 | 
			
		||||
          onValueChange={(value) => {
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              draft.config ??= {};
 | 
			
		||||
              draft.config.resourceType = value?.trim();
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <SelectTrigger>
 | 
			
		||||
            <SelectValue placeholder={t("domain.deployment.form.aliyun_clb_resource_type.placeholder")} />
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent>
 | 
			
		||||
            <SelectGroup>
 | 
			
		||||
              <SelectItem value="loadbalancer">{t("domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label")}</SelectItem>
 | 
			
		||||
              <SelectItem value="listener">{t("domain.deployment.form.aliyun_clb_resource_type.option.listener.label")}</SelectItem>
 | 
			
		||||
            </SelectGroup>
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.resourceType}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.aliyun_clb_loadbalancer_id.label")}</Label>
 | 
			
		||||
        <Input
 | 
			
		||||
          placeholder={t("domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder")}
 | 
			
		||||
          className="w-full mt-1"
 | 
			
		||||
          value={data?.config?.loadbalancerId}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              draft.config ??= {};
 | 
			
		||||
              draft.config.loadbalancerId = e.target.value?.trim();
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.loadbalancerId}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {data?.config?.resourceType === "listener" ? (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.aliyun_clb_listener_port.label")}</Label>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder={t("domain.deployment.form.aliyun_clb_listener_port.placeholder")}
 | 
			
		||||
            className="w-full mt-1"
 | 
			
		||||
            value={data?.config?.listenerPort}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newData = produce(data, (draft) => {
 | 
			
		||||
                draft.config ??= {};
 | 
			
		||||
                draft.config.listenerPort = e.target.value?.trim();
 | 
			
		||||
              });
 | 
			
		||||
              setDeploy(newData);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <div className="text-red-600 text-sm mt-1">{error?.listenerPort}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DeployToAliyunCLB;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,162 @@
 | 
			
		|||
import { useEffect } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
import { produce } from "immer";
 | 
			
		||||
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
 | 
			
		||||
import { useDeployEditContext } from "./DeployEdit";
 | 
			
		||||
 | 
			
		||||
const DeployToAliyunNLB = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!data.id) {
 | 
			
		||||
      setDeploy({
 | 
			
		||||
        ...data,
 | 
			
		||||
        config: {
 | 
			
		||||
          region: "cn-hangzhou",
 | 
			
		||||
          resourceType: "",
 | 
			
		||||
          loadbalancerId: "",
 | 
			
		||||
          listenerId: "",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setError({});
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const formSchema = z
 | 
			
		||||
    .object({
 | 
			
		||||
      region: z.string().min(1, t("domain.deployment.form.aliyun_nlb_region.placeholder")),
 | 
			
		||||
      resourceType: z.union([z.literal("loadbalancer"), z.literal("listener")], {
 | 
			
		||||
        message: t("domain.deployment.form.aliyun_nlb_resource_type.placeholder"),
 | 
			
		||||
      }),
 | 
			
		||||
      loadbalancerId: z.string().optional(),
 | 
			
		||||
      listenerId: z.string().optional(),
 | 
			
		||||
    })
 | 
			
		||||
    .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), {
 | 
			
		||||
      message: t("domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder"),
 | 
			
		||||
      path: ["loadbalancerId"],
 | 
			
		||||
    })
 | 
			
		||||
    .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), {
 | 
			
		||||
      message: t("domain.deployment.form.aliyun_nlb_listener_id.placeholder"),
 | 
			
		||||
      path: ["listenerId"],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const res = formSchema.safeParse(data.config);
 | 
			
		||||
    if (!res.success) {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        region: res.error.errors.find((e) => e.path[0] === "region")?.message,
 | 
			
		||||
        resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message,
 | 
			
		||||
        loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message,
 | 
			
		||||
        listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        region: undefined,
 | 
			
		||||
        resourceType: undefined,
 | 
			
		||||
        loadbalancerId: undefined,
 | 
			
		||||
        listenerId: undefined,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col space-y-8">
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.aliyun_nlb_region.label")}</Label>
 | 
			
		||||
        <Input
 | 
			
		||||
          placeholder={t("domain.deployment.form.aliyun_nlb_region.placeholder")}
 | 
			
		||||
          className="w-full mt-1"
 | 
			
		||||
          value={data?.config?.region}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              draft.config ??= {};
 | 
			
		||||
              draft.config.region = e.target.value?.trim();
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.region}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.aliyun_nlb_resource_type.label")}</Label>
 | 
			
		||||
        <Select
 | 
			
		||||
          value={data?.config?.resourceType}
 | 
			
		||||
          onValueChange={(value) => {
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              draft.config ??= {};
 | 
			
		||||
              draft.config.resourceType = value?.trim();
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <SelectTrigger>
 | 
			
		||||
            <SelectValue placeholder={t("domain.deployment.form.aliyun_nlb_resource_type.placeholder")} />
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent>
 | 
			
		||||
            <SelectGroup>
 | 
			
		||||
              <SelectItem value="loadbalancer">{t("domain.deployment.form.aliyun_nlb_resource_type.option.loadbalancer.label")}</SelectItem>
 | 
			
		||||
              <SelectItem value="listener">{t("domain.deployment.form.aliyun_nlb_resource_type.option.listener.label")}</SelectItem>
 | 
			
		||||
            </SelectGroup>
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.resourceType}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {data?.config?.resourceType === "loadbalancer" ? (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.aliyun_nlb_loadbalancer_id.label")}</Label>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder={t("domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder")}
 | 
			
		||||
            className="w-full mt-1"
 | 
			
		||||
            value={data?.config?.loadbalancerId}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newData = produce(data, (draft) => {
 | 
			
		||||
                draft.config ??= {};
 | 
			
		||||
                draft.config.loadbalancerId = e.target.value?.trim();
 | 
			
		||||
              });
 | 
			
		||||
              setDeploy(newData);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <div className="text-red-600 text-sm mt-1">{error?.loadbalancerId}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {data?.config?.resourceType === "listener" ? (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.aliyun_nlb_listener_id.label")}</Label>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder={t("domain.deployment.form.aliyun_nlb_listener_id.placeholder")}
 | 
			
		||||
            className="w-full mt-1"
 | 
			
		||||
            value={data?.config?.listenerId}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newData = produce(data, (draft) => {
 | 
			
		||||
                draft.config ??= {};
 | 
			
		||||
                draft.config.listenerId = e.target.value?.trim();
 | 
			
		||||
              });
 | 
			
		||||
              setDeploy(newData);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <div className="text-red-600 text-sm mt-1">{error?.listenerId}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DeployToAliyunNLB;
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ const DeployToHuaweiCloudCDN = () => {
 | 
			
		|||
      message: t("domain.deployment.form.huaweicloud_elb_certificate_id.placeholder"),
 | 
			
		||||
      path: ["certificateId"],
 | 
			
		||||
    })
 | 
			
		||||
    .refine((data) => (data.resourceType === "loadbalancer" ? !!data.certificateId?.trim() : true), {
 | 
			
		||||
    .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), {
 | 
			
		||||
      message: t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder"),
 | 
			
		||||
      path: ["loadbalancerId"],
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,131 @@
 | 
			
		|||
import { useEffect } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
import { produce } from "immer";
 | 
			
		||||
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { Textarea } from "@/components/ui/textarea";
 | 
			
		||||
import { useDeployEditContext } from "./DeployEdit";
 | 
			
		||||
 | 
			
		||||
const DeployToTencentTEO = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setError({});
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const resp = domainSchema.safeParse(data.config?.domain);
 | 
			
		||||
    if (!resp.success) {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        domain: JSON.parse(resp.error.message)[0].message,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        domain: "",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const resp = zoneIdSchema.safeParse(data.config?.zoneId);
 | 
			
		||||
    if (!resp.success) {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        zoneId: JSON.parse(resp.error.message)[0].message,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        zoneId: "",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
 | 
			
		||||
    message: t("common.errmsg.domain_invalid"),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const zoneIdSchema = z.string().regex(/^zone-[0-9a-zA-Z]{9}$/, {
 | 
			
		||||
    message: t("common.errmsg.zoneid_invalid"),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col space-y-8">
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.tencent_teo_zone_id.label")}</Label>
 | 
			
		||||
        <Input
 | 
			
		||||
          placeholder={t("domain.deployment.form.tencent_teo_zone_id.placeholder")}
 | 
			
		||||
          className="w-full mt-1"
 | 
			
		||||
          value={data?.config?.zoneId}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            const temp = e.target.value;
 | 
			
		||||
 | 
			
		||||
            const resp = zoneIdSchema.safeParse(temp);
 | 
			
		||||
            if (!resp.success) {
 | 
			
		||||
              setError({
 | 
			
		||||
                ...error,
 | 
			
		||||
                zoneId: JSON.parse(resp.error.message)[0].message,
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              setError({
 | 
			
		||||
                ...error,
 | 
			
		||||
                zoneId: "",
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              if (!draft.config) {
 | 
			
		||||
                draft.config = {};
 | 
			
		||||
              }
 | 
			
		||||
              draft.config.zoneId = temp;
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.zoneId}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <Label>{t("domain.deployment.form.tencent_teo_domain.label")}</Label>
 | 
			
		||||
        <Textarea
 | 
			
		||||
          placeholder={t("domain.deployment.form.tencent_teo_domain.placeholder")}
 | 
			
		||||
          className="w-full mt-1"
 | 
			
		||||
          value={data?.config?.domain}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            const temp = e.target.value;
 | 
			
		||||
 | 
			
		||||
            const resp = domainSchema.safeParse(temp);
 | 
			
		||||
            if (!resp.success) {
 | 
			
		||||
              setError({
 | 
			
		||||
                ...error,
 | 
			
		||||
                domain: JSON.parse(resp.error.message)[0].message,
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              setError({
 | 
			
		||||
                ...error,
 | 
			
		||||
                domain: "",
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const newData = produce(data, (draft) => {
 | 
			
		||||
              if (!draft.config) {
 | 
			
		||||
                draft.config = {};
 | 
			
		||||
              }
 | 
			
		||||
              draft.config.domain = temp;
 | 
			
		||||
            });
 | 
			
		||||
            setDeploy(newData);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="text-red-600 text-sm mt-1">{error?.domain}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DeployToTencentTEO;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,236 @@
 | 
			
		|||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { Switch } from "@/components/ui/switch";
 | 
			
		||||
import { useToast } from "@/components/ui/use-toast";
 | 
			
		||||
import { getErrMessage } from "@/lib/error";
 | 
			
		||||
import { isValidURL } from "@/lib/url";
 | 
			
		||||
import { NotifyChannels, NotifyChannelServerChan } from "@/domain/settings";
 | 
			
		||||
import { update } from "@/repository/settings";
 | 
			
		||||
import { useNotifyContext } from "@/providers/notify";
 | 
			
		||||
import { notifyTest } from "@/api/notify";
 | 
			
		||||
import Show from "@/components/Show";
 | 
			
		||||
 | 
			
		||||
type ServerChanSetting = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  data: NotifyChannelServerChan;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ServerChan = () => {
 | 
			
		||||
  const { config, setChannels } = useNotifyContext();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [changed, setChanged] = useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
  const [serverchan, setServerChan] = useState<ServerChanSetting>({
 | 
			
		||||
    id: config.id ?? "",
 | 
			
		||||
    name: "notifyChannels",
 | 
			
		||||
    data: {
 | 
			
		||||
      url: "",
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const [originServerChan, setOriginServerChan] = useState<ServerChanSetting>({
 | 
			
		||||
    id: config.id ?? "",
 | 
			
		||||
    name: "notifyChannels",
 | 
			
		||||
    data: {
 | 
			
		||||
      url: "",
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setChanged(false);
 | 
			
		||||
  }, [config]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const data = getDetailServerChan();
 | 
			
		||||
    setOriginServerChan({
 | 
			
		||||
      id: config.id ?? "",
 | 
			
		||||
      name: "serverchan",
 | 
			
		||||
      data,
 | 
			
		||||
    });
 | 
			
		||||
  }, [config]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const data = getDetailServerChan();
 | 
			
		||||
    setServerChan({
 | 
			
		||||
      id: config.id ?? "",
 | 
			
		||||
      name: "serverchan",
 | 
			
		||||
      data,
 | 
			
		||||
    });
 | 
			
		||||
  }, [config]);
 | 
			
		||||
 | 
			
		||||
  const { toast } = useToast();
 | 
			
		||||
 | 
			
		||||
  const checkChanged = (data: NotifyChannelServerChan) => {
 | 
			
		||||
    if (data.url !== originServerChan.data.url) {
 | 
			
		||||
      setChanged(true);
 | 
			
		||||
    } else {
 | 
			
		||||
      setChanged(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getDetailServerChan = () => {
 | 
			
		||||
    const df: NotifyChannelServerChan = {
 | 
			
		||||
      url: "",
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    };
 | 
			
		||||
    if (!config.content) {
 | 
			
		||||
      return df;
 | 
			
		||||
    }
 | 
			
		||||
    const chanels = config.content as NotifyChannels;
 | 
			
		||||
    if (!chanels.serverchan) {
 | 
			
		||||
      return df;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return chanels.serverchan as NotifyChannelServerChan;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSaveClick = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      serverchan.data.url = serverchan.data.url.trim();
 | 
			
		||||
      if (!isValidURL(serverchan.data.url)) {
 | 
			
		||||
        toast({
 | 
			
		||||
          title: t("common.save.failed.message"),
 | 
			
		||||
          description: t("settings.notification.url.errmsg.invalid"),
 | 
			
		||||
          variant: "destructive",
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const resp = await update({
 | 
			
		||||
        ...config,
 | 
			
		||||
        name: "notifyChannels",
 | 
			
		||||
        content: {
 | 
			
		||||
          ...config.content,
 | 
			
		||||
          serverchan: {
 | 
			
		||||
            ...serverchan.data,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      setChannels(resp);
 | 
			
		||||
      toast({
 | 
			
		||||
        title: t("common.save.succeeded.message"),
 | 
			
		||||
        description: t("settings.notification.config.saved.message"),
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      const msg = getErrMessage(e);
 | 
			
		||||
 | 
			
		||||
      toast({
 | 
			
		||||
        title: t("common.save.failed.message"),
 | 
			
		||||
        description: `${t("settings.notification.config.failed.message")}: ${msg}`,
 | 
			
		||||
        variant: "destructive",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePushTestClick = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await notifyTest("serverchan");
 | 
			
		||||
 | 
			
		||||
      toast({
 | 
			
		||||
        title: t("settings.notification.config.push.test.message.success.message"),
 | 
			
		||||
        description: t("settings.notification.config.push.test.message.success.message"),
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      const msg = getErrMessage(e);
 | 
			
		||||
 | 
			
		||||
      toast({
 | 
			
		||||
        title: t("settings.notification.config.push.test.message.failed.message"),
 | 
			
		||||
        description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
 | 
			
		||||
        variant: "destructive",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSwitchChange = async () => {
 | 
			
		||||
    const newData = {
 | 
			
		||||
      ...serverchan,
 | 
			
		||||
      data: {
 | 
			
		||||
        ...serverchan.data,
 | 
			
		||||
        enabled: !serverchan.data.enabled,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    setServerChan(newData);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const resp = await update({
 | 
			
		||||
        ...config,
 | 
			
		||||
        name: "notifyChannels",
 | 
			
		||||
        content: {
 | 
			
		||||
          ...config.content,
 | 
			
		||||
          serverchan: {
 | 
			
		||||
            ...newData.data,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      setChannels(resp);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      const msg = getErrMessage(e);
 | 
			
		||||
 | 
			
		||||
      toast({
 | 
			
		||||
        title: t("common.save.failed.message"),
 | 
			
		||||
        description: `${t("settings.notification.config.failed.message")}: ${msg}`,
 | 
			
		||||
        variant: "destructive",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Input
 | 
			
		||||
        placeholder={t("settings.notification.serverchan.url.placeholder")}
 | 
			
		||||
        value={serverchan.data.url}
 | 
			
		||||
        onChange={(e) => {
 | 
			
		||||
          const newData = {
 | 
			
		||||
            ...serverchan,
 | 
			
		||||
            data: {
 | 
			
		||||
              ...serverchan.data,
 | 
			
		||||
              url: e.target.value,
 | 
			
		||||
            },
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          checkChanged(newData.data);
 | 
			
		||||
          setServerChan(newData);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div className="flex items-center space-x-1 mt-2">
 | 
			
		||||
        <Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
 | 
			
		||||
        <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="flex justify-end mt-2">
 | 
			
		||||
        <Show when={changed}>
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              handleSaveClick();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {t("common.save")}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Show>
 | 
			
		||||
 | 
			
		||||
        <Show when={!changed && serverchan.id != ""}>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="secondary"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              handlePushTestClick();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {t("settings.notification.config.push.test.message")}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Show>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ServerChan;
 | 
			
		||||
| 
						 | 
				
			
			@ -75,9 +75,14 @@ export const deployTargetsMap: Map<DeployTarget["type"], DeployTarget> = new Map
 | 
			
		|||
    ["aliyun-oss", "common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"],
 | 
			
		||||
    ["aliyun-cdn", "common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"],
 | 
			
		||||
    ["aliyun-dcdn", "common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"],
 | 
			
		||||
    ["aliyun-clb", "common.provider.aliyun.clb", "/imgs/providers/aliyun.svg"],
 | 
			
		||||
    ["aliyun-alb", "common.provider.aliyun.alb", "/imgs/providers/aliyun.svg"],
 | 
			
		||||
    ["aliyun-nlb", "common.provider.aliyun.nlb", "/imgs/providers/aliyun.svg"],
 | 
			
		||||
    ["tencent-cdn", "common.provider.tencent.cdn", "/imgs/providers/tencent.svg"],
 | 
			
		||||
    ["tencent-ecdn", "common.provider.tencent.ecdn", "/imgs/providers/tencent.svg"],
 | 
			
		||||
    ["tencent-clb", "common.provider.tencent.clb", "/imgs/providers/tencent.svg"],
 | 
			
		||||
    ["tencent-cos", "common.provider.tencent.cos", "/imgs/providers/tencent.svg"],
 | 
			
		||||
    ["tencent-teo", "common.provider.tencent.teo", "/imgs/providers/tencent.svg"],
 | 
			
		||||
    ["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"],
 | 
			
		||||
    ["huaweicloud-elb", "common.provider.huaweicloud.elb", "/imgs/providers/huaweicloud.svg"],
 | 
			
		||||
    ["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,9 +22,10 @@ export type NotifyChannels = {
 | 
			
		|||
  lark?: NotifyChannel;
 | 
			
		||||
  telegram?: NotifyChannel;
 | 
			
		||||
  webhook?: NotifyChannel;
 | 
			
		||||
  serverchan?: NotifyChannel;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type NotifyChannel = NotifyChannelDingTalk | NotifyChannelLark | NotifyChannelTelegram | NotifyChannelWebhook;
 | 
			
		||||
export type NotifyChannel = NotifyChannelDingTalk | NotifyChannelLark | NotifyChannelTelegram | NotifyChannelWebhook | NotifyChannelServerChan;
 | 
			
		||||
 | 
			
		||||
export type NotifyChannelDingTalk = {
 | 
			
		||||
  accessToken: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +49,11 @@ export type NotifyChannelWebhook = {
 | 
			
		|||
  enabled: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type NotifyChannelServerChan = {
 | 
			
		||||
  url: string;
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const defaultNotifyTemplate: NotifyTemplate = {
 | 
			
		||||
  title: "您有 {COUNT} 张证书即将过期",
 | 
			
		||||
  content: "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,9 +69,9 @@
 | 
			
		|||
  "access.authorization.form.ssh_key_passphrase.placeholder": "Please enter Key Passphrase",
 | 
			
		||||
  "access.authorization.form.webhook_url.label": "Webhook URL",
 | 
			
		||||
  "access.authorization.form.webhook_url.placeholder": "Please enter Webhook URL",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig.label": "KubeConfig",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig.label": "KubeConfig (Null will use pod's ServiceAccount)",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig.placeholder": "Please enter KubeConfig",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig_file.placeholder": "Please select file",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig_file.placeholder": "Please select file (Null will use pod's ServiceAccount)",
 | 
			
		||||
 | 
			
		||||
  "access.group.tab": "Authorization Group",
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,15 +51,21 @@
 | 
			
		|||
  "common.errmsg.host_invalid": "Please enter the correct domain name or IP",
 | 
			
		||||
  "common.errmsg.ip_invalid": "Please enter IP",
 | 
			
		||||
  "common.errmsg.url_invalid": "Please enter a valid URL",
 | 
			
		||||
  "common.errmsg.zoneid_invalid": "Please enter Zone ID",
 | 
			
		||||
 | 
			
		||||
  "common.provider.aliyun": "Alibaba Cloud",
 | 
			
		||||
  "common.provider.aliyun.oss": "Alibaba Cloud - OSS",
 | 
			
		||||
  "common.provider.aliyun.cdn": "Alibaba Cloud - CDN",
 | 
			
		||||
  "common.provider.aliyun.dcdn": "Alibaba Cloud - DCDN",
 | 
			
		||||
  "common.provider.aliyun.clb": "Alibaba Cloud - CLB",
 | 
			
		||||
  "common.provider.aliyun.alb": "Alibaba Cloud - ALB",
 | 
			
		||||
  "common.provider.aliyun.nlb": "Alibaba Cloud - NLB",
 | 
			
		||||
  "common.provider.tencent": "Tencent Cloud",
 | 
			
		||||
  "common.provider.tencent.cdn": "Tencent Cloud - CDN",
 | 
			
		||||
  "common.provider.tencent.ecdn": "Tencent Cloud - ECDN",
 | 
			
		||||
  "common.provider.tencent.clb": "Tencent Cloud - CLB",
 | 
			
		||||
  "common.provider.tencent.cos": "Tencent Cloud - COS",
 | 
			
		||||
  "common.provider.tencent.teo": "Tencent Cloud - TEO",
 | 
			
		||||
  "common.provider.huaweicloud": "Huawei Cloud",
 | 
			
		||||
  "common.provider.huaweicloud.cdn": "Huawei Cloud - CDN",
 | 
			
		||||
  "common.provider.huaweicloud.elb": "Huawei Cloud - ELB",
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +80,7 @@
 | 
			
		|||
  "common.provider.local": "Local Deployment",
 | 
			
		||||
  "common.provider.ssh": "SSH Deployment",
 | 
			
		||||
  "common.provider.webhook": "Webhook",
 | 
			
		||||
  "common.provider.serverchan": "ServerChan",
 | 
			
		||||
  "common.provider.kubernetes": "Kubernetes",
 | 
			
		||||
  "common.provider.kubernetes.secret": "Kubernetes - Secret",
 | 
			
		||||
  "common.provider.dingtalk": "DingTalk",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,36 @@
 | 
			
		|||
  "domain.deployment.form.aliyun_oss_endpoint.placeholder": "Please enter endpoint",
 | 
			
		||||
  "domain.deployment.form.aliyun_oss_bucket.label": "Bucket",
 | 
			
		||||
  "domain.deployment.form.aliyun_oss_bucket.placeholder": "Please enter bucket",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_region.label": "Region",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_region.placeholder": "Please enter region (e.g. cn-hangzhou)",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.label": "Resource Type",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.placeholder": "Please select CLB resource type",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label": "CLB LoadBalancer",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.option.listener.label": "CLB Listener",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_loadbalancer_id.label": "LoadBalancer ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder": "Please enter CLB loadbalancer ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_listener_port.label": "Listener Port",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_listener_port.placeholder": "Please enter CLB listener port",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_region.label": "Region",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_region.placeholder": "Please enter region (e.g. cn-hangzhou)",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.label": "Resource Type",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.placeholder": "Please select ALB resource type",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.option.loadbalancer.label": "ALB LoadBalancer",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.option.listener.label": "ALB Listener",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_loadbalancer_id.label": "LoadBalancer ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder": "Please enter ALB loadbalancer ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_listener_id.label": "Listener ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_listener_id.placeholder": "Please enter ALB listener ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_region.label": "Region",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_region.placeholder": "Please enter region (e.g. cn-hangzhou)",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.label": "Resource Type",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.placeholder": "Please select NLB resource type",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.option.loadbalancer.label": "NLB LoadBalancer",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.option.listener.label": "NLB Listener",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_loadbalancer_id.label": "LoadBalancer ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder": "Please enter NLB loadbalancer ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_listener_id.label": "Listener ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_listener_id.placeholder": "Please enter NLB listener ID",
 | 
			
		||||
  "domain.deployment.form.tencent_cos_region.label": "Region",
 | 
			
		||||
  "domain.deployment.form.tencent_cos_region.placeholder": "Please enter region (e.g. ap-guangzhou)",
 | 
			
		||||
  "domain.deployment.form.tencent_cos_bucket.label": "Bucket",
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +103,10 @@
 | 
			
		|||
  "domain.deployment.form.tencent_clb_listener.placeholder": "Please enter listener ID (e.g. lbl-xxxxxxxx). The specific listener should have set the corresponding domain HTTPS forwarding, and the original certificate domain should be consistent with the certificate to be deployed.",
 | 
			
		||||
  "domain.deployment.form.tencent_clb_domain.label": "Deploy to domain (Wildcard domain is also supported)",
 | 
			
		||||
  "domain.deployment.form.tencent_clb_domain.placeholder": "Please enter domain to be deployed. If SNI is not enabled, you can leave it blank.",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_zone_id.label": "Zone ID",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_zone_id.placeholder": "Please enter zone id, e.g. zone-xxxxxxxxx",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_domain.label": "Deploy to domain (Wildcard domain is also supported, but should be same as the config on server, one domain each line)",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_domain.placeholder": "Please enter domain to be deployed.",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_region.label": "Region",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_region.placeholder": "Please enter region (e.g. cn-north-1)",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.label": "Resource Type",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,7 @@
 | 
			
		|||
  "settings.notification.config.push.test.message.success.message": "Send test notification successfully",
 | 
			
		||||
  "settings.notification.dingtalk.secret.placeholder": "Signature for signed addition",
 | 
			
		||||
  "settings.notification.url.errmsg.invalid": "Invalid Url format",
 | 
			
		||||
  "settings.notification.serverchan.url.placeholder": "Url, e.g. https://sctapi.ftqq.com/****************.send",
 | 
			
		||||
 | 
			
		||||
  "settings.ca.tab": "Certificate Authority",
 | 
			
		||||
  "settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,7 +69,7 @@
 | 
			
		|||
  "access.authorization.form.ssh_key_passphrase.placeholder": "请输入 Key 口令",
 | 
			
		||||
  "access.authorization.form.webhook_url.label": "Webhook URL",
 | 
			
		||||
  "access.authorization.form.webhook_url.placeholder": "请输入 Webhook URL",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig.label": "KubeConfig",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig.label": "KubeConfig(不选将使用Pod的ServiceAccount)",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig.placeholder": "请输入 KubeConfig",
 | 
			
		||||
  "access.authorization.form.k8s_kubeconfig_file.placeholder": "请选择文件",
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,19 +51,26 @@
 | 
			
		|||
  "common.errmsg.host_invalid": "请输入正确的域名或 IP 地址",
 | 
			
		||||
  "common.errmsg.ip_invalid": "请输入正确的 IP 地址",
 | 
			
		||||
  "common.errmsg.url_invalid": "请输入正确的 URL",
 | 
			
		||||
  "common.errmsg.zoneid_invalid": "请输入正确的 Zone ID",
 | 
			
		||||
 | 
			
		||||
  "common.provider.aliyun": "阿里云",
 | 
			
		||||
  "common.provider.aliyun.oss": "阿里云 - OSS",
 | 
			
		||||
  "common.provider.aliyun.cdn": "阿里云 - CDN",
 | 
			
		||||
  "common.provider.aliyun.dcdn": "阿里云 - DCDN",
 | 
			
		||||
  "common.provider.aliyun.oss": "阿里云 - 对象存储 OSS",
 | 
			
		||||
  "common.provider.aliyun.cdn": "阿里云 - 内容分发网络 CDN",
 | 
			
		||||
  "common.provider.aliyun.dcdn": "阿里云 - 全站加速 DCDN",
 | 
			
		||||
  "common.provider.aliyun.clb": "阿里云 - 传统型负载均衡 CLB",
 | 
			
		||||
  "common.provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB",
 | 
			
		||||
  "common.provider.aliyun.nlb": "阿里云 - 网络型负载均衡 NLB",
 | 
			
		||||
  "common.provider.tencent": "腾讯云",
 | 
			
		||||
  "common.provider.tencent.cos": "腾讯云 - COS",
 | 
			
		||||
  "common.provider.tencent.cdn": "腾讯云 - CDN",
 | 
			
		||||
  "common.provider.tencent.clb": "腾讯云 - CLB",
 | 
			
		||||
  "common.provider.tencent.cos": "腾讯云 - 对象存储 COS",
 | 
			
		||||
  "common.provider.tencent.cdn": "腾讯云 - 内容分发网络 CDN",
 | 
			
		||||
  "common.provider.tencent.ecdn": "腾讯云 - 全站加速网络 ECDN",
 | 
			
		||||
  "common.provider.tencent.clb": "腾讯云 - 负载均衡 CLB",
 | 
			
		||||
  "common.provider.tencent.teo": "腾讯云 - 边缘安全加速平台 EO",
 | 
			
		||||
  "common.provider.huaweicloud": "华为云",
 | 
			
		||||
  "common.provider.huaweicloud.cdn": "华为云 - CDN",
 | 
			
		||||
  "common.provider.huaweicloud.elb": "华为云 - ELB",
 | 
			
		||||
  "common.provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN",
 | 
			
		||||
  "common.provider.huaweicloud.elb": "华为云 - 弹性负载均衡 ELB",
 | 
			
		||||
  "common.provider.qiniu": "七牛云",
 | 
			
		||||
  "common.provider.qiniu.cdn": "七牛云 - CDN",
 | 
			
		||||
  "common.provider.qiniu.cdn": "七牛云 - 内容分发网络 CDN",
 | 
			
		||||
  "common.provider.aws": "AWS",
 | 
			
		||||
  "common.provider.cloudflare": "Cloudflare",
 | 
			
		||||
  "common.provider.namesilo": "Namesilo",
 | 
			
		||||
| 
						 | 
				
			
			@ -73,9 +80,11 @@
 | 
			
		|||
  "common.provider.local": "本地部署",
 | 
			
		||||
  "common.provider.ssh": "SSH 部署",
 | 
			
		||||
  "common.provider.webhook": "Webhook",
 | 
			
		||||
  "common.provider.serverchan": "Server酱",
 | 
			
		||||
  "common.provider.kubernetes": "Kubernetes",
 | 
			
		||||
  "common.provider.kubernetes.secret": "Kubernetes - Secret",
 | 
			
		||||
  "common.provider.dingtalk": "钉钉",
 | 
			
		||||
  "common.provider.telegram": "Telegram",
 | 
			
		||||
  "common.provider.lark": "飞书"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,36 @@
 | 
			
		|||
  "domain.deployment.form.aliyun_oss_endpoint.placeholder": "请输入 Endpoint",
 | 
			
		||||
  "domain.deployment.form.aliyun_oss_bucket.label": "存储桶",
 | 
			
		||||
  "domain.deployment.form.aliyun_oss_bucket.placeholder": "请输入存储桶名",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_region.label": "地域",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_region.placeholder": "请输入地域(如 cn-hangzhou)",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.label": "替换方式",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.placeholder": "请选择替换方式",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听的证书(仅支持 HTTPS 监听)",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_resource_type.option.listener.label": "替换指定负载均衡监听的证书",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_loadbalancer_id.label": "负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_listener_port.label": "监听端口",
 | 
			
		||||
  "domain.deployment.form.aliyun_clb_listener_port.placeholder": "请输入监听端口",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_region.label": "地域",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_region.placeholder": "请输入地域(如 cn-hangzhou)",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.label": "替换方式",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.placeholder": "请选择替换方式",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听的证书(仅支持 HTTPS/QUIC 监听)",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_resource_type.option.listener.label": "替换指定监听器的证书",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_loadbalancer_id.label": "负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_listener_id.label": "监听器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_alb_listener_id.placeholder": "请输入监听器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_region.label": "地域",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_region.placeholder": "请输入地域(如 cn-hangzhou)",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.label": "替换方式",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.placeholder": "请选择替换方式",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听的证书(仅支持 TCPSSL 监听)",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_resource_type.option.listener.label": "替换指定监听器的证书",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_loadbalancer_id.label": "负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_listener_id.label": "监听器 ID",
 | 
			
		||||
  "domain.deployment.form.aliyun_nlb_listener_id.placeholder": "请输入监听器 ID",
 | 
			
		||||
  "domain.deployment.form.tencent_cos_region.label": "地域",
 | 
			
		||||
  "domain.deployment.form.tencent_cos_region.placeholder": "请输入地域(如 ap-guangzhou)",
 | 
			
		||||
  "domain.deployment.form.tencent_cos_bucket.label": "存储桶",
 | 
			
		||||
| 
						 | 
				
			
			@ -73,19 +103,23 @@
 | 
			
		|||
  "domain.deployment.form.tencent_clb_listener.placeholder": "请输入监听器 ID(如 lb-xxxxxxxx)",
 | 
			
		||||
  "domain.deployment.form.tencent_clb_domain.label": "部署到域名(支持泛域名)",
 | 
			
		||||
  "domain.deployment.form.tencent_clb_domain.placeholder": "请输入部署到的域名, 如未开启 SNI, 可置空忽略此项",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_zone_id.label": "Zone ID",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_zone_id.placeholder": "请输入 Zone ID",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_domain.label": "部署到域名(支持泛域名, 应与服务器上配置的域名完全一致, 每行一个域名)",
 | 
			
		||||
  "domain.deployment.form.tencent_teo_domain.placeholder": "请输入部署到的域名",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_region.label": "地域",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_region.placeholder": "请输入地域(如 cn-north-1)",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.label": "资源类型替换方式",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.placeholder": "请选择资源类型替换方式",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.option.certificate.label": "按证书替换",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "按负载均衡器替换",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label": "按监听器替换",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.label": "替换方式",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.placeholder": "请选择替换方式",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.option.certificate.label": "替换指定证书",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听器的证书(仅支持 HTTPS 监听)",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label": "替换指定监听器",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_certificate_id.label": "证书 ID",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_certificate_id.placeholder": "请输入证书 ID(可从华为云控制面板获取)",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_certificate_id.placeholder": "请输入证书 ID",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_loadbalancer_id.label": "负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "请输入负载均衡器 ID(可从华为云控制面板获取)",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_listener_id.label": "监听器 ID",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "请输入监听器 ID(可从华为云控制面板获取)",
 | 
			
		||||
  "domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "请输入监听器 ID",
 | 
			
		||||
  "domain.deployment.form.file_format.label": "证书格式",
 | 
			
		||||
  "domain.deployment.form.file_format.placeholder": "请选择证书格式",
 | 
			
		||||
  "domain.deployment.form.file_cert_path.label": "证书保存路径",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,7 @@
 | 
			
		|||
  "settings.notification.config.push.test.message.success.message": "推送测试消息成功",
 | 
			
		||||
  "settings.notification.dingtalk.secret.placeholder": "加签的签名",
 | 
			
		||||
  "settings.notification.url.errmsg.invalid": "URL 格式不正确",
 | 
			
		||||
  "settings.notification.serverchan.url.placeholder": "Url, 形如: https://sctapi.ftqq.com/****************.send",
 | 
			
		||||
 | 
			
		||||
  "settings.ca.tab": "证书颁发机构(CA)",
 | 
			
		||||
  "settings.ca.provider.errmsg.empty": "请选择证书分发机构",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import Lark from "@/components/notify/Lark";
 | 
			
		|||
import NotifyTemplate from "@/components/notify/NotifyTemplate";
 | 
			
		||||
import Telegram from "@/components/notify/Telegram";
 | 
			
		||||
import Webhook from "@/components/notify/Webhook";
 | 
			
		||||
import ServerChan from "@/components/notify/ServerChan";
 | 
			
		||||
import { NotifyProvider } from "@/providers/notify";
 | 
			
		||||
 | 
			
		||||
const Notify = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +54,13 @@ const Notify = () => {
 | 
			
		|||
                <Webhook />
 | 
			
		||||
              </AccordionContent>
 | 
			
		||||
            </AccordionItem>
 | 
			
		||||
 | 
			
		||||
            <AccordionItem value="item-6" className="dark:border-stone-200">
 | 
			
		||||
              <AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger>
 | 
			
		||||
              <AccordionContent>
 | 
			
		||||
                <ServerChan />
 | 
			
		||||
              </AccordionContent>
 | 
			
		||||
            </AccordionItem>
 | 
			
		||||
          </Accordion>
 | 
			
		||||
        </div>
 | 
			
		||||
      </NotifyProvider>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue