This commit is contained in:
2025-07-19 22:49:32 +08:00
commit c01fd9130d
30 changed files with 1332 additions and 0 deletions

View File

@ -0,0 +1,91 @@
package config
import (
"SafelineAPI/internal/app/logger"
"flag"
"github.com/go-jose/go-jose/v4/json"
"os"
)
type Config struct {
SafeLine `json:"SafeLine"`
ApplyCert `json:"ApplyCert"`
}
func (config *Config) 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.Verify()
}
func (config *Config) Write(path string) {
data, _ := json.MarshalIndent(config, "", " ")
_ = os.WriteFile(path, data, 0644)
}
func (config *Config) Command() {
var hostname, port, apiToken, save, email *string
var days *int
var DNSProvider *string
hostname = flag.String("h", "172.22.222.4", "-h <hostname>")
port = flag.String("p", "9443", "-p <port>")
apiToken = flag.String("t", "", "-t <apiToken>")
days = flag.Int("d", 30, "-t <days>")
save = flag.String("s", "/tmp/ssl", "-s <save file>")
email = flag.String("e", "", "-e <email>")
DNSProvider = flag.String("D", "", "-D <DNS Provider> (e.g., TencentCloud, AliCloud, HuaweiCloud, WestCN, RainYun)")
kvp := flag.String("kv", "", "-kv <key=value>,<key=value>...")
flag.Parse()
var KVP = make(KVPair)
if *kvp != "" {
KVP.Set(*kvp)
}
config.SafeLine = SafeLine{
Host: Host{
HostName: *hostname,
Port: *port,
},
ApiToken: ApiToken(*apiToken),
}
config.ApplyCert = ApplyCert{
Days: *days,
SavePath: *save,
Email: *email,
DNSProviderConfig: DNSProviderConfig{
DNSProvider: *DNSProvider,
TencentCloud: TencentCloud{
SecretID: KVP["SecretID"],
SecretKey: KVP["SecretKey"],
},
AliCloud: AliCloud{
AccessKeyId: KVP["AccessKeyId"],
AccessKeySecret: KVP["AccessKeySecret"],
RAMRole: KVP["RAMRole"],
STSToken: KVP["STSToken"],
},
HuaweiCloud: HuaweiCloud{
AccessKeyId: KVP["AccessKeyId"],
Region: KVP["Region"],
SecretAccessKey: KVP["SecretAccessKey"],
},
WestCN: WestCN{
Username: KVP["Username"],
Password: KVP["Password"],
},
RainYun: RainYun{
ApiKey: KVP["ApiKey"],
},
},
}
config.VerifyCommand()
}

View File

@ -0,0 +1,37 @@
package config
type DNSProviderConfig struct {
DNSProvider string `json:"DNSProvider"`
TencentCloud `json:"TencentCloud,omitempty"`
AliCloud `json:"AliCloud,omitempty"`
HuaweiCloud `json:"HuaweiCloud,omitempty"`
WestCN `json:"WestCN,omitempty"`
RainYun `json:"RainYun,omitempty"`
}
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"`
}

View File

@ -0,0 +1,52 @@
package config
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/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
}

View File

@ -0,0 +1,70 @@
package config
import (
"SafelineAPI/internal/app/logger"
"log"
)
type ApplyCert struct {
Days int `json:"Days"`
Email string `json:"Email"`
SavePath string `json:"SavePath"`
DNSProviderConfig `json:"DNSProviderConfig"`
}
func (applyCert *ApplyCert) GetDays() int {
return applyCert.Days
}
func (applyCert *ApplyCert) GetEmail() string {
return applyCert.Email
}
func (applyCert *ApplyCert) Verify() bool {
var flag = false
if applyCert.Days == 0 {
applyCert.Days = 30
}
if applyCert.SavePath == "" {
applyCert.SavePath = "/tmp/ssl"
}
if applyCert.DNSProvider == "" {
logger.Warning.Printf("未设置 %sDNS服务提供商%s: 请检查配置文件中的 %sApplyCert.DNSProviderConfig.DNSProvider%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = true
}
if applyCert.Email == "" {
logger.Warning.Printf("未设置 %s证书申请邮箱%s: 请检查配置文件中的 %sApplyCert.Email%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = true
}
if !flag {
log.Printf("%sApplyCert%s 相关配置检验完成!", logger.Cyan, logger.Reset)
}
return flag
}
func (applyCert *ApplyCert) VerifyCommand() bool {
var flag = false
if applyCert.Days == 0 {
applyCert.Days = 30
}
if applyCert.SavePath == "" {
applyCert.SavePath = "/tmp/ssl"
}
if applyCert.DNSProvider == "" {
logger.Warning.Printf("未设置 %sDNS服务提供商%s: 请检查命令中的 %s-D%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = true
}
if applyCert.Email == "" {
logger.Warning.Printf("未设置 %s证书申请邮箱%s: 请检查命令中的 %s-e%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
flag = true
}
if !flag {
log.Printf("%sApplyCert%s 相关配置检验完成!", logger.Cyan, logger.Reset)
}
return flag
}

View File

@ -0,0 +1,44 @@
package config
func (config *Config) Default() {
a := Config{
SafeLine: SafeLine{
Host: Host{
HostName: "192.168.1.4",
Port: "1443",
},
ApiToken: "xxx",
},
ApplyCert: ApplyCert{
Days: 30,
Email: "xxx",
SavePath: "/tmp/ssl",
DNSProviderConfig: DNSProviderConfig{
DNSProvider: "xxx",
TencentCloud: TencentCloud{
SecretID: "xxx",
SecretKey: "xxx",
},
AliCloud: AliCloud{
AccessKeyId: "xxx",
AccessKeySecret: "xxx",
RAMRole: "xxx(可选)",
STSToken: "xxx(可选)",
},
HuaweiCloud: HuaweiCloud{
AccessKeyId: "xxx",
Region: "xxx",
SecretAccessKey: "xxx",
},
WestCN: WestCN{
Username: "xxx",
Password: "xxx",
},
RainYun: RainYun{
ApiKey: "xxx",
},
},
},
}
a.Write("./config.json")
}

View File

@ -0,0 +1,46 @@
package config
import (
"SafelineAPI/internal/app/logger"
"SafelineAPI/internal/app/safeLineApi"
"fmt"
"net/url"
)
type Host struct {
HostName string `json:"HostName"`
Port string `json:"Port"`
}
func (host Host) String() string {
if host.Port == "" {
return host.HostName
}
return host.HostName + ":" + host.Port
}
func (host Host) Verify() bool {
if host.HostName == "" {
logger.Warning.Printf("未设置 %s主机名称%s: 请检查配置文件中的 %sSafeLine.Host.HostName%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
return true
}
return false
}
func (host Host) VerifyCommand() bool {
if host.HostName == "" {
logger.Warning.Printf("未设置 %s主机名称%s: 请检查命令中的 %s-h%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
return true
}
return false
}
func (host Host) Url() *safeLineApi.URL {
var u *url.URL
if host.Port == "" {
u, _ = url.Parse(fmt.Sprintf("https://%s", host.HostName))
} else {
u, _ = url.Parse(fmt.Sprintf("https://%s:%s", host.HostName, host.Port))
}
return (*safeLineApi.URL)(u)
}

View File

@ -0,0 +1,26 @@
package config
import (
"log"
"os"
)
func (config *Config) Verify() {
a := config.SafeLine.Verify()
b := config.ApplyCert.Verify()
if a || b {
log.Printf("配置检查完毕,请检查相关配置后重新运行!")
os.Exit(0)
}
log.Printf("配置检查完毕,即将开始更新证书!")
}
func (config *Config) VerifyCommand() {
a := config.SafeLine.VerifyCommand()
b := config.ApplyCert.VerifyCommand()
if a || b {
log.Printf("配置检查完毕,请检查相关配置后重新运行!")
os.Exit(0)
}
log.Printf("配置检查完毕,即将开始更新证书!")
}

View File

@ -0,0 +1,15 @@
package config
import (
"strings"
)
type KVPair map[string]string
func (kvp *KVPair) Set(str string) {
kvps := strings.Split(str, ",")
for _, i := range kvps {
kv := strings.SplitN(i, "=", 2)
(*kvp)[kv[0]] = kv[1]
}
}

View File

@ -0,0 +1,100 @@
package config
import (
"SafelineAPI/internal/app/logger"
"SafelineAPI/pkg/utils"
)
type SafeLine struct {
Host `json:"Host"`
ApiToken `json:"ApiToken"`
}
type ApiToken string
func (apiToken ApiToken) GetApiToken() (string, string) {
return "X-SLCE-API-TOKEN", apiToken.String()
}
func (apiToken ApiToken) String() string {
return string(apiToken)
}
func (apiToken ApiToken) Verify() bool {
if apiToken.String() == "" {
logger.Warning.Printf("未设置 %sSafeLine API Token%s : 请检查配置文件中的 %sSafeLine.ApiToken%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
return true
}
return false
}
func (safeLine SafeLine) Verify() bool {
a := safeLine.ApiToken.Verify()
b := safeLine.Host.Verify()
if a || b {
return true
}
NoLoginAuthTokenResp, NoLoginStatusCode, NoLoginErr := utils.AuthSafeLine(*safeLine.Host.Url())
if NoLoginErr != nil {
logger.Error.Printf("请求服务端时发生错误: %s%s%s", logger.Red, NoLoginErr.Error(), logger.Reset)
return true
}
LoginAuthTokenResp, LoginStatusCode, LoginErr := utils.VerifyAuthToken(*safeLine.Host.Url(), safeLine.ApiToken.String())
if LoginErr != nil {
logger.Error.Printf("验证 %sSafeLine API Token%s 时发生错误: %s%s%s", logger.Cyan, logger.Reset, logger.Red, LoginErr.Error(), logger.Reset)
return true
}
if !(NoLoginAuthTokenResp.Err == "login-required" && NoLoginStatusCode == 401) {
logger.Warning.Printf("服务端接口 %s/open/auth/token%s 请求有误: 请检查配置文件中的 %sSafeLine.Host%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
return true
}
if LoginAuthTokenResp.Err == "login-required" && LoginStatusCode == 401 {
logger.Warning.Printf("%sSafeLine API Token%s 有误: 请检查后重试", logger.Cyan, logger.Reset)
return true
}
logger.Success.Printf("%sSafeLine%s 相关配置检验完成!", logger.Cyan, logger.Reset)
return false
}
func (apiToken ApiToken) VerifyCommand() bool {
if apiToken.String() == "" {
logger.Warning.Printf("未设置 %sSafeLine API Token%s : 请检查命令中的 %s-t%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
return true
}
return false
}
func (safeLine SafeLine) VerifyCommand() bool {
a := safeLine.ApiToken.VerifyCommand()
b := safeLine.Host.VerifyCommand()
if a || b {
return true
}
NoLoginAuthTokenResp, NoLoginStatusCode, NoLoginErr := utils.AuthSafeLine(*safeLine.Host.Url())
if NoLoginErr != nil {
logger.Error.Printf("请求服务端时发生错误: %s%s%s", logger.Red, NoLoginErr.Error(), logger.Reset)
return true
}
LoginAuthTokenResp, LoginStatusCode, LoginErr := utils.VerifyAuthToken(*safeLine.Host.Url(), safeLine.ApiToken.String())
if LoginErr != nil {
logger.Error.Printf("验证 %sSafeLine API Token%s 时发生错误: %s%s%s", logger.Cyan, logger.Reset, logger.Red, LoginErr.Error(), logger.Reset)
return true
}
if !(NoLoginAuthTokenResp.Err == "login-required" && NoLoginStatusCode == 401) {
logger.Warning.Printf("服务端接口 %s/open/auth/token%s 请求有误: 请检查命令中的 %s-h%s 参数", logger.Cyan, logger.Reset, logger.Yellow, logger.Reset)
return true
}
if LoginAuthTokenResp.Err == "login-required" && LoginStatusCode == 401 {
logger.Warning.Printf("%sSafeLine API Token%s 有误: 请检查后重试", logger.Cyan, logger.Reset)
return true
}
logger.Success.Printf("%sSafeLine%s 相关配置检验完成!", logger.Cyan, logger.Reset)
return 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,13 @@
package safeLineApi
import "encoding/json"
type AuthTokenResp struct {
Data string `json:"data"`
Err string `json:"err"`
Msg string `json:"msg"`
}
func (authResp *AuthTokenResp) Unmarshal(data []byte) {
_ = json.Unmarshal(data, authResp)
}

View File

@ -0,0 +1,11 @@
package safeLineApi
import (
"net/url"
)
func (u *URL) AuthTokenUrl() string {
path := "/api/open/auth/token"
u.Path = path
return (*url.URL)(u).String()
}

View File

@ -0,0 +1,74 @@
package safeLineApi
import (
"encoding/json"
"os"
"path/filepath"
"time"
)
type UpsertReq 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 (upsertReq *UpsertReq) Create(domains []string, email, dir string, id, Type int) {
certificate, _ := os.ReadFile(filepath.Join(dir, domains[0]+".crt"))
privateKey, _ := os.ReadFile(filepath.Join(dir, domains[0]+".key"))
upsertReq.Acme.Domains = domains
upsertReq.Acme.Email = email
upsertReq.Manual.Crt = string(certificate)
upsertReq.Manual.Key = string(privateKey)
upsertReq.Id = id
upsertReq.Type = Type
}
func (upsertReq *UpsertReq) Marshal() []byte {
data, _ := json.Marshal(upsertReq)
return data
}
type UpsertResp struct {
Data int `json:"data"`
Err interface{} `json:"err"`
Msg string `json:"msg"`
}
func (upsertResp *UpsertResp) Unmarshal(data []byte) {
_ = json.Unmarshal(data, &upsertResp)
}
type ListResp struct {
Data struct {
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,18 @@
package safeLineApi
import (
"net/url"
"strconv"
)
func (u *URL) SSLCertUrl() string {
path := "/api/open/cert"
u.Path = path
return (*url.URL)(u).String()
}
func (u *URL) SSLCertUrlWithParam(id int) string {
path := "/api/open/cert/" + strconv.Itoa(id)
u.Path = path
return (*url.URL)(u).String()
}

View File

@ -0,0 +1,5 @@
package safeLineApi
const (
GetTOKEN = "GET"
)

View File

@ -0,0 +1,9 @@
package safeLineApi
import "net/url"
type URL url.URL
func (u *URL) String() string {
return (*url.URL)(u).String()
}