diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..440db80 --- /dev/null +++ b/.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 diff --git a/backend/config/config.go b/backend/config/config.go index 4176cec..92a9666 100644 --- a/backend/config/config.go +++ b/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 diff --git a/backend/handlers/cert.go b/backend/handlers/cert.go index 756613c..430570a 100644 --- a/backend/handlers/cert.go +++ b/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 -} + diff --git a/backend/main.go b/backend/main.go index dfcc9c6..99043d1 100644 --- a/backend/main.go +++ b/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 { diff --git a/backend/services/acme.go b/backend/services/acme.go index 3f758ef..18ffa15 100644 --- a/backend/services/acme.go +++ b/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 } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 051960d..3e5ca36 100644 --- a/frontend/src/api/index.ts +++ b/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 { diff --git a/frontend/src/views/CertCreate.vue b/frontend/src/views/CertCreate.vue index 6e2e9ef..cbb2868 100644 --- a/frontend/src/views/CertCreate.vue +++ b/frontend/src/views/CertCreate.vue @@ -27,8 +27,8 @@ - HTTP-01(推荐) - DNS-01 + DNS-01(推荐) + HTTP-01(需80端口) @@ -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,