From 1bea44146ffaebafc7327e4ae6c8756bae9f9bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Mon, 14 Oct 2024 03:32:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E6=AD=A5=E5=AE=9E=E7=8E=B0ngi?= =?UTF-8?q?nx=E9=85=8D=E7=BD=AE=E8=A7=A3=E6=9E=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 2 + internal/http/request/website.go | 2 +- pkg/nginx/data.go | 36 ++++ pkg/nginx/getter.go | 163 +++++++++++++++ pkg/nginx/parser.go | 170 +++++++++++++++ pkg/nginx/parser_test.go | 26 +++ pkg/nginx/setter.go | 346 +++++++++++++++++++++++++++++++ 8 files changed, 745 insertions(+), 2 deletions(-) create mode 100644 pkg/nginx/data.go create mode 100644 pkg/nginx/getter.go create mode 100644 pkg/nginx/parser.go create mode 100644 pkg/nginx/parser_test.go create mode 100644 pkg/nginx/setter.go diff --git a/go.mod b/go.mod index f514a7bd..cb01100c 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +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/urfave/cli/v3 v3.0.0-alpha9 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.28.0 @@ -125,7 +126,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.1 // indirect modernc.org/libc v1.60.1 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect diff --git a/go.sum b/go.sum index 0edc1503..7ac928c1 100644 --- a/go.sum +++ b/go.sum @@ -297,6 +297,8 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= 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/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/http/request/website.go b/internal/http/request/website.go index 7976f0ae..7994fe6b 100644 --- a/internal/http/request/website.go +++ b/internal/http/request/website.go @@ -27,7 +27,7 @@ type WebsiteDelete struct { type WebsiteUpdate struct { ID uint `form:"id" json:"id" validate:"required"` Domains []string `form:"domains" json:"domains" validate:"required"` - Ports []uint `form:"ports" json:"ports" 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"` diff --git a/pkg/nginx/data.go b/pkg/nginx/data.go new file mode 100644 index 00000000..b56f3993 --- /dev/null +++ b/pkg/nginx/data.go @@ -0,0 +1,36 @@ +package nginx + +var order = []string{"listen", "server_name", "index", "root", + "ssl_certificate", "ssl_certificate_key", "ssl_session_timeout", "ssl_session_cache", "ssl_protocols", "ssl_ciphers", "ssl_prefer_server_ciphers", "ssl_early_data", "ssl_stapling", "ssl_stapling_verify", "ssl_trusted_certificate", + "resolver", "error_page", "include", "if", "location", "access_log", "error_log"} + +const defaultConf = `server +{ + listen 80; + server_name localhost; + index index.php index.html; + root /www/wwwroot/default; + # 错误页配置,可自行修改 + #error_page 502 /502.html; + error_page 404 /404.html; + include enable-php-0.conf; + # acme证书签发配置,不可修改 + include /www/server/vhost/acme/test.conf; + # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 + include /www/server/vhost/rewrite/test.conf; + # 禁止访问部分敏感目录,可自行修改 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) + { + return 404; + } + # 不记录静态资源的访问日志,可自行修改 + location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ + { + expires 1h; + error_log /dev/null; + access_log /dev/null; + } + access_log /www/wwwlogs/default.log; + error_log /www/wwwlogs/default.log; +} +` diff --git a/pkg/nginx/getter.go b/pkg/nginx/getter.go new file mode 100644 index 00000000..bb32272b --- /dev/null +++ b/pkg/nginx/getter.go @@ -0,0 +1,163 @@ +package nginx + +import ( + "fmt" + "slices" + "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, 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 directive.GetParameters(), nil +} + +func (p *Parser) GetIndex() ([]string, error) { + directive, err := p.FindOne("server.index") + if err != nil { + return nil, err + } + + return directive.GetParameters(), nil +} + +func (p *Parser) GetRoot() (string, error) { + directive, err := p.FindOne("server.root") + if err != nil { + return "", err + } + if len(directive.GetParameters()) == 0 { + return "", nil + } + + return directive.GetParameters()[0], nil +} + +func (p *Parser) GetIncludes() ([]string, error) { + directives, err := p.Find("server.include") + if err != nil { + return nil, err + } + + var result []string + for _, dir := range directives { + result = append(result, dir.GetParameters()...) + } + + return result, nil +} + +func (p *Parser) GetPHP() (int, error) { + directives, err := p.Find("server.include") + if err != nil { + return 0, err + } + + var result int + for _, dir := range directives { + 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) + } + } + + return result, err +} + +func (p *Parser) GetHTTPS() bool { + directive, err := p.FindOne("server.ssl_certificate") + if err != nil { + return false + } + if len(directive.GetParameters()) == 0 { + return false + } + + return true +} + +func (p *Parser) GetOCSP() (bool, error) { + directive, err := p.FindOne("server.ssl_stapling") + if err != nil { + return false, err + } + if len(directive.GetParameters()) == 0 { + return false, nil + } + + return directive.GetParameters()[0] == "on", nil +} + +func (p *Parser) GetHSTS() (bool, error) { + directives, err := p.Find("server.add_header") + if err != nil { + return false, err + } + + for _, dir := range directives { + if slices.Contains(dir.GetParameters(), "Strict-Transport-Security") { + return true, nil + } + } + + return false, nil +} + +func (p *Parser) GetHTTPRedirect() (bool, error) { + directives, err := p.Find("server.if") + if err != nil { + return false, err + } + + 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 false, nil +} + +func (p *Parser) GetAccessLog() (string, error) { + directive, err := p.FindOne("server.access_log") + if err != nil { + return "", err + } + if len(directive.GetParameters()) == 0 { + return "", nil + } + + return directive.GetParameters()[0], nil +} + +func (p *Parser) GetErrorLog() (string, error) { + directive, err := p.FindOne("server.error_log") + if err != nil { + return "", err + } + if len(directive.GetParameters()) == 0 { + return "", nil + } + + return directive.GetParameters()[0], nil +} diff --git a/pkg/nginx/parser.go b/pkg/nginx/parser.go new file mode 100644 index 00000000..2f781f0e --- /dev/null +++ b/pkg/nginx/parser.go @@ -0,0 +1,170 @@ +package nginx + +import ( + "errors" + "slices" + "strings" + + "github.com/tufanbarisyildirim/gonginx/config" + "github.com/tufanbarisyildirim/gonginx/dumper" + "github.com/tufanbarisyildirim/gonginx/parser" +) + +// Parser Nginx vhost 配置解析器 +type Parser struct { + c *config.Config + orderIndex map[string]int +} + +func NewParser(str ...string) (*Parser, error) { + if len(str) == 0 { + str = append(str, defaultConf) + } + p := parser.NewStringParser(str[0], parser.WithSkipIncludeParsingErr(), parser.WithSkipValidDirectivesErr()) + c, err := p.Parse() + if err != nil { + return nil, err + } + + orderIndex := make(map[string]int) + for i, name := range order { + orderIndex[name] = i + } + + return &Parser{c: c, orderIndex: orderIndex}, nil +} + +func (p *Parser) Config() *config.Config { + return p.c +} + +// Find 通过表达式查找配置 +// eg: Find("server.listen") +func (p *Parser) Find(key string) ([]config.IDirective, error) { + parts := strings.Split(key, ".") + var block *config.Block + var ok bool + block = p.c.Block + for i := 0; i < len(parts)-1; i++ { + key = parts[i] + directives := block.FindDirectives(key) + if len(directives) == 0 { + return nil, errors.New("given key not found") + } + if len(directives) > 1 { + return nil, errors.New("multiple directives found") + } + block, ok = directives[0].GetBlock().(*config.Block) + if !ok { + return nil, errors.New("block is not *config.Block") + } + } + + var result []config.IDirective + for _, dir := range block.GetDirectives() { + if dir.GetName() == parts[len(parts)-1] { + result = append(result, dir) + } + } + + return result, nil +} + +// FindOne 通过表达式查找一个配置 +// eg: FindOne("server.server_name") +func (p *Parser) FindOne(key string) (config.IDirective, error) { + directives, err := p.Find(key) + if err != nil { + return nil, err + } + if len(directives) == 0 { + return nil, errors.New("given key not found") + } + + return directives[0], nil +} + +// Clear 通过表达式移除配置 +// eg: Clear("server.server_name") +func (p *Parser) Clear(key string) error { + parts := strings.Split(key, ".") + last := parts[len(parts)-1] + parts = parts[:len(parts)-1] + + var block *config.Block + var ok bool + block = p.c.Block + for i := 0; i < len(parts); i++ { + directives := block.FindDirectives(parts[i]) + if len(directives) == 0 { + return errors.New("given key not found") + } + if len(directives) > 1 { + return errors.New("multiple directives found") + } + block, ok = directives[0].GetBlock().(*config.Block) + if !ok { + return errors.New("block is not *config.Block") + } + } + + var newDirectives []config.IDirective + for _, directive := range block.GetDirectives() { + if directive.GetName() != last { + newDirectives = append(newDirectives, directive) + } + } + block.Directives = newDirectives + + return nil +} + +// Set 通过表达式设置配置 +// eg: Set("server.server_name", []directive) +func (p *Parser) Set(key string, directives []*config.Directive) error { + parts := strings.Split(key, ".") + + var block *config.Block + var ok bool + block = p.c.Block + for i := 0; i < len(parts); i++ { + sub := block.FindDirectives(parts[i]) + if len(sub) == 0 { + return errors.New("given key not found") + } + if len(sub) > 1 { + return errors.New("multiple directives found") + } + block, ok = sub[0].GetBlock().(*config.Block) + if !ok { + return errors.New("block is not *config.Block") + } + } + + for _, directive := range directives { + directive.SetParent(block) + block.Directives = append(block.Directives, directive) + } + + return nil +} + +func (p *Parser) Sort() { + p.sortDirectives(p.c.Directives, p.orderIndex) +} + +func (p *Parser) Dump() string { + p.Sort() + return dumper.DumpConfig(p.c, dumper.IndentedStyle) +} + +func (p *Parser) sortDirectives(directives []config.IDirective, orderIndex map[string]int) { + slices.SortFunc(directives, func(a config.IDirective, b config.IDirective) int { + return orderIndex[a.GetName()] - orderIndex[b.GetName()] + }) + for _, directive := range directives { + if block, ok := directive.GetBlock().(*config.Block); ok { + p.sortDirectives(block.Directives, orderIndex) + } + } +} diff --git a/pkg/nginx/parser_test.go b/pkg/nginx/parser_test.go new file mode 100644 index 00000000..09018cae --- /dev/null +++ b/pkg/nginx/parser_test.go @@ -0,0 +1,26 @@ +package nginx + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type NginxTestSuite struct { + parser *Parser + suite.Suite +} + +func TestNginxTestSuite(t *testing.T) { + parser, err := NewParser() + if err != nil { + t.Errorf("parse error %v", err) + } + suite.Run(t, &NginxTestSuite{ + parser: parser, + }) +} + +func (suite *NginxTestSuite) TestA() { + suite.NoError(suite.parser.SetPHP(81)) +} diff --git a/pkg/nginx/setter.go b/pkg/nginx/setter.go new file mode 100644 index 00000000..3d716be8 --- /dev/null +++ b/pkg/nginx/setter.go @@ -0,0 +1,346 @@ +package nginx + +import ( + "fmt" + "slices" + "strings" + + "github.com/tufanbarisyildirim/gonginx/config" +) + +func (p *Parser) SetListen(listen [][]string) error { + var directives []*config.Directive + for _, l := range listen { + directives = append(directives, &config.Directive{ + Name: "listen", + Parameters: l, + }) + } + + if err := p.Clear("server.listen"); err != nil { + return err + } + + return p.Set("server", directives) +} + +func (p *Parser) SetServerName(serverName []string) error { + if err := p.Clear("server.server_name"); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "server_name", + Parameters: serverName, + }, + }) +} + +func (p *Parser) SetIndex(index []string) error { + if err := p.Clear("server.index"); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "index", + Parameters: index, + }, + }) +} + +func (p *Parser) SetRoot(root string) error { + if err := p.Clear("server.root"); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "root", + Parameters: []string{root}, + }, + }) +} + +func (p *Parser) SetIncludes(includes []string) error { + if err := p.Clear("server.include"); err != nil { + return err + } + + var directives []*config.Directive + for _, i := range includes { + directives = append(directives, &config.Directive{ + Name: "include", + Parameters: []string{i}, + }) + } + + return p.Set("server", directives) +} + +func (p *Parser) SetPHP(php int) error { + old, err := p.Find("server.include") + if err != nil { + return err + } + if err = p.Clear("server.include"); err != nil { + return err + } + + var directives []*config.Directive + var foundFlag bool + for _, item := range old { + // 查找enable-php的配置 + if slices.ContainsFunc(item.GetParameters(), func(s string) bool { + return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf") + }) { + foundFlag = true + directives = append(directives, &config.Directive{ + Name: item.GetName(), + Parameters: []string{fmt.Sprintf("enable-php-%d.conf", php)}, + Comment: item.GetComment(), + }) + } else { + // 其余的原样保留 + directives = append(directives, &config.Directive{ + Name: item.GetName(), + Parameters: item.GetParameters(), + Comment: item.GetComment(), + }) + } + } + + // 如果没有找到enable-php的配置,直接添加一个 + if !foundFlag { + directives = append(directives, &config.Directive{ + Name: "include", + Parameters: []string{fmt.Sprintf("enable-php-%d.conf", php)}, + }) + } + + return p.Set("server", directives) +} + +func (p *Parser) UnSetHTTPS() error { + if err := p.Clear("server.ssl_certificate"); err != nil { + return err + } + if err := p.Clear("server.ssl_certificate_key"); err != nil { + return err + } + if err := p.Clear("server.ssl_session_timeout"); err != nil { + return err + } + if err := p.Clear("server.ssl_session_cache"); err != nil { + return err + } + if err := p.Clear("server.ssl_protocols"); err != nil { + return err + } + if err := p.Clear("server.ssl_ciphers"); err != nil { + return err + } + if err := p.Clear("server.ssl_prefer_server_ciphers"); err != nil { + return err + } + if err := p.Clear("server.ssl_early_data"); err != nil { + return err + } + + return nil +} + +func (p *Parser) SetHTTPS(cert, key string) error { + if err := p.UnSetHTTPS(); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "ssl_certificate", + Parameters: []string{cert}, + Comment: []string{"# https配置"}, + }, + { + Name: "ssl_certificate_key", + Parameters: []string{key}, + }, + { + Name: "ssl_session_timeout", + Parameters: []string{"1d"}, + }, + { + Name: "ssl_session_cache", + Parameters: []string{"shared:SSL:10m"}, + }, + { + Name: "ssl_protocols", + Parameters: []string{"TLSv1.2", "TLSv1.3"}, + }, + { + Name: "ssl_ciphers", + Parameters: []string{"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:DHE-RSA-CHACHA20-POLY1305"}, + }, + { + Name: "ssl_prefer_server_ciphers", + Parameters: []string{"off"}, + }, + { + Name: "ssl_early_data", + Parameters: []string{"on"}, + }, + }) +} + +func (p *Parser) SetOCSP(ocsp bool) error { + if err := p.Clear("server.ssl_stapling"); err != nil { + return err + } + if err := p.Clear("server.ssl_stapling_verify"); err != nil { + return err + } + + if ocsp { + return p.Set("server", []*config.Directive{ + { + Name: "ssl_stapling", + Parameters: []string{"on"}, + }, + { + Name: "ssl_stapling_verify", + Parameters: []string{"on"}, + }, + }) + } + + return nil +} + +func (p *Parser) SetHSTS(hsts bool) error { + old, err := p.Find("server.add_header") + if err != nil { + return err + } + if err = p.Clear("server.add_header"); err != nil { + return err + } + + var directives []*config.Directive + var foundFlag bool + for _, dir := range old { + if slices.Contains(dir.GetParameters(), "Strict-Transport-Security") { + foundFlag = true + if hsts { + directives = append(directives, &config.Directive{ + Name: dir.GetName(), + Parameters: []string{"Strict-Transport-Security", "max-age=31536000"}, + Comment: dir.GetComment(), + }) + } + } else { + directives = append(directives, &config.Directive{ + Name: dir.GetName(), + Parameters: dir.GetParameters(), + Comment: dir.GetComment(), + }) + } + } + + if !foundFlag && hsts { + directives = append(directives, &config.Directive{ + Name: "add_header", + Parameters: []string{"Strict-Transport-Security", "max-age=31536000"}, + Comment: []string{"# hsts配置"}, + }) + } + + return p.Set("server", directives) +} + +func (p *Parser) SetHTTPRedirect(httpRedirect bool) error { + old, err := p.Find("server.if") + if err != nil { + return err + } + if err = p.Clear("server.if"); err != nil { + return err + } + + var directives []*config.Directive + var foundFlag bool + for _, dir := range old { + for _, dir2 := range dir.GetBlock().GetDirectives() { // if 中所有指令 + if block, ok := dir2.GetBlock().(*config.Block); ok { + var newDirectives []config.IDirective + for _, directive := range block.GetDirectives() { + if !httpRedirect { + // 不启用http重定向,则判断并移除特定的return指令 + if directive.GetName() != "return" && !slices.Contains(directive.GetParameters(), "https://$host$request_uri") { + newDirectives = append(newDirectives, directive) + } + } else { + // 启用http重定向,需要检查防止重复添加 + if directive.GetName() == "return" && slices.Contains(directive.GetParameters(), "https://$host$request_uri") { + foundFlag = true + } + newDirectives = append(newDirectives, directive) + } + + } + block.Directives = newDirectives + } + } + directives = append(directives, &config.Directive{ + Block: dir.GetBlock(), + Name: dir.GetName(), + Parameters: dir.GetParameters(), + Comment: dir.GetComment(), + }) + } + + if !foundFlag && httpRedirect { + ifDir := &config.Directive{ + Name: "if", + Block: &config.Block{}, + Parameters: []string{"($scheme", "=", "http)"}, + Comment: []string{"# http重定向"}, + } + redirectDir := &config.Directive{ + Name: "return", + Parameters: []string{"301", "https://$host$request_uri"}, + } + redirectDir.SetParent(ifDir.GetBlock()) + ifBlock := ifDir.GetBlock().(*config.Block) + ifBlock.Directives = append(ifBlock.Directives, redirectDir) + directives = append(directives, ifDir) + } + + return p.Set("server", directives) +} + +func (p *Parser) SetAccessLog(accessLog string) error { + if err := p.Clear("server.access_log"); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "access_log", + Parameters: []string{accessLog}, + }, + }) +} + +func (p *Parser) SetErrorLog(errorLog string) error { + if err := p.Clear("server.error_log"); err != nil { + return err + } + + return p.Set("server", []*config.Directive{ + { + Name: "error_log", + Parameters: []string{errorLog}, + }, + }) +}