Переглянути джерело

添加配置文件和工具函数

新增了多个配置文件和工具函数,包括阿里云配置、宝塔面板检测、命令行参数解析等功能,以支持项目的基础配置和环境检测。同时,添加了错误处理接口和相关实现,以便更好地管理和报告配置错误。
SongZihuan 3 місяців тому
коміт
c1d22caa50

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+.idea
+etc
+tmp
+cert
+
+*.exe
+*.out
+
+.DS_Store
+
+testdata
+
+pkg
+
+http-demo
+.htaccess
+
+*.bak

+ 8 - 0
LICENSE

@@ -0,0 +1,8 @@
+The MIT License (MIT)
+Copyright © 2024 宋子桓(Song Zihuan)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+# 阿里云CDN自动HTTPS证书更新系统
+## 介绍
+此项目的出发点在于宝塔,宝塔会为网站定时更新证书。因此,我们可以设置一个定时任务来运行此程序,负责把宝塔更新写入好的证书和密钥文件上传到阿里云的CAD。
+
+## 国内
+处于国内加速的目的,api的endpoint基本都选择杭州节点(cn-hangzhou),你若有需要也可以按需秀嘎哎。
+
+## 协议
+本程序由[MIT](./LICENSE)协议发布。
+
+## 宝塔SSL文件位置
+证书夹:/www/server/panel/vhost/ssl
+注意:证书夹里面似乎有冲突。例如域名通配符`*.example.com`和普通域名`example.com`会占用同一目录,导致覆盖。因此最好使用下面的Let's Encrypt文件夹。
+
+Let's Encrypt:/www/server/panel/vhost/letsencrypt

+ 13 - 0
REEPORT

@@ -0,0 +1,13 @@
+How to report of AUTO-ALIYUN-CDN-SSL
+
+Author: 宋子桓(Song Zihuan)
+Author Github: https://github.com/SongZihuan
+Author Website: https://song-zh.com
+Author Email: songzihuan@song-zh.com
+
+Github: https://github.com/SongZihuan/auto-aliyun-cdn-ssl
+Github Issues: https://github.com/SongZihuan/auto-aliyun-cdn-ssl/issues
+
+Report: You can report issues and contact the author through Github Issues or Author Email.
+Quality Assurance: If you only have the license in the LICENSE file in the root directory of this project, you will not get any quality assurance. But the author is happy to solve the problem for you, unless this project has been archived as read-only.
+Other Fork versions: Please contact the author of the Fork version for assistance.

+ 1 - 0
VERSION

@@ -0,0 +1 @@
+v1.0.0

+ 30 - 0
go.mod

@@ -0,0 +1,30 @@
+module github.com/SongZihuan/auto-aliyun-cdn-ssl
+
+go 1.22.0
+
+require (
+	github.com/alibabacloud-go/cas-20200407/v3 v3.0.4
+	github.com/alibabacloud-go/cdn-20180510/v5 v5.2.2
+	github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
+	github.com/alibabacloud-go/tea v1.2.2
+	github.com/alibabacloud-go/tea-utils/v2 v2.0.7
+	github.com/mattn/go-isatty v0.0.20
+	gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+	github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
+	github.com/alibabacloud-go/debug v1.0.1 // indirect
+	github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
+	github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
+	github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
+	github.com/aliyun/credentials-go v1.3.10 // indirect
+	github.com/clbanning/mxj/v2 v2.5.5 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/tjfoc/gmsm v1.4.1 // indirect
+	golang.org/x/net v0.23.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+)

+ 235 - 0
go.sum

@@ -0,0 +1,235 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+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=
+github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
+github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
+github.com/alibabacloud-go/cas-20200407/v3 v3.0.4 h1:ngRlctbt135zoujwX0lXSv9m4h1/bmg/yalQS0z1EWc=
+github.com/alibabacloud-go/cas-20200407/v3 v3.0.4/go.mod h1:6n9MZ9SH3HlSzfe2oKwjOqhJx3dxvW2gMDO+lq8t9U4=
+github.com/alibabacloud-go/cdn-20180510/v5 v5.2.2 h1:+KJOPukTM+xMyiLOW5qBwYKG2df3Ar7coRsqc1juKO8=
+github.com/alibabacloud-go/cdn-20180510/v5 v5.2.2/go.mod h1:GnPiPL3HlzCi8SGiLiVgKrAFkP1vTtcF4yGtjsl4wfo=
+github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
+github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
+github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
+github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
+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.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
+github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
+github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
+github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
+github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
+github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
+github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
+github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
+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/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
+github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=
+github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=
+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=
+github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
+github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
+github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
+github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
+github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
+github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
+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/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
+github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
+github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
+github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
+github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
+github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
+github.com/aliyun/credentials-go v1.3.10 h1:45Xxrae/evfzQL9V10zL3xX31eqgLWEaIdCoPipOEQA=
+github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
+github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+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=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 14 - 0
resource.go

@@ -0,0 +1,14 @@
+package resource
+
+import (
+	_ "embed"
+)
+
+//go:embed VERSION
+var Version string
+
+//go:embed LICENSE
+var License string
+
+//go:embed REEPORT
+var Report string

+ 38 - 0
src/aliyun/client.go

@@ -0,0 +1,38 @@
+package aliyun
+
+import (
+	"fmt"
+	cas "github.com/alibabacloud-go/cas-20200407/v3/client"
+	cdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
+	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
+	"github.com/alibabacloud-go/tea/tea"
+)
+
+var ErrCertExists = fmt.Errorf("cert exists")
+
+var casClient *cas.Client
+var cdnClient *cdn.Client
+
+func createCASClient(key string, secret string) (*cas.Client, error) {
+	result, err := cas.NewClient(&openapi.Config{
+		AccessKeyId:     tea.String(key),
+		AccessKeySecret: tea.String(secret),
+		Endpoint:        tea.String("cas.aliyuncs.com"),
+	})
+	if err != nil {
+		return nil, err
+	}
+	return result, nil
+}
+
+func createCDNClient(key string, secret string) (*cdn.Client, error) {
+	result, err := cdn.NewClient(&openapi.Config{
+		AccessKeyId:     tea.String(key),
+		AccessKeySecret: tea.String(secret),
+		Endpoint:        tea.String("cdn.aliyuncs.com"),
+	})
+	if err != nil {
+		return nil, err
+	}
+	return result, nil
+}

+ 64 - 0
src/aliyun/main.go

@@ -0,0 +1,64 @@
+package aliyun
+
+import (
+	"errors"
+	"fmt"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/config"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/logger"
+	"os"
+)
+
+var international = false
+
+func Init() (err error) {
+	if !config.IsReady() {
+		panic("config is not ready")
+	}
+
+	international = config.GetConfig().Aliyun.International.ToBool(false)
+	key := config.GetConfig().Aliyun.Key
+	secret := config.GetConfig().Aliyun.Secret
+
+	casClient, err = createCASClient(key, secret)
+	if err != nil {
+		return fmt.Errorf("init alibaba cloud sdk CAS client error: %s\n", err.Error())
+	}
+
+	cdnClient, err = createCDNClient(key, secret)
+	if err != nil {
+		return fmt.Errorf("init alibaba cloud sdk CDN client error: %s\n", err.Error())
+	}
+
+	return nil
+}
+
+func UpdateCDNHttpsByFilePath(domain string, cert string, prikey string) error {
+	certData, err := os.ReadFile(cert)
+	if err != nil {
+		return fmt.Errorf("read cert file error: %s\n", err.Error())
+	}
+
+	privateKeyData, err := os.ReadFile(prikey)
+	if err != nil {
+		return fmt.Errorf("read private key error: %s\n", err.Error())
+	}
+
+	return UpdateCDNHttps(domain, certData, privateKeyData)
+}
+
+func UpdateCDNHttps(domain string, certData []byte, privateKeyData []byte) error {
+	certID, certName, err := uploadCert(casClient, certData, privateKeyData)
+	if err != nil && errors.Is(err, ErrCertExists) {
+		logger.Warn("证书已存在, 不在重新更新CDN")
+		return nil
+	} else if err != nil {
+		return fmt.Errorf("upload error: %s\n", err.Error())
+	}
+
+	err = setDomainServerCertificate(cdnClient, domain, certID, certName)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 79 - 0
src/aliyun/operation.go

@@ -0,0 +1,79 @@
+package aliyun
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"errors"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/logger"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	cas "github.com/alibabacloud-go/cas-20200407/v3/client"
+	cdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
+	util "github.com/alibabacloud-go/tea-utils/v2/service"
+	"github.com/alibabacloud-go/tea/tea"
+	"strings"
+)
+
+func uploadCert(client *cas.Client, certData []byte, keyData []byte) (certID int64, name string, err error) {
+	cert, err := utils.ReadCertificate(certData)
+	if err != nil {
+		panic(err)
+	}
+
+	hash := sha256.Sum224(cert.RawSubjectPublicKeyInfo) // Sum256 太长
+	fingerprint := hex.EncodeToString(hash[:])
+
+	uploadUserCertificateRequest := &cas.UploadUserCertificateRequest{
+		Name: tea.String(fingerprint),
+		Cert: tea.String(string(certData)),
+		Key:  tea.String(string(keyData)),
+	}
+
+	resp, tryErr := func() (resp *cas.UploadUserCertificateResponse, err error) {
+		defer func() {
+			if r := tea.Recover(recover()); r != nil {
+				err = r
+			}
+		}()
+
+		return client.UploadUserCertificateWithOptions(uploadUserCertificateRequest, &util.RuntimeOptions{})
+	}()
+	if tryErr != nil {
+		var sdkErr *tea.SDKError
+		if errors.As(tryErr, &sdkErr) && tea.StringValue(sdkErr.Code) == "NameRepeat" {
+			logger.Warnf("证书已经存在, 证书名字:%s", fingerprint)
+			return 0, fingerprint, ErrCertExists
+		}
+		return 0, fingerprint, tryErr
+	}
+	logger.Infof("上传成功, 证书名字:%s, 证书ID:%d, 请求ID:%s", fingerprint, tea.Int64Value(resp.Body.CertId), tea.StringValue(resp.Body.RequestId))
+	return tea.Int64Value(resp.Body.CertId), fingerprint, nil
+}
+
+func setDomainServerCertificate(client *cdn.Client, domainName string, certID int64, certName string) (err error) {
+	request := &cdn.SetCdnDomainSSLCertificateRequest{}
+	request.DomainName = tea.String(strings.TrimPrefix(domainName, "*")) // 泛域名去除星号
+	request.CertName = tea.String(certName)
+	request.CertId = tea.Int64(certID)
+	request.CertType = tea.String("cas")
+	request.SSLProtocol = tea.String("on")
+	if international {
+		request.CertRegion = tea.String("ap-southeast-1") // 面向国际用农户
+	} else {
+		request.CertRegion = tea.String("cn-hangzhou") // 默认
+	}
+
+	_, tryErr := func() (resp *cdn.SetCdnDomainSSLCertificateResponse, err error) {
+		defer func() {
+			if r := tea.Recover(recover()); r != nil {
+				err = r
+			}
+		}()
+
+		return client.SetCdnDomainSSLCertificate(request)
+	}()
+	if tryErr != nil {
+		return tryErr
+	}
+	logger.Infof("CDN加速域名(%s)证书(%s)更新成功, 并启用SSL", domainName, certName)
+	return nil
+}

+ 92 - 0
src/baota/check.go

@@ -0,0 +1,92 @@
+package baota
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+)
+
+// 宝塔 Let's Encrypt 证书保存位置:/www/server/panel/vhost/letsencrypt
+const BT_LETSENCRYPT_SSL = "/www/server/panel/vhost/letsencrypt"
+
+var isBaota *bool
+
+func IsLinuxBaoTa() (res bool) {
+	if isBaota != nil {
+		return *isBaota
+	}
+
+	defer func() {
+		isBaota = &res
+	}()
+
+	if runtime.GOOS != "linux" {
+		fmt.Println("运行环境:不是 宝塔 面板")
+		return false
+	}
+
+	var stdout, stderr bytes.Buffer
+
+	cmd := exec.Command("pgrep", "-f", "BT-Panel")
+	cmd.Stdout = &stdout // 将 stdout 重定
+	cmd.Stdout = &stderr // 将 stderr 重定
+	err := cmd.Run()
+	if err != nil {
+		fmt.Println("运行环境:不是 宝塔 面板")
+		return false
+	}
+
+	_stdout := utils.StringToOnlyPrint(stdout.String())
+	_stderr := utils.StringToOnlyPrint(stderr.String())
+
+	if _stderr == "" || _stdout != "" {
+		fmt.Println("运行环境:Linux 宝塔版本")
+		return true
+	}
+
+	if utils.IsDir("/www/server/panel/BTPanel") {
+		fmt.Println("运行环境:Linux 宝塔版本")
+		return true
+	}
+
+	initsh, err := os.ReadFile("/www/server/panel/init.sh")
+	if err != nil {
+		fmt.Println("运行环境:不是 宝塔 面板")
+		return false
+	}
+
+	if strings.Contains(string(initsh), "宝塔") {
+		fmt.Println("运行环境:Linux 宝塔版本")
+		return true
+	}
+
+	licensetxt, err := os.ReadFile("/www/server/panel/license.txt")
+	if err != nil {
+		fmt.Println("运行环境:不是 宝塔 面板")
+		return false
+	}
+
+	if strings.Contains(string(licensetxt), "宝塔") {
+		fmt.Println("运行环境:Linux 宝塔版本")
+		return true
+	}
+
+	fmt.Println("运行环境:不是 宝塔 面板")
+	return false
+}
+
+func GetBaoTaLetsEncryptDir() string {
+	return BT_LETSENCRYPT_SSL
+}
+
+func HasBaoTaLetsEncrypt() bool {
+	if !IsLinuxBaoTa() {
+		return false
+	}
+
+	return utils.IsDir(GetBaoTaLetsEncryptDir())
+}

+ 10 - 0
src/cmd/version1/main.go

@@ -0,0 +1,10 @@
+package main
+
+import (
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/mainfunc"
+	"os"
+)
+
+func main() {
+	os.Exit(mainfunc.MainV1())
+}

+ 39 - 0
src/config/aliyunconfig.go

@@ -0,0 +1,39 @@
+package config
+
+import (
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"os"
+)
+
+const EnvPrefix = "ALIYUN_"
+const (
+	EnvAliyunKey    = EnvPrefix + "KEY"
+	EnvAliyunSecret = EnvPrefix + "SECRET"
+)
+
+type AliyunConfig struct {
+	Key           string           `yaml:"key"`
+	Secret        string           `yaml:"secret"`
+	International utils.StringBool `yaml:"international"`
+}
+
+func (a *AliyunConfig) SetDefault() {
+	if a.Key == "" {
+		a.Key = os.Getenv(EnvAliyunKey)
+	}
+
+	if a.Key == "" {
+		a.Key = os.Getenv(EnvAliyunSecret)
+	}
+
+	a.International.SetDefaultDisable()
+	return
+}
+
+func (a *AliyunConfig) Check() ConfigError {
+	if a.Key == "" || a.Secret == "" {
+		return NewConfigError("aliyun key or secret is empty")
+	}
+
+	return nil
+}

+ 134 - 0
src/config/config.go

@@ -0,0 +1,134 @@
+package config
+
+import (
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/flagparser"
+	"os"
+	"sync"
+)
+
+type ConfigStruct struct {
+	ConfigLock sync.Mutex
+
+	configReady   bool
+	yamlHasParser bool
+	sigchan       chan os.Signal
+	configPath    string
+
+	Yaml *YamlConfig
+}
+
+func newConfig(configPath string) (*ConfigStruct, error) {
+	if configPath == "" {
+		if !flagparser.IsReady() {
+			panic("flag is not ready")
+		}
+		configPath = flagparser.ConfigFile()
+	}
+
+	return &ConfigStruct{
+		configPath:    configPath,
+		configReady:   false,
+		yamlHasParser: false,
+		sigchan:       make(chan os.Signal),
+		Yaml:          nil,
+	}, nil
+}
+
+func (c *ConfigStruct) Init() (err ConfigError) {
+	if c.IsReady() { // 使用IsReady而不是isReady,确保上锁
+		return
+	}
+
+	initErr := c.init()
+	if initErr != nil {
+		return NewConfigError("init error: " + initErr.Error())
+	}
+
+	parserErr := c.Parser(c.configPath)
+	if parserErr != nil {
+		return NewConfigError("parser error: " + parserErr.Error())
+	} else if !c.yamlHasParser {
+		return NewConfigError("parser error: unknown")
+	}
+
+	c.SetDefault()
+
+	err = c.Check()
+	if err != nil && err.IsError() {
+		return err
+	}
+
+	c.configReady = true
+	return nil
+}
+
+func (c *ConfigStruct) Parser(filepath string) ParserError {
+	err := c.Yaml.Parser(filepath)
+	if err != nil {
+		return err
+	}
+
+	c.yamlHasParser = true
+	return nil
+}
+
+func (c *ConfigStruct) SetDefault() {
+	if !c.yamlHasParser {
+		panic("yaml must parser first")
+	}
+
+	c.Yaml.SetDefault(c.configPath)
+}
+
+func (c *ConfigStruct) Check() (err ConfigError) {
+	err = c.Yaml.Check()
+	if err != nil && err.IsError() {
+		return err
+	}
+
+	return nil
+}
+
+func (c *ConfigStruct) isReady() bool {
+	return c.yamlHasParser && c.configReady
+}
+
+func (c *ConfigStruct) init() error {
+	c.configReady = false
+	c.yamlHasParser = false
+
+	err := initSignal(c.sigchan)
+	if err != nil {
+		return err
+	}
+
+	c.Yaml = new(YamlConfig)
+	err = c.Yaml.Init()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// export func
+
+func (c *ConfigStruct) IsReady() bool {
+	return c.isReady()
+}
+
+func (c *ConfigStruct) GetSignalChan() chan os.Signal {
+	if !c.isReady() {
+		panic("config is not ready")
+	}
+
+	return c.sigchan
+}
+
+func (c *ConfigStruct) GetConfig() *YamlConfig {
+	if !c.isReady() {
+		panic("config is not ready")
+	}
+
+	return c.Yaml
+}

+ 82 - 0
src/config/domainconfg.go

@@ -0,0 +1,82 @@
+package config
+
+import (
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/baota"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"path"
+)
+
+type Domain struct {
+	Domain  string `yaml:"domain"`
+	RootDir string `yaml:"-"`
+	Dir     string `yaml:"dir"`
+	Cert    string `yaml:"cert"`
+	Key     string `yaml:"pri"`
+}
+
+type DomainConfig struct {
+	RootDir string   `yaml:"rootrir"`
+	Domains []Domain `yaml:"domains"`
+}
+
+func (d *DomainConfig) SetDefault(configPath string) {
+	if d.RootDir == "" {
+		if baota.HasBaoTaLetsEncrypt() {
+			d.RootDir = baota.GetBaoTaLetsEncryptDir()
+		} else {
+			d.RootDir = path.Dir(configPath)
+		}
+	}
+
+	for _, domain := range d.Domains {
+		domain.RootDir = d.RootDir
+	}
+
+	return
+}
+
+func (d *DomainConfig) Check() ConfigError {
+	if !utils.IsDir(d.RootDir) {
+		return NewConfigError("root dir is not a dir")
+	}
+
+	for _, domain := range d.Domains {
+		if domain.Domain == "" {
+			return NewConfigError("domain is empty")
+		} else if utils.IsValidDomain(domain.Domain) {
+			return NewConfigError("domain is not valid")
+		}
+	}
+
+	return nil
+}
+
+func (d *Domain) GetFilePath() (certPath string, prikeyPath string) {
+	rootDir := d.RootDir
+
+	if d.Dir != "" {
+		if path.IsAbs(d.Dir) {
+			rootDir = d.Dir
+		} else {
+			rootDir = path.Join(rootDir, d.Dir)
+		}
+	} else {
+		rootDir = path.Join(rootDir, d.Domain)
+	}
+
+	if rootDir == "" {
+		panic("root dir is empty") // 实际上这不可能发生,因为d.RootDir是非空(SetDefault),而d.Dir只有非空是才会被赋值
+	}
+
+	certPath = path.Join(rootDir, "fullchain.pem")
+	if d.Cert != "" {
+		certPath = path.Join(rootDir, d.Cert)
+	}
+
+	prikeyPath = path.Join(rootDir, "privkey.pem")
+	if d.Key != "" {
+		prikeyPath = path.Join(rootDir, d.Key)
+	}
+
+	return certPath, prikeyPath
+}

+ 83 - 0
src/config/error.go

@@ -0,0 +1,83 @@
+package config
+
+import (
+	"fmt"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+)
+
+type ConfigError interface {
+	error
+	Msg() string
+	Error() string
+	Warning() string
+	IsError() bool
+	IsWarning() bool
+}
+
+func NewConfigError(msg string) ConfigError {
+	fmt.Println(utils.FormatTextToWidth(fmt.Sprintf("config error: %s", msg), utils.NormalConsoleWidth))
+	return &configError{msg: msg, isError: true}
+}
+
+func NewConfigWarning(msg string) ConfigError {
+	fmt.Println(utils.FormatTextToWidth(fmt.Sprintf("config warning: %s", msg), utils.NormalConsoleWidth))
+	return &configError{msg: msg, isError: false}
+}
+
+type configError struct {
+	msg     string
+	isError bool
+}
+
+func (e *configError) Msg() string {
+	if e.isError {
+		return "config error: " + e.Error()
+	}
+	return "config warning: " + e.Warning()
+}
+
+func (e *configError) Error() string {
+	return e.msg
+}
+
+func (e *configError) Warning() string {
+	return e.msg
+}
+
+func (e *configError) IsError() bool {
+	return e.isError
+}
+
+func (e *configError) IsWarning() bool {
+	return !e.isError
+}
+
+type ParserError interface {
+	error
+	Error() string
+	Data() interface{}
+}
+
+type parserError struct {
+	msg  string
+	data interface{}
+}
+
+func NewParserError(data interface{}, msg ...string) ParserError {
+	if len(msg) == 1 {
+		return &parserError{msg[0], data}
+	}
+	return &parserError{"config parser error: " + fmt.Sprint(data), data}
+}
+
+func WarpParserError(err error) ParserError {
+	return &parserError{"config parser error: " + err.Error(), err}
+}
+
+func (e *parserError) Error() string {
+	return e.msg
+}
+
+func (e *parserError) Data() interface{} {
+	return e.data
+}

+ 85 - 0
src/config/globalconfig.go

@@ -0,0 +1,85 @@
+package config
+
+import (
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"os"
+)
+
+const EnvModeName = "HUAN_PROXY_MODE"
+
+const (
+	DebugMode   = "debug"
+	ReleaseMode = "release"
+	TestMode    = "test"
+)
+
+type LoggerLevel string
+
+var levelMap = map[string]bool{
+	"debug": true,
+	"info":  true,
+	"warn":  true,
+	"error": true,
+	"panic": true,
+	"none":  true,
+}
+
+type GlobalConfig struct {
+	Mode     string           `yaml:"mode"`
+	LogLevel string           `yaml:"loglevel"`
+	LogTag   utils.StringBool `yaml:"logtag"`
+}
+
+func (g *GlobalConfig) SetDefault() {
+	if g.Mode == "" {
+		g.Mode = os.Getenv(EnvModeName)
+	}
+
+	if g.Mode == "" {
+		g.Mode = DebugMode
+	}
+
+	_ = os.Setenv(EnvModeName, g.Mode)
+
+	if g.LogLevel == "" && (g.Mode == DebugMode || g.Mode == TestMode) {
+		g.LogLevel = "debug"
+	} else if g.LogLevel == "" {
+		g.LogLevel = "warn"
+	}
+
+	if g.Mode == DebugMode || g.Mode == TestMode {
+		g.LogTag.SetDefaultEnable()
+	} else {
+		g.LogTag.SetDefaultDisable()
+	}
+
+	return
+}
+
+func (g *GlobalConfig) Check() ConfigError {
+	if g.Mode != DebugMode && g.Mode != ReleaseMode && g.Mode != TestMode {
+		return NewConfigError("bad mode")
+	}
+
+	if _, ok := levelMap[g.LogLevel]; !ok {
+		return NewConfigError("log level error")
+	}
+
+	return nil
+}
+
+func (g *GlobalConfig) GetRunMode() string {
+	return g.Mode
+}
+
+func (g *GlobalConfig) IsDebug() bool {
+	return g.Mode == DebugMode
+}
+
+func (g *GlobalConfig) IsRelease() bool {
+	return g.Mode == ReleaseMode
+}
+
+func (g *GlobalConfig) IsTest() bool {
+	return g.Mode == TestMode
+}

+ 38 - 0
src/config/main.go

@@ -0,0 +1,38 @@
+package config
+
+import (
+	"os"
+)
+
+func InitConfig(configPath string) ConfigError {
+	var err error
+	config, err = newConfig(configPath)
+	if err != nil {
+		return NewConfigError(err.Error())
+	}
+
+	cfgErr := config.Init()
+	if cfgErr != nil && cfgErr.IsError() {
+		return cfgErr
+	}
+
+	if !config.IsReady() {
+		return NewConfigError("config not ready")
+	}
+
+	return nil
+}
+
+func IsReady() bool {
+	return config.IsReady()
+}
+
+func GetConfig() *YamlConfig {
+	return config.GetConfig()
+}
+
+func GetSignalChan() chan os.Signal {
+	return config.GetSignalChan()
+}
+
+var config *ConfigStruct

+ 19 - 0
src/config/signalconfig.go

@@ -0,0 +1,19 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+)
+
+func initSignal(sigChan chan os.Signal) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("init signal error: %v", r)
+		}
+	}()
+
+	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+	return nil
+}

+ 56 - 0
src/config/yamlconfig.go

@@ -0,0 +1,56 @@
+package config
+
+import (
+	"gopkg.in/yaml.v3"
+	"os"
+)
+
+type YamlConfig struct {
+	GlobalConfig `yaml:",inline"`
+	DomainConfig `yaml:",inline"`
+
+	Aliyun AliyunConfig `yaml:"aliyun"`
+}
+
+func (y *YamlConfig) Init() error {
+	return nil
+}
+
+func (y *YamlConfig) SetDefault(configPath string) {
+	y.GlobalConfig.SetDefault()
+	y.DomainConfig.SetDefault(configPath)
+	y.Aliyun.SetDefault()
+}
+
+func (y *YamlConfig) Check() (err ConfigError) {
+	err = y.GlobalConfig.Check()
+	if err != nil && err.IsError() {
+		return err
+	}
+
+	err = y.DomainConfig.Check()
+	if err != nil && err.IsError() {
+		return err
+	}
+
+	err = y.Aliyun.Check()
+	if err != nil && err.IsError() {
+		return err
+	}
+
+	return nil
+}
+
+func (y *YamlConfig) Parser(filepath string) ParserError {
+	file, err := os.ReadFile(filepath)
+	if err != nil {
+		return NewParserError(err, err.Error())
+	}
+
+	err = yaml.Unmarshal(file, y)
+	if err != nil {
+		return NewParserError(err, err.Error())
+	}
+
+	return nil
+}

+ 370 - 0
src/flagparser/data.go

@@ -0,0 +1,370 @@
+package flagparser
+
+import (
+	"flag"
+	"fmt"
+	resource "github.com/SongZihuan/auto-aliyun-cdn-ssl"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"io"
+	"reflect"
+	"strings"
+)
+
+const OptionIdent = "  "
+const OptionPrefix = "--"
+const UsagePrefixWidth = 10
+
+type flagData struct {
+	flagReady  bool
+	flagSet    bool
+	flagParser bool
+
+	HelpData               bool
+	HelpName               string
+	HelpUsage              string
+	VersionData            bool
+	VersionName            string
+	VersionUsage           string
+	LicenseData            bool
+	LicenseName            string
+	LicenseUsage           string
+	ReportData             bool
+	ReportName             string
+	ReportUsage            string
+	ConfigFileData         string
+	ConfigFileName         string
+	ConfigFileUsage        string
+	NotAutoReloadData      bool
+	NotAutoReloadName      string
+	NotAutoReloadShortName string
+	NotAutoReloadUsage     string
+
+	Usage string
+}
+
+func initData() {
+	data = flagData{
+		flagReady:  false,
+		flagSet:    false,
+		flagParser: false,
+
+		HelpData:               false,
+		HelpName:               "help",
+		HelpUsage:              fmt.Sprintf("Show usage of %s. If this option is set, the backend service will not run.", utils.GetArgs0Name()),
+		VersionData:            false,
+		VersionName:            "version",
+		VersionUsage:           fmt.Sprintf("Show version of %s. If this option is set, the backend service will not run.", utils.GetArgs0Name()),
+		LicenseData:            false,
+		LicenseName:            "license",
+		LicenseUsage:           fmt.Sprintf("Show license of %s. If this option is set, the backend service will not run.", utils.GetArgs0Name()),
+		ReportData:             false,
+		ReportName:             "report",
+		ReportUsage:            fmt.Sprintf("Show how to report questions/errors of %s. If this option is set, the backend service will not run.", utils.GetArgs0Name()),
+		ConfigFileData:         "config.yaml",
+		ConfigFileName:         "config",
+		ConfigFileUsage:        fmt.Sprintf("%s", "The location of the running configuration file of the backend service. The option is a string, the default value is config.yaml in the running directory."),
+		NotAutoReloadData:      false,
+		NotAutoReloadName:      "not-auto-reload",
+		NotAutoReloadShortName: "",
+		NotAutoReloadUsage:     fmt.Sprintf("%s", "Disable automatic detection of configuration file changes and reloading of system programs. This feature is enabled by default. This feature consumes a certain amount of performance. If your performance is not enough, you can choose to disable it."),
+		Usage:                  "",
+	}
+
+	data.ready()
+}
+
+func (d *flagData) writeUsage() {
+	if len(d.Usage) != 0 {
+		return
+	}
+
+	if d.isFlagSet() || d.isFlagParser() {
+		panic("flag is parser")
+	}
+
+	var result strings.Builder
+	result.WriteString(utils.FormatTextToWidth(fmt.Sprintf("Usage of %s:", utils.GetArgs0Name()), utils.NormalConsoleWidth))
+	result.WriteString("\n")
+
+	val := reflect.ValueOf(*d)
+	typ := val.Type()
+
+	for i := 0; i < val.NumField(); i++ {
+		field := typ.Field(i)
+
+		if !strings.HasSuffix(field.Name, "Data") {
+			continue
+		}
+
+		option := field.Name[:len(field.Name)-4]
+		optionName := ""
+		optionShortName := ""
+		optionUsage := ""
+
+		if utils.HasFieldByReflect(typ, option+"Name") {
+			var ok bool
+			optionName, ok = val.FieldByName(option + "Name").Interface().(string)
+			if !ok {
+				panic("can not get option name")
+			}
+		}
+
+		if utils.HasFieldByReflect(typ, option+"ShortName") {
+			var ok bool
+			optionShortName, ok = val.FieldByName(option + "ShortName").Interface().(string)
+			if !ok {
+				panic("can not get option short name")
+			}
+		} else if len(optionName) > 1 {
+			optionShortName = optionName[:1]
+		}
+
+		if utils.HasFieldByReflect(typ, option+"Usage") {
+			var ok bool
+			optionUsage, ok = val.FieldByName(option + "Usage").Interface().(string)
+			if !ok {
+				panic("can not get option usage")
+			}
+		}
+
+		var title string
+		var title1 string
+		var title2 string
+		if field.Type.Kind() == reflect.Bool {
+			var optionData bool
+			if utils.HasFieldByReflect(typ, option+"Data") {
+				var ok bool
+				optionData, ok = val.FieldByName(option + "Data").Interface().(bool)
+				if !ok {
+					panic("can not get option data")
+				}
+			}
+
+			if optionData == true {
+				panic("bool option can not be true")
+			}
+
+			if optionName != "" {
+				title1 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(optionName, utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			}
+
+			if optionShortName != "" {
+				title2 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(optionShortName, utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			}
+		} else if field.Type.Kind() == reflect.String {
+			var optionData string
+			if utils.HasFieldByReflect(typ, option+"Data") {
+				var ok bool
+				optionData, ok = val.FieldByName(option + "Data").Interface().(string)
+				if !ok {
+					panic("can not get option data")
+				}
+			}
+
+			if optionName != "" && optionData != "" {
+				title1 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s string, default: '%s'", optionName, optionData), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			} else if optionName != "" && optionData == "" {
+				title1 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s string", optionName), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			}
+
+			if optionShortName != "" && optionData != "" {
+				title2 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s string, default: '%s'", optionShortName, optionData), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			} else if optionShortName != "" && optionData == "" {
+				title2 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s string", optionShortName), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			}
+		} else if field.Type.Kind() == reflect.Uint {
+			var optionData uint
+			if utils.HasFieldByReflect(typ, option+"Data") {
+				var ok bool
+				optionData, ok = val.FieldByName(option + "Data").Interface().(uint)
+				if !ok {
+					panic("can not get option data")
+				}
+			}
+
+			if optionName != "" && optionData != 0 {
+				title1 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s number, default: %d", optionName, optionData), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			} else if optionName != "" && optionData == 0 {
+				title1 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s number", optionName), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			}
+
+			if optionShortName != "" && optionData != 0 {
+				title2 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s number, default: %d", optionShortName, optionData), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			} else if optionShortName != "" && optionData == 0 {
+				title2 = fmt.Sprintf("%s%s%s", OptionIdent, OptionPrefix, utils.FormatTextToWidth(fmt.Sprintf("%s number", optionShortName), utils.NormalConsoleWidth-len(OptionIdent)-len(OptionPrefix)))
+			}
+		} else {
+			panic("error flag type")
+		}
+
+		if title1 == "" && title2 == "" {
+			continue
+		} else if title1 != "" && title2 == "" {
+			title = title1
+		} else if title1 == "" {
+			title = title2
+		} else {
+			title = fmt.Sprintf("%s\n%s", title1, title2)
+		}
+
+		result.WriteString(title)
+		result.WriteString("\n")
+
+		usegae := utils.FormatTextToWidthAndPrefix(optionUsage, UsagePrefixWidth, utils.NormalConsoleWidth)
+		result.WriteString(usegae)
+		result.WriteString("\n\n")
+	}
+
+	d.Usage = strings.TrimRight(result.String(), "\n")
+}
+
+func (d *flagData) setFlag() {
+	if d.isFlagSet() {
+		return
+	}
+
+	flag.BoolVar(&d.HelpData, data.HelpName, data.HelpData, data.HelpUsage)
+	flag.BoolVar(&d.HelpData, data.HelpName[0:1], data.HelpData, data.HelpUsage)
+
+	flag.BoolVar(&d.VersionData, data.VersionName, data.VersionData, data.VersionUsage)
+	flag.BoolVar(&d.VersionData, data.VersionName[0:1], data.VersionData, data.VersionUsage)
+
+	flag.BoolVar(&d.LicenseData, data.LicenseName, data.LicenseData, data.LicenseUsage)
+	flag.BoolVar(&d.LicenseData, data.LicenseName[0:1], data.LicenseData, data.LicenseUsage)
+
+	flag.BoolVar(&d.ReportData, data.ReportName, data.ReportData, data.ReportUsage)
+	flag.BoolVar(&d.ReportData, data.ReportName[0:1], data.ReportData, data.ReportUsage)
+
+	flag.StringVar(&d.ConfigFileData, data.ConfigFileName, data.ConfigFileData, data.ConfigFileUsage)
+	flag.StringVar(&d.ConfigFileData, data.ConfigFileName[0:1], data.ConfigFileData, data.ConfigFileUsage)
+
+	flag.Usage = func() {
+		_, _ = d.PrintUsage()
+	}
+	d.flagSet = true
+}
+
+func (d *flagData) parser() {
+	if d.flagParser {
+		return
+	}
+
+	if !d.isFlagSet() {
+		panic("flag not set")
+	}
+
+	flag.Parse()
+	d.flagParser = true
+}
+
+func (d *flagData) ready() {
+	if d.isReady() {
+		return
+	}
+
+	d.writeUsage()
+	d.setFlag()
+	d.parser()
+	d.flagReady = true
+}
+
+func (d *flagData) isReady() bool {
+	return d.isFlagSet() && d.isFlagParser() && d.flagReady
+}
+
+func (d *flagData) isFlagSet() bool {
+	return d.flagSet
+}
+
+func (d *flagData) isFlagParser() bool {
+	return d.flagParser
+}
+
+func (d *flagData) Help() bool {
+	if !d.isReady() {
+		panic("flag not ready")
+	}
+
+	return d.HelpData
+}
+
+func (d *flagData) FprintUsage(writer io.Writer) (int, error) {
+	return fmt.Fprintf(writer, "%s\n", d.Usage)
+}
+
+func (d *flagData) PrintUsage() (int, error) {
+	return d.FprintUsage(flag.CommandLine.Output())
+}
+
+func (d *flagData) Version() bool {
+	if !d.isReady() {
+		panic("flag not ready")
+	}
+
+	return d.VersionData
+}
+
+func (d *flagData) FprintVersion(writer io.Writer) (int, error) {
+	version := utils.FormatTextToWidth(fmt.Sprintf("Version of %s: %s", utils.GetArgs0Name(), resource.Version), utils.NormalConsoleWidth)
+	return fmt.Fprintf(writer, "%s\n", version)
+}
+
+func (d *flagData) PrintVersion() (int, error) {
+	return d.FprintVersion(flag.CommandLine.Output())
+}
+
+func (d *flagData) FprintLicense(writer io.Writer) (int, error) {
+	title := utils.FormatTextToWidth(fmt.Sprintf("License of %s:", utils.GetArgs0Name()), utils.NormalConsoleWidth)
+	license := utils.FormatTextToWidth(resource.License, utils.NormalConsoleWidth)
+	return fmt.Fprintf(writer, "%s\n%s\n", title, license)
+}
+
+func (d *flagData) PrintLicense() (int, error) {
+	return d.FprintLicense(flag.CommandLine.Output())
+}
+
+func (d *flagData) FprintReport(writer io.Writer) (int, error) {
+	// 不需要title
+	report := utils.FormatTextToWidth(resource.Report, utils.NormalConsoleWidth)
+	return fmt.Fprintf(writer, "%s\n", report)
+}
+
+func (d *flagData) PrintReport() (int, error) {
+	return d.FprintReport(flag.CommandLine.Output())
+}
+
+func (d *flagData) FprintLF(writer io.Writer) (int, error) {
+	return fmt.Fprintf(writer, "\n")
+}
+
+func (d *flagData) PrintLF() (int, error) {
+	return d.FprintLF(flag.CommandLine.Output())
+}
+
+func (d *flagData) License() bool {
+	if !d.isReady() {
+		panic("flag not ready")
+	}
+
+	return d.LicenseData
+}
+
+func (d *flagData) Report() bool {
+	if !d.isReady() {
+		panic("flag not ready")
+	}
+
+	return d.ReportData
+}
+
+func (d *flagData) ConfigFile() string {
+	if !d.isReady() {
+		panic("flag not ready")
+	}
+
+	return d.ConfigFileData
+}
+
+func (d *flagData) SetOutput(writer io.Writer) {
+	flag.CommandLine.SetOutput(writer)
+}

+ 29 - 0
src/flagparser/error.go

@@ -0,0 +1,29 @@
+package flagparser
+
+import "fmt"
+
+type FlagError interface {
+	error
+	Error() string
+	Data() interface{}
+}
+
+type flagError struct {
+	msg  string
+	data interface{}
+}
+
+func NewFlagError(data interface{}, msg ...string) FlagError {
+	if len(msg) == 1 {
+		return &flagError{msg[0], data}
+	}
+	return &flagError{"flag error: " + fmt.Sprint(data), data}
+}
+
+func (e *flagError) Error() string {
+	return e.msg
+}
+
+func (e *flagError) Data() interface{} {
+	return e.data
+}

+ 111 - 0
src/flagparser/export.go

@@ -0,0 +1,111 @@
+package flagparser
+
+import (
+	"fmt"
+	"io"
+	"strings"
+)
+
+var data flagData
+
+func Help() bool {
+	return data.Help()
+}
+
+func FprintUsage(writer io.Writer) (int, error) {
+	return data.FprintUsage(writer)
+}
+
+func PrintUsage() (int, error) {
+	return data.PrintUsage()
+}
+
+func FprintVersion(writer io.Writer) (int, error) {
+	return data.FprintVersion(writer)
+}
+
+func PrintVersion() (int, error) {
+	return data.PrintVersion()
+}
+
+func FprintLicense(writer io.Writer) (int, error) {
+	return data.FprintLicense(writer)
+}
+
+func PrintLicense() (int, error) {
+	return data.PrintLicense()
+}
+
+func FprintReport(writer io.Writer) (int, error) {
+	return data.FprintReport(writer)
+}
+
+func PrintReport() (int, error) {
+	return data.PrintReport()
+}
+
+func FprintLF(writer io.Writer) (int, error) {
+	return data.FprintLF(writer)
+}
+
+func PrintLF() (int, error) {
+	return data.PrintLF()
+}
+
+func Version() bool {
+	return data.Version()
+}
+
+func License() bool {
+	return data.License()
+}
+
+func Report() bool {
+	return data.Report()
+}
+
+func NotRunMode() bool {
+	return Help() || Version() || License() || Report()
+}
+
+func NotRunModeOption() string {
+	if !NotRunMode() {
+		return ""
+	}
+
+	var result strings.Builder
+
+	if data.Help() {
+		result.WriteString(fmt.Sprintf("%s%s, ", OptionPrefix, data.HelpName))
+	}
+
+	if data.Version() {
+		result.WriteString(fmt.Sprintf("%s%s, ", OptionPrefix, data.VersionName))
+	}
+
+	if data.License() {
+		result.WriteString(fmt.Sprintf("%s%s, ", OptionPrefix, data.LicenseName))
+	}
+
+	if data.Report() {
+		result.WriteString(fmt.Sprintf("%s%s, ", OptionPrefix, data.ReportName))
+	}
+
+	return strings.TrimSuffix(result.String(), ", ")
+}
+
+func ConfigFile() string {
+	return data.ConfigFile()
+}
+
+func NotRunAutoReload() bool {
+	return data.NotAutoReloadData
+}
+
+func RunAutoReload() bool {
+	return !NotRunAutoReload()
+}
+
+func SetOutput(writer io.Writer) {
+	data.SetOutput(writer)
+}

+ 82 - 0
src/flagparser/flag.go

@@ -0,0 +1,82 @@
+package flagparser
+
+import (
+	"fmt"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"os"
+)
+
+var isReady = false
+
+func IsReady() bool {
+	return data.isReady() && isReady
+}
+
+var StopFlag = fmt.Errorf("stop")
+
+func InitFlag() (err error) {
+	if isReady {
+		return nil
+	}
+
+	defer func() {
+		if e := recover(); e != nil {
+			err = NewFlagError(e)
+			return
+		}
+	}()
+
+	initData()
+
+	SetOutput(os.Stdout)
+
+	var hasPrint = false
+
+	if Version() {
+		_, _ = PrintVersion()
+		hasPrint = true
+	}
+
+	if License() {
+		if hasPrint {
+			_, _ = PrintLF()
+		}
+		_, _ = PrintLicense()
+		hasPrint = true
+	}
+
+	if Report() {
+		if hasPrint {
+			_, _ = PrintLF()
+		}
+		_, _ = PrintReport()
+	}
+
+	if Help() {
+		if hasPrint {
+			_, _ = PrintLF()
+		}
+		_, _ = PrintUsage()
+		hasPrint = true
+	}
+
+	if NotRunMode() {
+		return StopFlag
+	}
+
+	err = checkFlag()
+	if err != nil {
+		return err
+	}
+
+	isReady = true
+	return nil
+}
+
+func checkFlag() error {
+	if !utils.IsExists(ConfigFile()) {
+		return fmt.Errorf("config file not exists")
+	}
+
+	return nil
+}

+ 373 - 0
src/logger/logger.go

@@ -0,0 +1,373 @@
+package logger
+
+import (
+	"fmt"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/config"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"github.com/mattn/go-isatty"
+	"io"
+	"os"
+)
+
+type LoggerLevel string
+
+const (
+	LevelDebug LoggerLevel = "debug"
+	LevelInfo  LoggerLevel = "info"
+	LevelWarn  LoggerLevel = "warn"
+	LevelError LoggerLevel = "error"
+	LevelPanic LoggerLevel = "panic"
+	LevelNone  LoggerLevel = "none"
+)
+
+type loggerLevel int64
+
+const (
+	levelDebug loggerLevel = 1
+	levelInfo  loggerLevel = 2
+	levelWarn  loggerLevel = 3
+	levelError loggerLevel = 4
+	levelPanic loggerLevel = 5
+	levelNone  loggerLevel = 6
+)
+
+var levelMap = map[LoggerLevel]loggerLevel{
+	LevelDebug: levelDebug,
+	LevelInfo:  levelInfo,
+	LevelWarn:  levelWarn,
+	LevelError: levelError,
+	LevelPanic: levelPanic,
+	LevelNone:  levelNone,
+}
+
+type Logger struct {
+	level      LoggerLevel
+	logLevel   loggerLevel
+	logTag     bool
+	warnWriter io.Writer
+	errWriter  io.Writer
+	args0      string
+	args0Name  string
+}
+
+var globalLogger *Logger = nil
+var DefaultWarnWriter = os.Stdout
+var DefaultErrorWriter = os.Stderr
+
+func InitLogger(warnWriter, errWriter io.Writer) error {
+	if !config.IsReady() {
+		panic("config is not ready")
+	}
+
+	level := LoggerLevel(config.GetConfig().GlobalConfig.LogLevel)
+	logLevel, ok := levelMap[level]
+	if !ok {
+		return fmt.Errorf("invalid log level: %s", level)
+	}
+
+	if warnWriter == nil {
+		warnWriter = DefaultWarnWriter
+	}
+
+	if errWriter == nil {
+		errWriter = DefaultErrorWriter
+	}
+
+	logger := &Logger{
+		level:      level,
+		logLevel:   logLevel,
+		logTag:     config.GetConfig().LogTag.ToBool(true),
+		warnWriter: os.Stdout,
+		errWriter:  os.Stderr,
+		args0:      utils.GetArgs0(),
+		args0Name:  utils.GetArgs0Name(),
+	}
+
+	globalLogger = logger
+	return nil
+}
+
+func IsReady() bool {
+	return globalLogger != nil
+}
+
+func (l *Logger) Executablef(format string, args ...interface{}) string {
+	str := fmt.Sprintf(format, args...)
+	if str == "" {
+		_, _ = fmt.Fprintf(l.warnWriter, "[Executable]: %s\n", l.args0)
+	} else {
+		_, _ = fmt.Fprintf(l.warnWriter, "{Executable %s]: %s\n", l.args0, str)
+	}
+	return l.args0
+}
+
+func (l *Logger) Tagf(format string, args ...interface{}) {
+	l.TagSkipf(1, format, args...)
+}
+
+func (l *Logger) TagSkipf(skip int, format string, args ...interface{}) {
+	if !l.logTag {
+		return
+	}
+
+	funcName, file, _, line := utils.GetCallingFunctionInfo(skip + 1)
+
+	str := fmt.Sprintf(format, args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Tag %s]: %s %s %s:%d\n", l.args0Name, str, funcName, file, line)
+}
+
+func (l *Logger) Debugf(format string, args ...interface{}) {
+	if l.logLevel > levelDebug {
+		return
+	}
+
+	str := fmt.Sprintf(format, args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Debug %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Infof(format string, args ...interface{}) {
+	if l.logLevel > levelInfo {
+		return
+	}
+
+	str := fmt.Sprintf(format, args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Info %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Warnf(format string, args ...interface{}) {
+	if l.logLevel > levelWarn {
+		return
+	}
+
+	str := fmt.Sprintf(format, args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Warning %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Errorf(format string, args ...interface{}) {
+	if l.logLevel > levelError {
+		return
+	}
+
+	str := fmt.Sprintf(format, args...)
+	_, _ = fmt.Fprintf(l.errWriter, "[Error %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Panicf(format string, args ...interface{}) {
+	if l.logLevel > levelPanic {
+		return
+	}
+
+	str := fmt.Sprintf(format, args...)
+	_, _ = fmt.Fprintf(l.errWriter, "[Panic %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Tag(args ...interface{}) {
+	l.TagSkip(1, args...)
+}
+
+func (l *Logger) TagSkip(skip int, args ...interface{}) {
+	if !l.logTag {
+		return
+	}
+
+	funcName, file, _, line := utils.GetCallingFunctionInfo(skip + 1)
+
+	str := fmt.Sprint(args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Tag %s]: %s %s %s:%d\n", l.args0Name, str, funcName, file, line)
+}
+
+func (l *Logger) Debug(args ...interface{}) {
+	if l.logLevel > levelDebug {
+		return
+	}
+
+	str := fmt.Sprint(args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Debug %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Info(args ...interface{}) {
+	if l.logLevel > levelInfo {
+		return
+	}
+
+	str := fmt.Sprint(args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Info %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Warn(args ...interface{}) {
+	if l.logLevel > levelWarn {
+		return
+	}
+
+	str := fmt.Sprint(args...)
+	_, _ = fmt.Fprintf(l.warnWriter, "[Warning %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Error(args ...interface{}) {
+	if l.logLevel > levelError {
+		return
+	}
+
+	str := fmt.Sprint(args...)
+	_, _ = fmt.Fprintf(l.errWriter, "[Error %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) Panic(args ...interface{}) {
+	if l.logLevel > levelPanic {
+		return
+	}
+
+	str := fmt.Sprint(args...)
+	_, _ = fmt.Fprintf(l.errWriter, "[Panic %s]: %s\n", l.args0Name, str)
+}
+
+func (l *Logger) TagWrite(msg string) {
+	l.TagSkipWrite(1, msg)
+}
+
+func (l *Logger) TagSkipWrite(skip int, msg string) {
+	if !l.logTag {
+		return
+	}
+
+	funcName, file, _, line := utils.GetCallingFunctionInfo(skip + 1)
+
+	_, _ = fmt.Fprintf(l.warnWriter, "[Debug %s]: %s %s %s:%d\n", l.args0Name, msg, funcName, file, line)
+}
+
+func (l *Logger) DebugWrite(msg string) {
+	if l.logLevel > levelDebug {
+		return
+	}
+
+	_, _ = fmt.Fprintf(l.warnWriter, "[Debug %s]: %s\n", l.args0Name, msg)
+}
+
+func (l *Logger) InfoWrite(msg string) {
+	if l.logLevel > levelInfo {
+		return
+	}
+
+	_, _ = fmt.Fprintf(l.warnWriter, "[Info %s]: %s\n", l.args0Name, msg)
+}
+
+func (l *Logger) WarnWrite(msg string) {
+	if l.logLevel > levelWarn {
+		return
+	}
+
+	_, _ = fmt.Fprintf(l.warnWriter, "[Warning %s]: %s\n", l.args0Name, msg)
+}
+
+func (l *Logger) ErrorWrite(msg string) {
+	if l.logLevel > levelError {
+		return
+	}
+
+	_, _ = fmt.Fprintf(l.errWriter, "[Error %s]: %s\n", l.args0Name, msg)
+}
+
+func (l *Logger) PanicWrite(msg string) {
+	if l.logLevel > levelPanic {
+		return
+	}
+
+	_, _ = fmt.Fprintf(l.errWriter, "[Panic %s]: %s\n", l.args0Name, msg)
+}
+
+func (l *Logger) GetDebugWriter() io.Writer {
+	return l.warnWriter
+}
+
+func (l *Logger) GetInfoWriter() io.Writer {
+	return l.warnWriter
+}
+
+func (l *Logger) GetWarningWriter() io.Writer {
+	return l.warnWriter
+}
+
+func (l *Logger) GetTagWriter() io.Writer {
+	return l.warnWriter
+}
+
+func (l *Logger) GetErrorWriter() io.Writer {
+	return l.errWriter
+}
+
+func (l *Logger) GetPanicWriter() io.Writer {
+	return l.errWriter
+}
+
+func (l *Logger) isWarnWriterTerm() bool {
+	w, ok := l.warnWriter.(*os.File)
+	if !ok {
+		return false
+	} else if !isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()) { // 非终端
+		return false
+	}
+	return true
+}
+
+func (l *Logger) isErrWriterTerm() bool {
+	w, ok := l.errWriter.(*os.File)
+	if !ok {
+		return false
+	} else if !isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()) { // 非终端
+		return false
+	}
+	return true
+}
+
+func (l *Logger) isTermDump() bool {
+	// TERM为dump表示终端为基础模式,不支持高级显示
+	return os.Getenv("TERM") == "dumb"
+}
+
+func (l *Logger) IsDebugTerm() bool {
+	return l.isWarnWriterTerm()
+}
+
+func (l *Logger) IsInfoTerm() bool {
+	return l.isWarnWriterTerm()
+}
+
+func (l *Logger) IsWarnTerm() bool {
+	return l.isWarnWriterTerm()
+}
+
+func (l *Logger) IsTagTerm() bool {
+	return l.isWarnWriterTerm()
+}
+
+func (l *Logger) IsErrorTerm() bool {
+	return l.isErrWriterTerm()
+}
+
+func (l *Logger) IsPanicTerm() bool {
+	return l.isErrWriterTerm()
+}
+
+func (l *Logger) IsDebugTermNotDumb() bool {
+	return l.isWarnWriterTerm() && !l.isTermDump()
+}
+
+func (l *Logger) IsInfoTermNotDumb() bool {
+	return l.isWarnWriterTerm() && !l.isTermDump()
+}
+
+func (l *Logger) IsWarnTermNotDumb() bool {
+	return l.isWarnWriterTerm() && !l.isTermDump()
+}
+
+func (l *Logger) IsTagTermNotDumb() bool {
+	return l.isWarnWriterTerm() && !l.isTermDump()
+}
+
+func (l *Logger) IsErrorTermNotDumb() bool {
+	return l.isErrWriterTerm() && !l.isTermDump()
+}
+
+func (l *Logger) IsPanicTermNotDumb() bool {
+	return l.isErrWriterTerm() && !l.isTermDump()
+}

+ 264 - 0
src/logger/main.go

@@ -0,0 +1,264 @@
+package logger
+
+import (
+	"io"
+)
+
+func Executablef(format string, args ...interface{}) string {
+	if !IsReady() {
+		return ""
+	}
+	return globalLogger.Executablef(format, args...)
+}
+
+func Tagf(format string, args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.TagSkipf(1, format, args...)
+}
+
+func Debugf(format string, args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Debugf(format, args...)
+}
+
+func Infof(format string, args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Infof(format, args...)
+}
+
+func Warnf(format string, args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Warnf(format, args...)
+}
+
+func Errorf(format string, args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Errorf(format, args...)
+}
+
+func Panicf(format string, args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Panicf(format, args...)
+}
+
+func Tag(args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.TagSkip(1, args...)
+}
+
+func Debug(args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Debug(args...)
+}
+
+func Info(args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Info(args...)
+}
+
+func Warn(args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Warn(args...)
+}
+
+func Error(args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Error(args...)
+}
+
+func Panic(args ...interface{}) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.Panic(args...)
+}
+
+func TagWrite(msg string) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.TagSkip(1, msg)
+}
+
+func DebugWrite(msg string) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.DebugWrite(msg)
+}
+
+func InfoWrite(msg string) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.InfoWrite(msg)
+}
+
+func WarnWrite(msg string) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.WarnWrite(msg)
+}
+
+func ErrorWrite(msg string) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.ErrorWrite(msg)
+}
+
+func PanicWrite(msg string) {
+	if !IsReady() {
+		return
+	}
+	globalLogger.PanicWrite(msg)
+}
+
+func GetDebugWriter() io.Writer {
+	if !IsReady() {
+		return DefaultWarnWriter
+	}
+	return globalLogger.GetDebugWriter()
+}
+
+func GetInfoWriter() io.Writer {
+	if !IsReady() {
+		return DefaultWarnWriter
+	}
+	return globalLogger.GetInfoWriter()
+}
+
+func GetWarningWriter() io.Writer {
+	if !IsReady() {
+		return DefaultWarnWriter
+	}
+	return globalLogger.GetWarningWriter()
+}
+
+func GetTagWriter() io.Writer {
+	if !IsReady() {
+		return DefaultWarnWriter
+	}
+	return globalLogger.GetTagWriter()
+}
+
+func GetErrorWriter() io.Writer {
+	if !IsReady() {
+		return DefaultWarnWriter
+	}
+	return globalLogger.GetErrorWriter()
+}
+
+func GetPanicWriter() io.Writer {
+	if !IsReady() {
+		return DefaultWarnWriter
+	}
+	return globalLogger.GetPanicWriter()
+}
+
+func IsDebugTerm() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsDebugTerm()
+}
+
+func IsInfoTerm() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsDebugTerm()
+}
+
+func IsTagTerm() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsTagTerm()
+}
+
+func IsWarnTerm() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsWarnTerm()
+}
+
+func IsErrorTerm() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsErrorTerm()
+}
+
+func IsPanicTerm() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsPanicTerm()
+}
+
+func IsDebugTermNotDumb() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsDebugTerm()
+}
+
+func IsInfoTermNotDumb() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsInfoTermNotDumb()
+}
+
+func IsTagTermNotDumb() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsTagTermNotDumb()
+}
+
+func IsWarnTermNotDumb() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsWarnTermNotDumb()
+}
+
+func IsErrorTermNotDumb() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsErrorTermNotDumb()
+}
+
+func IsPanicTermNotDumb() bool {
+	if !IsReady() {
+		return false
+	}
+	return globalLogger.IsPanicTermNotDumb()
+}

+ 65 - 0
src/mainfunc/v1.go

@@ -0,0 +1,65 @@
+package mainfunc
+
+import (
+	"errors"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/aliyun"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/config"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/flagparser"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/logger"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/server"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/utils"
+	"os"
+)
+
+func MainV1() int {
+	var err error
+
+	err = flagparser.InitFlag()
+	if errors.Is(err, flagparser.StopFlag) {
+		return 0
+	} else if err != nil {
+		return utils.ExitByError(err)
+	}
+
+	if !flagparser.IsReady() {
+		return utils.ExitByErrorMsg("flag parser unknown error")
+	}
+
+	utils.SayHellof("%s", "The backend service program starts normally, thank you.")
+	defer func() {
+		utils.SayGoodByef("%s", "The backend service program is offline/shutdown normally, thank you.")
+	}()
+
+	cfgErr := config.InitConfig(flagparser.ConfigFile())
+	if cfgErr != nil && cfgErr.IsError() {
+		return utils.ExitByError(cfgErr)
+	}
+
+	if !config.IsReady() {
+		return utils.ExitByErrorMsg("config parser unknown error")
+	}
+
+	err = logger.InitLogger(os.Stdout, os.Stderr)
+	if err != nil {
+		return utils.ExitByError(err)
+	}
+
+	if !logger.IsReady() {
+		return utils.ExitByErrorMsg("logger unknown error")
+	}
+
+	logger.Executablef("%s", "ready")
+	logger.Infof("run mode: %s", config.GetConfig().GlobalConfig.GetRunMode())
+
+	err = aliyun.Init()
+	if err != nil {
+		return utils.ExitByError(err)
+	}
+
+	err = server.Server()
+	if err != nil {
+		return utils.ExitByError(err)
+	}
+
+	return 0
+}

+ 35 - 0
src/server/server.go

@@ -0,0 +1,35 @@
+package server
+
+import (
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/aliyun"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/config"
+	"github.com/SongZihuan/auto-aliyun-cdn-ssl/src/logger"
+)
+
+func Server() error {
+	cfg := config.GetConfig().DomainConfig
+
+	logger.Info("Server start...")
+	for _, domain := range cfg.Domains {
+		func() {
+			defer func() {
+				if r := recover(); r != nil {
+					if err, ok := r.(error); ok {
+						logger.Panicf("aliyun update CDN HTTOS by domain (%s) panic: %s", domain, err.Error())
+					} else {
+						logger.Panicf("aliyun update CDN HTTOS by domain (%s) panic: %v", domain, r)
+					}
+				}
+			}()
+
+			certPath, prikeyPath := domain.GetFilePath()
+			err := aliyun.UpdateCDNHttpsByFilePath(domain.Domain, certPath, prikeyPath)
+			if err != nil {
+				logger.Errorf("aliyun update CDN HTTOS by domain (%s) error: %s", domain, err.Error())
+			}
+		}()
+	}
+	logger.Info("Server finish...")
+
+	return nil
+}

+ 53 - 0
src/utils/cmd.go

@@ -0,0 +1,53 @@
+package utils
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+var _args0 = ""
+
+func init() {
+	var err error
+	if len(os.Args) > 0 {
+		_args0, err = os.Executable()
+		if err != nil {
+			_args0 = os.Args[0]
+		}
+	}
+
+	if _args0 == "" {
+		panic("args was empty")
+	}
+}
+
+func GetArgs0() string {
+	return _args0
+}
+
+func GetArgs0Name() string {
+	return filepath.Base(_args0)
+}
+
+func SayHellof(format string, args ...interface{}) {
+	var msg string
+	if len(format) == 0 && len(args) == 0 {
+		msg = fmt.Sprintf("%s: %s", GetArgs0Name(), "Normal startup, thank you.")
+	} else {
+		str := fmt.Sprintf(format, args...)
+		msg = fmt.Sprintf("%s: %s", GetArgs0Name(), str)
+	}
+	fmt.Println(FormatTextToWidth(msg, NormalConsoleWidth))
+}
+
+func SayGoodByef(format string, args ...interface{}) {
+	var msg string
+	if len(format) == 0 && len(args) == 0 {
+		msg = fmt.Sprintf("%s: %s", GetArgs0Name(), "Normal shutdown, thank you.")
+	} else {
+		str := fmt.Sprintf(format, args...)
+		msg = fmt.Sprintf("%s: %s", GetArgs0Name(), str)
+	}
+	fmt.Println(FormatTextToWidth(msg, NormalConsoleWidth))
+}

+ 45 - 0
src/utils/exit.go

@@ -0,0 +1,45 @@
+package utils
+
+import (
+	"os"
+)
+
+func ExitByError(err error, code ...int) int {
+	if err == nil {
+		return ExitByErrorMsg("")
+	} else {
+		return ExitByErrorMsg(err.Error(), code...)
+	}
+}
+
+func ExitByErrorMsg(msg string, code ...int) int {
+	if len(msg) == 0 {
+		msg = "exit: unknown error"
+	}
+
+	return ErrorExit(msg, code...)
+}
+
+func ErrorExit(msg string, code ...int) int {
+	if len(msg) == 0 {
+		SayGoodByef("%s", "Encountered an error, abnormal offline/shutdown.")
+	} else {
+		SayGoodByef("Encountered an error, abnormal offline/shutdown: %s\n", msg)
+	}
+
+	if len(code) == 1 && code[0] != 0 {
+		return Exit(code[0])
+	} else {
+		return Exit(1)
+	}
+}
+
+func Exit(code ...int) int {
+	if len(code) == 1 {
+		os.Exit(code[0])
+		return code[0]
+	} else {
+		os.Exit(0)
+		return 0
+	}
+}

+ 32 - 0
src/utils/file.go

@@ -0,0 +1,32 @@
+package utils
+
+import (
+	"errors"
+	"os"
+)
+
+func IsExists(path string) bool {
+	_, err := os.Stat(path)
+	if err != nil && errors.Is(err, os.ErrNotExist) {
+		return false
+	}
+	return true
+}
+
+func IsDir(path string) bool {
+	s, err := os.Stat(path)
+	if err != nil {
+		return false
+	}
+
+	return s.IsDir()
+}
+
+func IsFile(path string) bool {
+	s, err := os.Stat(path)
+	if err != nil {
+		return false
+	}
+
+	return !s.IsDir()
+}

+ 20 - 0
src/utils/rand.go

@@ -0,0 +1,20 @@
+package utils
+
+import (
+	"math/rand"
+	"time"
+)
+
+var r *rand.Rand = nil
+
+func init() {
+	r = rand.New(rand.NewSource(time.Now().UnixNano()))
+}
+
+func Rand() *rand.Rand {
+	if r == nil {
+		panic("nil Rand")
+	}
+
+	return r
+}

+ 25 - 0
src/utils/runtime.go

@@ -0,0 +1,25 @@
+package utils
+
+import (
+	"path/filepath"
+	"runtime"
+	"strings"
+)
+
+func GetCallingFunctionInfo(skip int) (string, string, string, int) {
+	pc, file, line, ok := runtime.Caller(skip + 1)
+	if !ok {
+		return "", "", "", 0
+	}
+
+	var funcName string
+	tmp := runtime.FuncForPC(pc).Name()
+	tmpLst := strings.Split(tmp, "/")
+	if len(tmpLst) == 0 {
+		funcName = tmp
+	} else {
+		funcName = tmpLst[len(tmpLst)-1]
+	}
+
+	return funcName, file, filepath.Base(file), line
+}

+ 145 - 0
src/utils/string.go

@@ -0,0 +1,145 @@
+package utils
+
+import (
+	"regexp"
+	"strings"
+	"unicode"
+)
+
+const BASE_CHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func RandStr(length int) string {
+	bytes := []byte(BASE_CHAR)
+
+	var result []byte
+	for i := 0; i < length; i++ {
+		result = append(result, bytes[Rand().Intn(len(bytes))])
+	}
+
+	return string(result)
+}
+
+func InvalidPhone(phone string) bool {
+	pattern := `^1[3-9]\d{9}$`
+	matched, _ := regexp.MatchString(pattern, phone)
+	return matched
+}
+
+func IsValidEmail(email string) bool {
+	pattern := `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`
+	matched, _ := regexp.MatchString(pattern, email)
+	return matched
+}
+
+const NormalConsoleWidth = 80
+
+func FormatTextToWidth(text string, width int) string {
+	return FormatTextToWidthAndPrefix(text, 0, width)
+}
+
+func FormatTextToWidthAndPrefix(text string, prefixWidth int, overallWidth int) string {
+	var result strings.Builder
+
+	width := overallWidth - prefixWidth
+	if width <= 0 {
+		panic("bad width")
+	}
+
+	text = strings.ReplaceAll(text, "\r\n", "\n")
+
+	for _, line := range strings.Split(text, "\n") {
+		result.WriteString(strings.Repeat(" ", prefixWidth))
+
+		if line == "" {
+			result.WriteString("\n")
+			continue
+		}
+
+		spaceCount := CountSpaceInStringPrefix(line) % width
+		newLineLength := 0
+		if spaceCount < 80 {
+			result.WriteString(strings.Repeat(" ", spaceCount))
+			newLineLength = spaceCount
+		}
+
+		for _, word := range strings.Fields(line) {
+			if newLineLength+len(word) >= width {
+				result.WriteString("\n")
+				result.WriteString(strings.Repeat(" ", prefixWidth))
+				newLineLength = 0
+			}
+
+			// 不是第一个词时,添加空格
+			if newLineLength != 0 {
+				result.WriteString(" ")
+				newLineLength += 1
+			}
+
+			result.WriteString(word)
+			newLineLength += len(word)
+		}
+
+		if newLineLength != 0 {
+			result.WriteString("\n")
+			newLineLength = 0
+		}
+	}
+
+	return strings.TrimRight(result.String(), "\n")
+}
+
+func CountSpaceInStringPrefix(str string) int {
+	var res int
+	for _, r := range str {
+		if r == ' ' {
+			res += 1
+		} else {
+			break
+		}
+	}
+
+	return res
+}
+
+func IsValidURLPath(path string) bool {
+	if path == "" {
+		return true
+	} else if path == "/" {
+		return false
+	}
+
+	pattern := `^\/[a-zA-Z0-9\-._~:/?#\[\]@!$&'()*+,;%=]+$`
+	matched, _ := regexp.MatchString(pattern, path)
+	return matched
+}
+
+func IsValidDomain(domain string) bool {
+	pattern := `^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`
+	matched, _ := regexp.MatchString(pattern, domain)
+	return matched
+}
+
+func StringToOnlyPrint(str string) string {
+	runeLst := []rune(str)
+	res := make([]rune, 0, len(runeLst))
+
+	for _, r := range runeLst {
+		if unicode.IsPrint(r) {
+			res = append(res, r)
+		}
+	}
+
+	return string(res)
+}
+
+func IsGoodQueryKey(key string) bool {
+	pattern := `^[a-zA-Z0-9\-._~]+$`
+	matched, _ := regexp.MatchString(pattern, key)
+	return matched
+}
+
+func IsValidHTTPHeaderKey(key string) bool {
+	pattern := `^[a-zA-Z0-9!#$%&'*+.^_` + "`" + `|~-]+$`
+	matched, _ := regexp.MatchString(pattern, key)
+	return matched
+}

+ 79 - 0
src/utils/stringbool.go

@@ -0,0 +1,79 @@
+package utils
+
+import (
+	"strings"
+)
+
+type StringBool string
+
+const enable StringBool = "enable"
+const disable StringBool = "disable"
+const enableBool StringBool = "true"
+const disableBool StringBool = "false"
+
+func (s *StringBool) check() bool {
+	*s = StringBool(strings.ToLower(string(*s)))
+	return *s == enable || *s == disable || *s == enableBool || *s == disableBool
+}
+
+func (s *StringBool) is(v StringBool, defaultVal ...bool) (res bool) {
+	if !s.check() {
+		if len(defaultVal) == 1 {
+			res = defaultVal[0]
+			return
+		} else {
+			return false
+		}
+	}
+
+	return *s == v
+}
+
+func (s *StringBool) IsEnable(defaultVal ...bool) (res bool) {
+	res = s.is(enable, defaultVal...) || s.is(enableBool, defaultVal...)
+	return
+}
+
+func (s *StringBool) IsDisable(defaultVal ...bool) (res bool) {
+	res = s.is(disable, defaultVal...) || s.is(disableBool, defaultVal...)
+	return
+}
+
+func (s *StringBool) setDefault(v StringBool) {
+	if !s.check() {
+		*s = v
+	}
+}
+
+func (s *StringBool) SetDefaultEnable() {
+	s.setDefault(enable)
+}
+
+func (s *StringBool) SetDefaultDisable() {
+	s.setDefault(disable)
+}
+
+func (s *StringBool) ToString() string {
+	if s.IsEnable() {
+		return string(enable)
+	}
+	return string(disable)
+}
+
+func (s *StringBool) ToStringDefaultEnable() string {
+	if s.IsEnable(true) {
+		return string(enable)
+	}
+	return string(disable)
+}
+
+func (s *StringBool) ToStringDefaultDisable() string {
+	if s.IsEnable(false) {
+		return string(enable)
+	}
+	return string(disable)
+}
+
+func (s *StringBool) ToBool(defaultVal ...bool) bool {
+	return s.IsEnable(defaultVal...)
+}

+ 12 - 0
src/utils/struct.go

@@ -0,0 +1,12 @@
+package utils
+
+import "reflect"
+
+func HasFieldByReflect(typ reflect.Type, fieldName string) bool {
+	for i := 0; i < typ.NumField(); i++ {
+		if typ.Field(i).Name == fieldName {
+			return true
+		}
+	}
+	return false
+}

+ 54 - 0
src/utils/x509.go

@@ -0,0 +1,54 @@
+package utils
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"time"
+)
+
+func ReadCertificate(data []byte) (*x509.Certificate, error) {
+	block, _ := pem.Decode(data)
+	if block == nil || block.Type != "CERTIFICATE" {
+		return nil, fmt.Errorf("failed to decode PEM block containing certificate")
+	}
+
+	cert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse certificate: %s", err.Error())
+	} else if cert == nil {
+		return nil, fmt.Errorf("failed to parse certificate: return nil, unknown reason")
+	}
+
+	return cert, nil
+}
+
+func CheckCertWithDomain(cert *x509.Certificate, domain string) bool {
+	// 遍历主题备用名称查找匹配的域名
+	for _, name := range cert.DNSNames {
+		if name == domain {
+			return true // 找到了匹配的域名
+		}
+	}
+
+	// 检查通用名作为回退,虽然现代实践倾向于使用SAN
+	if cert.Subject.CommonName != "" && cert.Subject.CommonName == domain {
+		return true // 通用名匹配
+	}
+
+	// 如果没有找到匹配,则返回错误
+	return false
+}
+
+func CheckCertWithTime(cert *x509.Certificate, gracePeriod time.Duration) bool {
+	now := time.Now()
+	nowWithGracePeriod := now.Add(gracePeriod)
+
+	if now.Before(cert.NotBefore) {
+		return false
+	} else if nowWithGracePeriod.After(cert.NotAfter) {
+		return false
+	}
+
+	return true
+}