From 2b8890305c22a8086cb3abb907da897143c33357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Mon, 1 Dec 2025 22:50:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BD=91=E7=AB=99=E9=87=8D=E6=9E=841?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/ace/wire_gen.go | 2 +- cmd/cli/wire_gen.go | 2 +- go.sum | 6 - internal/biz/setting.go | 1 + internal/biz/website.go | 3 +- internal/data/website.go | 113 +++++++++++------- internal/http/request/website.go | 15 ++- internal/service/cli.go | 2 +- internal/service/website.go | 5 +- pkg/webserver/apache/vhost.go | 19 +-- pkg/webserver/apache/vhost_test.go | 8 +- pkg/webserver/nginx/getter.go | 71 ----------- pkg/webserver/nginx/parser_test.go | 71 ----------- pkg/webserver/nginx/proxy.go | 1 - pkg/webserver/nginx/vhost.go | 97 +++++++-------- pkg/webserver/types/proxy.go | 26 ++-- pkg/webserver/types/vhost.go | 15 ++- web/src/api/panel/website/index.ts | 3 +- web/src/views/apps/fail2ban/IndexView.vue | 2 +- web/src/views/backup/ListView.vue | 2 +- web/src/views/cert/IndexView.vue | 2 +- web/src/views/task/CreateModal.vue | 2 +- web/src/views/website/IndexView.vue | 4 +- .../website/{PhpView.vue => ListView.vue} | 4 +- 24 files changed, 175 insertions(+), 301 deletions(-) rename web/src/views/website/{PhpView.vue => ListView.vue} (98%) diff --git a/cmd/ace/wire_gen.go b/cmd/ace/wire_gen.go index 4ff93513..362d727a 100644 --- a/cmd/ace/wire_gen.go +++ b/cmd/ace/wire_gen.go @@ -78,8 +78,8 @@ func initWeb() (*app.Web, error) { databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo) certRepo := data.NewCertRepo(locale, db, logger) certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger) - websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo) settingRepo := data.NewSettingRepo(locale, db, koanf, taskRepo) + websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo) cronRepo := data.NewCronRepo(locale, db) backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo) homeService := service.NewHomeService(locale, koanf, taskRepo, websiteRepo, appRepo, settingRepo, cronRepo, backupRepo) diff --git a/cmd/cli/wire_gen.go b/cmd/cli/wire_gen.go index a5b4bd13..0c0ec7ab 100644 --- a/cmd/cli/wire_gen.go +++ b/cmd/cli/wire_gen.go @@ -69,7 +69,7 @@ func initCli() (*app.Cli, error) { databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo) certRepo := data.NewCertRepo(locale, db, logger) certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger) - websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo) + websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo) backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo) cliService := service.NewCliService(locale, koanf, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo) cli := route.NewCli(locale, cliService) diff --git a/go.sum b/go.sum index 12efd332..f5bb7f86 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= @@ -464,8 +462,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -538,8 +534,6 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= diff --git a/internal/biz/setting.go b/internal/biz/setting.go index 4f636c9b..910e4be6 100644 --- a/internal/biz/setting.go +++ b/internal/biz/setting.go @@ -19,6 +19,7 @@ const ( SettingKeyMySQLRootPassword SettingKey = "mysql_root_password" SettingKeyOfflineMode SettingKey = "offline_mode" SettingKeyAutoUpdate SettingKey = "auto_update" + SettingKeyWebServer SettingKey = "web_server" ) type Setting struct { diff --git a/internal/biz/website.go b/internal/biz/website.go index f5f00e95..dfbdf558 100644 --- a/internal/biz/website.go +++ b/internal/biz/website.go @@ -20,6 +20,7 @@ type Website struct { UpdatedAt time.Time `json:"updated_at"` CertExpire string `gorm:"-:all" json:"cert_expire"` // 仅显示 + PHP uint `gorm:"-:all" json:"php"` // 仅显示 Cert *Cert `gorm:"foreignKey:WebsiteID" json:"cert"` } @@ -30,7 +31,7 @@ type WebsiteRepo interface { Count() (int64, error) Get(id uint) (*types.WebsiteSetting, error) GetByName(name string) (*types.WebsiteSetting, error) - List(page, limit uint) ([]*Website, int64, error) + List(typ string, page, limit uint) ([]*Website, int64, error) Create(req *request.WebsiteCreate) (*Website, error) Update(req *request.WebsiteUpdate) error Delete(req *request.WebsiteDelete) error diff --git a/internal/data/website.go b/internal/data/website.go index 6ec14854..857cb1c7 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -29,6 +29,8 @@ import ( "github.com/acepanel/panel/pkg/shell" "github.com/acepanel/panel/pkg/systemctl" "github.com/acepanel/panel/pkg/types" + "github.com/acepanel/panel/pkg/webserver" + webservertypes "github.com/acepanel/panel/pkg/webserver/types" ) type websiteRepo struct { @@ -40,9 +42,10 @@ type websiteRepo struct { databaseUser biz.DatabaseUserRepo cert biz.CertRepo certAccount biz.CertAccountRepo + setting biz.SettingRepo } -func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) biz.WebsiteRepo { +func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo, setting biz.SettingRepo) biz.WebsiteRepo { return &websiteRepo{ t: t, db: db, @@ -52,6 +55,7 @@ func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database databaseUser: databaseUser, cert: cert, certAccount: certAccount, + setting: setting, } } @@ -211,7 +215,7 @@ func (r *websiteRepo) GetByName(name string) (*types.WebsiteSetting, error) { } -func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) { +func (r *websiteRepo) List(typ string, page, limit uint) ([]*biz.Website, int64, error) { websites := make([]*biz.Website, 0) var total int64 @@ -219,35 +223,56 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) { return nil, 0, err } - if err := r.db.Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&websites).Error; err != nil { + query := r.db + if typ != "all" { + query = query.Where("type = ?", typ) + } + if err := query.Order("id DESC").Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&websites).Error; err != nil { return nil, 0, err } - // 取证书剩余有效时间 + // 取证书剩余有效时间和PHP版本 for _, website := range websites { crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem")) if decode, err := cert.ParseCert(crt); err == nil { hours := time.Until(decode.NotAfter).Hours() website.CertExpire = fmt.Sprintf("%.2f", hours/24) } + if website.Type == "php" { + website.PHP = r.getPHPVersion(website.Name) + } } return websites, total, nil } func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { - // 初始化nginx配置 - config := nginx.DefaultConf - p, err := nginx.NewParser(config) + webServer, err := r.setting.Get(biz.SettingKeyWebServer) if err != nil { return nil, err } - // 监听地址 - var listens [][]string - for _, listen := range req.Listens { - listens = append(listens, []string{listen}) + + var vhost webservertypes.Vhost + switch req.Type { + case "proxy": + vhost, err = webserver.NewProxyVhost(webserver.Type(webServer), filepath.Join(app.Root, "sites", req.Name, "config")) + case "php": + vhost, err = webserver.NewPHPVhost(webserver.Type(webServer), filepath.Join(app.Root, "sites", req.Name, "config")) + case "static": + vhost, err = webserver.NewStaticVhost(webserver.Type(webServer), filepath.Join(app.Root, "sites", req.Name, "config")) + default: + return nil, errors.New(r.t.Get("unsupported website type: %s", req.Type)) } - if err = p.SetListen(listens); err != nil { + if err != nil { + return nil, err + } + + // 监听地址 + var listens []webservertypes.Listen + for _, listen := range req.Listens { + listens = append(listens, webservertypes.Listen{Address: listen}) + } + if err = vhost.SetListen(listens); err != nil { return nil, err } // 域名 @@ -255,37 +280,35 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { if err != nil { return nil, err } - if err = p.SetServerName(domains); err != nil { + if err = vhost.SetServerName(domains); err != nil { return nil, err } // 运行目录 - if err = p.SetRoot(req.Path); err != nil { - return nil, err - } - // PHP - if err = p.SetPHP(req.PHP); err != nil { - return nil, err - } - // 伪静态和acme - includes, comments, err := p.GetIncludes() - if err != nil { - return nil, err - } - includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf")) - includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf")) - comments = append(comments, []string{r.t.Get("# Rewrite rule")}) - comments = append(comments, []string{"# acme http-01"}) - if err = p.SetIncludes(includes, comments); err != nil { + if err = vhost.SetRoot(req.Path); err != nil { return nil, err } // 日志 - if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", req.Name+".log")); err != nil { + if err = vhost.SetAccessLog(filepath.Join(app.Root, "sites", req.Name, "log", "access.log")); err != nil { return nil, err } - if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", req.Name+".error.log")); err != nil { + if err = vhost.SetErrorLog(filepath.Join(app.Root, "sites", req.Name, "log", "error.log")); err != nil { return nil, err } + // 反向代理支持 + if proxyVhost, ok := vhost.(webservertypes.ProxyVhost); ok { + if err = proxyVhost.SetProxies([]webservertypes.Proxy{req.Proxy}); err != nil { + return nil, err + } + } + + // PHP 支持 + if phpVhost, ok := vhost.(webservertypes.PHPVhost); ok { + if err = phpVhost.SetPHP(req.PHP); err != nil { + return nil, err + } + } + // 初始化网站目录 if err = os.MkdirAll(req.Path, 0755); err != nil { return nil, err @@ -321,20 +344,20 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { return nil, err } - // 写nginx配置 - if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), p.Dump(), 0644); err != nil { + // 写配置 + if err = vhost.Save(); err != nil { return nil, err } if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil { return nil, err } - if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "vhost", "001-acme.conf"), "", 0644); err != nil { return nil, err } - if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".pem"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "fullchain.pem"), "", 0644); err != nil { return nil, err } - if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".key"), "", 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "sites", req.Name, "config", "privatekey.key"), "", 0644); err != nil { return nil, err } @@ -347,7 +370,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { } // PHP 网站默认开启防跨站 - if req.PHP > 0 { + if req.Type == "php" { userIni := filepath.Join(req.Path, ".user.ini") if !io.Exists(userIni) { if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Path), 0644); err != nil { @@ -360,7 +383,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { // 创建面板网站 w := &biz.Website{ Name: req.Name, - Type: "php", // TODO 支持网站类型 + Type: req.Type, Status: true, Path: req.Path, Https: false, @@ -370,8 +393,8 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { return nil, err } - if err = systemctl.Reload("nginx"); err != nil { - _, err = shell.Execf("nginx -t") + // 重载 Web 服务器 + if err = vhost.Reload(); err != nil { return nil, err } @@ -839,3 +862,11 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error { return r.cert.Deploy(newCert.ID, website.ID) } + +func (r *websiteRepo) getPHPVersion(name string) uint { + vhost, err := webserver.NewPHPVhost(webserver.TypeNginx, filepath.Join(app.Root, "sites", name, "config")) + if err != nil { + return 0 + } + return vhost.PHP() +} diff --git a/internal/http/request/website.go b/internal/http/request/website.go index a27e4ede..cfeb98e1 100644 --- a/internal/http/request/website.go +++ b/internal/http/request/website.go @@ -1,24 +1,35 @@ package request -import "github.com/acepanel/panel/pkg/types" +import ( + "github.com/acepanel/panel/pkg/types" + webservertypes "github.com/acepanel/panel/pkg/webserver/types" +) type WebsiteDefaultConfig struct { Index string `json:"index" form:"index" validate:"required"` Stop string `json:"stop" form:"stop" validate:"required"` } +type WebsiteList struct { + Type string `json:"type" form:"type" validate:"required|in:all,proxy,static,php"` + Paginate +} + type WebsiteCreate struct { + Type string `json:"type" form:"type" validate:"required|in:proxy,static,php"` Name string `form:"name" json:"name" validate:"required|notExists:websites,name|not_in:phpmyadmin,default|regex:^[a-zA-Z0-9_-]+$"` Listens []string `form:"listens" json:"listens" validate:"required|isSlice"` Domains []string `form:"domains" json:"domains" validate:"required|isSlice"` Path string `form:"path" json:"path"` - PHP int `form:"php" json:"php"` DB bool `form:"db" json:"db"` DBType string `form:"db_type" json:"db_type" validate:"requiredIf:DB,true"` DBName string `form:"db_name" json:"db_name" validate:"requiredIf:DB,true"` DBUser string `form:"db_user" json:"db_user" validate:"requiredIf:DB,true"` DBPassword string `form:"db_password" json:"db_password" validate:"requiredIf:DB,true"` Remark string `form:"remark" json:"remark"` + + PHP uint `form:"php" json:"php" validate:"requiredIf:Type,php"` // 仅 PHP 网站需要 + Proxy webservertypes.Proxy `form:"proxy" json:"proxy" validate:"requiredIf:Type,proxy"` // 仅反向代理网站需要 } type WebsiteDelete struct { diff --git a/internal/service/cli.go b/internal/service/cli.go index 0ff70fc3..f3d764c6 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -570,7 +570,7 @@ func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error Domains: cmd.StringSlice("domains"), Listens: cmd.StringSlice("listens"), Path: cmd.String("path"), - PHP: int(cmd.Int("php")), + PHP: cmd.Uint("php"), DB: false, } diff --git a/internal/service/website.go b/internal/service/website.go index 8e01aeca..260a946e 100644 --- a/internal/service/website.go +++ b/internal/service/website.go @@ -83,14 +83,15 @@ func (s *WebsiteService) UpdateCert(w http.ResponseWriter, r *http.Request) { Success(w, nil) } +// List 网站列表 func (s *WebsiteService) List(w http.ResponseWriter, r *http.Request) { - req, err := Bind[request.Paginate](r) + req, err := Bind[request.WebsiteList](r) if err != nil { Error(w, http.StatusUnprocessableEntity, "%v", err) return } - websites, total, err := s.websiteRepo.List(req.Page, req.Limit) + websites, total, err := s.websiteRepo.List(req.Type, req.Page, req.Limit) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return diff --git a/pkg/webserver/apache/vhost.go b/pkg/webserver/apache/vhost.go index 6a909c95..aa828b8c 100644 --- a/pkg/webserver/apache/vhost.go +++ b/pkg/webserver/apache/vhost.go @@ -137,18 +137,7 @@ func (v *baseVhost) Listen() []types.Listen { // Apache 的监听配置通常在 VirtualHost 的参数中 // 例如: for _, arg := range v.vhost.Args { - listen := types.Listen{ - Address: arg, - Options: make(map[string]string), - } - - // 检查是否是 HTTPS - if strings.Contains(arg, ":443") || v.HTTPS() { - listen.Protocol = "https" - } else { - listen.Protocol = "http" - } - + listen := types.Listen{Address: arg} result = append(result, listen) } @@ -583,7 +572,7 @@ func (v *baseVhost) SetRedirects(redirects []types.Redirect) error { // ========== PHPVhost ========== -func (v *PHPVhost) PHP() int { +func (v *PHPVhost) PHP() uint { // Apache 通常通过 FilesMatch 块配置 PHP // 或者通过 SetHandler 指令 handler := v.vhost.GetDirectiveValue("SetHandler") @@ -606,7 +595,7 @@ func (v *PHPVhost) PHP() int { if len(parts) >= 2 { major, _ := strconv.Atoi(parts[0]) minor, _ := strconv.Atoi(parts[1]) - return major*10 + minor + return uint(major*10 + minor) } } } @@ -630,7 +619,7 @@ func (v *PHPVhost) PHP() int { return 0 } -func (v *PHPVhost) SetPHP(version int) error { +func (v *PHPVhost) SetPHP(version uint) error { // 移除现有的 PHP 配置 v.vhost.RemoveDirective("SetHandler") diff --git a/pkg/webserver/apache/vhost_test.go b/pkg/webserver/apache/vhost_test.go index f8e1bc5c..28889252 100644 --- a/pkg/webserver/apache/vhost_test.go +++ b/pkg/webserver/apache/vhost_test.go @@ -131,8 +131,8 @@ func (s *VhostTestSuite) TestIndexEmpty() { func (s *VhostTestSuite) TestListen() { listens := []types.Listen{ - {Address: "*:80", Protocol: "http"}, - {Address: "*:443", Protocol: "https"}, + {Address: "*:80"}, + {Address: "*:443"}, } err := s.vhost.SetListen(listens) s.NoError(err) @@ -345,7 +345,7 @@ func (s *VhostTestSuite) TestExportWithSSL() { func (s *VhostTestSuite) TestListenProtocolDetection() { listens := []types.Listen{ - {Address: "*:443", Protocol: "https"}, + {Address: "*:443"}, } s.NoError(s.vhost.SetListen(listens)) @@ -357,7 +357,7 @@ func (s *VhostTestSuite) TestListenProtocolDetection() { got := s.vhost.Listen() s.Len(got, 1) - s.Equal("https", got[0].Protocol) + s.Equal("*:443", got[0].Address) } func (s *VhostTestSuite) TestDirectoryBlock() { diff --git a/pkg/webserver/nginx/getter.go b/pkg/webserver/nginx/getter.go index 139ec6dc..8277d023 100644 --- a/pkg/webserver/nginx/getter.go +++ b/pkg/webserver/nginx/getter.go @@ -6,47 +6,6 @@ import ( "strings" ) -func (p *Parser) GetListen() ([][]string, error) { - directives, err := p.Find("server.listen") - if err != nil { - return nil, err - } - - var result [][]string - for _, dir := range directives { - result = append(result, p.parameters2Slices(dir.GetParameters())) - } - - return result, nil -} - -func (p *Parser) GetServerName() ([]string, error) { - directive, err := p.FindOne("server.server_name") - if err != nil { - return nil, err - } - - return p.parameters2Slices(directive.GetParameters()), nil -} - -func (p *Parser) GetIndex() ([]string, error) { - directive, err := p.FindOne("server.index") - if err != nil { - return nil, err - } - - return p.parameters2Slices(directive.GetParameters()), nil -} - -func (p *Parser) GetIndexWithComment() ([]string, []string, error) { - directive, err := p.FindOne("server.index") - if err != nil { - return nil, nil, err - } - - return p.parameters2Slices(directive.GetParameters()), directive.GetComment(), nil -} - func (p *Parser) GetRoot() (string, error) { directive, err := p.FindOne("server.root") if err != nil { @@ -88,36 +47,6 @@ func (p *Parser) GetIncludes() (includes []string, comments [][]string, err erro return includes, comments, nil } -func (p *Parser) GetPHP() int { - directives, err := p.Find("server.include") - if err != nil { - return 0 - } - - var result int - for _, dir := range directives { - if slices.ContainsFunc(p.parameters2Slices(dir.GetParameters()), func(s string) bool { - return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf") - }) { - _, _ = fmt.Sscanf(dir.GetParameters()[0].GetValue(), "enable-php-%d.conf", &result) - } - } - - return result -} - -func (p *Parser) GetHTTPS() bool { - directive, err := p.FindOne("server.ssl_certificate") - if err != nil { - return false - } - if len(p.parameters2Slices(directive.GetParameters())) == 0 { - return false - } - - return true -} - func (p *Parser) GetHTTPSProtocols() []string { directive, err := p.FindOne("server.ssl_protocols") if err != nil { diff --git a/pkg/webserver/nginx/parser_test.go b/pkg/webserver/nginx/parser_test.go index b119cdc5..9d5e808c 100644 --- a/pkg/webserver/nginx/parser_test.go +++ b/pkg/webserver/nginx/parser_test.go @@ -15,56 +15,6 @@ func TestNginxTestSuite(t *testing.T) { suite.Run(t, &NginxTestSuite{}) } -func (s *NginxTestSuite) TestListen() { - parser, err := NewParser() - s.NoError(err) - listen, err := parser.GetListen() - s.NoError(err) - s.Equal([][]string{{"80"}}, listen) - s.NoError(parser.SetListen([][]string{{"80"}, {"443"}})) - listen, err = parser.GetListen() - s.NoError(err) - s.Equal([][]string{{"80"}, {"443"}}, listen) -} - -func (s *NginxTestSuite) TestServerName() { - parser, err := NewParser() - s.NoError(err) - serverName, err := parser.GetServerName() - s.NoError(err) - s.Equal([]string{"localhost"}, serverName) - s.NoError(parser.SetServerName([]string{"example.com"})) - serverName, err = parser.GetServerName() - s.NoError(err) - s.Equal([]string{"example.com"}, serverName) -} - -func (s *NginxTestSuite) TestIndex() { - parser, err := NewParser() - s.NoError(err) - index, err := parser.GetIndex() - s.NoError(err) - s.Equal([]string{"index.php", "index.html"}, index) - s.NoError(parser.SetIndex([]string{"index.html", "index.php"})) - index, err = parser.GetIndex() - s.NoError(err) - s.Equal([]string{"index.html", "index.php"}, index) -} - -func (s *NginxTestSuite) TestIndexWithComment() { - parser, err := NewParser() - s.NoError(err) - index, comment, err := parser.GetIndexWithComment() - s.NoError(err) - s.Equal([]string{"index.php", "index.html"}, index) - s.Equal([]string(nil), comment) - s.NoError(parser.SetIndexWithComment([]string{"index.html", "index.php"}, []string{"# 测试"})) - index, comment, err = parser.GetIndexWithComment() - s.NoError(err) - s.Equal([]string{"index.html", "index.php"}, index) - s.Equal([]string{"# 测试"}, comment) -} - func (s *NginxTestSuite) TestRoot() { parser, err := NewParser() s.NoError(err) @@ -110,16 +60,6 @@ func (s *NginxTestSuite) TestIncludes() { s.Equal([][]string{{"# 伪静态规则测试"}}, comments) } -func (s *NginxTestSuite) TestPHP() { - parser, err := NewParser() - s.NoError(err) - s.Equal(0, parser.GetPHP()) - s.NoError(parser.SetPHP(80)) - s.Equal(80, parser.GetPHP()) - s.NoError(parser.SetPHP(0)) - s.Equal(0, parser.GetPHP()) -} - func (s *NginxTestSuite) TestHTTP() { parser, err := NewParser() s.NoError(err) @@ -128,17 +68,6 @@ func (s *NginxTestSuite) TestHTTP() { s.Equal(string(expect), parser.Dump()) } -func (s *NginxTestSuite) TestHTTPS() { - parser, err := NewParser() - s.NoError(err) - s.False(parser.GetHTTPS()) - s.NoError(parser.SetHTTPSCert("/www/server/vhost/cert/default.pem", "/www/server/vhost/cert/default.key")) - s.True(parser.GetHTTPS()) - expect, err := os.ReadFile("testdata/https.conf") - s.NoError(err) - s.Equal(string(expect), parser.Dump()) -} - func (s *NginxTestSuite) TestHTTPSProtocols() { parser, err := NewParser() s.NoError(err) diff --git a/pkg/webserver/nginx/proxy.go b/pkg/webserver/nginx/proxy.go index 926b7080..4a989071 100644 --- a/pkg/webserver/nginx/proxy.go +++ b/pkg/webserver/nginx/proxy.go @@ -216,7 +216,6 @@ func generateProxyConfig(proxy types.Proxy) string { location = "/" } - sb.WriteString(fmt.Sprintf("# Reverse proxy: %s -> %s\n", location, proxy.Pass)) sb.WriteString(fmt.Sprintf("location %s {\n", location)) // resolver 配置(如果启用自动刷新) diff --git a/pkg/webserver/nginx/vhost.go b/pkg/webserver/nginx/vhost.go index 9d830970..36df3996 100644 --- a/pkg/webserver/nginx/vhost.go +++ b/pkg/webserver/nginx/vhost.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "github.com/acepanel/panel/pkg/webserver/types" @@ -123,39 +124,17 @@ func (v *baseVhost) SetEnable(enable bool, _ ...string) error { } func (v *baseVhost) Listen() []types.Listen { - listens, err := v.parser.GetListen() + directives, err := v.parser.Find("server.listen") if err != nil { return nil } var result []types.Listen - for _, l := range listens { - if len(l) == 0 { - continue - } - - listen := types.Listen{ - Address: l[0], - Options: make(map[string]string), - } - - // 解析 Nginx 特有的选项 + for _, dir := range directives { + l := v.parser.parameters2Slices(dir.GetParameters()) + listen := types.Listen{Address: l[0]} for i := 1; i < len(l); i++ { - switch l[i] { - case "ssl": - listen.Protocol = "https" - case "http2": - listen.Protocol = "http2" - case "http3", "quic": - listen.Protocol = "http3" - default: - listen.Options[l[i]] = "true" - } - } - - // 如果没有指定协议,默认为 http - if listen.Protocol == "" { - listen.Protocol = "http" + listen.Args = append(listen.Args, l[i]) } result = append(result, listen) @@ -169,26 +148,7 @@ func (v *baseVhost) SetListen(listens []types.Listen) error { var nginxListens [][]string for _, l := range listens { listen := []string{l.Address} - - // 添加协议标识 - switch l.Protocol { - case "https": - listen = append(listen, "ssl") - case "http2": - listen = append(listen, "http2") - case "http3": - listen = append(listen, "http3") - } - - // 添加其他选项 - for k, v := range l.Options { - if v == "true" { - listen = append(listen, k) - } else { - listen = append(listen, fmt.Sprintf("%s=%s", k, v)) - } - } - + listen = append(listen, l.Args...) nginxListens = append(nginxListens, listen) } @@ -196,11 +156,12 @@ func (v *baseVhost) SetListen(listens []types.Listen) error { } func (v *baseVhost) ServerName() []string { - names, err := v.parser.GetServerName() + directive, err := v.parser.FindOne("server.server_name") if err != nil { return nil } - return names + + return v.parser.parameters2Slices(directive.GetParameters()) } func (v *baseVhost) SetServerName(serverName []string) error { @@ -208,11 +169,12 @@ func (v *baseVhost) SetServerName(serverName []string) error { } func (v *baseVhost) Index() []string { - index, err := v.parser.GetIndex() + directive, err := v.parser.FindOne("server.index") if err != nil { return nil } - return index + + return v.parser.parameters2Slices(directive.GetParameters()) } func (v *baseVhost) SetIndex(index []string) error { @@ -294,6 +256,9 @@ func (v *baseVhost) Save() error { func (v *baseVhost) Reload() error { parts := strings.Fields("systemctl reload openresty") if err := exec.Command(parts[0], parts[1:]...).Run(); err != nil { + if testErr := exec.Command("nginx", "-t").Run(); testErr != nil { + return fmt.Errorf("nginx config test failed: %w", testErr) + } return fmt.Errorf("failed to reload nginx config: %w", err) } @@ -317,7 +282,15 @@ func (v *baseVhost) Reset() error { } func (v *baseVhost) HTTPS() bool { - return v.parser.GetHTTPS() + directive, err := v.parser.FindOne("server.ssl_certificate") + if err != nil { + return false + } + if len(v.parser.parameters2Slices(directive.GetParameters())) == 0 { + return false + } + + return true } func (v *baseVhost) SSLConfig() *types.SSLConfig { @@ -479,11 +452,25 @@ func (v *baseVhost) SetRedirects(redirects []types.Redirect) error { // ========== PHPVhost ========== -func (v *PHPVhost) PHP() int { - return v.parser.GetPHP() +func (v *PHPVhost) PHP() uint { + directives, err := v.parser.Find("server.include") + if err != nil { + return 0 + } + + var result uint + for _, dir := range directives { + if slices.ContainsFunc(v.parser.parameters2Slices(dir.GetParameters()), func(s string) bool { + return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf") + }) { + _, _ = fmt.Sscanf(dir.GetParameters()[0].GetValue(), "enable-php-%d.conf", &result) + } + } + + return result } -func (v *PHPVhost) SetPHP(version int) error { +func (v *PHPVhost) SetPHP(version uint) error { // 先移除所有 PHP 相关的 include includes := v.Includes() var newIncludes []types.IncludeFile diff --git a/pkg/webserver/types/proxy.go b/pkg/webserver/types/proxy.go index 9d2e73c9..c48b1850 100644 --- a/pkg/webserver/types/proxy.go +++ b/pkg/webserver/types/proxy.go @@ -4,21 +4,21 @@ import "time" // Proxy 反向代理配置 type Proxy struct { - Location string // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/" - AutoRefresh bool // 是否自动刷新解析 - Pass string // 代理地址,如: "http://example.com", "http://backend" - Host string // 代理 Host,如: "example.com" - SNI string // 代理 SNI,如: "example.com" - Cache bool // 是否启用缓存 - Buffering bool // 是否启用缓冲 - Resolver []string // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"] - ResolverTimeout time.Duration // DNS 解析超时时间,如: 5 * time.Second - Replaces map[string]string // 响应内容替换,如: map["/old"] = "/new" + Location string `form:"location" json:"location" validate:"required"` // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/" + AutoRefresh bool `form:"auto_refresh" json:"auto_refresh"` // 是否自动刷新解析 + Pass string `form:"pass" json:"pass" validate:"required"` // 代理地址,如: "http://example.com", "http://backend" + Host string `form:"host" json:"host"` // 代理 Host,如: "example.com" + SNI string `form:"sni" json:"sni"` // 代理 SNI,如: "example.com" + Cache bool `form:"cache" json:"cache"` // 是否启用缓存 + Buffering bool `form:"buffering" json:"buffering"` // 是否启用缓冲 + Resolver []string `form:"resolver" json:"resolver"` // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"] + ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间,如: 5 * time.Second + Replaces map[string]string `form:"replaces" json:"replaces"` // 响应内容替换,如: map["/old"] = "/new" } // Upstream 上游服务器配置 type Upstream struct { - Servers map[string]string // 上游服务器及权重,如: map["server1"] = "weight=5" - Algo string // 负载均衡算法,如: "least_conn", "ip_hash" - Keepalive int // 保持连接数,如: 32 + Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及权重,如: map["server1"] = "weight=5" + Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "ip_hash" + Keepalive int `form:"keepalive" json:"keepalive"` // 保持连接数,如: 32 } diff --git a/pkg/webserver/types/vhost.go b/pkg/webserver/types/vhost.go index c5143c8d..d9abb8a9 100644 --- a/pkg/webserver/types/vhost.go +++ b/pkg/webserver/types/vhost.go @@ -92,15 +92,15 @@ type PHPVhost interface { type ProxyVhost interface { Vhost VhostRedirect - VhostProxyConfig + VhostProxy } // VhostPHP PHP 相关接口 type VhostPHP interface { // PHP 取 PHP 版本,如: 84, 81, 80, 0 表示未启用 PHP - PHP() int + PHP() uint // SetPHP 设置 PHP 版本 - SetPHP(version int) error + SetPHP(version uint) error } // VhostRedirect 重定向相关接口 @@ -111,8 +111,8 @@ type VhostRedirect interface { SetRedirects(redirects []Redirect) error } -// VhostProxyConfig 反向代理相关接口 -type VhostProxyConfig interface { +// VhostProxy 反向代理相关接口 +type VhostProxy interface { // Proxies 取所有反向代理配置 Proxies() []Proxy // SetProxies 设置反向代理配置 @@ -130,9 +130,8 @@ type VhostProxyConfig interface { // Listen 监听配置 type Listen struct { - Address string // 监听地址,如: "80", "0.0.0.0:80", "[::]:443" - Protocol string // 协议类型,如: "http", "https", "http2", "http3" - Options map[string]string // 服务器特定选项,如: map["default_server"] = "true" + Address string // 监听地址,如: "80", "0.0.0.0:80", "[::]:443" + Args []string // 其他参数,如: ["default_server", "ssl", "quic"] } // SSLConfig SSL/TLS 配置 diff --git a/web/src/api/panel/website/index.ts b/web/src/api/panel/website/index.ts index dd775c78..f3113ed7 100644 --- a/web/src/api/panel/website/index.ts +++ b/web/src/api/panel/website/index.ts @@ -2,7 +2,8 @@ import { http } from '@/utils' export default { // 列表 - list: (page: number, limit: number): any => http.Get('/website', { params: { page, limit } }), + list: (type: string, page: number, limit: number): any => + http.Get('/website', { params: { type, page, limit } }), // 创建 create: (data: any): any => http.Post('/website', data), // 删除 diff --git a/web/src/views/apps/fail2ban/IndexView.vue b/web/src/views/apps/fail2ban/IndexView.vue index f569ac83..8bb28c45 100644 --- a/web/src/views/apps/fail2ban/IndexView.vue +++ b/web/src/views/apps/fail2ban/IndexView.vue @@ -162,7 +162,7 @@ const handleSaveWhiteList = () => { } const getWebsiteList = async (page: number, limit: number) => { - const data = await website.list(page, limit) + const data = await website.list('all', page, limit) for (const item of data.items) { websites.value.push({ label: item.name, diff --git a/web/src/views/backup/ListView.vue b/web/src/views/backup/ListView.vue index 21c10292..3b3f0dac 100644 --- a/web/src/views/backup/ListView.vue +++ b/web/src/views/backup/ListView.vue @@ -164,7 +164,7 @@ watch( onMounted(() => { useRequest(app.isInstalled('nginx')).onSuccess(({ data }) => { if (data) { - useRequest(website.list(1, 10000)).onSuccess(({ data }: { data: any }) => { + useRequest(website.list('all', 1, 10000)).onSuccess(({ data }: { data: any }) => { for (const item of data.items) { websites.value.push({ label: item.name, diff --git a/web/src/views/cert/IndexView.vue b/web/src/views/cert/IndexView.vue index 42511826..99088444 100644 --- a/web/src/views/cert/IndexView.vue +++ b/web/src/views/cert/IndexView.vue @@ -40,7 +40,7 @@ const getAsyncData = () => { websites.value = [] useRequest(app.isInstalled('nginx')).onSuccess(({ data }) => { if (data) { - useRequest(website.list(1, 10000)).onSuccess(({ data }) => { + useRequest(website.list('all', 1, 10000)).onSuccess(({ data }) => { for (const item of data.items) { websites.value.push({ label: item.name, diff --git a/web/src/views/task/CreateModal.vue b/web/src/views/task/CreateModal.vue index 72f1fcb2..f0e68468 100644 --- a/web/src/views/task/CreateModal.vue +++ b/web/src/views/task/CreateModal.vue @@ -68,7 +68,7 @@ watch(createModel, (value) => { onMounted(() => { useRequest(app.isInstalled('nginx')).onSuccess(({ data }) => { if (data) { - useRequest(website.list(1, 10000)).onSuccess(({ data }: { data: any }) => { + useRequest(website.list('all', 1, 10000)).onSuccess(({ data }: { data: any }) => { for (const item of data.items) { websites.value.push({ label: item.name, diff --git a/web/src/views/website/IndexView.vue b/web/src/views/website/IndexView.vue index 84a0cbca..3437be4e 100644 --- a/web/src/views/website/IndexView.vue +++ b/web/src/views/website/IndexView.vue @@ -3,7 +3,7 @@ defineOptions({ name: 'website-index' }) -import PhpView from '@/views/website/PhpView.vue' +import ListView from '@/views/website/ListView.vue' import SettingView from '@/views/website/SettingView.vue' const currentTab = ref('proxy') @@ -19,7 +19,7 @@ const currentTab = ref('proxy') - + diff --git a/web/src/views/website/PhpView.vue b/web/src/views/website/ListView.vue similarity index 98% rename from web/src/views/website/PhpView.vue rename to web/src/views/website/ListView.vue index 1f36f7ea..fe063712 100644 --- a/web/src/views/website/PhpView.vue +++ b/web/src/views/website/ListView.vue @@ -8,6 +8,8 @@ import { useFileStore } from '@/store' import { generateRandomString, isNullOrUndef } from '@/utils' import BulkCreate from '@/views/website/BulkCreate.vue' +const type = defineModel('type', { type: String, required: true }) // 网站类型 + const fileStore = useFileStore() const { $gettext } = useGettext() const router = useRouter() @@ -240,7 +242,7 @@ const { data: installedDbAndPhp } = useRequest(home.installedDbAndPhp, { }) const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination( - (page, pageSize) => website.list(page, pageSize), + (page, pageSize) => website.list(type.value, page, pageSize), { initialData: { total: 0, list: [] }, initialPageSize: 20,