package apply import ( "ALLinSSL/backend/internal/access" "ALLinSSL/backend/internal/cert" "ALLinSSL/backend/internal/cert/apply/lego/jdcloud" "ALLinSSL/backend/public" "encoding/json" "fmt" azcorecloud "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/providers/dns/alidns" "github.com/go-acme/lego/v4/providers/dns/azuredns" "github.com/go-acme/lego/v4/providers/dns/baiducloud" "github.com/go-acme/lego/v4/providers/dns/bunny" "github.com/go-acme/lego/v4/providers/dns/cloudflare" "github.com/go-acme/lego/v4/providers/dns/cloudns" "github.com/go-acme/lego/v4/providers/dns/gcore" "github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/go-acme/lego/v4/providers/dns/huaweicloud" "github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namesilo" "github.com/go-acme/lego/v4/providers/dns/ns1" "github.com/go-acme/lego/v4/providers/dns/route53" "github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/volcengine" "github.com/go-acme/lego/v4/providers/dns/westcn" "github.com/go-acme/lego/v4/registration" "net/http" "net/url" "os" "strconv" "strings" "time" ) var AlgorithmMap = map[string]certcrypto.KeyType{ "RSA2048": certcrypto.RSA2048, "RSA3072": certcrypto.RSA3072, "RSA4096": certcrypto.RSA4096, "RSA8192": certcrypto.RSA8192, "EC256": certcrypto.EC256, "EC384": certcrypto.EC384, } var CADirURLMap = map[string]string{ "Let's Encrypt": "https://acme-v02.api.letsencrypt.org/directory", "zerossl": "https://acme.zerossl.com/v2/DV90", "google": "https://dv.acme-v02.api.pki.goog/directory", "sslcom": "https://acme.ssl.com/sslcom-dv-rsa", "sslcom-rsa": "https://acme.ssl.com/sslcom-dv-rsa", "sslcom-ecc": "https://acme.ssl.com/sslcom-dv-ecc", "buypass": "https://api.buypass.com/acme/directory", } func GetSqlite() (*public.Sqlite, error) { s, err := public.NewSqlite("data/accounts.db", "") if err != nil { return nil, err } s.TableName = "accounts" return s, nil } func GetDNSProvider(providerName string, creds map[string]string, httpClient *http.Client, maxWait time.Duration) (challenge.Provider, error) { switch providerName { case "tencentcloud": config := tencentcloud.NewDefaultConfig() config.SecretID = creds["secret_id"] config.SecretKey = creds["secret_key"] config.PropagationTimeout = maxWait return tencentcloud.NewDNSProviderConfig(config) case "cloudflare": config := cloudflare.NewDefaultConfig() config.AuthEmail = creds["email"] config.AuthKey = creds["api_key"] config.PropagationTimeout = maxWait return cloudflare.NewDNSProviderConfig(config) case "aliyun": config := alidns.NewDefaultConfig() config.APIKey = creds["access_key_id"] config.SecretKey = creds["access_key_secret"] config.PropagationTimeout = maxWait return alidns.NewDNSProviderConfig(config) case "huaweicloud": config := huaweicloud.NewDefaultConfig() config.AccessKeyID = creds["access_key"] config.SecretAccessKey = creds["secret_key"] // 不传会报错 config.Region = "cn-north-1" config.PropagationTimeout = maxWait return huaweicloud.NewDNSProviderConfig(config) case "baidu": config := baiducloud.NewDefaultConfig() config.AccessKeyID = creds["access_key"] config.SecretAccessKey = creds["secret_key"] config.PropagationTimeout = maxWait return baiducloud.NewDNSProviderConfig(config) case "westcn": config := westcn.NewDefaultConfig() config.Username = creds["username"] config.Password = creds["password"] config.PropagationTimeout = maxWait return westcn.NewDNSProviderConfig(config) case "volcengine": config := volcengine.NewDefaultConfig() config.AccessKey = creds["access_key"] config.SecretKey = creds["secret_key"] config.PropagationTimeout = maxWait return volcengine.NewDNSProviderConfig(config) case "godaddy": config := godaddy.NewDefaultConfig() config.APIKey = creds["api_key"] config.APISecret = creds["api_secret"] if httpClient != nil { config.HTTPClient = httpClient } config.PropagationTimeout = maxWait return godaddy.NewDNSProviderConfig(config) case "namecheap": config := namecheap.NewDefaultConfig() config.APIUser = creds["api_user"] config.APIKey = creds["api_key"] config.PropagationTimeout = maxWait return namecheap.NewDNSProviderConfig(config) case "ns1": config := ns1.NewDefaultConfig() config.APIKey = creds["api_key"] config.PropagationTimeout = maxWait return ns1.NewDNSProviderConfig(config) case "cloudns": config := cloudns.NewDefaultConfig() config.AuthID = creds["auth_id"] config.AuthPassword = creds["auth_password"] config.PropagationTimeout = maxWait return cloudns.NewDNSProviderConfig(config) case "aws": config := route53.NewDefaultConfig() config.AccessKeyID = creds["access_key_id"] config.SecretAccessKey = creds["secret_access_key"] config.PropagationTimeout = maxWait return route53.NewDNSProviderConfig(config) case "azure": config := azuredns.NewDefaultConfig() config.TenantID = creds["tenant_id"] config.ClientID = creds["client_id"] config.ClientSecret = creds["client_secret"] switch strings.ToLower(creds["environment"]) { case "", "default", "public", "azurecloud": config.Environment = azcorecloud.AzurePublic case "china", "chinacloud", "azurechina", "azurechinacloud": config.Environment = azcorecloud.AzureChina case "usgovernment", "government", "azureusgovernment", "azuregovernment": config.Environment = azcorecloud.AzureGovernment default: return nil, fmt.Errorf("不支持的 Azure 环境: %s", creds["environment"]) } config.PropagationTimeout = maxWait return azuredns.NewDNSProviderConfig(config) case "namesilo": config := namesilo.NewDefaultConfig() config.APIKey = creds["api_key"] config.PropagationTimeout = maxWait return namesilo.NewDNSProviderConfig(config) case "namedotcom": config := namedotcom.NewDefaultConfig() config.Username = creds["username"] config.APIToken = creds["api_token"] config.PropagationTimeout = maxWait return namedotcom.NewDNSProviderConfig(config) case "bunny": config := bunny.NewDefaultConfig() config.APIKey = creds["api_key"] config.PropagationTimeout = maxWait return bunny.NewDNSProviderConfig(config) case "gcore": config := gcore.NewDefaultConfig() config.APIToken = creds["api_token"] config.PropagationTimeout = maxWait return gcore.NewDNSProviderConfig(config) case "jdcloud": config := jdcloud.NewDefaultConfig() config.AccessKeyID = creds["access_key_id"] config.AccessKeySecret = creds["secret_access_key"] config.RegionId = "cn-north-1" config.PropagationTimeout = maxWait return jdcloud.NewDNSProviderConfig(config) default: return nil, fmt.Errorf("不支持的 DNS Provider: %s", providerName) } } func GetZeroSSLEabFromEmail(email string, httpClient *http.Client) (map[string]any, error) { APIPath := "https://api.zerossl.com/acme/eab-credentials-email" data := map[string]any{ "email": email, } jsonData, err := json.Marshal(data) if err != nil { return nil, err } req, err := http.NewRequest("POST", APIPath, strings.NewReader(string(jsonData))) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if httpClient == nil { httpClient = &http.Client{} } resp, err := httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("获取ZeroSSL EAB信息失败,状态码:%d", resp.StatusCode) } var result map[string]any err = json.NewDecoder(resp.Body).Decode(&result) if err != nil { return nil, fmt.Errorf("解析ZeroSSL EAB信息失败:%v", err) } if result["eab_kid"] == nil || result["eab_hmac_key"] == nil { return nil, fmt.Errorf("ZeroSSL EAB信息不完整,缺少kid或hmacEncoded") } return map[string]any{ "Kid": result["eab_kid"], "HmacEncoded": result["eab_hmac_key"], }, nil } func getEABFromAccData(accData map[string]any, eabData *map[string]any) bool { if accData == nil { return false } kid := accData["Kid"] hmac := accData["HmacEncoded"] if kid != nil && hmac != nil { *eabData = map[string]any{ "Kid": kid, "HmacEncoded": hmac, } return true } return false } func GetAcmeClient(email, algorithm, eabId, ca string, httpClient *http.Client, logger *public.Logger) (*lego.Client, error) { var ( eabData map[string]any err error ) switch eabId { case "": if ca == "" || ca == "letsencrypt" { ca = "Let's Encrypt" } case "let": ca = "Let's Encrypt" case "buy", "buypass": ca = "buypass" default: eabData, err = access.GetEAB(eabId) if err != nil { return nil, err } if eabData == nil { return nil, fmt.Errorf("未找到EAB信息") } if eabData["Kid"] == nil { return nil, fmt.Errorf("Kid不能为空") } if eabData["HmacEncoded"] == nil { return nil, fmt.Errorf("HmacEncoded不能为空") } ca = eabData["ca"].(string) } CADirURL := CADirURLMap[ca] if ca == "sslcom" { if algorithm == "EC256" || algorithm == "EC384" { CADirURL = CADirURLMap["sslcom-ecc"] } else { CADirURL = CADirURLMap["sslcom-rsa"] } } db, err := GetSqlite() var accData map[string]any if err != nil { logger.Debug("获取数据库连接失败", err) if ca != "Let's Encrypt" && ca != "zerossl" && ca != "buypass" { return nil, fmt.Errorf("当前CA【%s】 需要从数据库获取预设账号,但是连接数据库失败,请稍后重试,err:%w", ca, err) } } else { defer db.Close() accData, err = GetAccount(db, email, ca) if err != nil || accData == nil { logger.Debug("获取acme账号信息失败") if ca != "Let's Encrypt" && ca != "zerossl" && ca != "buypass" { return nil, fmt.Errorf("未找到%s账号信息,请先在账号管理中添加%s账号, email:%s", ca, ca, email) } } if CADirURL == "" { accCADirURL, ok := accData["CADirURL"].(string) if !ok || accCADirURL == "" { logger.Debug("未找到此CA的请求地址") return nil, fmt.Errorf("未找到CA【%s】请求地址,请先在账号管理中检查%s账号, email:%s", ca, ca, email) } CADirURL = accCADirURL } } user := GetAcmeUser(email, logger, accData) config := lego.NewConfig(user) config.Certificate.KeyType = AlgorithmMap[algorithm] config.CADirURL = CADirURL if httpClient != nil { config.HTTPClient = httpClient } client, err := lego.NewClient(config) if err != nil { return nil, err } if user.Registration == nil { logger.Debug("正在注册账号:" + email) if eabData == nil { // 走新的逻辑,eab已合并到账号中 if !getEABFromAccData(accData, &eabData) { switch ca { case "zerossl": eabData, err = GetZeroSSLEabFromEmail(email, httpClient) if err != nil { return nil, fmt.Errorf("获取ZeroSSL EAB信息失败: %v", err) } case "sslcom", "google": return nil, fmt.Errorf("未找到EAB信息,请在账号管理中添加%s账号", ca) } } } var ( reg *registration.Resource Kid, HmacEncoded string ) if eabData != nil { Kid = eabData["Kid"].(string) HmacEncoded = eabData["HmacEncoded"].(string) } if Kid != "" && HmacEncoded != "" { Kid := eabData["Kid"].(string) HmacEncoded := eabData["HmacEncoded"].(string) reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ TermsOfServiceAgreed: true, Kid: Kid, HmacEncoded: HmacEncoded, }) } else { reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) } if err != nil { return nil, err } user.Registration = reg err = SaveUserToDB(db, user, ca) if err != nil { logger.Debug("acme账号注册成功,但保存到数据库失败", err) } logger.Debug("acme账号注册并保存成功") } return client, nil } func GetCert(runId string, domainArr []string, endDay int, logger *public.Logger) (map[string]any, error) { if runId == "" { return nil, fmt.Errorf("参数错误:_runId") } s, err := public.NewSqlite("data/data.db", "") if err != nil { return nil, err } s.TableName = "workflow_history" defer s.Close() // 查询 workflowId wh, err := s.Where("id=?", []interface{}{runId}).Select() if err != nil { return nil, err } if len(wh) <= 0 { return nil, fmt.Errorf("未获取到对应的workflowId") } s.TableName = "cert" certs, err := s.Where("workflow_id=?", []interface{}{wh[0]["workflow_id"]}).Select() if err != nil { return nil, err } if len(certs) <= 0 { return nil, fmt.Errorf("未获取到当前工作流下的证书") } layout := "2006-01-02 15:04:05" var maxDays float64 var maxItem map[string]any for i := range certs { if !public.ContainsAllIgnoreBRepeats(strings.Split(certs[i]["domains"].(string), ","), domainArr) { continue } endTimeStr, ok := certs[i]["end_time"].(string) if !ok { continue } endTime, err := time.Parse(layout, endTimeStr) if err != nil { continue } diff := endTime.Sub(time.Now()).Hours() / 24 if diff > maxDays { maxDays = diff maxItem = certs[i] } } if maxItem == nil { return nil, fmt.Errorf("未获取到对应的证书") } if int(maxDays) <= endDay { return nil, fmt.Errorf("证书已过期或即将过期,剩余天数:%d 小于%d天", int(maxDays), endDay) } // 证书未过期,直接返回 logger.Debug(fmt.Sprintf("上次证书申请成功,域名:%s,剩余天数:%d 大于%d天,已跳过申请复用此证书", maxItem["domains"], int(maxDays), endDay)) return map[string]any{ "cert": maxItem["cert"], "key": maxItem["key"], "issuerCert": maxItem["issuer_cert"], "skip": true, }, nil } func Apply(cfg map[string]any, logger *public.Logger) (map[string]any, error) { log.Logger = logger.GetLogger() var err error email, ok := cfg["email"].(string) if !ok { return nil, fmt.Errorf("参数错误:email") } domains, ok := cfg["domains"].(string) if !ok { return nil, fmt.Errorf("参数错误:domains") } providerStr, ok := cfg["provider"].(string) if !ok { return nil, fmt.Errorf("参数错误:provider") } endDay := 30 switch v := cfg["end_day"].(type) { case float64: endDay = int(v) case int: endDay = v case string: if v != "" { endDay, err = strconv.Atoi(v) if err != nil { return nil, fmt.Errorf("参数错误:end_day") } } case int64: endDay = int(v) } algorithm, ok := cfg["algorithm"].(string) if !ok { algorithm = "RSA2048" } var httpClient *http.Client proxy, ok := cfg["proxy"].(string) if ok && proxy != "" { // 构建代理 HTTP 客户端 proxyURL, err := url.Parse(proxy) // 替换为你的代理地址 if err != nil { return nil, fmt.Errorf("无效的代理地址: %v", err) } httpClient = &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(proxyURL), }, Timeout: 30 * time.Second, } } var eabId string switch v := cfg["eabId"].(type) { case float64: eabId = strconv.Itoa(int(v)) case string: eabId = v default: eabId = "" } ca, ok := cfg["ca"].(string) if !ok { ca = "" } var providerID string switch v := cfg["provider_id"].(type) { case float64: providerID = strconv.Itoa(int(v)) case string: providerID = v default: return nil, fmt.Errorf("参数错误:provider_id") } var NameServers []string if cfg["name_server"] == nil { NameServers = []string{ "8.8.8.8:53", "1.1.1.1:53", } } else { if nameServerStr, ok := cfg["name_server"].(string); ok { NameServers = strings.Split(nameServerStr, ",") for i := range NameServers { NameServers[i] = strings.TrimSpace(NameServers[i]) } } else { return nil, fmt.Errorf("参数错误:name_server") } } var skipCheck bool if cfg["skip_check"] == nil { // 默认跳过预检查 skipCheck = false } else { switch v := cfg["skip_check"].(type) { case int: if v > 0 { skipCheck = true } else { skipCheck = false } case float64: if v > 0 { skipCheck = true } else { skipCheck = false } case string: if v == "true" || v == "1" { skipCheck = true } else { skipCheck = false } case bool: skipCheck = v default: return nil, fmt.Errorf("参数错误:skip_check") } } var ignoreCheck bool if cfg["ignore_check"] == nil { // 默认不忽略预检查 ignoreCheck = false } else { switch v := cfg["ignore_check"].(type) { case int: if v > 0 { ignoreCheck = true } else { ignoreCheck = false } case float64: if v > 0 { ignoreCheck = true } else { ignoreCheck = false } case string: if v == "true" || v == "1" { ignoreCheck = true } else { ignoreCheck = false } case bool: ignoreCheck = v default: return nil, fmt.Errorf("参数错误:ignore_check") } } var closeCname bool if cfg["close_cname"] == nil { // 默认开启CNAME跟随 closeCname = false } else { switch v := cfg["close_cname"].(type) { case int: if v > 0 { closeCname = true } else { closeCname = false } case float64: if v > 0 { closeCname = true } else { closeCname = false } case string: if v == "true" || v == "1" { closeCname = true } else { closeCname = false } case bool: closeCname = v default: return nil, fmt.Errorf("参数错误:close_cname") } } var maxWait time.Duration if cfg["max_wait"] == nil { // 默认最大等待时间为2分钟 maxWait = 2 * time.Minute } else { switch v := cfg["max_wait"].(type) { case int: maxWait = time.Duration(v) * time.Second case float64: maxWait = time.Duration(v) * time.Second case string: maxWait = 2 * time.Minute // 默认值 if v != "" { d, err := strconv.Atoi(v) if err == nil { maxWait = time.Duration(d) * time.Second } } default: maxWait = 2 * time.Minute } } domainArr := strings.Split(domains, ",") for i := range domainArr { domainArr[i] = strings.TrimSpace(domainArr[i]) } // 获取上次申请的证书 runId, ok := cfg["_runId"].(string) if !ok { return nil, fmt.Errorf("参数错误:_runId") } certData, err := GetCert(runId, domainArr, endDay, logger) if err != nil { logger.Debug("未获取到符合条件的本地证书:" + err.Error()) } else { return certData, nil } logger.Debug("正在申请证书,域名: " + domains) os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(closeCname)) // 创建 ACME 客户端 client, err := GetAcmeClient(email, algorithm, eabId, ca, httpClient, logger) if err != nil { return nil, err } // 获取 DNS 验证提供者 providerData, err := access.GetAccess(providerID) if err != nil { return nil, err } providerConfigStr, ok := providerData["config"].(string) if !ok { return nil, fmt.Errorf("api配置错误") } // 解析 JSON 配置 var providerConfig map[string]string err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) if err != nil { return nil, err } // DNS 验证 provider, err := GetDNSProvider(providerStr, providerConfig, httpClient, maxWait) if err != nil { return nil, fmt.Errorf("创建 DNS provider 失败: %v", err) } if skipCheck { // 跳过预检查 err = client.Challenge.SetDNS01Provider(provider, dns01.WrapPreCheck(func(domain, fqdn, value string, check dns01.PreCheckFunc) (bool, error) { return true, nil }), ) } else { start := time.Now() if ignoreCheck { err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(NameServers), dns01.WrapPreCheck(func(domain, fqdn, value string, check dns01.PreCheckFunc) (bool, error) { ok, err := check(fqdn, value) elapsed := time.Since(start) if err != nil { log.Printf("[WARN] DNS precheck error for %s: %v", fqdn, err) if elapsed >= maxWait { log.Printf("[WARN] Precheck error but forcing continue due to timeout for %s", fqdn) return true, nil } return false, nil } if ok { log.Printf("[OK] TXT record for %s is present.", fqdn) return true, nil } if elapsed >= maxWait { log.Printf("[WARN] TXT record for %s not found after %v, forcing continue.", fqdn, elapsed) return true, nil } log.Printf("[INFO] TXT record for %s not yet found, waiting... elapsed %v", fqdn, elapsed) return false, nil }), ) } else { err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(NameServers), ) } } if err != nil { return nil, err } // fmt.Println(strings.Split(domains, ",")) request := certificate.ObtainRequest{ Domains: domainArr, Bundle: true, } certObj, err := client.Certificate.Obtain(request) if err != nil { return nil, err } certStr := string(certObj.Certificate) keyStr := string(certObj.PrivateKey) issuerCertStr := string(certObj.IssuerCertificate) // 保存证书和私钥 data := map[string]any{ "cert": certStr, "key": keyStr, "issuerCert": issuerCertStr, } _, err = cert.SaveCert("workflow", keyStr, certStr, issuerCertStr, runId) if err != nil { return nil, err } return data, nil }