v0.01: 修复死锁Bug + DNS-01默认 + 超时调整

This commit is contained in:
2026-05-13 10:58:29 +08:00
parent 3fa77d9bc0
commit a9842e9212
7 changed files with 114 additions and 51 deletions
+15 -10
View File
@@ -43,9 +43,9 @@ func Load() *Config {
// CertStore is the in-memory store for certificates with file persistence
type CertStore struct {
mu sync.RWMutex
data map[string]*Certificate // key: domain
path string
mu sync.RWMutex
data map[string]*Certificate // key: domain
path string
nextID uint
}
@@ -99,11 +99,9 @@ func (s *CertStore) Load() error {
return nil
}
// Save writes the current in-memory certificates to JSON file
func (s *CertStore) Save() error {
s.mu.RLock()
defer s.mu.RUnlock()
// save writes the current in-memory certificates to JSON file
// IMPORTANT: caller must hold the lock (read or write) before calling this
func (s *CertStore) save() error {
certs := make([]*Certificate, 0, len(s.data))
for _, c := range s.data {
certs = append(certs, c)
@@ -117,6 +115,13 @@ func (s *CertStore) Save() error {
return os.WriteFile(s.path, data, 0600)
}
// Save writes the current in-memory certificates to JSON file (public, acquires own lock)
func (s *CertStore) Save() error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.save()
}
// GetAll returns all certificates sorted by ID descending
func (s *CertStore) GetAll() []*Certificate {
s.mu.RLock()
@@ -170,7 +175,7 @@ func (s *CertStore) Upsert(cert *Certificate) error {
cert.UpdatedAt = time.Now()
s.data[cert.Domain] = cert
return s.Save()
return s.save()
}
// Delete removes a certificate by ID
@@ -181,7 +186,7 @@ func (s *CertStore) Delete(id uint) error {
for domain, c := range s.data {
if c.ID == id {
delete(s.data, domain)
return s.Save()
return s.save()
}
}
return nil
+5 -13
View File
@@ -1,11 +1,9 @@
package handlers
import (
"auto-ssl/config"
"auto-ssl/services"
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
@@ -246,13 +244,13 @@ func (h *CertHandler) GetCertFiles(c *gin.Context) {
return
}
fullchain, privkey, chain := services.GetCertFilesPaths(cert.Domain, h.Cfg)
fullchain, privkey, chain := services.GetCertFileContents(cert.Domain, h.Cfg)
result := gin.H{
"domain": cert.Domain,
"fullchain": readFileSafe(fullchain),
"privkey": readFileSafe(privkey),
"chain": readFileSafe(chain),
"fullchain": fullchain,
"privkey": privkey,
"chain": chain,
}
c.JSON(http.StatusOK, result)
}
@@ -310,10 +308,4 @@ func (h *CertHandler) Stats(c *gin.Context) {
})
}
func readFileSafe(path string) string {
data, err := filepath.Abs(path)
if err != nil {
return ""
}
return data
}
+12 -16
View File
@@ -34,10 +34,8 @@ func main() {
// Serve static files for frontend
r.Static("/assets", "./dist/assets")
r.StaticFile("/favicon.ico", "./dist/favicon.ico")
r.StaticFile("/favicon.svg", "./dist/favicon.svg")
r.StaticFile("/", "./dist/index.html")
r.NoRoute(func(c *gin.Context) {
c.File("./dist/index.html")
})
// API routes
api := r.Group("/api")
@@ -58,6 +56,10 @@ func main() {
api.GET("/stats", certHandler.Stats)
}
r.NoRoute(func(c *gin.Context) {
c.File("./dist/index.html")
})
// Setup cron for auto-renewal (runs daily at 3:00 AM)
c := cron.New()
c.AddFunc("0 3 * * *", func() {
@@ -80,20 +82,14 @@ func main() {
})
c.Start()
// Setup HTTP server for ACME HTTP-01 challenges (port 80)
httpPort := os.Getenv("HTTP_PORT")
if httpPort == "" {
httpPort = "80"
// Set ACME HTTP-01 challenge port from env (default 8082)
// Nginx on port 80 should proxy .well-known/acme-challenge/ to this port
acmePort := os.Getenv("ACME_PORT")
if acmePort == "" {
acmePort = "8082"
}
go func() {
acme := gin.New()
acme.Use(gin.Recovery())
// HTTP-01 challenge handler from lego
log.Printf("ACME HTTP challenge server listening on :%s", httpPort)
if err := acme.Run(":" + httpPort); err != nil {
log.Printf("ACME HTTP server (port %s) exited: %v", httpPort, err)
}
}()
services.SetHTTP01Port(acmePort)
log.Printf("ACME HTTP-01 challenge port: %s (nginx should proxy .well-known/acme-challenge/ to this port)", acmePort)
log.Printf("AutoSSL server starting on :%s", cfg.Port)
if err := r.Run(":" + cfg.Port); err != nil {
+57 -8
View File
@@ -28,6 +28,14 @@ import (
"github.com/go-acme/lego/v4/registration"
)
// http01Port is the port for HTTP-01 challenge server
// Nginx on port 80 should proxy .well-known/acme-challenge/ requests to this port
var http01Port = "8082"
func SetHTTP01Port(port string) {
http01Port = port
}
type ACMEAccount struct {
Email string
PrivateKey crypto.PrivateKey
@@ -79,7 +87,9 @@ func GetACMECertificate(cert *config.Certificate, cfg *config.Config) error {
return fmt.Errorf("failed to set DNS-01 provider: %v", err)
}
} else {
if err := client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80")); err != nil {
// Use HTTP-01 challenge with the configured port
// Nginx on port 80 proxies .well-known/acme-challenge/ to this port
if err := client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", http01Port)); err != nil {
return fmt.Errorf("failed to set HTTP-01 provider: %v", err)
}
}
@@ -145,22 +155,33 @@ func RenewCertificate(cert *config.Certificate, cfg *config.Config) error {
return fmt.Errorf("failed to set DNS-01 provider: %v", err)
}
} else {
if err := client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80")); err != nil {
// Use HTTP-01 challenge with the configured port
if err := client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", http01Port)); err != nil {
return fmt.Errorf("failed to set HTTP-01 provider: %v", err)
}
}
// Load existing certificate files for renewal
certDir := filepath.Join(cfg.CertDir, sanitizeDomain(cert.Domain))
existingCert, err := os.ReadFile(filepath.Join(certDir, "fullchain.pem"))
if err != nil {
return fmt.Errorf("failed to read existing certificate for renewal: %v", err)
}
existingKey, err := os.ReadFile(filepath.Join(certDir, "privkey.pem"))
if err != nil {
return fmt.Errorf("failed to read existing private key for renewal: %v", err)
}
certRes, err := client.Certificate.Renew(certificate.Resource{
Domain: cert.Domain,
CertURL: cert.CertURL,
PrivateKey: nil,
Certificate: nil,
PrivateKey: existingKey,
Certificate: existingCert,
}, 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)
@@ -186,6 +207,34 @@ func GetCertFilesPaths(domain string, cfg *config.Config) (fullchain, privkey, c
filepath.Join(dir, "chain.pem")
}
// GetCertFileContents reads and returns certificate file contents
func GetCertFileContents(domain string, cfg *config.Config) (fullchain, privkey, chain string) {
fc, pk, ch := GetCertFilesPaths(domain, cfg)
data, err := os.ReadFile(fc)
if err != nil {
fullchain = ""
} else {
fullchain = string(data)
}
data, err = os.ReadFile(pk)
if err != nil {
privkey = ""
} else {
privkey = string(data)
}
data, err = os.ReadFile(ch)
if err != nil {
chain = ""
} else {
chain = string(data)
}
return
}
func getOrCreateAccount(email, provider, dir string) (*ACMEAccount, error) {
keyFile := filepath.Join(dir, "account.key")
regFile := filepath.Join(dir, "registration.json")
@@ -293,7 +342,7 @@ func getDNSProvider(cert *config.Certificate) (challenge.Provider, error) {
return provider, nil
default:
return nil, fmt.Errorf("unsupported DNS provider: %s", dnsCfg.Provider)
return nil, fmt.Errorf("unsupported DNS provider: %s", cert.DNSProvider)
}
}
@@ -318,6 +367,6 @@ func sanitizeDomain(domain string) string {
}
// lego User interface implementation
func (a *ACMEAccount) GetEmail() string { return a.Email }
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 }
func (a *ACMEAccount) GetPrivateKey() crypto.PrivateKey { return a.PrivateKey }