From e8a01e2d04e980923635ff837d60132fe1bbe1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Mon, 14 Oct 2024 21:34:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=BD=91=E7=AB=99nginx=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=A7=A3=E6=9E=90=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/apps/fail2ban/service.go | 9 +- internal/biz/website.go | 3 +- internal/data/website.go | 691 +++++++++++++----------------- internal/http/request/website.go | 40 +- internal/route/cli.go | 10 +- internal/service/cli.go | 6 +- pkg/nginx/data.go | 2 - pkg/nginx/getter.go | 35 +- pkg/nginx/parser_test.go | 24 +- pkg/nginx/setter.go | 34 +- pkg/nginx/testdata/http.conf | 2 - pkg/nginx/testdata/https.conf | 2 - pkg/punycode/punycode.go | 52 +++ pkg/types/website.go | 55 +-- 14 files changed, 488 insertions(+), 477 deletions(-) create mode 100644 pkg/punycode/punycode.go diff --git a/internal/apps/fail2ban/service.go b/internal/apps/fail2ban/service.go index d539667b..eefdca6a 100644 --- a/internal/apps/fail2ban/service.go +++ b/internal/apps/fail2ban/service.go @@ -1,6 +1,7 @@ package fail2ban import ( + "fmt" "net/http" "regexp" "strings" @@ -111,10 +112,12 @@ func (s *Service) Create(w http.ResponseWriter, r *http.Request) { return } var ports string - for _, port := range website.Ports { - fields := strings.Fields(cast.ToString(port)) - ports += fields[0] + "," + for _, listen := range website.Listens { + if port, err := cast.ToIntE(listen.Address); err == nil { + ports += fmt.Sprintf("%d", port) + "," + } } + ports = strings.TrimSuffix(ports, ",") rule := ` # ` + jailWebsiteName + `-` + jailWebsiteMode + `-START diff --git a/internal/biz/website.go b/internal/biz/website.go index e8abd7cf..9532038f 100644 --- a/internal/biz/website.go +++ b/internal/biz/website.go @@ -12,8 +12,7 @@ type Website struct { Name string `gorm:"not null;unique" json:"name"` Status bool `gorm:"not null;default:true" json:"status"` Path string `gorm:"not null" json:"path"` - PHP int `gorm:"not null" json:"php"` - SSL bool `gorm:"not null" json:"ssl"` + HTTPS bool `gorm:"not null" json:"https"` Remark string `gorm:"not null" json:"remark"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/internal/data/website.go b/internal/data/website.go index 8827af07..63fa95fd 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -4,13 +4,9 @@ import ( "errors" "fmt" "path/filepath" - "regexp" "slices" - "strconv" "strings" - "github.com/spf13/cast" - "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/embed" @@ -18,8 +14,9 @@ import ( "github.com/TheTNB/panel/pkg/cert" "github.com/TheTNB/panel/pkg/db" "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/nginx" + "github.com/TheTNB/panel/pkg/punycode" "github.com/TheTNB/panel/pkg/shell" - "github.com/TheTNB/panel/pkg/str" "github.com/TheTNB/panel/pkg/systemctl" "github.com/TheTNB/panel/pkg/types" ) @@ -59,79 +56,72 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { if err := app.Orm.Where("id", id).First(website).Error; err != nil { return nil, err } - + // 解析nginx配置 config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) if err != nil { return nil, err } + p, err := nginx.NewParser(config) + if err != nil { + return nil, err + } setting := new(types.WebsiteSetting) setting.ID = website.ID setting.Name = website.Name setting.Path = website.Path - setting.SSL = website.SSL - setting.PHP = website.PHP + setting.HTTPS = website.HTTPS + setting.PHP = p.GetPHP() setting.Raw = config - - portStr := str.Cut(config, "# port标记位开始", "# port标记位结束") - matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1) - for _, match := range matches { - if len(match) < 2 { + // 监听地址 + listens, err := p.GetListen() + if err != nil { + return nil, err + } + for listen := range slices.Values(listens) { + if len(listen) == 0 { continue } - // 跳过 ipv6 - if strings.Contains(match[1], "[::]") { - continue - } - - // 处理 443 ssl 之类的情况 - ports := strings.Fields(match[1]) - if len(ports) == 0 { - continue - } - if !slices.Contains(setting.Ports, cast.ToUint(ports[0])) { - setting.Ports = append(setting.Ports, cast.ToUint(ports[0])) - } - if len(ports) > 1 && ports[1] == "ssl" { - setting.SSLPorts = append(setting.SSLPorts, cast.ToUint(ports[0])) - } else if len(ports) > 1 && ports[1] == "quic" { - setting.QUICPorts = append(setting.QUICPorts, cast.ToUint(ports[0])) - } + setting.Listens = append(setting.Listens, types.WebsiteListen{ + Address: listen[0], + HTTPS: slices.Contains(listen, "ssl"), + QUIC: slices.Contains(listen, "quic"), + }) } - serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束") - match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName) - if len(match) > 1 { - setting.Domains = strings.Split(match[1], " ") + // 域名 + domains, err := p.GetServerName() + if err != nil { + return nil, err } - root := str.Cut(config, "# root标记位开始", "# root标记位结束") - match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root) - if len(match) > 1 { - setting.Root = match[1] + domains, err = punycode.DecodeDomains(domains) + if err != nil { + return nil, err } - index := str.Cut(config, "# index标记位开始", "# index标记位结束") - match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index) - if len(match) > 1 { - setting.Index = match[1] - } - + setting.Domains = domains + // 运行目录 + root, _ := p.GetRoot() + setting.Root = root + // 默认文档 + index, _ := p.GetIndex() + setting.Index = index + // 防跨站 if io.Exists(filepath.Join(setting.Root, ".user.ini")) { userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini")) if strings.Contains(userIni, "open_basedir") { setting.OpenBasedir = true } } - + // HTTPS + if setting.HTTPS { + setting.HTTPRedirect = p.GetHTTPSRedirect() + setting.HSTS = p.GetHSTS() + setting.OCSP = p.GetOCSP() + } + // 证书 crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")) setting.SSLCertificate = crt key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")) setting.SSLCertificateKey = key - if setting.SSL { - ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束") - setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位") - setting.HSTS = strings.Contains(ssl, "# hsts标记位") - setting.OCSP = strings.Contains(ssl, "# ocsp标记位") - } - // 解析证书信息 if decode, err := cert.ParseCert(crt); err == nil { setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05") @@ -140,9 +130,10 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { setting.SSLOCSPServer = decode.OCSPServer setting.SSLDNSNames = decode.DNSNames } - + // 伪静态 rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf")) setting.Rewrite = rewrite + // 访问日志 log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name) setting.Log = log @@ -175,21 +166,58 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) { } func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { - w := &biz.Website{ - Name: req.Name, - Status: true, - Path: req.Path, - PHP: req.PHP, - SSL: false, + // 初始化nginx配置 + p, err := nginx.NewParser() + if err != nil { + return nil, err } - if err := app.Orm.Create(w).Error; err != nil { + // 监听地址 + var listens [][]string + for _, listen := range req.Listens { + listens = append(listens, []string{listen}) // ipv4 + listens = append(listens, []string{"[::]:" + listen}) // ipv6 + } + if err = p.SetListen(listens); err != nil { + return nil, err + } + // 域名 + domains, err := punycode.EncodeDomains(req.Domains) + if err != nil { + return nil, err + } + if err = p.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 + } + // 伪静态 + includes, comments, err := p.GetIncludes() + if err != nil { + return nil, err + } + includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf")) + comments = append(comments, []string{"# 伪静态规则"}) + if err = p.SetIncludes(includes, comments); err != nil { + return nil, err + } + // 日志 + if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", req.Name+".log")); err != nil { + return nil, err + } + if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", req.Name+".error.log")); err != nil { return nil, err } - if err := io.Mkdir(req.Path, 0755); err != nil { + // 初始化网站目录 + if err = io.Mkdir(req.Path, 0755); err != nil { return nil, err } - index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html")) if err != nil { return nil, fmt.Errorf("获取index模板文件失败: %w", err) @@ -197,100 +225,21 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil { return nil, err } - notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html")) if err != nil { return nil, fmt.Errorf("获取404模板文件失败: %w", err) } - if err = io.Write(req.Path+"/404.html", string(notFound), 0644); err != nil { + if err = io.Write(filepath.Join(req.Path, "404.html"), string(notFound), 0644); err != nil { return nil, err } - portList := "" - domainList := "" - portUsed := make(map[uint]bool) - domainUsed := make(map[string]bool) - - for i, port := range req.Ports { - if _, ok := portUsed[port]; !ok { - if i == len(req.Ports)-1 { - portList += " listen " + cast.ToString(port) + ";\n" - portList += " listen [::]:" + cast.ToString(port) + ";" - } else { - portList += " listen " + cast.ToString(port) + ";\n" - portList += " listen [::]:" + cast.ToString(port) + ";\n" - } - portUsed[port] = true - } - } - for _, domain := range req.Domains { - if _, ok := domainUsed[domain]; !ok { - domainList += " " + domain - domainUsed[domain] = true - } - } - - nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 -%s - # port标记位结束 - # server_name标记位开始 - server_name%s; - # server_name标记位结束 - # index标记位开始 - index index.php index.html; - # index标记位结束 - # root标记位开始 - root %s; - # root标记位结束 - - # ssl标记位开始 - # ssl标记位结束 - - # php标记位开始 - include enable-php-%d.conf; - # php标记位结束 - - # 错误页配置,可自行设置 - error_page 404 /404.html; - #error_page 502 /502.html; - - # acme证书签发配置,不可修改 - include %s/server/vhost/acme/%s.conf; - - # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 - include %s/server/vhost/rewrite/%s.conf; - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log %s/wwwlogs/%s.log; - error_log %s/wwwlogs/%s.log; -} -`, portList, domainList, req.Path, req.PHP, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name) - - if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), nginxConf, 0644); err != nil { + // 写nginx配置 + if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), p.Dump(), 0644); 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 { - return nil, err - } if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".pem"), "", 0644); err != nil { return nil, err } @@ -298,6 +247,7 @@ server return nil, err } + // 设置目录权限 if err = io.Chmod(req.Path, 0755); err != nil { return nil, err } @@ -305,11 +255,23 @@ server return nil, err } + // 创建面板网站 + w := &biz.Website{ + Name: req.Name, + Status: true, + Path: req.Path, + HTTPS: false, + } + if err = app.Orm.Create(w).Error; err != nil { + return nil, err + } + if err = systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") return nil, err } + // 创建数据库 rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword) if err == nil && req.DB && req.DBType == "mysql" { mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") @@ -344,17 +306,17 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil { return err } - if !website.Status { return errors.New("网站已停用,请先启用") } - // 原文 - raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + // 解析nginx配置 + config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) if err != nil { return err } - if strings.TrimSpace(raw) != strings.TrimSpace(req.Raw) { + // 如果修改了原文,直接写入返回 + if strings.TrimSpace(config) != strings.TrimSpace(req.Raw) { if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil { return err } @@ -362,188 +324,129 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { _, err = shell.Execf("nginx -t") return err } - return nil } - // 目录 - path := req.Path - if !io.Exists(path) { + // 初始化nginx配置 + p, err := nginx.NewParser(config) + if err != nil { + return err + } + // 监听地址 + var listens [][]string + for _, listen := range req.Listens { + listens = append(listens, []string{listen.Address}) + if listen.HTTPS { + listens = append(listens, []string{listen.Address, "ssl"}) + } + if listen.QUIC { + listens = append(listens, []string{listen.Address, "quic"}) + } + } + if err = p.SetListen(listens); err != nil { + return err + } + // 域名 + domains, err := punycode.EncodeDomains(req.Domains) + if err != nil { + return err + } + if err = p.SetServerName(domains); err != nil { + return err + } + // 首页文件 + if err = p.SetIndex(req.Index); err != nil { + return err + } + // 运行目录 + if !io.Exists(req.Root) { + return errors.New("运行目录不存在") + } + if err = p.SetRoot(req.Root); err != nil { + return err + } + // 运行目录 + if !io.Exists(req.Path) { return errors.New("网站目录不存在") } - website.Path = path - - // 域名 - domain := "server_name" - domains := req.Domains - for _, v := range domains { - if v == "" { - continue + website.Path = req.Path + // PHP + if err = p.SetPHP(req.PHP); err != nil { + return err + } + // HTTPS + certPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem") + keyPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key") + if err = io.Write(certPath, req.SSLCertificate, 0644); err != nil { + return err + } + if err = io.Write(keyPath, req.SSLCertificateKey, 0644); err != nil { + return err + } + website.HTTPS = req.HTTPS + if req.HTTPS { + if err = p.SetHTTPS(certPath, keyPath); err != nil { + return err } - domain += " " + v - } - domain += ";" - domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束") - if len(strings.TrimSpace(domainConfigOld)) == 0 { - return errors.New("配置文件中缺少server_name标记位") - } - raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1) - - // 端口 - var portConf strings.Builder - ports := req.Ports - for _, port := range ports { - https := "" - quic := false - if slices.Contains(req.SSLPorts, port) { - https = " ssl" - if slices.Contains(req.QUICPorts, port) { - quic = true - } + if err = p.SetHTTPRedirect(req.HTTPRedirect); err != nil { + return err + } + if err = p.SetHSTS(req.HSTS); err != nil { + return err + } + if err = p.SetOCSP(req.OCSP); err != nil { + return err } - portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https)) - portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https)) - if quic { - portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic")) - portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic")) + } else { + if err = p.ClearSetHTTPS(); err != nil { + return err + } + if err = p.SetHTTPRedirect(false); err != nil { + return err + } + if err = p.SetHSTS(false); err != nil { + return err + } + if err = p.SetOCSP(false); err != nil { + return err } } - portConf.WriteString(" ") - portConfNew := portConf.String() - portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束") - if len(strings.TrimSpace(portConfOld)) == 0 { - return errors.New("配置文件中缺少port标记位") - } - raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1) - - // 运行目录 - root := str.Cut(raw, "# root标记位开始", "# root标记位结束") - if len(strings.TrimSpace(root)) == 0 { - return errors.New("配置文件中缺少root标记位") - } - match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root) - if len(match) != 2 { - return errors.New("配置文件中root标记位格式错误") - } - rootNew := strings.Replace(root, match[1], req.Root, -1) - raw = strings.Replace(raw, root, rootNew, -1) - - // 默认文件 - index := str.Cut(raw, "# index标记位开始", "# index标记位结束") - if len(strings.TrimSpace(index)) == 0 { - return errors.New("配置文件中缺少index标记位") - } - match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index) - if len(match) != 2 { - return errors.New("配置文件中index标记位格式错误") - } - indexNew := strings.Replace(index, match[1], req.Index, -1) - raw = strings.Replace(raw, index, indexNew, -1) - // 防跨站 if !strings.HasSuffix(req.Root, "/") { req.Root += "/" } + userIni := filepath.Join(req.Root, ".user.ini") if req.OpenBasedir { - if err = io.Write(req.Root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil { + if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Root), 0644); err != nil { return err } + _, _ = shell.Execf(`chattr +i '%s'`, userIni) } else { - if io.Exists(req.Root + ".user.ini") { - if err = io.Remove(req.Root + ".user.ini"); err != nil { + if io.Exists(userIni) { + _, _ = shell.Execf(`chattr -i '%s'`, userIni) + if err = io.Remove(userIni); err != nil { return err } } } - // SSL - if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"), req.SSLCertificate, 0644); err != nil { - return err - } - if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"), req.SSLCertificateKey, 0644); err != nil { - return err - } - website.SSL = req.SSL - if req.SSL { - if _, err = cert.ParseCert(req.SSLCertificate); err != nil { - return errors.New("TLS证书格式错误") - } - if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil { - return errors.New("TLS私钥格式错误") - } - sslConfig := fmt.Sprintf(`# ssl标记位开始 - ssl_certificate %s/server/vhost/ssl/%s.pem; - ssl_certificate_key %s/server/vhost/ssl/%s.key; - ssl_session_timeout 1d; - ssl_session_cache shared:SSL:10m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; - ssl_early_data on; - `, app.Root, website.Name, app.Root, website.Name) - if req.HTTPRedirect { - sslConfig += `# http重定向标记位开始 - if ($server_port !~ 443){ - return 301 https://$host$request_uri; - } - error_page 497 https://$host$request_uri; - # http重定向标记位结束 - ` - } - if req.HSTS { - sslConfig += `# hsts标记位开始 - add_header Strict-Transport-Security "max-age=63072000" always; - # hsts标记位结束 - ` - } - if req.OCSP { - sslConfig += `# ocsp标记位开始 - ssl_stapling on; - ssl_stapling_verify on; - # ocsp标记位结束 - ` - } - sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") - if len(strings.TrimSpace(sslConfigOld)) != 0 { - raw = strings.Replace(raw, sslConfigOld, "", -1) - } - raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1) - } else { - sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") - if len(strings.TrimSpace(sslConfigOld)) != 0 { - raw = strings.Replace(raw, sslConfigOld, "\n ", -1) - } - } - - if website.PHP != req.PHP { - website.PHP = req.PHP - phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束") - phpConfig := ` - include enable-php-` + strconv.Itoa(website.PHP) + `.conf; - ` - if len(strings.TrimSpace(phpConfigOld)) != 0 { - raw = strings.Replace(raw, phpConfigOld, phpConfig, -1) - } - } - - if err = app.Orm.Save(website).Error; err != nil { - return err - } - - if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil { return err } if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil { return err } - err = systemctl.Reload("nginx") - if err != nil { + if err = app.Orm.Save(website).Error; err != nil { + return err + } + + if err = systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") } - return err + return nil } func (r *websiteRepo) Delete(req *request.WebsiteDelete) error { @@ -551,15 +454,10 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error { if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil { return err } - if website.Cert != nil { return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书") } - if err := app.Orm.Delete(website).Error; err != nil { - return err - } - _ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) _ = io.Remove(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf")) _ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf")) @@ -584,12 +482,15 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error { _, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) } - err := systemctl.Reload("nginx") - if err != nil { + if err := app.Orm.Delete(website).Error; err != nil { + return err + } + + if err := systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") } - return err + return nil } func (r *websiteRepo) ClearLog(id uint) error { @@ -618,75 +519,47 @@ func (r *websiteRepo) ResetConfig(id uint) error { return err } + // 初始化nginx配置 + p, err := nginx.NewParser() + if err != nil { + return err + } + // 运行目录 + if err = p.SetRoot(website.Path); err != nil { + return err + } + // 伪静态 + includes, comments, err := p.GetIncludes() + if err != nil { + return err + } + includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf")) + comments = append(comments, []string{"# 伪静态规则"}) + if err = p.SetIncludes(includes, comments); err != nil { + return err + } + // 日志 + if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil { + return err + } + if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", website.Name+".error.log")); err != nil { + return err + } + + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil { + return nil + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil { + return nil + } + website.Status = true - website.SSL = false + website.HTTPS = false if err := app.Orm.Save(website).Error; err != nil { return err } - raw := fmt.Sprintf(` -# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 - listen 80; - # port标记位结束 - # server_name标记位开始 - server_name localhost; - # server_name标记位结束 - # index标记位开始 - index index.php index.html; - # index标记位结束 - # root标记位开始 - root %s; - # root标记位结束 - - # ssl标记位开始 - # ssl标记位结束 - - # php标记位开始 - include enable-php-%d.conf; - # php标记位结束 - - # 错误页配置,可自行设置 - error_page 404 /404.html; - #error_page 502 /502.html; - - # acme证书签发配置,不可修改 - include %s/server/vhost/acme/%s.conf; - - # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 - include %s/server/vhost/rewrite/%s.conf; - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log %s/wwwlogs/%s.log; - error_log %s/wwwlogs/%s.log; -} - -`, website.Path, website.PHP, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name) - if err := io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { - return nil - } - if err := io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil { - return nil - } - if err := io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil { - return nil - } - if err := systemctl.Reload("nginx"); err != nil { + if err = systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") return err } @@ -700,43 +573,61 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error { return err } - website.Status = status - if err := app.Orm.Save(website).Error; err != nil { + // 解析nginx配置 + config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + if err != nil { return err } - - raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + p, err := nginx.NewParser(config) if err != nil { return err } - // 运行目录 - rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束") - match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig) - if len(match) == 2 { - if website.Status { - root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig) - raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s;\n ", root[1])) - } else { - raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s/server/nginx/html;\n # root %s;\n ", app.Root, match[1])) - } - } - - // 默认文件 - indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束") - match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig) - if len(match) == 2 { - if website.Status { - index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig) - raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ") - } else { - raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ") - } - } - - if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + // 取运行目录和默认文档 + root, rootComment, err := p.GetRootWithComment() + if err != nil { return err } + index, indexComment, err := p.GetIndexWithComment() + if err != nil { + return err + } + indexStr := strings.Join(index, " ") + + if status { + if len(rootComment) == 0 { + return fmt.Errorf("未找到运行目录注释") + } + if !io.Exists(rootComment[0]) { + return fmt.Errorf("运行目录不存在") + } + if err = p.SetRoot(rootComment[0]); err != nil { + return err + } + if len(indexComment) == 0 { + return fmt.Errorf("未找到默认文档注释") + } + if err = p.SetIndex(strings.Fields(indexStr)); err != nil { + return err + } + } else { + if err = p.SetRootWithComment(filepath.Join(app.Root, "server/nginx/html"), []string{root}); err != nil { + return err + } + if err = p.SetIndexWithComment([]string{"stop.html"}, []string{indexStr}); err != nil { + return err + } + } + + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil { + return err + } + + website.Status = status + if err = app.Orm.Save(website).Error; err != nil { + return err + } + if err = systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") return err diff --git a/internal/http/request/website.go b/internal/http/request/website.go index 7994fe6b..96b54ff9 100644 --- a/internal/http/request/website.go +++ b/internal/http/request/website.go @@ -1,5 +1,7 @@ package request +import "github.com/TheTNB/panel/pkg/types" + type WebsiteDefaultConfig struct { Index string `json:"index" form:"index" validate:"required"` Stop string `json:"stop" form:"stop" validate:"required"` @@ -7,10 +9,10 @@ type WebsiteDefaultConfig struct { type WebsiteCreate struct { Name string `form:"name" json:"name" validate:"required"` + Listens []string `form:"listens" json:"listens" validate:"required"` Domains []string `form:"domains" json:"domains" validate:"required"` - Ports []uint `form:"ports" json:"ports" validate:"required"` Path string `form:"path" json:"path"` - PHP int `form:"php" json:"php"` + PHP int `form:"php" json:"php" validate:"required,number,gte=0"` DB bool `form:"db" json:"db"` DBType string `form:"db_type" json:"db_type"` DBName string `form:"db_name" json:"db_name"` @@ -25,24 +27,22 @@ type WebsiteDelete struct { } type WebsiteUpdate struct { - ID uint `form:"id" json:"id" validate:"required"` - Domains []string `form:"domains" json:"domains" validate:"required"` - Listens []string `form:"listens" json:"listens" validate:"required"` - SSLPorts []uint `form:"ssl_ports" json:"ssl_ports" validate:"required"` - QUICPorts []uint `form:"quic_ports" json:"quic_ports" validate:"required"` - OCSP bool `form:"ocsp" json:"ocsp"` - HSTS bool `form:"hsts" json:"hsts"` - SSL bool `form:"ssl" json:"ssl"` - HTTPRedirect bool `form:"http_redirect" json:"http_redirect"` - OpenBasedir bool `form:"open_basedir" json:"open_basedir"` - Index string `form:"index" json:"index" validate:"required"` - Path string `form:"path" json:"path" validate:"required"` - Root string `form:"root" json:"root" validate:"required"` - Raw string `form:"raw" json:"raw"` - Rewrite string `form:"rewrite" json:"rewrite"` - PHP int `form:"php" json:"php"` - SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"` - SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"` + ID uint `form:"id" json:"id" validate:"required"` + Listens []types.WebsiteListen `form:"listens" json:"listens" validate:"required"` + Domains []string `form:"domains" json:"domains" validate:"required"` + HTTPS bool `form:"https" json:"https"` + OCSP bool `form:"ocsp" json:"ocsp"` + HSTS bool `form:"hsts" json:"hsts"` + HTTPRedirect bool `form:"http_redirect" json:"http_redirect"` + OpenBasedir bool `form:"open_basedir" json:"open_basedir"` + Index []string `form:"index" json:"index" validate:"required"` + Path string `form:"path" json:"path" validate:"required"` // 网站目录 + Root string `form:"root" json:"root" validate:"required"` // 运行目录 + Raw string `form:"raw" json:"raw"` + Rewrite string `form:"rewrite" json:"rewrite"` + PHP int `form:"php" json:"php"` + SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"` + SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"` } type WebsiteUpdateRemark struct { diff --git a/internal/route/cli.go b/internal/route/cli.go index 9e58b457..768d03ea 100644 --- a/internal/route/cli.go +++ b/internal/route/cli.go @@ -108,15 +108,15 @@ func Cli() []*cli.Command { Required: true, }, &cli.StringSliceFlag{ - Name: "domains", + Name: "domain", Usage: "与网站关联的域名列表", Aliases: []string{"d"}, Required: true, }, - &cli.UintSliceFlag{ - Name: "ports", - Usage: "网站使用的端口列表", - Aliases: []string{"p"}, + &cli.StringSliceFlag{ + Name: "listen", + Usage: "与网站关联的监听地址列表", + Aliases: []string{"l"}, Required: true, }, &cli.StringFlag{ diff --git a/internal/service/cli.go b/internal/service/cli.go index 6dc4373a..3432c001 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "time" "github.com/go-rat/utils/hash" @@ -350,16 +351,17 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error { func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error { var ports []uint - for _, port := range cmd.IntSlice("ports") { + for port := range slices.Values(cmd.IntSlice("ports")) { if port < 1 || port > 65535 { return fmt.Errorf("端口范围错误") } ports = append(ports, uint(port)) } + req := &request.WebsiteCreate{ Name: cmd.String("name"), Domains: cmd.StringSlice("domains"), - Ports: ports, + Listens: cmd.StringSlice("listen"), Path: cmd.String("path"), PHP: int(cmd.Int("php")), DB: false, diff --git a/pkg/nginx/data.go b/pkg/nginx/data.go index d1db5687..5b804fb1 100644 --- a/pkg/nginx/data.go +++ b/pkg/nginx/data.go @@ -11,8 +11,6 @@ const defaultConf = `server { root /www/wwwroot/default; # 错误页配置 error_page 404 /404.html; - # 伪静态规则 - include /www/server/vhost/rewrite/default.conf; include enable-php-0.conf; # 不记录静态文件日志 location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ { diff --git a/pkg/nginx/getter.go b/pkg/nginx/getter.go index 81c045b6..a869963f 100644 --- a/pkg/nginx/getter.go +++ b/pkg/nginx/getter.go @@ -38,6 +38,15 @@ func (p *Parser) GetIndex() ([]string, error) { return directive.GetParameters(), nil } +func (p *Parser) GetIndexWithComment() ([]string, []string, error) { + directive, err := p.FindOne("server.index") + if err != nil { + return nil, nil, err + } + + return directive.GetParameters(), directive.GetComment(), nil +} + func (p *Parser) GetRoot() (string, error) { directive, err := p.FindOne("server.root") if err != nil { @@ -50,6 +59,18 @@ func (p *Parser) GetRoot() (string, error) { return directive.GetParameters()[0], nil } +func (p *Parser) GetRootWithComment() (string, []string, error) { + directive, err := p.FindOne("server.root") + if err != nil { + return "", nil, err + } + if len(directive.GetParameters()) == 0 { + return "", directive.GetComment(), nil + } + + return directive.GetParameters()[0], directive.GetComment(), nil +} + func (p *Parser) GetIncludes() (includes []string, comments [][]string, err error) { directives, err := p.Find("server.include") if err != nil { @@ -67,10 +88,10 @@ func (p *Parser) GetIncludes() (includes []string, comments [][]string, err erro return includes, comments, nil } -func (p *Parser) GetPHP() (int, error) { +func (p *Parser) GetPHP() int { directives, err := p.Find("server.include") if err != nil { - return 0, err + return 0 } var result int @@ -82,7 +103,7 @@ func (p *Parser) GetPHP() (int, error) { } } - return result, err + return result } func (p *Parser) GetHTTPS() bool { @@ -145,21 +166,21 @@ func (p *Parser) GetHSTS() bool { return false } -func (p *Parser) GetHTTPSRedirect() (bool, error) { +func (p *Parser) GetHTTPSRedirect() bool { directives, err := p.Find("server.if") if err != nil { - return false, err + return false } for _, dir := range directives { for _, dir2 := range dir.GetBlock().GetDirectives() { if dir2.GetName() == "return" && slices.Contains(dir2.GetParameters(), "https://$host$request_uri") { - return true, nil + return true } } } - return false, nil + return false } func (p *Parser) GetAccessLog() (string, error) { diff --git a/pkg/nginx/parser_test.go b/pkg/nginx/parser_test.go index a3057a71..f26dda19 100644 --- a/pkg/nginx/parser_test.go +++ b/pkg/nginx/parser_test.go @@ -68,8 +68,8 @@ func (s *NginxTestSuite) TestIncludes() { s.NoError(err) includes, comments, err := parser.GetIncludes() s.NoError(err) - s.Equal([]string{"/www/server/vhost/rewrite/default.conf", "enable-php-0.conf"}, includes) - s.Equal([][]string{{"# 伪静态规则"}, []string(nil)}, comments) + s.Equal([]string{"enable-php-0.conf"}, includes) + s.Equal([][]string{[]string(nil)}, comments) s.NoError(parser.SetIncludes([]string{"/www/server/vhost/rewrite/default.conf"}, nil)) includes, comments, err = parser.GetIncludes() s.NoError(err) @@ -82,6 +82,16 @@ 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) @@ -126,6 +136,8 @@ func (s *NginxTestSuite) TestOCSP() { s.NoError(err) s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key")) s.False(parser.GetOCSP()) + s.NoError(parser.SetOCSP(false)) + s.False(parser.GetOCSP()) s.NoError(parser.SetOCSP(true)) s.True(parser.GetOCSP()) s.NoError(parser.SetOCSP(false)) @@ -137,6 +149,8 @@ func (s *NginxTestSuite) TestHSTS() { s.NoError(err) s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key")) s.False(parser.GetHSTS()) + s.NoError(parser.SetHSTS(false)) + s.False(parser.GetHSTS()) s.NoError(parser.SetHSTS(true)) s.True(parser.GetHSTS()) s.NoError(parser.SetHSTS(false)) @@ -148,9 +162,11 @@ func (s *NginxTestSuite) TestHTTPSRedirect() { s.NoError(err) s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key")) s.False(parser.GetHTTPSRedirect()) - s.NoError(parser.SetHTTPSRedirect(true)) + s.NoError(parser.SetHTTPRedirect(false)) + s.False(parser.GetHTTPSRedirect()) + s.NoError(parser.SetHTTPRedirect(true)) s.True(parser.GetHTTPSRedirect()) - s.NoError(parser.SetHTTPSRedirect(false)) + s.NoError(parser.SetHTTPRedirect(false)) s.False(parser.GetHTTPSRedirect()) } diff --git a/pkg/nginx/setter.go b/pkg/nginx/setter.go index 172baaa6..d5c64da5 100644 --- a/pkg/nginx/setter.go +++ b/pkg/nginx/setter.go @@ -50,6 +50,20 @@ func (p *Parser) SetIndex(index []string) error { }) } +func (p *Parser) SetIndexWithComment(index []string, comment []string) error { + if err := p.Clear("server.index"); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "index", + Parameters: index, + Comment: comment, + }, + }) +} + func (p *Parser) SetRoot(root string) error { if err := p.Clear("server.root"); err != nil { return err @@ -63,6 +77,20 @@ func (p *Parser) SetRoot(root string) error { }) } +func (p *Parser) SetRootWithComment(root string, comment []string) error { + if err := p.Clear("server.root"); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "root", + Parameters: []string{root}, + Comment: comment, + }, + }) +} + func (p *Parser) SetIncludes(includes []string, comments [][]string) error { if err := p.Clear("server.include"); err != nil { return err @@ -127,7 +155,7 @@ func (p *Parser) SetPHP(php int) error { return p.Set("server", directives) } -func (p *Parser) UnSetHTTPS() error { +func (p *Parser) ClearSetHTTPS() error { if err := p.Clear("server.ssl_certificate"); err != nil { return err } @@ -157,7 +185,7 @@ func (p *Parser) UnSetHTTPS() error { } func (p *Parser) SetHTTPS(cert, key string) error { - if err := p.UnSetHTTPS(); err != nil { + if err := p.ClearSetHTTPS(); err != nil { return err } @@ -287,7 +315,7 @@ func (p *Parser) SetHSTS(hsts bool) error { return p.Set("server", directives) } -func (p *Parser) SetHTTPSRedirect(httpRedirect bool) error { +func (p *Parser) SetHTTPRedirect(httpRedirect bool) error { // if 重定向 ifs, err := p.Find("server.if") if err != nil { diff --git a/pkg/nginx/testdata/http.conf b/pkg/nginx/testdata/http.conf index 9a374281..72e0bafb 100644 --- a/pkg/nginx/testdata/http.conf +++ b/pkg/nginx/testdata/http.conf @@ -5,8 +5,6 @@ server { root /www/wwwroot/default; # 错误页配置 error_page 404 /404.html; - # 伪静态规则 - include /www/server/vhost/rewrite/default.conf; include enable-php-0.conf; # 不记录静态文件日志 location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ { diff --git a/pkg/nginx/testdata/https.conf b/pkg/nginx/testdata/https.conf index 0391313e..2769e653 100644 --- a/pkg/nginx/testdata/https.conf +++ b/pkg/nginx/testdata/https.conf @@ -13,8 +13,6 @@ server { ssl_early_data on; # 错误页配置 error_page 404 /404.html; - # 伪静态规则 - include /www/server/vhost/rewrite/default.conf; include enable-php-0.conf; # 不记录静态文件日志 location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ { diff --git a/pkg/punycode/punycode.go b/pkg/punycode/punycode.go new file mode 100644 index 00000000..e2b10af1 --- /dev/null +++ b/pkg/punycode/punycode.go @@ -0,0 +1,52 @@ +package punycode + +import ( + "fmt" + "slices" + + "golang.org/x/net/idna" +) + +// EncodeDomain 将 Unicode 域名编码为 Punycode +func EncodeDomain(domain string) (string, error) { + ascii, err := idna.ToASCII(domain) + if err != nil { + return "", fmt.Errorf("domain encode failed: %w", err) + } + return ascii, nil +} + +// EncodeDomains 将 Unicode 域名列表编码为 Punycode +func EncodeDomains(domain []string) (encoded []string, err error) { + var punycode string + for item := range slices.Values(domain) { + punycode, err = EncodeDomain(item) + if err != nil { + return nil, err + } + encoded = append(encoded, punycode) + } + return encoded, nil +} + +// DecodeDomain 将 Punycode 域名解码为 Unicode 域名 +func DecodeDomain(punycodeDomain string) (string, error) { + unicode, err := idna.ToUnicode(punycodeDomain) + if err != nil { + return "", fmt.Errorf("domain decode failed: %w", err) + } + return unicode, nil +} + +// DecodeDomains 将 Punycode 域名列表解码为 Unicode 域名 +func DecodeDomains(punycode []string) (decoded []string, err error) { + var unicode string + for item := range slices.Values(punycode) { + unicode, err = DecodeDomain(item) + if err != nil { + return nil, err + } + decoded = append(decoded, unicode) + } + return decoded, nil +} diff --git a/pkg/types/website.go b/pkg/types/website.go index b5de91cb..4c454fac 100644 --- a/pkg/types/website.go +++ b/pkg/types/website.go @@ -1,30 +1,35 @@ package types +// WebsiteListen 网站监听配置 +type WebsiteListen struct { + Address string `form:"address" json:"address" validate:"required"` // 监听地址 e.g. 80 0.0.0.0:80 [::]:80 + HTTPS bool `form:"https" json:"https" validate:"required"` // 是否启用HTTPS + QUIC bool `form:"quic" json:"quic"` // 是否启用QUIC +} + // WebsiteSetting 网站设置 type WebsiteSetting struct { - ID uint `json:"id"` - Name string `json:"name"` - Domains []string `json:"domains"` - Ports []uint `json:"ports"` - SSLPorts []uint `json:"ssl_ports"` - QUICPorts []uint `json:"quic_ports"` - Root string `json:"root"` - Path string `json:"path"` - Index string `json:"index"` - PHP int `json:"php"` - OpenBasedir bool `json:"open_basedir"` - SSL bool `json:"ssl"` - SSLCertificate string `json:"ssl_certificate"` - SSLCertificateKey string `json:"ssl_certificate_key"` - SSLNotBefore string `json:"ssl_not_before"` - SSLNotAfter string `json:"ssl_not_after"` - SSLDNSNames []string `json:"ssl_dns_names"` - SSLIssuer string `json:"ssl_issuer"` - SSLOCSPServer []string `json:"ssl_ocsp_server"` - HTTPRedirect bool `json:"http_redirect"` - HSTS bool `json:"hsts"` - OCSP bool `json:"ocsp"` - Rewrite string `json:"rewrite"` - Raw string `json:"raw"` - Log string `json:"log"` + ID uint `json:"id"` + Name string `json:"name"` + Listens []WebsiteListen `form:"listens" json:"listens" validate:"required"` + Domains []string `json:"domains"` + Path string `json:"path"` // 网站目录 + Root string `json:"root"` // 运行目录 + Index []string `json:"index"` + PHP int `json:"php"` + OpenBasedir bool `json:"open_basedir"` + HTTPS bool `json:"https"` + SSLCertificate string `json:"ssl_certificate"` + SSLCertificateKey string `json:"ssl_certificate_key"` + SSLNotBefore string `json:"ssl_not_before"` + SSLNotAfter string `json:"ssl_not_after"` + SSLDNSNames []string `json:"ssl_dns_names"` + SSLIssuer string `json:"ssl_issuer"` + SSLOCSPServer []string `json:"ssl_ocsp_server"` + HTTPRedirect bool `json:"http_redirect"` + HSTS bool `json:"hsts"` + OCSP bool `json:"ocsp"` + Rewrite string `json:"rewrite"` + Raw string `json:"raw"` + Log string `json:"log"` }