diff --git a/internal/apps/nginx/app.go b/internal/apps/nginx/app.go index 9d19d061..493143c9 100644 --- a/internal/apps/nginx/app.go +++ b/internal/apps/nginx/app.go @@ -55,7 +55,7 @@ func (s *App) SaveConfig(w http.ResponseWriter, r *http.Request) { return } - if err = io.Write(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root), req.Config, 0644); err != nil { + if err = io.Write(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root), req.Config, 0600); err != nil { service.Error(w, http.StatusInternalServerError, "%v", err) return } diff --git a/internal/apps/phpmyadmin/app.go b/internal/apps/phpmyadmin/app.go index f02f55f9..b3500750 100644 --- a/internal/apps/phpmyadmin/app.go +++ b/internal/apps/phpmyadmin/app.go @@ -85,7 +85,7 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) { return } conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(req.Port)+";") - if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), conf, 0644); err != nil { + if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), conf, 0600); err != nil { service.Error(w, http.StatusInternalServerError, "%v", err) return } @@ -129,7 +129,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) { return } - if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), req.Config, 0644); err != nil { + if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), req.Config, 0600); err != nil { service.Error(w, http.StatusInternalServerError, "%v", err) return } diff --git a/internal/biz/cert.go b/internal/biz/cert.go index 30849b4a..707ee4f5 100644 --- a/internal/biz/cert.go +++ b/internal/biz/cert.go @@ -17,7 +17,7 @@ type Cert struct { DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096) Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"` - AutoRenew bool `gorm:"not null;default:false" json:"auto_renew"` // 自动续签 + AutoRenewal bool `gorm:"not null;default:false" json:"auto_renewal"` // 自动续签 RenewalInfo mholtacme.RenewalInfo `gorm:"not null;default:'{}';serializer:json" json:"renewal_info"` // 续签信息 CertURL string `gorm:"not null;default:''" json:"cert_url"` // 证书 URL (续签时使用) Cert string `gorm:"not null;default:''" json:"cert"` // 证书内容 diff --git a/internal/data/cert.go b/internal/data/cert.go index 8bdc9881..f1833fd1 100644 --- a/internal/data/cert.go +++ b/internal/data/cert.go @@ -49,19 +49,20 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) { list := make([]*types.CertList, 0) for cert := range slices.Values(certs) { item := &types.CertList{ - ID: cert.ID, - AccountID: cert.AccountID, - WebsiteID: cert.WebsiteID, - DNSID: cert.DNSID, - Type: cert.Type, - Domains: cert.Domains, - AutoRenew: cert.AutoRenew, - Cert: cert.Cert, - Key: cert.Key, - CertURL: cert.CertURL, - Script: cert.Script, - CreatedAt: cert.CreatedAt, - UpdatedAt: cert.UpdatedAt, + ID: cert.ID, + AccountID: cert.AccountID, + WebsiteID: cert.WebsiteID, + DNSID: cert.DNSID, + Type: cert.Type, + Domains: cert.Domains, + AutoRenewal: cert.AutoRenewal, + NextRenewal: cert.RenewalInfo.SelectedTime, + Cert: cert.Cert, + Key: cert.Key, + CertURL: cert.CertURL, + Script: cert.Script, + CreatedAt: cert.CreatedAt, + UpdatedAt: cert.UpdatedAt, } if decode, err := pkgcert.ParseCert(cert.Cert); err == nil { item.NotBefore = decode.NotBefore @@ -112,12 +113,12 @@ func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) { func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) { cert := &biz.Cert{ - AccountID: req.AccountID, - WebsiteID: req.WebsiteID, - DNSID: req.DNSID, - Type: req.Type, - Domains: req.Domains, - AutoRenew: req.AutoRenew, + AccountID: req.AccountID, + WebsiteID: req.WebsiteID, + DNSID: req.DNSID, + Type: req.Type, + Domains: req.Domains, + AutoRenewal: req.AutoRenewal, } if err := r.db.Create(cert).Error; err != nil { return nil, err @@ -130,21 +131,21 @@ func (r *certRepo) Update(req *request.CertUpdate) error { if err == nil && req.Type == "upload" { req.Domains = info.DNSNames } - if req.Type == "upload" && req.AutoRenew { - return errors.New(r.t.Get("upload certificate cannot be set to auto renew")) + if req.Type == "upload" && req.AutoRenewal { + return errors.New(r.t.Get("upload certificate cannot be set to auto renewal")) } return r.db.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{ - ID: req.ID, - AccountID: req.AccountID, - WebsiteID: req.WebsiteID, - DNSID: req.DNSID, - Type: req.Type, - Cert: req.Cert, - Key: req.Key, - Script: req.Script, - Domains: req.Domains, - AutoRenew: req.AutoRenew, + ID: req.ID, + AccountID: req.AccountID, + WebsiteID: req.WebsiteID, + DNSID: req.DNSID, + Type: req.Type, + Cert: req.Cert, + Key: req.Key, + Script: req.Script, + Domains: req.Domains, + AutoRenewal: req.AutoRenewal, }).Error } @@ -403,10 +404,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error { if err = r.db.Where("id", WebsiteID).First(website).Error; err != nil { return err } - if err = io.Write(fmt.Sprintf("%s/sites/%s/config/fullchain.pem", app.Root, website.Name), cert.Cert, 0644); err != nil { + if err = io.Write(fmt.Sprintf("%s/sites/%s/config/fullchain.pem", app.Root, website.Name), cert.Cert, 0600); err != nil { return err } - if err = io.Write(fmt.Sprintf("%s/sites/%s/config/private.key", app.Root, website.Name), cert.Key, 0644); err != nil { + if err = io.Write(fmt.Sprintf("%s/sites/%s/config/private.key", app.Root, website.Name), cert.Key, 0600); err != nil { return err } if err = systemctl.Reload("nginx"); err != nil { diff --git a/internal/data/setting.go b/internal/data/setting.go index ec1fe8f3..4e6f2fc0 100644 --- a/internal/data/setting.go +++ b/internal/data/setting.go @@ -318,10 +318,10 @@ func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) { if _, err := cert.ParseKey(req.Key); err != nil { return false, errors.New(r.t.Get("failed to parse private key: %v", err)) } - if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0644); err != nil { + if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0600); err != nil { return false, err } - if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0644); err != nil { + if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0600); err != nil { return false, err } @@ -387,10 +387,10 @@ func (r *settingRepo) UpdateCert(req *request.SettingCert) error { return errors.New(r.t.Get("failed to parse private key: %v", err)) } - if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0644); err != nil { + if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0600); err != nil { return err } - if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0644); err != nil { + if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0600); err != nil { return err } diff --git a/internal/data/website.go b/internal/data/website.go index e838a5de..f4481e76 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -248,14 +248,14 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { } // 创建配置文件目录 - if err = os.MkdirAll(filepath.Join(app.Root, "sites", req.Name, "config", "site"), 0644); err != nil { + if err = os.MkdirAll(filepath.Join(app.Root, "sites", req.Name, "config", "site"), 0600); err != nil { return nil, err } - if err = os.MkdirAll(filepath.Join(app.Root, "sites", req.Name, "config", "shared"), 0644); err != nil { + if err = os.MkdirAll(filepath.Join(app.Root, "sites", req.Name, "config", "shared"), 0600); err != nil { return nil, err } // 创建日志目录 - if err = os.MkdirAll(filepath.Join(app.Root, "sites", req.Name, "log"), 0644); err != nil { + if err = os.MkdirAll(filepath.Join(app.Root, "sites", req.Name, "log"), 0755); err != nil { return nil, err } @@ -381,20 +381,36 @@ location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) { return nil, err } - if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "fullchain.pem"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "fullchain.pem"), "", 0600); err != nil { return nil, err } - if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "private.key"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "private.key"), "", 0600); err != nil { return nil, err } // 设置目录权限 + // sites/site_name 0755 root + // sites/site_name/config 0600 root + // sites/site_name/log 644 www + // sites/site_name/public 0755 www + if err = io.Chmod(filepath.Join(app.Root, "sites", req.Name), 0755); err != nil { + return nil, err + } if err = io.Chmod(req.Path, 0755); err != nil { return nil, err } if err = io.Chown(req.Path, "www", "www"); err != nil { return nil, err } + if err = io.Chmod(filepath.Join(app.Root, "sites", req.Name, "log"), 0644); err != nil { + return nil, err + } + if err = io.Chown(filepath.Join(app.Root, "sites", req.Name, "log"), "www", "www"); err != nil { + return nil, err + } + if err = io.Chmod(filepath.Join(app.Root, "sites", req.Name, "config"), 0600); err != nil { + return nil, err + } // PHP 网站默认开启防跨站 if req.Type == "php" { @@ -482,10 +498,10 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { // SSL certPath := filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem") keyPath := filepath.Join(app.Root, "sites", website.Name, "config", "private.key") - if err = io.Write(certPath, req.SSLCert, 0644); err != nil { + if err = io.Write(certPath, req.SSLCert, 0600); err != nil { return err } - if err = io.Write(keyPath, req.SSLKey, 0644); err != nil { + if err = io.Write(keyPath, req.SSLKey, 0600); err != nil { return err } website.SSL = req.SSL @@ -665,15 +681,15 @@ func (r *websiteRepo) ResetConfig(id uint) error { if err = vhost.Save(); err != nil { return err } - if err = io.Write(filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem"), "", 0600); err != nil { return err } - if err = io.Write(filepath.Join(app.Root, "sites", website.Name, "config", "private.key"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", website.Name, "config", "private.key"), "", 0600); err != nil { return err } // PHP 网站默认伪静态 if website.Type == biz.WebsiteTypePHP { - if err = io.Write(filepath.Join(app.Root, "sites", website.Name, "config", "site", "010-rewrite.conf"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", website.Name, "config", "site", "010-rewrite.conf"), "", 0600); err != nil { return err } } @@ -727,10 +743,10 @@ func (r *websiteRepo) UpdateCert(req *request.WebsiteUpdateCert) error { certPath := filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem") keyPath := filepath.Join(app.Root, "sites", website.Name, "config", "private.key") - if err := io.Write(certPath, req.Cert, 0644); err != nil { + if err := io.Write(certPath, req.Cert, 0600); err != nil { return err } - if err := io.Write(keyPath, req.Key, 0644); err != nil { + if err := io.Write(keyPath, req.Key, 0600); err != nil { return err } @@ -759,11 +775,11 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error { if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { newCert, err = r.cert.Create(&request.CertCreate{ - Type: string(acme.KeyEC256), - Domains: website.Domains, - AutoRenew: true, - AccountID: account.ID, - WebsiteID: website.ID, + Type: string(acme.KeyEC256), + Domains: website.Domains, + AutoRenewal: true, + AccountID: account.ID, + WebsiteID: website.ID, }) if err != nil { return err diff --git a/internal/http/request/cert.go b/internal/http/request/cert.go index 8588b986..9b85a2b9 100644 --- a/internal/http/request/cert.go +++ b/internal/http/request/cert.go @@ -6,25 +6,25 @@ type CertUpload struct { } type CertCreate struct { - Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096"` - Domains []string `form:"domains" json:"domains" validate:"required|isSlice"` - AutoRenew bool `form:"auto_renew" json:"auto_renew"` - AccountID uint `form:"account_id" json:"account_id"` - DNSID uint `form:"dns_id" json:"dns_id"` - WebsiteID uint `form:"website_id" json:"website_id"` + Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096"` + Domains []string `form:"domains" json:"domains" validate:"required|isSlice"` + AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"` + AccountID uint `form:"account_id" json:"account_id"` + DNSID uint `form:"dns_id" json:"dns_id"` + WebsiteID uint `form:"website_id" json:"website_id"` } type CertUpdate struct { - ID uint `form:"id" json:"id" validate:"required|exists:certs,id"` - Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096,upload"` - Domains []string `form:"domains" json:"domains" validate:"required|isSlice"` - Cert string `form:"cert" json:"cert"` - Key string `form:"key" json:"key"` - Script string `form:"script" json:"script"` - AutoRenew bool `form:"auto_renew" json:"auto_renew"` - AccountID uint `form:"account_id" json:"account_id"` - DNSID uint `form:"dns_id" json:"dns_id"` - WebsiteID uint `form:"website_id" json:"website_id"` + ID uint `form:"id" json:"id" validate:"required|exists:certs,id"` + Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096,upload"` + Domains []string `form:"domains" json:"domains" validate:"required|isSlice"` + Cert string `form:"cert" json:"cert"` + Key string `form:"key" json:"key"` + Script string `form:"script" json:"script"` + AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"` + AccountID uint `form:"account_id" json:"account_id"` + DNSID uint `form:"dns_id" json:"dns_id"` + WebsiteID uint `form:"website_id" json:"website_id"` } type CertDeploy struct { diff --git a/internal/job/cert_renew.go b/internal/job/cert_renew.go index 8474e844..05e5baf8 100644 --- a/internal/job/cert_renew.go +++ b/internal/job/cert_renew.go @@ -50,7 +50,7 @@ func (r *CertRenew) Run() { for _, cert := range certs { // 跳过上传类型或未开启自动续签的证书 - if cert.Type == "upload" || !cert.AutoRenew { + if cert.Type == "upload" || !cert.AutoRenewal { continue } diff --git a/internal/service/cli.go b/internal/service/cli.go index 72c06218..be20dc98 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -392,10 +392,10 @@ func (s *CliService) HTTPSGenerate(ctx context.Context, cmd *cli.Command) error fmt.Println(s.t.Get("Successfully obtained ACME certificate")) } - if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), string(crt), 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), string(crt), 0600); err != nil { return err } - if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), string(key), 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), string(key), 0600); err != nil { return err } diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go index dc0c6ce6..19e8246c 100644 --- a/pkg/acme/solvers.go +++ b/pkg/acme/solvers.go @@ -115,7 +115,7 @@ func (s *panelSolver) writeNginxConfig() error { } conf.WriteString("}\n") - if err := os.WriteFile(s.conf, []byte(conf.String()), 0644); err != nil { + if err := os.WriteFile(s.conf, []byte(conf.String()), 0600); err != nil { return fmt.Errorf("failed to write nginx config %q: %w", s.conf, err) } @@ -149,7 +149,7 @@ func (s *panelSolver) CleanUp(ctx context.Context, _ acme.Challenge) error { } // 清理 nginx 配置 - if err := os.WriteFile(s.conf, []byte(""), 0644); err != nil { + if err := os.WriteFile(s.conf, []byte(""), 0600); err != nil { return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err) } @@ -172,7 +172,7 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error { } `, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization) - file, err := os.OpenFile(s.conf, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + file, err := os.OpenFile(s.conf, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return fmt.Errorf("failed to open nginx config %q: %w", s.conf, err) } @@ -204,7 +204,7 @@ func (s httpSolver) CleanUp(_ context.Context, challenge acme.Challenge) error { `, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization) newConf := strings.ReplaceAll(string(conf), target, "") - if err = os.WriteFile(s.conf, []byte(newConf), 0644); err != nil { + if err = os.WriteFile(s.conf, []byte(newConf), 0600); err != nil { return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err) } diff --git a/pkg/types/cert.go b/pkg/types/cert.go index bae52669..d17cf999 100644 --- a/pkg/types/cert.go +++ b/pkg/types/cert.go @@ -3,22 +3,23 @@ package types import "time" type CertList struct { - ID uint `json:"id"` - AccountID uint `json:"account_id"` - WebsiteID uint `json:"website_id"` - DNSID uint `json:"dns_id"` - Type string `json:"type"` - Domains []string `json:"domains"` - AutoRenew bool `json:"auto_renew"` - Cert string `json:"cert"` - Key string `json:"key"` - CertURL string `json:"cert_url"` - Script string `json:"script"` - NotBefore time.Time `json:"not_before"` - NotAfter time.Time `json:"not_after"` - Issuer string `json:"issuer"` - OCSPServer []string `json:"ocsp_server"` - DNSNames []string `json:"dns_names"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `json:"id"` + AccountID uint `json:"account_id"` + WebsiteID uint `json:"website_id"` + DNSID uint `json:"dns_id"` + Type string `json:"type"` + Domains []string `json:"domains"` + AutoRenewal bool `json:"auto_renewal"` + NextRenewal time.Time `json:"next_renewal"` + Cert string `json:"cert"` + Key string `json:"key"` + CertURL string `json:"cert_url"` + Script string `json:"script"` + NotBefore time.Time `json:"not_before"` + NotAfter time.Time `json:"not_after"` + Issuer string `json:"issuer"` + OCSPServer []string `json:"ocsp_server"` + DNSNames []string `json:"dns_names"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } diff --git a/pkg/webserver/apache/proxy.go b/pkg/webserver/apache/proxy.go index 21565b97..5e4d5efa 100644 --- a/pkg/webserver/apache/proxy.go +++ b/pkg/webserver/apache/proxy.go @@ -140,7 +140,7 @@ func writeProxyFiles(siteDir string, proxies []types.Proxy) error { filePath := filepath.Join(siteDir, fileName) content := generateProxyConfig(proxy) - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write proxy config: %w", err) } } @@ -355,7 +355,7 @@ func writeBalancerFiles(sharedDir string, upstreams []types.Upstream) error { filePath := filepath.Join(sharedDir, fileName) content := generateBalancerConfig(upstream) - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write balancer config: %w", err) } } diff --git a/pkg/webserver/apache/redirect.go b/pkg/webserver/apache/redirect.go index 39604d22..7d556c29 100644 --- a/pkg/webserver/apache/redirect.go +++ b/pkg/webserver/apache/redirect.go @@ -135,7 +135,7 @@ func writeRedirectFiles(siteDir string, redirects []types.Redirect) error { filePath := filepath.Join(siteDir, fileName) content := generateRedirectConfig(redirect) - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write redirect config: %w", err) } } diff --git a/pkg/webserver/apache/vhost.go b/pkg/webserver/apache/vhost.go index 0e5ed0e1..af6a7abb 100644 --- a/pkg/webserver/apache/vhost.go +++ b/pkg/webserver/apache/vhost.go @@ -29,6 +29,7 @@ type baseVhost struct { config *Config vhost *VirtualHost configDir string // 配置目录 + siteName string // 网站名 } // newBaseVhost 创建基础虚拟主机实例 @@ -39,6 +40,7 @@ func newBaseVhost(configDir string) (*baseVhost, error) { v := &baseVhost{ configDir: configDir, + siteName: filepath.Base(filepath.Dir(configDir)), } // 加载配置 @@ -56,7 +58,8 @@ func newBaseVhost(configDir string) (*baseVhost, error) { // 如果没有配置文件,使用默认配置 if config == nil { - config, err = ParseString(DefaultVhostConf) + defaultConf := strings.ReplaceAll(DefaultVhostConf, "/opt/ace/sites/default", fmt.Sprintf("/opt/ace/sites/%s", v.siteName)) + config, err = ParseString(defaultConf) if err != nil { return nil, fmt.Errorf("failed to parse default config: %w", err) } @@ -121,7 +124,7 @@ func (v *baseVhost) SetEnable(enable bool) error { // 禁用时,保存当前根目录 currentRoot := v.Root() if currentRoot != "" && currentRoot != DisablePagePath { - if err := os.WriteFile(filepath.Join(v.configDir, "root.saved"), []byte(currentRoot), 0644); err != nil { + if err := os.WriteFile(filepath.Join(v.configDir, "root.saved"), []byte(currentRoot), 0600); err != nil { return fmt.Errorf("failed to save current root: %w", err) } } @@ -329,7 +332,7 @@ func (v *baseVhost) SetErrorLog(errorLog string) error { func (v *baseVhost) Save() error { configFile := filepath.Join(v.configDir, "apache.conf") content := v.config.Export() - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { + if err := os.WriteFile(configFile, []byte(content), 0600); err != nil { return fmt.Errorf("failed to save config file: %w", err) } @@ -338,7 +341,8 @@ func (v *baseVhost) Save() error { func (v *baseVhost) Reset() error { // 重置配置为默认值 - config, err := ParseString(DefaultVhostConf) + defaultConf := strings.ReplaceAll(DefaultVhostConf, "/opt/ace/sites/default", fmt.Sprintf("/opt/ace/sites/%s", v.siteName)) + config, err := ParseString(defaultConf) if err != nil { return fmt.Errorf("failed to reset config: %w", err) } @@ -362,7 +366,7 @@ func (v *baseVhost) Config(name string, typ string) string { func (v *baseVhost) SetConfig(name string, typ string, content string) error { conf := filepath.Join(v.configDir, typ, name) - if err := os.WriteFile(conf, []byte(content), 0644); err != nil { + if err := os.WriteFile(conf, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write config file: %w", err) } return nil diff --git a/pkg/webserver/nginx/parser.go b/pkg/webserver/nginx/parser.go index 2a2a9b61..41431bc3 100644 --- a/pkg/webserver/nginx/parser.go +++ b/pkg/webserver/nginx/parser.go @@ -18,17 +18,9 @@ type Parser struct { cfgPath string // 配置文件路径 } -func NewParser(website ...string) (*Parser, error) { - str := DefaultConf - cfgPath := "" - if len(website) != 0 && website[0] != "" { - cfgPath = fmt.Sprintf("/opt/ace/sites/%s/config/nginx.conf", website[0]) - if cfg, err := os.ReadFile(cfgPath); err == nil { - str = string(cfg) - } else { - return nil, err - } - } +// NewParser 使用网站名创建解析器,将默认配置中的 default 替换为实际网站名 +func NewParser(siteName string) (*Parser, error) { + str := strings.ReplaceAll(DefaultConf, "/opt/ace/sites/default", fmt.Sprintf("/opt/ace/sites/%s", siteName)) p := parser.NewStringParser(str, parser.WithSkipIncludeParsingErr(), parser.WithSkipValidDirectivesErr()) cfg, err := p.Parse() @@ -36,7 +28,7 @@ func NewParser(website ...string) (*Parser, error) { return nil, err } - return &Parser{cfg: cfg, cfgPath: cfgPath}, nil + return &Parser{cfg: cfg, cfgPath: ""}, nil } // NewParserFromFile 从指定文件路径创建解析器 @@ -201,7 +193,7 @@ func (p *Parser) Dump() string { func (p *Parser) Save() error { p.sortDirectives(p.cfg.Directives, order) content := p.Dump() + "\n" - if err := os.WriteFile(p.cfgPath, []byte(content), 0644); err != nil { + if err := os.WriteFile(p.cfgPath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to save config file: %w", err) } @@ -214,7 +206,7 @@ func (p *Parser) SetConfigPath(path string) { } func (p *Parser) sortDirectives(directives []config.IDirective, orderIndex map[string]int) { - slices.SortFunc(directives, func(a config.IDirective, b config.IDirective) int { + slices.SortStableFunc(directives, func(a config.IDirective, b config.IDirective) int { // 块指令(如 server、location)应该排在普通指令(如 include)后面 aIsBlock := a.GetBlock() != nil && len(a.GetBlock().GetDirectives()) > 0 bIsBlock := b.GetBlock() != nil && len(b.GetBlock().GetDirectives()) > 0 @@ -226,11 +218,8 @@ func (p *Parser) sortDirectives(directives []config.IDirective, orderIndex map[s return -1 // b 是块指令,排在前面 } - // 同类指令,按 order 排序 - if orderIndex[a.GetName()] != orderIndex[b.GetName()] { - return orderIndex[a.GetName()] - orderIndex[b.GetName()] - } - return slices.Compare(p.parameters2Slices(a.GetParameters()), p.parameters2Slices(b.GetParameters())) + // 同类指令,按 order 排序;相同名称的指令保持原有顺序 + return orderIndex[a.GetName()] - orderIndex[b.GetName()] }) for _, directive := range directives { diff --git a/pkg/webserver/nginx/proxy.go b/pkg/webserver/nginx/proxy.go index 658dbfe5..d2b1de9d 100644 --- a/pkg/webserver/nginx/proxy.go +++ b/pkg/webserver/nginx/proxy.go @@ -167,7 +167,7 @@ func writeProxyFiles(siteDir string, proxies []types.Proxy) error { filePath := filepath.Join(siteDir, fileName) content := generateProxyConfig(proxy) - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write proxy config: %w", err) } } diff --git a/pkg/webserver/nginx/redirect.go b/pkg/webserver/nginx/redirect.go index dc295ddf..f437f245 100644 --- a/pkg/webserver/nginx/redirect.go +++ b/pkg/webserver/nginx/redirect.go @@ -122,7 +122,7 @@ func writeRedirectFiles(siteDir string, redirects []types.Redirect) error { filePath := filepath.Join(siteDir, fileName) content := generateRedirectConfig(redirect) - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write redirect config: %w", err) } } diff --git a/pkg/webserver/nginx/upstream.go b/pkg/webserver/nginx/upstream.go index 2b192664..f8f35d2d 100644 --- a/pkg/webserver/nginx/upstream.go +++ b/pkg/webserver/nginx/upstream.go @@ -154,7 +154,7 @@ func writeUpstreamFiles(sharedDir string, upstreams []types.Upstream) error { filePath := filepath.Join(sharedDir, fileName) content := generateUpstreamConfig(upstream) - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write upstream config: %w", err) } } diff --git a/pkg/webserver/nginx/vhost.go b/pkg/webserver/nginx/vhost.go index 5c6a68f1..c7f5014e 100644 --- a/pkg/webserver/nginx/vhost.go +++ b/pkg/webserver/nginx/vhost.go @@ -31,6 +31,7 @@ type ProxyVhost struct { type baseVhost struct { parser *Parser configDir string // 配置目录 + siteName string // 网站名 } // newBaseVhost 创建基础虚拟主机实例 @@ -41,6 +42,7 @@ func newBaseVhost(configDir string) (*baseVhost, error) { v := &baseVhost{ configDir: configDir, + siteName: filepath.Base(filepath.Dir(configDir)), } // 加载配置 @@ -58,8 +60,7 @@ func newBaseVhost(configDir string) (*baseVhost, error) { // 如果没有配置文件,使用默认配置 if parser == nil { - // 使用空字符串创建默认配置,而不尝试读取文件 - parser, err = NewParser("") + parser, err = NewParser(v.siteName) if err != nil { return nil, fmt.Errorf("failed to load default config: %w", err) } @@ -123,7 +124,7 @@ func (v *baseVhost) SetEnable(enable bool) error { // 禁用时,保存当前根目录 currentRoot := v.Root() if currentRoot != "" && currentRoot != DisablePagePath { - if err := os.WriteFile(filepath.Join(v.configDir, "root.saved"), []byte(currentRoot), 0644); err != nil { + if err := os.WriteFile(filepath.Join(v.configDir, "root.saved"), []byte(currentRoot), 0600); err != nil { return fmt.Errorf("failed to save current root: %w", err) } } @@ -160,15 +161,38 @@ func (v *baseVhost) Listen() []types.Listen { return nil } - var result []types.Listen + // 使用 map 合并相同地址的 listen 指令 + // nginx 中 ssl 和 quic 需要分开写 + listenMap := make(map[string]types.Listen) + var order []string // 保持顺序 + for _, dir := range directives { l := v.parser.parameters2Slices(dir.GetParameters()) - listen := types.Listen{Address: l[0], Args: []string{}} - for i := 1; i < len(l); i++ { - listen.Args = append(listen.Args, l[i]) + if len(l) == 0 { + continue } + address := l[0] - result = append(result, listen) + if existing, ok := listenMap[address]; ok { + // 合并 args + for i := 1; i < len(l); i++ { + if !slices.Contains(existing.Args, l[i]) { + existing.Args = append(existing.Args, l[i]) + } + } + } else { + listen := types.Listen{Address: address, Args: []string{}} + for i := 1; i < len(l); i++ { + listen.Args = append(listen.Args, l[i]) + } + listenMap[address] = listen + order = append(order, address) + } + } + + var result []types.Listen + for _, addr := range order { + result = append(result, listenMap[addr]) } return result @@ -177,12 +201,39 @@ func (v *baseVhost) Listen() []types.Listen { func (v *baseVhost) SetListen(listens []types.Listen) error { var directives []*config.Directive for _, l := range listens { - listen := []string{l.Address} - listen = append(listen, l.Args...) - directives = append(directives, &config.Directive{ - Name: "listen", - Parameters: v.parser.slices2Parameters(listen), - }) + hasSSL := slices.Contains(l.Args, "ssl") + hasQUIC := slices.Contains(l.Args, "quic") + + // nginx 中 ssl 和 quic 不能在同一个 listen 指令中 + // 需要分成两行:listen 443 ssl; 和 listen 443 quic; + if hasSSL && hasQUIC { + // 生成 ssl 行(包含除 quic 外的所有参数) + sslArgs := []string{l.Address} + for _, arg := range l.Args { + if arg != "quic" { + sslArgs = append(sslArgs, arg) + } + } + directives = append(directives, &config.Directive{ + Name: "listen", + Parameters: v.parser.slices2Parameters(sslArgs), + }) + + // 生成 quic 行 + quicArgs := []string{l.Address, "quic"} + directives = append(directives, &config.Directive{ + Name: "listen", + Parameters: v.parser.slices2Parameters(quicArgs), + }) + } else { + // 普通情况,直接生成一行 + listen := []string{l.Address} + listen = append(listen, l.Args...) + directives = append(directives, &config.Directive{ + Name: "listen", + Parameters: v.parser.slices2Parameters(listen), + }) + } } _ = v.parser.Clear("server.listen") @@ -337,7 +388,7 @@ func (v *baseVhost) Save() error { func (v *baseVhost) Reset() error { // 重置配置为默认值 - parser, err := NewParser("") + parser, err := NewParser(v.siteName) if err != nil { return fmt.Errorf("failed to reset config: %w", err) } @@ -362,7 +413,7 @@ func (v *baseVhost) Config(name string, typ string) string { func (v *baseVhost) SetConfig(name string, typ string, content string) error { conf := filepath.Join(v.configDir, typ, name) - if err := os.WriteFile(conf, []byte(content), 0644); err != nil { + if err := os.WriteFile(conf, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write config file: %w", err) } return nil diff --git a/pkg/webserver/nginx/vhost_test.go b/pkg/webserver/nginx/vhost_test.go index 05debdbd..04eed848 100644 --- a/pkg/webserver/nginx/vhost_test.go +++ b/pkg/webserver/nginx/vhost_test.go @@ -118,6 +118,41 @@ func (s *VhostTestSuite) TestListenWithHTTP3() { s.Equal("quic", got[0].Args[0]) } +func (s *VhostTestSuite) TestListenWithSSLAndQUIC() { + // 测试 ssl 和 quic 同时存在时,应该分成两行 listen 指令 + // 但读取时应该合并为一个 Listen 对象 + listens := []types.Listen{ + {Address: "80"}, + {Address: "443", Args: []string{"ssl", "quic"}}, + } + s.NoError(s.vhost.SetListen(listens)) + + // 保存后验证顺序 + s.NoError(s.vhost.Save()) + + // 验证生成的配置中 ssl 和 quic 是分开的 + dump := s.vhost.parser.Dump() + s.Contains(dump, "listen 443 ssl;") + s.Contains(dump, "listen 443 quic;") + // 确保没有 "listen 443 ssl quic;" 这样的行 + s.NotContains(dump, "listen 443 ssl quic;") + + // 验证顺序:80 应该在 443 前面 + idx80 := strings.Index(dump, "listen 80;") + idx443 := strings.Index(dump, "listen 443") + s.Greater(idx443, idx80, "listen 80 should come before listen 443") + + // 读取时应该合并为一个 Listen 对象 + got := s.vhost.Listen() + s.Len(got, 2) // 80 和 443 + // 验证顺序 + s.Equal("80", got[0].Address) + s.Equal("443", got[1].Address) + // 验证 443 的 args + s.Contains(got[1].Args, "ssl") + s.Contains(got[1].Args, "quic") +} + func (s *VhostTestSuite) TestSSL() { s.False(s.vhost.SSL()) s.Nil(s.vhost.SSLConfig()) diff --git a/web/src/views/cert/CertView.vue b/web/src/views/cert/CertView.vue index e493c6ef..cdbf397c 100644 --- a/web/src/views/cert/CertView.vue +++ b/web/src/views/cert/CertView.vue @@ -38,7 +38,7 @@ const updateModel = ref({ dns_id: null, account_id: null, website_id: null, - auto_renew: true, + auto_renewal: true, cert: '', key: '', script: '' @@ -115,7 +115,7 @@ const columns: any = [ { title: $gettext('Associated Account'), key: 'account_id', - minWidth: 200, + minWidth: 240, resizable: true, ellipsis: { tooltip: true }, render(row: any) { @@ -128,7 +128,7 @@ const columns: any = [ { title: $gettext('Issuer'), key: 'issuer', - width: 150, + width: 120, ellipsis: { tooltip: true }, render(row: any) { return row.issuer == '' ? $gettext('None') : row.issuer @@ -144,35 +144,25 @@ const columns: any = [ } }, { - title: 'OCSP', - key: 'ocsp_server', + title: $gettext('Next Renewal Time'), + key: 'next_renewal', minWidth: 200, resizable: true, render(row: any) { - if (row.ocsp_server == null || row.ocsp_server.length == 0) { - return h(NTag, null, { default: () => $gettext('None') }) - } - return h(NFlex, null, { - default: () => - row.ocsp_server.map((server: any) => - h(NTag, null, { - default: () => server - }) - ) - }) + return row.next_renewal == 0 ? $gettext('None') : formatDateTime(row.next_renewal) } }, { - title: $gettext('Auto Renew'), - key: 'auto_renew', + title: $gettext('Auto Renewal'), + key: 'auto_renewal', width: 140, resizable: true, render(row: any) { return h(NSwitch, { size: 'small', rubberBand: false, - value: row.auto_renew, - onUpdateValue: () => handleAutoRenewUpdate(row) + value: row.auto_renewal, + onUpdateValue: () => handleAutoRenewalUpdate(row) }) } }, @@ -241,7 +231,7 @@ const columns: any = [ } }, { - default: () => $gettext('Renew') + default: () => $gettext('Renewal') } ) : null, @@ -276,7 +266,7 @@ const columns: any = [ updateModel.value.dns_id = row.dns_id == 0 ? null : row.dns_id updateModel.value.account_id = row.account_id == 0 ? null : row.account_id updateModel.value.website_id = row.website_id == 0 ? null : row.website_id - updateModel.value.auto_renew = row.auto_renew + updateModel.value.auto_renewal = row.auto_renewal updateModel.value.cert = row.cert updateModel.value.key = row.key updateModel.value.script = row.script @@ -340,7 +330,7 @@ const handleUpdateCert = () => { updateModel.value.dns_id = null updateModel.value.account_id = null updateModel.value.website_id = null - updateModel.value.auto_renew = true + updateModel.value.auto_renewal = true updateModel.value.cert = '' updateModel.value.key = '' updateModel.value.script = '' @@ -348,13 +338,13 @@ const handleUpdateCert = () => { }) } -const handleAutoRenewUpdate = (row: any) => { +const handleAutoRenewalUpdate = (row: any) => { updateModel.value.domains = row.domains updateModel.value.type = row.type updateModel.value.dns_id = row.dns_id == 0 ? null : row.dns_id updateModel.value.account_id = row.account_id == 0 ? null : row.account_id updateModel.value.website_id = row.website_id == 0 ? null : row.website_id - updateModel.value.auto_renew = !row.auto_renew + updateModel.value.auto_renewal = !row.auto_renewal updateModel.value.cert = row.cert updateModel.value.key = row.key updateModel.value.script = row.script @@ -369,7 +359,7 @@ const handleAutoRenewUpdate = (row: any) => { updateModel.value.dns_id = null updateModel.value.account_id = null updateModel.value.website_id = null - updateModel.value.auto_renew = true + updateModel.value.auto_renewal = true updateModel.value.cert = '' updateModel.value.key = '' updateModel.value.script = '' diff --git a/web/src/views/cert/CreateCertModal.vue b/web/src/views/cert/CreateCertModal.vue index 5ba0242a..b3c7b0a8 100644 --- a/web/src/views/cert/CreateCertModal.vue +++ b/web/src/views/cert/CreateCertModal.vue @@ -33,7 +33,7 @@ const model = ref({ type: 'P256', account_id: null, website_id: null, - auto_renew: true + auto_renewal: true }) const handleCreateCert = () => { @@ -46,7 +46,7 @@ const handleCreateCert = () => { model.value.type = 'P256' model.value.account_id = null model.value.website_id = null - model.value.auto_renew = true + model.value.auto_renewal = true window.$message.success($gettext('Created successfully')) }) } diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue index 063028bd..f84f5b5b 100644 --- a/web/src/views/website/EditView.vue +++ b/web/src/views/website/EditView.vue @@ -95,15 +95,15 @@ const certOptions = computed(() => { const selectedCert = ref(null) const handleSave = () => { - // 如果没有任何监听地址设置了https,则自动添加443 - if (setting.value.https && !setting.value.listens.some((item: any) => item.https)) { + // 如果开启了ssl但没有任何监听地址设置了ssl,则自动添加443 + if (setting.value.ssl && !setting.value.listens.some((item: any) => item.args?.includes('ssl'))) { setting.value.listens.push({ address: '443', args: ['ssl', 'quic'] }) } - // 如果关闭了https,自动禁用所有https和quic - if (!setting.value.https) { + // 如果关闭了ssl,自动禁用所有ssl和quic + if (!setting.value.ssl) { setting.value.listens = setting.value.listens.filter((item: any) => item.address !== '443') // 443直接删掉 setting.value.listens.forEach((item: any) => { item.args = [] @@ -304,19 +304,11 @@ const removeProxy = (index: number) => { // 处理 Proxy Pass 变化,自动更新 Host const handleProxyPassChange = (proxy: any, value: string) => { proxy.pass = value - // 如果 host 是 $host 或为空,则自动提取 - if (!proxy.host || proxy.host === '$host') { - const extracted = extractHostFromUrl(value) - // 只有当提取到的不是 IP 地址时才设置 - if ( - extracted && - !/^\d+\.\d+\.\d+\.\d+(:\d+)?$/.test(extracted) && - !/^localhost(:\d+)?$/i.test(extracted) - ) { - proxy.host = extracted - } else { - proxy.host = '$host' - } + const extracted = extractHostFromUrl(value) + if (extracted !== '') { + proxy.host = extracted + } else { + proxy.host = '$host' } }