324 wiersze
9.7 KiB
Go
324 wiersze
9.7 KiB
Go
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 }
|