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
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
6 changes: 6 additions & 0 deletions agent/app/service/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
}

func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, 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
165 changes: 165 additions & 0 deletions agent/app/service/nginx_gzip_upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package service

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

"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"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 = os.WriteFile(configPath, []byte(updated), constant.FilePerm); 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
indent := " "

for _, line := range lines {
match := nginxGzipDirectiveRe.FindStringSubmatch(line)
if match == nil {
result = append(result, line)
continue
}
name := match[1]
if leading := line[:len(line)-len(strings.TrimLeft(line, " \t"))]; leading != "" {
indent = leading
}
if _, obsolete := obsoleteNginxGzipDirectives[name]; obsolete {
continue
}
seen[name] = struct{}{}
if replacement, ok := correctedNginxGzipDirectives[name]; ok {
result = append(result, indent+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, indent+name+" "+correctedNginxGzipDirectives[name]+";")
}
tail := append(added, result[lastGzipIndex+1:]...)
return strings.Join(append(result[:lastGzipIndex+1], tail...), "\n")
}
147 changes: 147 additions & 0 deletions agent/app/service/nginx_gzip_upgrade_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package service

import (
"strings"
"testing"
)

const stockNginxConf = `user root;
worker_processes auto;

include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;

events {
use epoll;
}

http {
include mime.types;
default_type application/octet-stream;

server_names_hash_bucket_size 512;
keepalive_requests 5000;

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]\.";

limit_conn_zone $binary_remote_addr zone=perip:10m;

include /usr/local/openresty/nginx/conf/http.d/*.conf;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
}
`

func TestIsStockNginxGzipConfig(t *testing.T) {
if !isStockNginxGzipConfig(stockNginxConf) {
t.Fatal("factory configuration should be detected as stock")
}
}

func TestIsStockNginxGzipConfigRejectsTunedValues(t *testing.T) {
cases := map[string]string{
"comp level changed": strings.Replace(stockNginxConf, "gzip_comp_level 2;", "gzip_comp_level 6;", 1),
"gzip disabled": strings.Replace(stockNginxConf, "gzip on;", "gzip off;", 1),
"types extended": strings.Replace(stockNginxConf,
"application/xml;", "application/xml application/json;", 1),
"directive removed": strings.Replace(stockNginxConf, " gzip_vary on;\n", "", 1),
"directive added": strings.Replace(stockNginxConf, " gzip_vary on;\n",
" gzip_vary on;\n gzip_static on;\n", 1),
}
for name, content := range cases {
if isStockNginxGzipConfig(content) {
t.Errorf("%s: tuned configuration must not be rewritten", name)
}
}
}

func TestIsStockNginxGzipConfigRejectsDuplicateDirective(t *testing.T) {
content := strings.Replace(stockNginxConf, " gzip on;\n", " gzip on;\n gzip on;\n", 1)
if isStockNginxGzipConfig(content) {
t.Fatal("a duplicated directive indicates a hand-edited config")
}
}

func TestRewriteNginxGzipDirectives(t *testing.T) {
result := rewriteNginxGzipDirectives(stockNginxConf)

for _, expected := range []string{
" gzip_comp_level 5;",
" gzip_proxied any;",
" gzip on;",
" gzip_vary on;",
} {
if !strings.Contains(result, expected) {
t.Errorf("expected directive missing: %s\n%s", expected, result)
}
}
if !strings.Contains(result, "application/json") {
t.Error("gzip_types should now cover application/json")
}
if strings.Contains(result, "gzip_disable") {
t.Error("obsolete gzip_disable should have been dropped")
}
if strings.Contains(result, "gzip_comp_level 2;") {
t.Error("stale comp level should have been replaced")
}
// Everything outside the gzip block must survive untouched.
for _, keep := range []string{
"server_names_hash_bucket_size 512;",
"keepalive_requests 5000;",
"limit_conn_zone $binary_remote_addr zone=perip:10m;",
"include /usr/local/openresty/nginx/conf/http.d/*.conf;",
"include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
"include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;",
"user root;",
} {
if !strings.Contains(result, keep) {
t.Errorf("unrelated line was altered or dropped: %s", keep)
}
}
if !strings.HasSuffix(result, "}\n") {
t.Error("trailing newline was not preserved")
}
}

func TestRewriteNginxGzipDirectivesIsIdempotent(t *testing.T) {
once := rewriteNginxGzipDirectives(stockNginxConf)
twice := rewriteNginxGzipDirectives(once)
if once != twice {
t.Errorf("rewrite is not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", once, twice)
}
}

func TestRewriteNginxGzipDirectivesAppendsMissing(t *testing.T) {
// gzip_proxied absent from the source must be appended, not silently lost.
content := strings.Replace(stockNginxConf,
" gzip_proxied expired no-cache no-store private auth;\n", "", 1)
result := rewriteNginxGzipDirectives(content)
if !strings.Contains(result, "gzip_proxied any;") {
t.Errorf("missing directive was not appended:\n%s", result)
}
if !strings.Contains(result, "limit_conn_zone $binary_remote_addr zone=perip:10m;") {
t.Error("appending must not clobber following lines")
}
}

func TestRewriteNginxGzipDirectivesKeepsGzipLikeNames(t *testing.T) {
// gunzip and proxy_set_header must survive: only directives whose name
// starts with "gzip" are managed here.
content := "http {\n gunzip on;\n gzip on;\n proxy_set_header Accept-Encoding gzip;\n}\n"
result := rewriteNginxGzipDirectives(content)
if !strings.Contains(result, "gunzip on;") {
t.Error("gunzip directive must be preserved")
}
if !strings.Contains(result, "proxy_set_header Accept-Encoding gzip;") {
t.Error("proxy_set_header must be preserved")
}
if !strings.Contains(result, " gzip on;") {
t.Error("gzip directive should be kept in place")
}
}
Loading