迁移并重构项目,优化了执行流程

This commit is contained in:
2025-10-31 15:43:25 +08:00
commit 35bcd62ad5
29 changed files with 1312 additions and 0 deletions

View File

@ -0,0 +1,18 @@
package Func
import (
"SafeLine-Acme/internal/class/SafeLine"
"time"
)
func CheckNodes(nodes []SafeLine.Nodes, n int) []SafeLine.Nodes {
var need []SafeLine.Nodes
date := time.Now()
for _, node := range nodes {
days := int(node.ValidBefore.Sub(date).Hours() / 24)
if days <= n {
need = append(need, node)
}
}
return need
}

View File

@ -0,0 +1,20 @@
package Func
import (
"SafeLine-Acme/internal/app/logger"
"SafeLine-Acme/internal/class/Acme"
"github.com/go-acme/lego/v4/challenge"
)
func ChooseProvider(config Acme.Object) ([]challenge.Provider, bool) {
providers, errors := config.DNSProvider.Choose()
for _, e := range errors {
logger.Error.Println(e)
}
if len(providers) == 0 {
logger.Error.Println()
return nil, true
}
return providers, false
}

View File

@ -0,0 +1,19 @@
package Func
import (
"SafeLine-Acme/internal/app/logger"
"SafeLine-Acme/internal/class/SafeLine"
"log"
)
func UpdateCertCheck(server SafeLine.Object, days int) ([]SafeLine.Nodes, bool) {
nodes := server.GetCertNodes()
needUpdate := CheckNodes(nodes, days)
if len(needUpdate) != 0 {
log.Printf("本次需要更新证书数量有 %s%d%s 个,现在开始更新...", logger.Yellow, len(needUpdate), logger.Reset)
} else {
log.Print("本次无需更新证书,即将退出本程序。")
return nil, true
}
return needUpdate, false
}

View File

@ -0,0 +1,34 @@
package logger
import (
"fmt"
"log"
"os"
)
const (
Reset = "\033[0m"
Red = "\033[31m"
Green = "\033[32m"
Yellow = "\033[33m"
Blue = "\033[34m"
Magenta = "\033[35m"
Cyan = "\033[36m"
White = "\033[37m"
)
var (
Success *log.Logger
Error *log.Logger
Warning *log.Logger
)
func LogInit() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Ldate | log.Ltime)
log.SetPrefix(fmt.Sprintf("%s[INFO]%s ", Cyan, Reset))
Success = log.New(os.Stdout, fmt.Sprintf("%s[SUCCESS]%s ", Green, Reset), log.Ldate|log.Ltime)
Error = log.New(os.Stdout, fmt.Sprintf("%s[ERROR]%s ", Red, Reset), log.Ldate|log.Ltime)
Warning = log.New(os.Stdout, fmt.Sprintf("%s[WARNING]%s ", Yellow, Reset), log.Ldate|log.Ltime)
}

View File

@ -0,0 +1,57 @@
package Acme
import (
"SafeLine-Acme/internal/app/logger"
"reflect"
)
func (acme *Object) CheckFile() bool {
var flag = true
if acme.Email == "" {
logger.Warning.Printf("未设置 %s证书申请邮箱%s: 请检查配置文件中的 %sAcme.Email%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = false
}
v := reflect.ValueOf(acme.DNSProvider)
var dns = true
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Struct {
for ii := 0; ii < field.NumField(); ii++ {
if field.Field(ii).String() != "" {
dns = false
}
}
}
}
if dns {
logger.Warning.Printf("未设置 %sDNS服务提供商%s: 请检查配置文件中的 %sAcme.DNSProvider%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
}
return flag
}
func (acme *Object) CheckCommand() bool {
var flag = true
if acme.Email == "" {
logger.Warning.Printf("未设置 %s证书申请邮箱%s: 请检查命令中的 %s-e%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = false
}
v := reflect.ValueOf(acme.DNSProvider).Elem()
var dns = true
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Struct {
for ii := 0; ii < field.NumField(); ii++ {
if field.Field(ii).String() != "" {
dns = false
}
}
}
}
if dns {
logger.Warning.Printf("未设置 %sDNS服务提供商%s: 请检查命令中的 %s-k%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
}
return flag
}

View File

@ -0,0 +1,20 @@
package Acme
import (
"regexp"
"strings"
)
func (acme *Object) EmailVerify() bool {
var EmailRegex = regexp.MustCompile(`^[a-zA-Z0-9.!#$%&'*+/=?^_{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`)
email := strings.TrimSpace(acme.Email)
if len(email) < 3 || len(email) > 254 {
return false
}
return EmailRegex.MatchString(email)
}
func (acme *Object) DNSProviderVerify() bool {
acme.DNSProvider.Choose()
return true
}

View File

@ -0,0 +1,10 @@
package Acme
import (
"SafeLine-Acme/internal/class/DNSProvider"
)
type Object struct {
Email string `json:"Email"`
DNSProvider DNSProvider.Object `json:"DNSProvider"`
}

View File

@ -0,0 +1,107 @@
package Config
import (
"SafeLine-Acme/internal/app/logger"
"SafeLine-Acme/internal/class/Acme"
"SafeLine-Acme/internal/class/DNSProvider"
"SafeLine-Acme/internal/class/SafeLine"
"SafeLine-Acme/pkg/utils"
"encoding/json"
"flag"
"log"
"os"
)
func (config *Object) Read(path string) {
data, err := os.ReadFile(path)
if err != nil {
logger.Error.Printf("配置文件读取失败: %s%s%s", logger.Red, err, logger.Reset)
os.Exit(0)
}
err = json.Unmarshal(data, &config)
if err != nil {
logger.Error.Printf("配置文件读取失败: %s%s%s", logger.Red, err, logger.Reset)
os.Exit(0)
}
config.CheckFile()
}
func (config *Object) Write(path string) {
data, _ := json.MarshalIndent(config, "", " ")
_ = os.WriteFile(path, data, 0644)
}
func (config *Object) Command() {
var host, apiToken, email *string
var days, concurrency *int
host = flag.String("h", "172.22.222.4:1443", "-h <host>")
apiToken = flag.String("t", "", "-t <apiToken>")
days = flag.Int("d", 30, "-t <days>")
concurrency = flag.Int("c", 3, "-c <concurrency>")
email = flag.String("e", "", "-e <email>")
kvp := flag.String("kv", "", "-kv <key=value>,<key=value>...")
flag.Parse()
var KVP = make(utils.KVPair)
if *kvp != "" {
KVP.Set(*kvp)
}
config.Days = *days
config.Concurrency = *concurrency
config.Server = SafeLine.Object{
Host: *host,
ApiToken: *apiToken,
}
config.Acme = Acme.Object{
Email: *email,
DNSProvider: DNSProvider.Object{
TencentCloud: DNSProvider.TencentCloud{
SecretID: KVP["SecretID"],
SecretKey: KVP["SecretKey"],
},
AliCloud: DNSProvider.AliCloud{
AccessKeyId: KVP["AccessKeyId"],
AccessKeySecret: KVP["AccessKeySecret"],
RAMRole: KVP["RAMRole"],
STSToken: KVP["STSToken"],
},
HuaweiCloud: DNSProvider.HuaweiCloud{
AccessKeyId: KVP["AccessKeyId"],
Region: KVP["Region"],
SecretAccessKey: KVP["SecretAccessKey"],
},
WestCN: DNSProvider.WestCN{
Username: KVP["Username"],
Password: KVP["Password"],
},
RainYun: DNSProvider.RainYun{
ApiKey: KVP["ApiKey"],
},
Dode: DNSProvider.Dode{
Token: KVP["Token"],
},
},
}
config.CheckCommand()
}
func (config *Object) CheckFile() {
a := config.Server.CheckFile()
b := config.Acme.CheckFile()
if !a || !b {
log.Printf("配置检查完毕,请检查相关配置后重新运行!")
os.Exit(0)
}
log.Printf("配置检查完毕,即将开始更新证书!")
}
func (config *Object) CheckCommand() {
a := config.Server.CheckCommand()
b := config.Acme.CheckCommand()
if !a || !b {
log.Printf("配置检查完毕,请检查相关配置后重新运行!")
os.Exit(0)
}
log.Printf("配置检查完毕,即将开始更新证书!")
}

View File

@ -0,0 +1,13 @@
package Config
import (
"SafeLine-Acme/internal/class/Acme"
"SafeLine-Acme/internal/class/SafeLine"
)
type Object struct {
Concurrency int `json:"Concurrency"`
Days int `json:"Days"`
Server SafeLine.Object `json:"Server"`
Acme Acme.Object `json:"Acme"`
}

View File

@ -0,0 +1,49 @@
package Config
import (
"SafeLine-Acme/internal/class/Acme"
"SafeLine-Acme/internal/class/DNSProvider"
"SafeLine-Acme/internal/class/SafeLine"
)
func (config *Object) Default() {
a := Object{
Concurrency: 3,
Days: 15,
Server: SafeLine.Object{
Host: "192.168.1.4:1443",
ApiToken: "xxx",
},
Acme: Acme.Object{
Email: "xxx",
DNSProvider: DNSProvider.Object{
TencentCloud: DNSProvider.TencentCloud{
SecretID: "xxx",
SecretKey: "xxx",
},
AliCloud: DNSProvider.AliCloud{
AccessKeyId: "xxx",
AccessKeySecret: "xxx",
RAMRole: "xxx(可选)",
STSToken: "xxx(可选)",
},
HuaweiCloud: DNSProvider.HuaweiCloud{
AccessKeyId: "xxx",
Region: "xxx",
SecretAccessKey: "xxx",
},
WestCN: DNSProvider.WestCN{
Username: "xxx",
Password: "xxx",
},
RainYun: DNSProvider.RainYun{
ApiKey: "xxx",
},
Dode: DNSProvider.Dode{
Token: "xxx",
},
},
},
}
a.Write("./config.json")
}

View File

@ -0,0 +1,60 @@
package DNSProvider
import (
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/go-acme/lego/v4/providers/dns/dode"
"github.com/go-acme/lego/v4/providers/dns/huaweicloud"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/go-acme/lego/v4/providers/dns/westcn"
)
func (tencent TencentCloud) Provider() (challenge.Provider, error) {
cfg := tencentcloud.NewDefaultConfig()
cfg.SecretID = tencent.SecretID
cfg.SecretKey = tencent.SecretKey
p, err := tencentcloud.NewDNSProviderConfig(cfg)
return p, err
}
func (ali AliCloud) Provider() (challenge.Provider, error) {
cfg := alidns.NewDefaultConfig()
cfg.SecurityToken = ali.STSToken
cfg.SecretKey = ali.AccessKeySecret
cfg.RAMRole = ali.RAMRole
cfg.APIKey = ali.AccessKeyId
p, err := alidns.NewDNSProviderConfig(cfg)
return p, err
}
func (huawei HuaweiCloud) Provider() (challenge.Provider, error) {
cfg := huaweicloud.NewDefaultConfig()
cfg.Region = huawei.Region
cfg.AccessKeyID = huawei.AccessKeyId
cfg.SecretAccessKey = huawei.SecretAccessKey
p, err := huaweicloud.NewDNSProviderConfig(cfg)
return p, err
}
func (west WestCN) Provider() (challenge.Provider, error) {
cfg := westcn.NewDefaultConfig()
cfg.Username = west.Username
cfg.Password = west.Password
p, err := westcn.NewDNSProviderConfig(cfg)
return p, err
}
func (rain RainYun) Provider() (challenge.Provider, error) {
cfg := rainyun.NewDefaultConfig()
cfg.APIKey = rain.ApiKey
p, err := rainyun.NewDNSProviderConfig(cfg)
return p, err
}
func (Dode Dode) Provider() (challenge.Provider, error) {
cfg := dode.NewDefaultConfig()
cfg.Token = Dode.Token
p, err := dode.NewDNSProviderConfig(cfg)
return p, err
}

View File

@ -0,0 +1,43 @@
package DNSProvider
import (
"reflect"
"github.com/go-acme/lego/v4/challenge"
)
type Object struct {
TencentCloud TencentCloud `json:"TencentCloud,omitempty"`
AliCloud AliCloud `json:"AliCloud,omitempty"`
HuaweiCloud HuaweiCloud `json:"HuaweiCloud,omitempty"`
WestCN WestCN `json:"WestCN,omitempty"`
RainYun RainYun `json:"RainYun,omitempty"`
Dode Dode `json:"Dode,omitempty"`
}
func (dnsProvider *Object) Choose() ([]challenge.Provider, []error) {
var Providers []challenge.Provider
var Errors []error
v := reflect.ValueOf(dnsProvider).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Struct {
for ii := 0; ii < field.NumField(); ii++ {
if field.Field(ii).String() != "" {
method, _ := field.Type().MethodByName("Provider")
results := method.Func.Call([]reflect.Value{field})
err, _ := results[1].Interface().(error)
if err != nil {
Errors = append(Errors, err)
} else {
Providers = append(Providers, results[0].Interface().(challenge.Provider))
}
break
}
}
}
}
return Providers, Errors
}

View File

@ -0,0 +1,32 @@
package DNSProvider
type TencentCloud struct {
SecretID string `json:"SecretId,omitempty"`
SecretKey string `json:"SecretKey,omitempty"`
}
type AliCloud struct {
AccessKeyId string `json:"AccessKeyId,omitempty"`
AccessKeySecret string `json:"AccessKeySecret,omitempty"`
RAMRole string `json:"RAMRole,omitempty"`
STSToken string `json:"STSToken,omitempty"`
}
type HuaweiCloud struct {
AccessKeyId string `json:"AccessKeyId,omitempty"`
Region string `json:"Region,omitempty"`
SecretAccessKey string `json:"SecretAccessKey,omitempty"`
}
type WestCN struct {
Username string `json:"Username,omitempty"`
Password string `json:"Password,omitempty"`
}
type RainYun struct {
ApiKey string `json:"ApiKey,omitempty"`
}
type Dode struct {
Token string `json:"Token,omitempty"`
}

View File

@ -0,0 +1,42 @@
package SafeLine
import (
"encoding/json"
)
type UpdateReq struct {
Acme struct {
Domains []string `json:"domains"`
Email string `json:"email"`
} `json:"acme"`
Id int `json:"id"`
Manual struct {
Crt string `json:"crt"`
Key string `json:"key"`
} `json:"manual"`
Type int `json:"type"`
}
func (updateReq *UpdateReq) Create(Certificate, PrivateKey []byte, domains []string, email string, id, Type int) {
updateReq.Acme.Domains = domains
updateReq.Acme.Email = email
updateReq.Manual.Crt = string(Certificate)
updateReq.Manual.Key = string(PrivateKey)
updateReq.Id = id
updateReq.Type = Type
}
func (updateReq *UpdateReq) Marshal() []byte {
data, _ := json.Marshal(updateReq)
return data
}
type UpdateResp struct {
Data int `json:"data"`
Err interface{} `json:"err"`
Msg string `json:"msg"`
}
func (updateResp *UpdateResp) Unmarshal(data []byte) {
_ = json.Unmarshal(data, &updateResp)
}

View File

@ -0,0 +1,29 @@
package SafeLine
import "SafeLine-Acme/internal/app/logger"
func (safeLine *Object) CheckFile() bool {
var flag = true
if safeLine.Host == "" {
logger.Warning.Printf("未设置 %s服务器主机%s: 请检查配置文件中的 %sServer.Host%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = false
}
if safeLine.ApiToken == "" {
logger.Warning.Printf("未设置 %sSafeLine API Token%s : 请检查配置文件中的 %sServer.ApiToken%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = false
}
return flag
}
func (safeLine *Object) CheckCommand() bool {
var flag = true
if safeLine.Host == "" {
logger.Warning.Printf("未设置 %s服务器主机%s: 请检查命令中的 %s-h%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = false
}
if safeLine.ApiToken == "" {
logger.Warning.Printf("未设置 %sSafeLine API Token%s : 请检查命令中的 %s-t%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = false
}
return flag
}

View File

@ -0,0 +1,52 @@
package SafeLine
import (
"SafeLine-Acme/internal/app/logger"
"SafeLine-Acme/pkg/utils"
"bytes"
"io"
"net/http"
"os"
)
func (safeLine *Object) GetCertNodes() []Nodes {
header := http.Header{
"X-SLCE-API-TOKEN": []string{safeLine.ApiToken},
}
resp, err := utils.Request("GET", safeLine.SSLCertUrl(), nil, header)
if err != nil {
logger.Error.Printf("请求接口 %s/api/open/cert%s 时发生错误: %s%s%s", logger.Cyan, logger.Reset, logger.Red, err, logger.Reset)
os.Exit(0)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var listResp ListResp
listResp.Unmarshal(data)
return listResp.Data.Nodes
}
func (safeLine *Object) UpdateCert(email string, Certificate, PrivateKey []byte, cert Nodes) ([]string, []string) {
header := http.Header{
"X-SLCE-API-TOKEN": []string{safeLine.ApiToken},
"Content-Type": []string{"application/json"},
}
var updateReq UpdateReq
updateReq.Create(Certificate, PrivateKey, cert.Domains, email, cert.Id, cert.Type)
resp, err := utils.Request("POST", safeLine.SSLCertUrl(), bytes.NewReader(updateReq.Marshal()), header)
if err != nil {
logger.Error.Printf("更新证书时发生错误: %s%s%s", logger.Red, err, logger.Reset)
return nil, nil
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
var updateResp UpdateResp
updateResp.Unmarshal(data)
if updateResp.Msg != "" {
logger.Error.Printf("域名 %s%s%s 证书更新失败: %s%s%s", logger.Cyan, cert.Domains, logger.Reset, logger.Red, updateResp.Msg, logger.Reset)
return nil, cert.Domains
}
logger.Success.Printf("域名 %s%s%s 证书更新成功!", logger.Cyan, cert.Domains, logger.Reset)
return cert.Domains, nil
}

View File

@ -0,0 +1,6 @@
package SafeLine
type Object struct {
Host string `json:"Host"`
ApiToken string `json:"ApiToken"`
}

View File

@ -0,0 +1,33 @@
package SafeLine
import (
"encoding/json"
"time"
)
type ListResp struct {
Data struct {
Nodes []Nodes `json:"nodes"`
Total int `json:"total"`
} `json:"data"`
Err string `json:"err"`
Msg string `json:"msg"`
}
type Nodes struct {
Id int `json:"id"`
Domains []string `json:"domains"`
Issuer string `json:"issuer"`
SelfSignature bool `json:"self_signature"`
Trusted bool `json:"trusted"`
Revoked bool `json:"revoked"`
Expired bool `json:"expired"`
Type int `json:"type"`
AcmeMessage string `json:"acme_message"`
ValidBefore time.Time `json:"valid_before"`
RelatedSites []string `json:"related_sites"`
}
func (listResp *ListResp) Unmarshal(data []byte) {
_ = json.Unmarshal(data, &listResp)
}

View File

@ -0,0 +1,42 @@
package SafeLine
import (
"SafeLine-Acme/internal/app/logger"
"SafeLine-Acme/pkg/utils"
"errors"
"net/http"
)
func (safeLine *Object) Verify() bool {
err := safeLine.ServerVerify()
if err != nil {
logger.Error.Printf("请求服务端时发生错误: %s%s%s", logger.Red, err.Error(), logger.Reset)
return false
}
err = safeLine.AuthTokenVerify()
if err != nil {
logger.Warning.Printf("%sSafeLine API Token%s 验证失败,请检查后重试", logger.Cyan, logger.Reset)
return false
}
logger.Success.Printf("%sSafeLine%s 相关配置检验完成!", logger.Cyan, logger.Reset)
return true
}
func (safeLine *Object) ServerVerify() error {
_, err := utils.Request("GET", safeLine.Url().String(), nil, nil)
return err
}
func (safeLine *Object) AuthTokenVerify() error {
header := http.Header{
"X-SLCE-API-TOKEN": []string{safeLine.ApiToken},
}
resp, err := utils.Request("GET", safeLine.AuthTokenUrl(), nil, header)
if err != nil || resp.StatusCode != 200 {
return errors.New("token 验证失败")
}
return nil
}

View File

@ -0,0 +1,26 @@
package SafeLine
import (
"fmt"
"net/url"
)
func (safeLine *Object) Url() *url.URL {
var u *url.URL
u, _ = url.Parse(fmt.Sprintf("https://%s", safeLine.Host))
return u
}
func (safeLine *Object) AuthTokenUrl() string {
path := "/api/open/auth/token"
u := safeLine.Url()
u.Path = path
return u.String()
}
func (safeLine *Object) SSLCertUrl() string {
path := "/api/open/cert"
u := safeLine.Url()
u.Path = path
return u.String()
}

View File

@ -0,0 +1 @@
package moudle