2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

fix: 优化权限

This commit is contained in:
2026-01-10 02:54:53 +08:00
parent 0deeb281ea
commit ea240a9350
23 changed files with 269 additions and 190 deletions

View File

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

View File

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

View File

@@ -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"` // 证书内容

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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())

View File

@@ -38,7 +38,7 @@ const updateModel = ref<any>({
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 = ''

View File

@@ -33,7 +33,7 @@ const model = ref<any>({
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'))
})
}

View File

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