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
+21
View File
@@ -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
View File
@@ -43,9 +43,9 @@ func Load() *Config {
// CertStore is the in-memory store for certificates with file persistence // CertStore is the in-memory store for certificates with file persistence
type CertStore struct { type CertStore struct {
mu sync.RWMutex mu sync.RWMutex
data map[string]*Certificate // key: domain data map[string]*Certificate // key: domain
path string path string
nextID uint nextID uint
} }
@@ -99,11 +99,9 @@ func (s *CertStore) Load() error {
return nil return nil
} }
// Save writes the current in-memory certificates to JSON file // save writes the current in-memory certificates to JSON file
func (s *CertStore) Save() error { // IMPORTANT: caller must hold the lock (read or write) before calling this
s.mu.RLock() func (s *CertStore) save() error {
defer s.mu.RUnlock()
certs := make([]*Certificate, 0, len(s.data)) certs := make([]*Certificate, 0, len(s.data))
for _, c := range s.data { for _, c := range s.data {
certs = append(certs, c) certs = append(certs, c)
@@ -117,6 +115,13 @@ func (s *CertStore) Save() error {
return os.WriteFile(s.path, data, 0600) 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 // GetAll returns all certificates sorted by ID descending
func (s *CertStore) GetAll() []*Certificate { func (s *CertStore) GetAll() []*Certificate {
s.mu.RLock() s.mu.RLock()
@@ -170,7 +175,7 @@ func (s *CertStore) Upsert(cert *Certificate) error {
cert.UpdatedAt = time.Now() cert.UpdatedAt = time.Now()
s.data[cert.Domain] = cert s.data[cert.Domain] = cert
return s.Save() return s.save()
} }
// Delete removes a certificate by ID // Delete removes a certificate by ID
@@ -181,7 +186,7 @@ func (s *CertStore) Delete(id uint) error {
for domain, c := range s.data { for domain, c := range s.data {
if c.ID == id { if c.ID == id {
delete(s.data, domain) delete(s.data, domain)
return s.Save() return s.save()
} }
} }
return nil return nil
+5 -13
View File
@@ -1,11 +1,9 @@
package handlers package handlers
import ( import (
"auto-ssl/config" "auto-ssl/config"
"auto-ssl/services" "auto-ssl/services"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -246,13 +244,13 @@ func (h *CertHandler) GetCertFiles(c *gin.Context) {
return return
} }
fullchain, privkey, chain := services.GetCertFilesPaths(cert.Domain, h.Cfg) fullchain, privkey, chain := services.GetCertFileContents(cert.Domain, h.Cfg)
result := gin.H{ result := gin.H{
"domain": cert.Domain, "domain": cert.Domain,
"fullchain": readFileSafe(fullchain), "fullchain": fullchain,
"privkey": readFileSafe(privkey), "privkey": privkey,
"chain": readFileSafe(chain), "chain": chain,
} }
c.JSON(http.StatusOK, result) 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 // Serve static files for frontend
r.Static("/assets", "./dist/assets") r.Static("/assets", "./dist/assets")
r.StaticFile("/favicon.ico", "./dist/favicon.ico") r.StaticFile("/favicon.ico", "./dist/favicon.ico")
r.StaticFile("/favicon.svg", "./dist/favicon.svg")
r.StaticFile("/", "./dist/index.html") r.StaticFile("/", "./dist/index.html")
r.NoRoute(func(c *gin.Context) {
c.File("./dist/index.html")
})
// API routes // API routes
api := r.Group("/api") api := r.Group("/api")
@@ -58,6 +56,10 @@ func main() {
api.GET("/stats", certHandler.Stats) 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) // Setup cron for auto-renewal (runs daily at 3:00 AM)
c := cron.New() c := cron.New()
c.AddFunc("0 3 * * *", func() { c.AddFunc("0 3 * * *", func() {
@@ -80,20 +82,14 @@ func main() {
}) })
c.Start() c.Start()
// Setup HTTP server for ACME HTTP-01 challenges (port 80) // Set ACME HTTP-01 challenge port from env (default 8082)
httpPort := os.Getenv("HTTP_PORT") // Nginx on port 80 should proxy .well-known/acme-challenge/ to this port
if httpPort == "" { acmePort := os.Getenv("ACME_PORT")
httpPort = "80" if acmePort == "" {
acmePort = "8082"
} }
go func() { services.SetHTTP01Port(acmePort)
acme := gin.New() log.Printf("ACME HTTP-01 challenge port: %s (nginx should proxy .well-known/acme-challenge/ to this port)", acmePort)
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)
}
}()
log.Printf("AutoSSL server starting on :%s", cfg.Port) log.Printf("AutoSSL server starting on :%s", cfg.Port)
if err := r.Run(":" + cfg.Port); err != nil { if err := r.Run(":" + cfg.Port); err != nil {
+57 -8
View File
@@ -28,6 +28,14 @@ import (
"github.com/go-acme/lego/v4/registration" "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 { type ACMEAccount struct {
Email string Email string
PrivateKey crypto.PrivateKey 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) return fmt.Errorf("failed to set DNS-01 provider: %v", err)
} }
} else { } 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) 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) return fmt.Errorf("failed to set DNS-01 provider: %v", err)
} }
} else { } 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) 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{ certRes, err := client.Certificate.Renew(certificate.Resource{
Domain: cert.Domain, Domain: cert.Domain,
CertURL: cert.CertURL, CertURL: cert.CertURL,
PrivateKey: nil, PrivateKey: existingKey,
Certificate: nil, Certificate: existingCert,
}, true, false, "") }, true, false, "")
if err != nil { if err != nil {
return fmt.Errorf("failed to renew certificate: %v", err) return fmt.Errorf("failed to renew certificate: %v", err)
} }
certDir := filepath.Join(cfg.CertDir, sanitizeDomain(cert.Domain))
os.MkdirAll(certDir, 0700) os.MkdirAll(certDir, 0700)
os.WriteFile(filepath.Join(certDir, "fullchain.pem"), certRes.Certificate, 0644) 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, "privkey.pem"), certRes.PrivateKey, 0600)
@@ -186,6 +207,34 @@ func GetCertFilesPaths(domain string, cfg *config.Config) (fullchain, privkey, c
filepath.Join(dir, "chain.pem") 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) { func getOrCreateAccount(email, provider, dir string) (*ACMEAccount, error) {
keyFile := filepath.Join(dir, "account.key") keyFile := filepath.Join(dir, "account.key")
regFile := filepath.Join(dir, "registration.json") regFile := filepath.Join(dir, "registration.json")
@@ -293,7 +342,7 @@ func getDNSProvider(cert *config.Certificate) (challenge.Provider, error) {
return provider, nil return provider, nil
default: 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 // 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) 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
View File
@@ -2,7 +2,7 @@ import axios from 'axios'
const api = axios.create({ const api = axios.create({
baseURL: '/api', baseURL: '/api',
timeout: 60000, timeout: 300000,
}) })
export interface Certificate { export interface Certificate {
+3 -3
View File
@@ -27,8 +27,8 @@
<el-form-item label="验证方式" prop="challenge_type"> <el-form-item label="验证方式" prop="challenge_type">
<el-radio-group v-model="form.challenge_type" @change="onChallengeChange"> <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-radio-group>
</el-form-item> </el-form-item>
@@ -100,7 +100,7 @@ const form = reactive({
domain: '', domain: '',
email: '', email: '',
provider: 'letsencrypt', provider: 'letsencrypt',
challenge_type: 'http', challenge_type: 'dns',
dns_provider: 'alidns', dns_provider: 'alidns',
auto_renew: true, auto_renew: true,
renew_days: 30, renew_days: 30,