Browse Source

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

cnbugs 1 week ago
parent
commit
a9842e9212

+ 21 - 0
.gitignore

@@ -0,0 +1,21 @@
+# 数据目录
+backend/data/
+*.db
+*.sqlite
+*.sqlite3
+
+# 编译产物
+backend/autossl
+backend/dist/assets/
+frontend/dist/
+node_modules/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# 日志
+*.log

+ 15 - 10
backend/config/config.go

@@ -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
backend/handlers/cert.go

@@ -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
backend/main.go

@@ -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
backend/services/acme.go

@@ -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 }

+ 1 - 1
frontend/src/api/index.ts

@@ -2,7 +2,7 @@ import axios from 'axios'
 
 const api = axios.create({
   baseURL: '/api',
-  timeout: 60000,
+  timeout: 300000,
 })
 
 export interface Certificate {

+ 3 - 3
frontend/src/views/CertCreate.vue

@@ -27,8 +27,8 @@
 
         <el-form-item label="验证方式" prop="challenge_type">
           <el-radio-group v-model="form.challenge_type" @change="onChallengeChange">
-            <el-radio value="http">HTTP-01(推荐)</el-radio>
-            <el-radio value="dns">DNS-01</el-radio>
+            <el-radio value="dns">DNS-01(推荐)</el-radio>
+            <el-radio value="http">HTTP-01(需80端口)</el-radio>
           </el-radio-group>
         </el-form-item>
 
@@ -100,7 +100,7 @@ const form = reactive({
   domain: '',
   email: '',
   provider: 'letsencrypt',
-  challenge_type: 'http',
+  challenge_type: 'dns',
   dns_provider: 'alidns',
   auto_renew: true,
   renew_days: 30,