v0.01: 修复死锁Bug + DNS-01默认 + 超时调整
This commit is contained in:
+21
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user