diff --git a/go.mod b/go.mod index f7b9dfcc..6af0e77d 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/shirou/gopsutil v2.21.11+incompatible github.com/spf13/cast v1.7.0 github.com/stretchr/testify v1.9.0 - github.com/tufanbarisyildirim/gonginx v0.0.0-20240907135031-d38eb71142ac + github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8 github.com/urfave/cli/v3 v3.0.0-alpha9.1 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.28.0 @@ -103,6 +103,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/samber/lo v1.47.0 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect diff --git a/go.sum b/go.sum index d4b5a16a..069cee12 100644 --- a/go.sum +++ b/go.sum @@ -271,6 +271,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U= @@ -299,6 +301,8 @@ github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYg github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/tufanbarisyildirim/gonginx v0.0.0-20240907135031-d38eb71142ac h1:IXccMEFcB+UqGWae8OF9EoA0/8GCLlDj6s84LCU7y58= github.com/tufanbarisyildirim/gonginx v0.0.0-20240907135031-d38eb71142ac/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g= +github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8 h1:7yw1yHkylDcu/CwY4hEEe8MycDLo7B9LjcqwhoL8NrM= +github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= diff --git a/internal/biz/website.go b/internal/biz/website.go index 9532038f..3295ebe2 100644 --- a/internal/biz/website.go +++ b/internal/biz/website.go @@ -12,7 +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"` - HTTPS bool `gorm:"not null" json:"https"` + 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 63fa95fd..1d7af111 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -4,9 +4,10 @@ import ( "errors" "fmt" "path/filepath" - "slices" "strings" + "github.com/samber/lo" + "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/embed" @@ -70,7 +71,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { setting.ID = website.ID setting.Name = website.Name setting.Path = website.Path - setting.HTTPS = website.HTTPS + setting.HTTPS = website.Https setting.PHP = p.GetPHP() setting.Raw = config // 监听地址 @@ -78,16 +79,28 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { if err != nil { return nil, err } - for listen := range slices.Values(listens) { - if len(listen) == 0 { - continue - } - setting.Listens = append(setting.Listens, types.WebsiteListen{ - Address: listen[0], - HTTPS: slices.Contains(listen, "ssl"), - QUIC: slices.Contains(listen, "quic"), - }) - } + setting.Listens = lo.Map( + lo.UniqBy(listens, func(listen []string) string { + if len(listen) == 0 { + return "" + } + return listen[0] + }), + func(listen []string, _ int) types.WebsiteListen { + addr := listen[0] + grouped := lo.GroupBy(listens, func(listen []string) string { + if len(listen) == 0 { + return "" + } + return listen[0] + })[addr] + return types.WebsiteListen{ + Address: addr, + HTTPS: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "ssl") }), + QUIC: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "quic") }), + } + }, + ) // 域名 domains, err := p.GetServerName() if err != nil { @@ -174,8 +187,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { // 监听地址 var listens [][]string for _, listen := range req.Listens { - listens = append(listens, []string{listen}) // ipv4 - listens = append(listens, []string{"[::]:" + listen}) // ipv6 + listens = append(listens, []string{listen}) } if err = p.SetListen(listens); err != nil { return nil, err @@ -260,7 +272,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { Name: req.Name, Status: true, Path: req.Path, - HTTPS: false, + Https: false, } if err = app.Orm.Create(w).Error; err != nil { return nil, err @@ -335,7 +347,9 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { // 监听地址 var listens [][]string for _, listen := range req.Listens { - listens = append(listens, []string{listen.Address}) + if !listen.HTTPS && !listen.QUIC { + listens = append(listens, []string{listen.Address}) + } if listen.HTTPS { listens = append(listens, []string{listen.Address, "ssl"}) } @@ -383,7 +397,7 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { if err = io.Write(keyPath, req.SSLCertificateKey, 0644); err != nil { return err } - website.HTTPS = req.HTTPS + website.Https = req.HTTPS if req.HTTPS { if err = p.SetHTTPS(certPath, keyPath); err != nil { return err @@ -418,6 +432,7 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { } userIni := filepath.Join(req.Root, ".user.ini") if req.OpenBasedir { + _, _ = shell.Execf(`chattr -i '%s'`, userIni) if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Root), 0644); err != nil { return err } @@ -444,6 +459,7 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { if err = systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") + return err } return nil @@ -488,6 +504,7 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error { if err := systemctl.Reload("nginx"); err != nil { _, err = shell.Execf("nginx -t") + return err } return nil @@ -554,7 +571,7 @@ func (r *websiteRepo) ResetConfig(id uint) error { } website.Status = true - website.HTTPS = false + website.Https = false if err := app.Orm.Save(website).Error; err != nil { return err } @@ -598,6 +615,10 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error { if len(rootComment) == 0 { return fmt.Errorf("未找到运行目录注释") } + if len(rootComment) != 1 { + return fmt.Errorf("运行目录注释数量不正确,预期1个,实际%d个", len(rootComment)) + } + rootComment[0] = strings.TrimPrefix(rootComment[0], "# ") if !io.Exists(rootComment[0]) { return fmt.Errorf("运行目录不存在") } @@ -607,14 +628,18 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error { if len(indexComment) == 0 { return fmt.Errorf("未找到默认文档注释") } - if err = p.SetIndex(strings.Fields(indexStr)); err != nil { + if len(indexComment) != 1 { + return fmt.Errorf("默认文档注释数量不正确,预期1个,实际%d个", len(indexComment)) + } + indexComment[0] = strings.TrimPrefix(indexComment[0], "# ") + if err = p.SetIndex(strings.Fields(indexComment[0])); err != nil { return err } } else { - if err = p.SetRootWithComment(filepath.Join(app.Root, "server/nginx/html"), []string{root}); err != nil { + 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 { + if err = p.SetIndexWithComment([]string{"stop.html"}, []string{"# " + indexStr}); err != nil { return err } } diff --git a/internal/http/request/cert.go b/internal/http/request/cert.go index 4e186c1c..12945ca6 100644 --- a/internal/http/request/cert.go +++ b/internal/http/request/cert.go @@ -2,7 +2,7 @@ package request type CertCreate struct { Type string `form:"type" json:"type" validate:"required"` - Domains []string `form:"domains" json:"domains" validate:"required"` + Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"` AutoRenew bool `form:"auto_renew" json:"auto_renew"` AccountID uint `form:"account_id" json:"account_id"` DNSID uint `form:"dns_id" json:"dns_id"` @@ -12,7 +12,7 @@ type CertCreate struct { type CertUpdate struct { ID uint `form:"id" json:"id" validate:"required"` Type string `form:"type" json:"type" validate:"required"` - Domains []string `form:"domains" json:"domains" validate:"required"` + Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"` AutoRenew bool `form:"auto_renew" json:"auto_renew"` AccountID uint `form:"account_id" json:"account_id"` DNSID uint `form:"dns_id" json:"dns_id"` diff --git a/internal/http/request/file.go b/internal/http/request/file.go index a309a6ef..6e76566a 100644 --- a/internal/http/request/file.go +++ b/internal/http/request/file.go @@ -34,7 +34,7 @@ type FilePermission struct { } type FileCompress struct { - Paths []string `form:"paths" json:"paths" validate:"required"` + Paths []string `form:"paths" json:"paths" validate:"min=1,dive,required"` File string `form:"file" json:"file" validate:"required"` } diff --git a/internal/http/request/website.go b/internal/http/request/website.go index 96b54ff9..238cdaa8 100644 --- a/internal/http/request/website.go +++ b/internal/http/request/website.go @@ -9,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"` + Listens []string `form:"listens" json:"listens" validate:"min=1,dive,required"` + Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"` Path string `form:"path" json:"path"` - PHP int `form:"php" json:"php" validate:"required,number,gte=0"` + PHP int `form:"php" json:"php" validate:"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"` @@ -28,21 +28,21 @@ type WebsiteDelete struct { type WebsiteUpdate struct { 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"` + Listens []types.WebsiteListen `form:"listens" json:"listens" validate:"min=1"` + Domains []string `form:"domains" json:"domains" validate:"min=1,dive,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"` + Index []string `form:"index" json:"index" validate:"min=1,dive,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"` + SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"required_if=HTTPS true"` + SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"required_if=HTTPS true"` } type WebsiteUpdateRemark struct { diff --git a/internal/route/cli.go b/internal/route/cli.go index 768d03ea..4f8ef2e2 100644 --- a/internal/route/cli.go +++ b/internal/route/cli.go @@ -108,13 +108,13 @@ func Cli() []*cli.Command { Required: true, }, &cli.StringSliceFlag{ - Name: "domain", + Name: "domains", Usage: "与网站关联的域名列表", Aliases: []string{"d"}, Required: true, }, &cli.StringSliceFlag{ - Name: "listen", + Name: "listens", Usage: "与网站关联的监听地址列表", Aliases: []string{"l"}, Required: true, diff --git a/internal/service/cli.go b/internal/service/cli.go index 3432c001..f7474efe 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "path/filepath" - "slices" "time" "github.com/go-rat/utils/hash" @@ -350,18 +349,10 @@ 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 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"), - Listens: cmd.StringSlice("listen"), + Listens: cmd.StringSlice("listens"), Path: cmd.String("path"), PHP: int(cmd.Int("php")), DB: false, diff --git a/pkg/io/path.go b/pkg/io/path.go index 8aa4c7a4..be848ebe 100644 --- a/pkg/io/path.go +++ b/pkg/io/path.go @@ -2,13 +2,14 @@ package io import ( "fmt" - "github.com/TheTNB/panel/pkg/shell" "io" "os" "os/exec" "path/filepath" "strconv" "strings" + + "github.com/TheTNB/panel/pkg/shell" ) // Remove 删除文件/目录 diff --git a/pkg/nginx/getter.go b/pkg/nginx/getter.go index a869963f..9bb8a704 100644 --- a/pkg/nginx/getter.go +++ b/pkg/nginx/getter.go @@ -99,7 +99,7 @@ func (p *Parser) GetPHP() int { if slices.ContainsFunc(dir.GetParameters(), func(s string) bool { return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf") }) { - _, err = fmt.Sscanf(dir.GetParameters()[0], "enable-php-%d.conf", &result) + _, _ = fmt.Sscanf(dir.GetParameters()[0], "enable-php-%d.conf", &result) } } diff --git a/pkg/nginx/parser_test.go b/pkg/nginx/parser_test.go index f26dda19..216f5340 100644 --- a/pkg/nginx/parser_test.go +++ b/pkg/nginx/parser_test.go @@ -3,8 +3,9 @@ package nginx import ( "testing" - "github.com/TheTNB/panel/pkg/io" "github.com/stretchr/testify/suite" + + "github.com/TheTNB/panel/pkg/io" ) type NginxTestSuite struct { @@ -51,6 +52,20 @@ func (s *NginxTestSuite) TestIndex() { s.Equal([]string{"index.html", "index.htm"}, 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.htm"}, index) + s.Equal([]string(nil), comment) + s.NoError(parser.SetIndexWithComment([]string{"index.html", "index.htm"}, []string{"# 测试"})) + index, comment, err = parser.GetIndexWithComment() + s.NoError(err) + s.Equal([]string{"index.html", "index.htm"}, index) + s.Equal([]string{"# 测试"}, comment) +} + func (s *NginxTestSuite) TestRoot() { parser, err := NewParser() s.NoError(err) @@ -63,6 +78,20 @@ func (s *NginxTestSuite) TestRoot() { s.Equal("/www/wwwroot/test", root) } +func (s *NginxTestSuite) TestRootWithComment() { + parser, err := NewParser() + s.NoError(err) + root, comment, err := parser.GetRootWithComment() + s.NoError(err) + s.Equal("/www/wwwroot/default", root) + s.Equal([]string(nil), comment) + s.NoError(parser.SetRootWithComment("/www/wwwroot/test", []string{"# 测试"})) + root, comment, err = parser.GetRootWithComment() + s.NoError(err) + s.Equal("/www/wwwroot/test", root) + s.Equal([]string{"# 测试"}, comment) +} + func (s *NginxTestSuite) TestIncludes() { parser, err := NewParser() s.NoError(err) @@ -106,7 +135,6 @@ func (s *NginxTestSuite) TestHTTPS() { s.False(parser.GetHTTPS()) s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key")) s.True(parser.GetHTTPS()) - io.Write("testdata/https.conf", parser.Dump(), 0755) expect, err := io.Read("testdata/https.conf") s.NoError(err) s.Equal(expect, parser.Dump()) diff --git a/pkg/nginx/setter.go b/pkg/nginx/setter.go index d5c64da5..995308c3 100644 --- a/pkg/nginx/setter.go +++ b/pkg/nginx/setter.go @@ -328,6 +328,11 @@ func (p *Parser) SetHTTPRedirect(httpRedirect bool) error { var directives []*config.Directive var foundFlag bool for _, dir := range ifs { // 所有 if + if !httpRedirect { + if len(dir.GetParameters()) == 3 && dir.GetParameters()[0] == "($scheme" && dir.GetParameters()[1] == "=" && dir.GetParameters()[2] == "http)" { + continue + } + } var ifDirectives []config.IDirective for _, dir2 := range dir.GetBlock().GetDirectives() { // 每个 if 中所有指令 if !httpRedirect { diff --git a/web/src/api/panel/website/index.ts b/web/src/api/panel/website/index.ts index a9c9678c..72dc9edd 100644 --- a/web/src/api/panel/website/index.ts +++ b/web/src/api/panel/website/index.ts @@ -9,7 +9,8 @@ export default { // 创建 create: (data: any): Promise> => request.post('/website', data), // 删除 - delete: (data: any): Promise> => request.post('/website/' + data.id, data), + delete: (id: number, path: boolean, db: boolean): Promise> => + request.delete(`/website/${id}`, { params: { path, db } }), // 获取默认配置 defaultConfig: (): Promise> => request.get('/website/defaultConfig'), // 保存默认配置 diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue index 15ffed62..371742aa 100644 --- a/web/src/views/website/EditView.vue +++ b/web/src/views/website/EditView.vue @@ -4,7 +4,7 @@ import { NButton } from 'naive-ui' import info from '@/api/panel/info' import website from '@/api/panel/website' -import type { WebsiteSetting } from '@/views/website/types' +import type { WebsiteListen, WebsiteSetting } from '@/views/website/types' const route = useRoute() const { id } = route.params @@ -12,16 +12,14 @@ const { id } = route.params const setting = ref({ id: 0, name: '', - ports: [], - ssl_ports: [], - quic_ports: [], + listens: [] as WebsiteListen[], domains: [], root: '', path: '', - index: '', + index: [], php: 0, open_basedir: false, - ssl: false, + https: false, ssl_certificate: '', ssl_certificate_key: '', ssl_not_before: '', @@ -63,16 +61,23 @@ const getWebsiteSetting = async () => { } const handleSave = async () => { - if (setting.value.ssl) { - if (setting.value.ssl_certificate == '') { - window.$message.error('请填写证书') - return - } - if (setting.value.ssl_certificate_key == '') { - window.$message.error('请填写私钥') - return - } + // 如果没有任何监听地址设置了https,则自动添加443 + if (setting.value.https && !setting.value.listens.some((item) => item.https)) { + setting.value.listens.push({ + address: '443', + https: true, + quic: true + }) } + // 如果关闭了https,自动禁用所有https和quic + if (!setting.value.https) { + setting.value.listens = setting.value.listens.filter((item) => item.address !== '443') // 443直接删掉 + setting.value.listens.forEach((item) => { + item.https = false + item.quic = false + }) + } + await website.saveConfig(Number(id), setting.value).then(() => { getWebsiteSetting() window.$message.success('保存成功') @@ -100,6 +105,14 @@ const title = computed(() => { return '编辑网站 - 加载中...' }) +const onCreateListen = () => { + return { + address: '', + https: false, + quic: false + } +} + onMounted(() => { getWebsiteSetting() getPhpAndDb() @@ -119,7 +132,7 @@ onMounted(() => { - + { show-sort-button /> - - -