Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion agent/app/dto/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,19 @@ const (
CACHE NginxKey = "cache"
HttpPer NginxKey = "http-per"
ProxyCache NginxKey = "proxy-cache"
Brotli NginxKey = "brotli"
)

// BrotliKeys are served from the panel-managed http.d file rather than
// nginx.conf, because the module is optional: its directives must disappear
// together with the module, otherwise nginx refuses to start.
var BrotliKeys = []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"}

var ScopeKeyMap = map[NginxKey][]string{
Index: {"index"},
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
SSL: {"ssl_certificate", "ssl_certificate_key"},
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level", "gzip_types", "gzip_vary", "gzip_proxied"},
}

var StaticFileKeyMap = map[NginxKey]struct {
Expand Down
11 changes: 11 additions & 0 deletions agent/app/dto/response/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ type NginxParam struct {
Params []string `json:"params"`
}

// NginxBrotliRes carries the brotli settings together with where they live.
// ManagedExternally is true when the user defined brotli by hand, in which
// case the panel only reports the values and must not write its own copy.
// ManagedUnavailable is true when the panel could not wire the managed
// configuration into nginx.conf at all, so the reported values are inert.
type NginxBrotliRes struct {
Params []NginxParam `json:"params"`
ManagedExternally bool `json:"managedExternally"`
ManagedUnavailable bool `json:"managedUnavailable"`
}

type NginxAuthRes struct {
Enable bool `json:"enable"`
Items []dto.NginxAuth `json:"items"`
Expand Down
7 changes: 7 additions & 0 deletions agent/app/service/app_upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,13 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
}); err != nil {
return err
}
// Upgrades deliberately keep the user's nginx.conf, so corrected gzip
// defaults shipped with a new version would never reach existing
// installations. Rewrite only an untouched factory configuration, and
// never fail the upgrade over it.
if gzipErr := upgradeStockNginxGzipConfig(u.candidate); gzipErr != nil {
t.Logf("WARNING: update stock gzip configuration failed, keeping the current one: %v", gzipErr)
}
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err
}
Expand Down
10 changes: 8 additions & 2 deletions agent/app/service/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type NginxService struct {

type INginxService interface {
GetNginxConfig() (*response.NginxFile, error)
GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error)
GetConfigByScope(req request.NginxScopeReq) (interface{}, error)
UpdateConfigByScope(req request.NginxConfigUpdate) error
GetStatus() (response.NginxStatus, error)
UpdateConfigFile(req request.NginxConfigFileUpdate) error
Expand Down Expand Up @@ -62,7 +62,10 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
return &response.NginxFile{Content: string(byteContent)}, nil
}

func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) {
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) (interface{}, error) {
if req.Scope == dto.Brotli {
return getNginxBrotliParams()
}
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil, nil
Expand All @@ -71,6 +74,9 @@ func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.Ng
}

func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error {
if req.Scope == dto.Brotli {
return updateNginxBrotliParams(getNginxParams(req.Params, dto.BrotliKeys))
}
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil
Expand Down
168 changes: 168 additions & 0 deletions agent/app/service/nginx_gzip_upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package service

import (
"os"
"path"
"regexp"
"sort"
"strings"

"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
)

// stockNginxGzipDirectives is the gzip block shipped by the OpenResty app
// since 1.21.4.3. The upgrade only rewrites values when the installed
// nginx.conf still carries exactly these directives and values, which proves
// the user never tuned compression. Any deviation aborts the rewrite.
var stockNginxGzipDirectives = map[string]string{
"gzip": "on",
"gzip_min_length": "1k",
"gzip_buffers": "4 16k",
"gzip_http_version": "1.1",
"gzip_comp_level": "2",
"gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml",
"gzip_vary": "on",
"gzip_proxied": "expired no-cache no-store private auth",
"gzip_disable": `"MSIE [1-6]\."`,
}

// correctedNginxGzipDirectives replaces the stock values in place. gzip lives
// in the http block of nginx.conf and must stay there: repeating it from an
// included file would make nginx reject the configuration with a duplicate
// directive error, and the compression settings page reads and writes these
// same keys in nginx.conf.
var correctedNginxGzipDirectives = map[string]string{
"gzip_comp_level": "5",
"gzip_types": strings.Join(nginxCompressibleTypes, " "),
"gzip_proxied": "any",
}

// obsoleteNginxGzipDirectives are dropped outright.
var obsoleteNginxGzipDirectives = map[string]struct{}{
// A per-request User-Agent regex for browsers with no measurable share.
"gzip_disable": {},
}

var nginxGzipDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*(gzip[a-z_]*)[ \t]+([^;\n]*);[ \t]*$`)

func nginxMainConfigPath(install model.AppInstall) string {
return path.Join(install.GetPath(), nginxModuleConfDir, "nginx.conf")
}

// upgradeStockNginxGzipConfig rewrites the factory gzip defaults in place.
//
// Upgrades deliberately preserve the user's nginx.conf, so corrected defaults
// shipped with a new OpenResty version would otherwise never reach existing
// installations.
//
// The config parser is not used: its dumper regenerates the whole file, drops
// standalone comments and reorders proxy includes, which would be destructive
// on a user's main config. Lines are edited individually so everything outside
// the gzip block stays byte-identical.
func upgradeStockNginxGzipConfig(install model.AppInstall) error {
configPath := nginxMainConfigPath(install)
content, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !isStockNginxGzipConfig(string(content)) {
return nil
}
updated := rewriteNginxGzipDirectives(string(content))
if updated == string(content) {
return nil
}
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
return err
}
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
return err
}
global.LOG.Info("updated the stock OpenResty gzip configuration to the current defaults")
return nil
}

// isStockNginxGzipConfig reports whether every gzip directive in the config
// matches the factory defaults exactly, with none missing and none extra.
func isStockNginxGzipConfig(content string) bool {
found := make(map[string]string)
for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(content, -1) {
name := match[1]
value := strings.Join(strings.Fields(match[2]), " ")
if _, ok := found[name]; ok {
// A directive repeated in the http block means the config was
// edited by hand; leave it alone.
return false
}
found[name] = value
}
if len(found) != len(stockNginxGzipDirectives) {
return false
}
for name, expected := range stockNginxGzipDirectives {
if found[name] != expected {
return false
}
}
return true
}

// rewriteNginxGzipDirectives updates known values in place, drops obsolete
// directives and appends directives that are missing, preserving the original
// indentation and leaving every other line untouched.
func rewriteNginxGzipDirectives(content string) string {
lines := strings.Split(content, "\n")
result := make([]string, 0, len(lines))
seen := make(map[string]struct{})
lastGzipIndex := -1
lastGzipIndent := " "

for _, line := range lines {
match := nginxGzipDirectiveRe.FindStringSubmatch(line)
if match == nil {
result = append(result, line)
continue
}
name := match[1]
// The indentation belongs to the line itself; a top-level directive
// must not inherit the indent a previous, nested directive used.
lineIndent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
if lineIndent == "" {
lineIndent = " "
}
lastGzipIndent = lineIndent
if _, obsolete := obsoleteNginxGzipDirectives[name]; obsolete {
continue
}
seen[name] = struct{}{}
if replacement, ok := correctedNginxGzipDirectives[name]; ok {
result = append(result, lineIndent+name+" "+replacement+";")
} else {
result = append(result, line)
}
lastGzipIndex = len(result) - 1
}

// Directives introduced by a newer default set are appended right after
// the existing block so they stay visually grouped.
var missing []string
for name := range correctedNginxGzipDirectives {
if _, ok := seen[name]; !ok {
missing = append(missing, name)
}
}
if len(missing) == 0 || lastGzipIndex < 0 {
return strings.Join(result, "\n")
}
sort.Strings(missing)
added := make([]string, 0, len(missing))
for _, name := range missing {
added = append(added, lastGzipIndent+name+" "+correctedNginxGzipDirectives[name]+";")
}
tail := append(added, result[lastGzipIndex+1:]...)
return strings.Join(append(result[:lastGzipIndex+1], tail...), "\n")
}
Loading