package services import ( "auto-ssl/config" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "log" "os" "path/filepath" "strings" "time" "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/http01" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/lego" alidnsprov "github.com/go-acme/lego/v4/providers/dns/alidns" cloudflareprov "github.com/go-acme/lego/v4/providers/dns/cloudflare" dnspodprov "github.com/go-acme/lego/v4/providers/dns/dnspod" "github.com/go-acme/lego/v4/registration" ) type ACMEAccount struct { Email string PrivateKey crypto.PrivateKey Registration *registration.Resource } // DNSConfig represents DNS provider configuration type DNSConfig struct { Provider string `json:"provider"` // Aliyun DNS AliKey string `json:"ali_key,omitempty"` AliSecret string `json:"ali_secret,omitempty"` // Cloudflare CFAPIToken string `json:"cf_api_token,omitempty"` // DNSPod DNSPodID string `json:"dnspod_id,omitempty"` DNSPodKey string `json:"dnspod_key,omitempty"` } // GetACMECertificate obtains a certificate from ACME provider func GetACMECertificate(cert *config.Certificate, cfg *config.Config) error { dir := filepath.Join(cfg.AccountsDir, sanitizeEmail(cert.Email)) account, err := getOrCreateAccount(cert.Email, cert.Provider, dir) if err != nil { return fmt.Errorf("failed to setup ACME account: %v", err) } legoCfg := lego.NewConfig(account) legoCfg.CADirURL = getCADirURL(cert.Provider) legoCfg.Certificate.KeyType = certcrypto.RSA2048 client, err := lego.NewClient(legoCfg) if err != nil { return fmt.Errorf("failed to create lego client: %v", err) } if strings.ToLower(cert.ChallengeType) == "dns" { provider, err := getDNSProvider(cert) if err != nil { return fmt.Errorf("failed to create DNS provider: %v", err) } if err := client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(dns01.ParseNameservers([]string{"8.8.8.8:53", "1.1.1.1:53"})), ); err != nil { return fmt.Errorf("failed to set DNS-01 provider: %v", err) } } else { if err := client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80")); err != nil { return fmt.Errorf("failed to set HTTP-01 provider: %v", err) } } request := certificate.ObtainRequest{ Domains: []string{cert.Domain}, Bundle: true, MustStaple: false, } certRes, err := client.Certificate.Obtain(request) if err != nil { return fmt.Errorf("failed to obtain certificate: %v", err) } // Save certificate files certDir := filepath.Join(cfg.CertDir, sanitizeDomain(cert.Domain)) if err := os.MkdirAll(certDir, 0700); err != nil { return fmt.Errorf("failed to create cert directory: %v", err) } os.WriteFile(filepath.Join(certDir, "fullchain.pem"), certRes.Certificate, 0644) os.WriteFile(filepath.Join(certDir, "privkey.pem"), certRes.PrivateKey, 0600) os.WriteFile(filepath.Join(certDir, "chain.pem"), certRes.IssuerCertificate, 0644) now := time.Now() expiresAt := parseCertExpiry(certRes.Certificate) cert.Status = "active" cert.CertURL = certRes.CertURL cert.ExpiresAt = expiresAt cert.LastRenewedAt = &now cert.ErrorMessage = "" log.Printf("Certificate obtained successfully for %s, expires at %s", cert.Domain, expiresAt.Format(time.RFC3339)) return nil } // RenewCertificate renews an existing certificate func RenewCertificate(cert *config.Certificate, cfg *config.Config) error { dir := filepath.Join(cfg.AccountsDir, sanitizeEmail(cert.Email)) account, err := getOrCreateAccount(cert.Email, cert.Provider, dir) if err != nil { return fmt.Errorf("failed to setup ACME account: %v", err) } legoCfg := lego.NewConfig(account) legoCfg.CADirURL = getCADirURL(cert.Provider) legoCfg.Certificate.KeyType = certcrypto.RSA2048 client, err := lego.NewClient(legoCfg) if err != nil { return fmt.Errorf("failed to create lego client: %v", err) } if strings.ToLower(cert.ChallengeType) == "dns" { provider, err := getDNSProvider(cert) if err != nil { return fmt.Errorf("failed to create DNS provider: %v", err) } if err := client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(dns01.ParseNameservers([]string{"8.8.8.8:53", "1.1.1.1:53"})), ); err != nil { return fmt.Errorf("failed to set DNS-01 provider: %v", err) } } else { if err := client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80")); err != nil { return fmt.Errorf("failed to set HTTP-01 provider: %v", err) } } certRes, err := client.Certificate.Renew(certificate.Resource{ Domain: cert.Domain, CertURL: cert.CertURL, PrivateKey: nil, Certificate: nil, }, true, false, "") if err != nil { return fmt.Errorf("failed to renew certificate: %v", err) } certDir := filepath.Join(cfg.CertDir, sanitizeDomain(cert.Domain)) os.MkdirAll(certDir, 0700) os.WriteFile(filepath.Join(certDir, "fullchain.pem"), certRes.Certificate, 0644) os.WriteFile(filepath.Join(certDir, "privkey.pem"), certRes.PrivateKey, 0600) os.WriteFile(filepath.Join(certDir, "chain.pem"), certRes.IssuerCertificate, 0644) now := time.Now() expiresAt := parseCertExpiry(certRes.Certificate) cert.Status = "active" cert.CertURL = certRes.CertURL cert.ExpiresAt = expiresAt cert.LastRenewedAt = &now cert.ErrorMessage = "" log.Printf("Certificate renewed successfully for %s, expires at %s", cert.Domain, expiresAt.Format(time.RFC3339)) return nil } // GetCertFilesPaths returns paths to certificate files func GetCertFilesPaths(domain string, cfg *config.Config) (fullchain, privkey, chain string) { dir := filepath.Join(cfg.CertDir, sanitizeDomain(domain)) return filepath.Join(dir, "fullchain.pem"), filepath.Join(dir, "privkey.pem"), filepath.Join(dir, "chain.pem") } func getOrCreateAccount(email, provider, dir string) (*ACMEAccount, error) { keyFile := filepath.Join(dir, "account.key") regFile := filepath.Join(dir, "registration.json") os.MkdirAll(dir, 0700) // Try to load existing account if data, err := os.ReadFile(keyFile); err == nil { block, _ := pem.Decode(data) if block != nil { key, err := x509.ParseECPrivateKey(block.Bytes) if err == nil { reg := ®istration.Resource{} if regData, err := os.ReadFile(regFile); err == nil { json.Unmarshal(regData, reg) } return &ACMEAccount{Email: email, PrivateKey: key, Registration: reg}, nil } } } // Create new account privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, fmt.Errorf("failed to generate private key: %v", err) } account := &ACMEAccount{Email: email, PrivateKey: privateKey} legoCfg := lego.NewConfig(account) legoCfg.CADirURL = getCADirURL(provider) legoCfg.Certificate.KeyType = certcrypto.RSA2048 client, err := lego.NewClient(legoCfg) if err != nil { return nil, fmt.Errorf("failed to create lego client: %v", err) } reg, err := client.Registration.Register(registration.RegisterOptions{ TermsOfServiceAgreed: true, }) if err != nil { return nil, fmt.Errorf("failed to register ACME account: %v", err) } account.Registration = reg keyData, _ := x509.MarshalECPrivateKey(privateKey) pemData := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyData}) os.WriteFile(keyFile, pemData, 0600) regData, _ := json.MarshalIndent(reg, "", " ") os.WriteFile(regFile, regData, 0600) log.Printf("New ACME account created for %s with %s", email, provider) return account, nil } func getCADirURL(provider string) string { switch strings.ToLower(provider) { case "zerossl": return "https://acme.zerossl.com/v2/DV90" default: return "https://acme-v02.api.letsencrypt.org/directory" } } func getDNSProvider(cert *config.Certificate) (challenge.Provider, error) { var dnsCfg DNSConfig if cert.DNSConfig != "" { if err := json.Unmarshal([]byte(cert.DNSConfig), &dnsCfg); err != nil { return nil, fmt.Errorf("invalid DNS config JSON: %v", err) } } switch strings.ToLower(cert.DNSProvider) { case "alidns", "aliyun": cfg := alidnsprov.NewDefaultConfig() cfg.APIKey = dnsCfg.AliKey cfg.SecretKey = dnsCfg.AliSecret provider, err := alidnsprov.NewDNSProviderConfig(cfg) if err != nil { return nil, fmt.Errorf("failed to create Aliyun DNS provider: %v", err) } return provider, nil case "cloudflare": cfg := cloudflareprov.NewDefaultConfig() if dnsCfg.CFAPIToken != "" { cfg.AuthToken = dnsCfg.CFAPIToken } provider, err := cloudflareprov.NewDNSProviderConfig(cfg) if err != nil { return nil, fmt.Errorf("failed to create Cloudflare DNS provider: %v", err) } return provider, nil case "dnspod": cfg := dnspodprov.NewDefaultConfig() if dnsCfg.DNSPodID != "" && dnsCfg.DNSPodKey != "" { cfg.LoginToken = dnsCfg.DNSPodID + "," + dnsCfg.DNSPodKey } provider, err := dnspodprov.NewDNSProviderConfig(cfg) if err != nil { return nil, fmt.Errorf("failed to create DNSPod DNS provider: %v", err) } return provider, nil default: return nil, fmt.Errorf("unsupported DNS provider: %s", dnsCfg.Provider) } } func parseCertExpiry(certPEM []byte) *time.Time { block, _ := pem.Decode(certPEM) if block == nil { return nil } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil } return &cert.NotAfter } func sanitizeEmail(email string) string { return strings.NewReplacer("@", "_at_", ".", "_dot_").Replace(email) } func sanitizeDomain(domain string) string { return strings.NewReplacer("*", "wildcard_", ".", "_").Replace(domain) } // lego User interface implementation func (a *ACMEAccount) GetEmail() string { return a.Email } func (a *ACMEAccount) GetRegistration() *registration.Resource { return a.Registration } func (a *ACMEAccount) GetPrivateKey() crypto.PrivateKey { return a.PrivateKey }