From f61fcb9cb102805ad264e1452f0ce1e18e7cccae Mon Sep 17 00:00:00 2001 From: Snrat Date: Wed, 26 Aug 2026 04:17:53 +0800 Subject: [PATCH 1/8] feat(openresty): manage http-context directives via conf/http.d Add a managed-file mechanism for http-context nginx directives, mirroring the existing one for conf/modules-enabled. A separate directory is required because load_module is a main-context directive, so modules-enabled is included at the top level of nginx.conf and cannot host http-context directives. Files carry a 1panel-http- prefix; anything else in the directory is left untouched. Writes are atomic via a temporary file plus rename, and the directory is snapshotted so a failed nginx -t can be rolled back. The mechanism is inert when conf/http.d does not exist, which is the case for OpenResty installations predating the directory. --- agent/app/service/nginx_http_config.go | 128 +++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 agent/app/service/nginx_http_config.go diff --git a/agent/app/service/nginx_http_config.go b/agent/app/service/nginx_http_config.go new file mode 100644 index 000000000000..9682d7a1f6c8 --- /dev/null +++ b/agent/app/service/nginx_http_config.go @@ -0,0 +1,128 @@ +package service + +import ( + "fmt" + "os" + "path" + "sort" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" +) + +const ( + // nginxHTTPConfDir holds http-context directives generated by 1Panel. + // load_module is a main-context directive and therefore lives in + // modules-enabled, which cannot host http-context directives such as + // "brotli on". The directory is included by nginx.conf before conf.d so + // that per-site configuration keeps overriding these defaults. + nginxHTTPConfDir = "http.d" + + nginxHTTPConfigPrefix = "1panel-http-" + nginxHTTPConfigHeader = "# Managed by 1Panel. Manual changes will be overwritten.\n" +) + +// nginxHTTPDirective is a single http-context directive rendered into a +// managed file. +type nginxHTTPDirective struct { + Name string + Params []string +} + +func (d nginxHTTPDirective) render() string { + if len(d.Params) == 0 { + return d.Name + ";" + } + return d.Name + " " + strings.Join(d.Params, " ") + ";" +} + +// nginxHTTPConfigSupported reports whether the installed OpenResty exposes the +// managed http.d directory. Installations created before http.d was introduced +// have no such directory and no include for it, so writing files there would +// silently have no effect. +func nginxHTTPConfigSupported(install model.AppInstall) bool { + info, err := os.Stat(nginxHTTPConfigDir(install)) + return err == nil && info.IsDir() +} + +func nginxHTTPConfigDir(install model.AppInstall) string { + return path.Join(install.GetPath(), nginxModuleConfDir, nginxHTTPConfDir) +} + +func nginxHTTPConfigFileName(order int, name string) string { + return fmt.Sprintf("%s%04d-%s.conf", nginxHTTPConfigPrefix, order, nginxModulePathName(name)) +} + +// renderNginxHTTPConfig builds the content of a managed http.d file. +func renderNginxHTTPConfig(directives []nginxHTTPDirective) []byte { + var content strings.Builder + content.WriteString(nginxHTTPConfigHeader) + for _, directive := range directives { + content.WriteString(directive.render()) + content.WriteString("\n") + } + return []byte(content.String()) +} + +// snapshotManagedNginxHTTPConfigs captures every managed file so a failed +// nginx -t can be rolled back. +func snapshotManagedNginxHTTPConfigs(configDir string) (nginxModuleConfigSnapshot, error) { + snapshot := make(nginxModuleConfigSnapshot) + entries, err := os.ReadDir(configDir) + if err != nil { + if os.IsNotExist(err) { + return snapshot, nil + } + return nil, err + } + for _, entry := range entries { + if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) { + continue + } + content, readErr := os.ReadFile(path.Join(configDir, entry.Name())) + if readErr != nil { + return nil, readErr + } + snapshot[entry.Name()] = content + } + return snapshot, nil +} + +// applyManagedNginxHTTPConfigs writes the desired managed files and removes +// managed files that are no longer wanted. Files not carrying the managed +// prefix are never touched. +func applyManagedNginxHTTPConfigs(configDir string, desired map[string][]byte) error { + if err := os.MkdirAll(configDir, constant.DirPerm); err != nil { + return err + } + entries, err := os.ReadDir(configDir) + if err != nil { + return err + } + names := make([]string, 0, len(desired)) + for fileName := range desired { + names = append(names, fileName) + } + sort.Strings(names) + for _, fileName := range names { + tmpPath := path.Join(configDir, "."+fileName+".tmp") + if err = os.WriteFile(tmpPath, desired[fileName], constant.FilePerm); err != nil { + return err + } + if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil { + return err + } + } + for _, entry := range entries { + if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) { + continue + } + if _, ok := desired[entry.Name()]; !ok { + if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) { + return err + } + } + } + return nil +} From 0db2a9c9ad1384b3cf63990ab29556338a7dd275 Mon Sep 17 00:00:00 2001 From: Snrat Date: Wed, 26 Aug 2026 04:20:56 +0800 Subject: [PATCH 2/8] fix(openresty): correct gzip defaults and add missing compressible types Bring the embedded gzip template in line with how sites are actually served. It was previously dead code: nothing referenced gzip.conf, so the values never reached an installation. It is now embedded and used by the migration that follows. gzip_types was missing application/json, so JSON API responses were served uncompressed. Also add ld+json, text/xml, xhtml+xml, rss+xml, atom+xml, wasm, svg+xml and ttf/otf. Already compressed formats (images, woff2, archives) stay out on purpose. gzip_comp_level 6 -> 5, at the cost/ratio knee for gzip. gzip_proxied any, so that proxied responses are compressed regardless of their Cache-Control semantics. gzip_static is intentionally not enabled: nginx does not verify that a .gz file is newer than its source, so a stale artifact would be served indefinitely with no error. --- agent/cmd/server/nginx_conf/gzip.conf | 8 ++++++-- agent/cmd/server/nginx_conf/nginx_conf.go | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/agent/cmd/server/nginx_conf/gzip.conf b/agent/cmd/server/nginx_conf/gzip.conf index b277f1f890df..27d27a84f109 100644 --- a/agent/cmd/server/nginx_conf/gzip.conf +++ b/agent/cmd/server/nginx_conf/gzip.conf @@ -1,4 +1,8 @@ gzip on; -gzip_comp_level 6; +gzip_vary on; gzip_min_length 1k; -gzip_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype; \ No newline at end of file +gzip_buffers 4 16k; +gzip_http_version 1.1; +gzip_comp_level 5; +gzip_proxied any; +gzip_types text/plain text/css text/xml text/javascript application/json application/ld+json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/wasm image/svg+xml font/ttf font/otf; diff --git a/agent/cmd/server/nginx_conf/nginx_conf.go b/agent/cmd/server/nginx_conf/nginx_conf.go index cf2176146a6d..e540065fddf5 100644 --- a/agent/cmd/server/nginx_conf/nginx_conf.go +++ b/agent/cmd/server/nginx_conf/nginx_conf.go @@ -44,6 +44,9 @@ var Upstream []byte //go:embed sse.conf var SSE []byte +//go:embed gzip.conf +var Gzip []byte + //go:embed *.json *.conf var websitesFiles embed.FS From 23484fe7fc205a760ac2db2db463fa1dde62c0fb Mon Sep 17 00:00:00 2001 From: Snrat Date: Wed, 26 Aug 2026 04:21:36 +0800 Subject: [PATCH 3/8] feat(openresty): activate brotli directives when the module is enabled Enabling ngx_brotli only emitted load_module, leaving the module loaded but inert: no response was ever brotli-encoded until the user added `brotli on` and `brotli_types` to nginx.conf by hand. The module is prebuilt into the OpenResty image and listed in the catalog, so the only missing step was the runtime configuration. Enabling the module now also writes its http-context directives to conf/http.d, and disabling or deleting it removes them. Removal matters: leaving `brotli on` behind after the .so is unloaded makes nginx fail to start on an unknown directive. Both directory sets are written before nginx -t runs, so nginx only ever observes a consistent state, and a failed check rolls back load_module files and runtime directives together. Runtime defaults are declared per module in a table, so other modules needing http-context configuration can be added without touching the reconcile logic. brotli_types matches gzip_types so both encoders cover the same content. brotli_comp_level is 5 rather than the nginx default of 6: level 5 reaches roughly gzip level 9 ratio at a fraction of the cost, while 6 is tuned for static assets and is too expensive for dynamic responses. brotli_static is deliberately omitted, for the same reason gzip_static is: nginx does not verify that a precompressed artifact is newer than its source, so a stale file would be served indefinitely with no error. Installations without conf/http.d keep the previous behaviour instead of failing. --- agent/app/service/nginx_module.go | 33 +++++++- agent/app/service/nginx_module_runtime.go | 97 +++++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 agent/app/service/nginx_module_runtime.go diff --git a/agent/app/service/nginx_module.go b/agent/app/service/nginx_module.go index d299cb6b0a4b..e70922c016fc 100644 --- a/agent/app/service/nginx_module.go +++ b/agent/app/service/nginx_module.go @@ -654,10 +654,37 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N return fmt.Errorf("validate combined dynamic module configuration: %w", err) } } - if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil { + + // Runtime directives live in http.d because load_module is main-context + // while directives such as "brotli on" are http-context. Both sets are + // written before nginx -t runs, so nginx only ever observes the final, + // consistent state; on failure both are rolled back together. + httpConfigDir := nginxHTTPConfigDir(install) + httpSupported := nginxHTTPConfigSupported(install) + var httpSnapshot nginxModuleConfigSnapshot + if httpSupported { + if httpSnapshot, err = snapshotManagedNginxHTTPConfigs(httpConfigDir); err != nil { + return err + } + } + restore := func() { _ = applyManagedNginxModuleConfigs(configDir, snapshot) + if httpSupported { + _ = applyManagedNginxHTTPConfigs(httpConfigDir, httpSnapshot) + } + } + + if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil { + restore() return err } + if httpSupported { + desiredHTTP := desiredNginxModuleRuntimeConfigs(install, modules, target) + if err = applyManagedNginxHTTPConfigs(httpConfigDir, desiredHTTP); err != nil { + restore() + return err + } + } if !reload { return nil } @@ -666,11 +693,11 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N return nil } if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil { - _ = applyManagedNginxModuleConfigs(configDir, snapshot) + restore() return err } if err = opNginx(install.ContainerName, constant.NginxReload); err != nil { - _ = applyManagedNginxModuleConfigs(configDir, snapshot) + restore() return err } return nil diff --git a/agent/app/service/nginx_module_runtime.go b/agent/app/service/nginx_module_runtime.go new file mode 100644 index 000000000000..956a7a4c9a83 --- /dev/null +++ b/agent/app/service/nginx_module_runtime.go @@ -0,0 +1,97 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +// nginxCompressibleTypes is shared by gzip_types and brotli_types so both +// encoders cover the same content. Already compressed formats (images other +// than SVG, woff/woff2, archives, media) are deliberately excluded: +// recompressing them costs CPU and usually grows the payload. +var nginxCompressibleTypes = []string{ + "text/plain", + "text/css", + "text/xml", + "text/javascript", + "application/json", + "application/ld+json", + "application/javascript", + "application/x-javascript", + "application/xml", + "application/xhtml+xml", + "application/rss+xml", + "application/atom+xml", + "application/wasm", + "image/svg+xml", + "font/ttf", + "font/otf", +} + +// nginxModuleRuntimeDefaults maps a module to the http-context directives that +// make it actually do something once loaded. Without these, enabling a module +// only emits load_module, leaving it loaded but inert. +// +// brotli_static is intentionally omitted: nginx does not verify that a .br +// file is newer than its source, so a stale artifact would be served +// indefinitely with no error. +var nginxModuleRuntimeDefaults = map[string][]nginxHTTPDirective{ + "ngx_brotli": { + {Name: "brotli", Params: []string{"on"}}, + // Brotli level 5 reaches roughly gzip level 9 ratio at a fraction of + // the cost. The nginx default of 6 is tuned for static assets and is + // too expensive for dynamic responses. + {Name: "brotli_comp_level", Params: []string{"5"}}, + {Name: "brotli_min_length", Params: []string{"1k"}}, + {Name: "brotli_types", Params: nginxCompressibleTypes}, + }, +} + +// nginxModuleRuntimeLoadOrder keeps managed file names stable and ordered +// independently of the module load order used for load_module. +var nginxModuleRuntimeLoadOrder = map[string]int{ + "ngx_brotli": 100, +} + +func nginxModuleRuntimeOrder(name string) int { + if order, ok := nginxModuleRuntimeLoadOrder[name]; ok { + return order + } + return 900 +} + +// desiredNginxModuleRuntimeConfigs renders the managed http.d files for every +// enabled module that has a ready build and known runtime defaults. +func desiredNginxModuleRuntimeConfigs(install model.AppInstall, modules []dto.NginxModule, target dto.NginxModuleTarget) map[string][]byte { + desired := make(map[string][]byte) + for _, module := range modules { + normalizeNginxModule(&module) + directives, ok := nginxModuleRuntimeDefaults[module.Name] + if !ok || !module.Enable { + continue + } + if !nginxModuleRuntimeReady(module, target) { + continue + } + fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(module.Name), module.Name) + desired[fileName] = renderNginxHTTPConfig(directives) + } + return desired +} + +// nginxModuleRuntimeReady reports whether the module is actually usable. +// +// Dynamic modules need a ready build for the current target, otherwise the +// .so is missing and nginx would reject the directives. Static modules are +// compiled into the binary and carry no artifacts, so an enabled static +// module is considered ready. +func nginxModuleRuntimeReady(module dto.NginxModule, target dto.NginxModuleTarget) bool { + if module.BuildMode == nginxModuleBuildStatic { + return true + } + build := findCurrentNginxModuleBuild(module, target) + if build == nil || build.Status != nginxModuleStatusReady { + build = findLatestNginxModuleBuild(module, target) + } + return build != nil && build.Status == nginxModuleStatusReady +} From 77bb04d49c5a9fd65e6f217e99cae93929abb199 Mon Sep 17 00:00:00 2001 From: Snrat Date: Wed, 26 Aug 2026 04:29:06 +0800 Subject: [PATCH 4/8] feat(openresty): refresh stock gzip defaults on upgrade Upgrades deliberately preserve the user's nginx.conf, so corrected gzip defaults shipped with a new OpenResty version never reach existing installations. Rewrite the values in place during upgrade, but only when the block is provably untouched. The rewrite requires every gzip directive to match the factory values byte for byte, with none missing, none added and none duplicated. Any deviation means the user tuned compression, and their configuration is left alone. gzip stays in the http block of nginx.conf rather than moving to an included file: nginx rejects a duplicate gzip directive across contexts, and the compression settings page reads and writes these same keys in nginx.conf, so a relocated block would be reintroduced on the next save and break nginx -t. The config parser is not used either. 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. The rewrite is idempotent, and a failed nginx -t restores the previous file. A failure is logged as a warning instead of failing the upgrade. --- agent/app/service/app_upgrade.go | 7 + agent/app/service/nginx_gzip_upgrade.go | 165 +++++++++++++++++++ agent/app/service/nginx_gzip_upgrade_test.go | 147 +++++++++++++++++ 3 files changed, 319 insertions(+) create mode 100644 agent/app/service/nginx_gzip_upgrade.go create mode 100644 agent/app/service/nginx_gzip_upgrade_test.go diff --git a/agent/app/service/app_upgrade.go b/agent/app/service/app_upgrade.go index 2a768abc03e0..05bafc9444ac 100644 --- a/agent/app/service/app_upgrade.go +++ b/agent/app/service/app_upgrade.go @@ -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 } diff --git a/agent/app/service/nginx_gzip_upgrade.go b/agent/app/service/nginx_gzip_upgrade.go new file mode 100644 index 000000000000..16a798072569 --- /dev/null +++ b/agent/app/service/nginx_gzip_upgrade.go @@ -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") +} diff --git a/agent/app/service/nginx_gzip_upgrade_test.go b/agent/app/service/nginx_gzip_upgrade_test.go new file mode 100644 index 000000000000..03942e68ab47 --- /dev/null +++ b/agent/app/service/nginx_gzip_upgrade_test.go @@ -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") + } +} From eef443808e5f950048e7d59a4370d2cb64ff9a36 Mon Sep 17 00:00:00 2001 From: Snrat Date: Wed, 26 Aug 2026 04:29:45 +0800 Subject: [PATCH 5/8] fix(website): preserve size units in nginx performance settings The form stripped the unit suffix when reading a directive and then always appended a fixed one when saving, so the unit was silently reinterpreted. A config carrying `gzip_min_length 512;`, meaning 512 bytes, was read as 512 and written back as `512k`, inflating the threshold by 1024 and effectively disabling compression for every response under 512 KB. The same applied to client_header_buffer_size and client_max_body_size, where the value grew by a factor of 1024 in the opposite, riskier direction. Remember the unit that was read and write it back unchanged, defaulting to the previous suffix only when the directive carries no unit information. The input suffix now shows the unit actually in use instead of a hardcoded label. Also fix the value parsing itself: `Number(value.match(/\d+/g))` coerces a multi-number match to NaN, so a directive such as `gzip_buffers 4 16k` would blank the field. Take the first captured number instead. --- .../website/nginx/performance/index.vue | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/website/website/nginx/performance/index.vue b/frontend/src/views/website/website/nginx/performance/index.vue index 62719a6311dc..3d2c1434316d 100644 --- a/frontend/src/views/website/website/nginx/performance/index.vue +++ b/frontend/src/views/website/website/nginx/performance/index.vue @@ -13,13 +13,13 @@ - + {{ $t('nginx.clientHeaderBufferSizeHelper') }} - + {{ $t('nginx.clientMaxBodySizeHelper') }} @@ -38,7 +38,7 @@ - + {{ $t('nginx.gzipMinLengthHelper') }} @@ -96,6 +96,33 @@ const variablesRules = reactive({ gzip_comp_level: [checkNumberRange(1, 9)], }); +// nginx size directives may be written with or without a unit suffix, and +// the suffix carries a factor of 1024. Remember the unit that was read so it +// can be written back unchanged, instead of assuming a fixed one. +const sizeKeys = ['client_header_buffer_size', 'client_max_body_size', 'gzip_min_length']; +const units = ref>({}); + +const parseSizeParam = (name: string, value: string) => { + const matched = /^(\d+)\s*([kKmMgG]?)$/.exec(value.trim()); + if (!matched) { + return Number(value.match(/\d+/g)?.[0] ?? 0); + } + units.value[name] = matched[2] === '' ? '' : matched[2].toLowerCase(); + return Number(matched[1]); +}; + +const withUnit = (name: string, value: number, defaultUnit: string) => { + const unit = units.value[name] ?? defaultUnit; + return String(value) + unit; +}; + +// Label the input with the unit actually in use, so a value stored in bytes +// is not presented as if it were kilobytes. +const unitLabel = (name: string, defaultUnit: string) => { + const unit = units.value[name] ?? defaultUnit; + return unit === '' ? 'B' : unit.toUpperCase(); +}; + const getParams = async () => { const res = await getNginxConfigByScope(req.value); data.value = res.data; @@ -105,8 +132,10 @@ const getParams = async () => { } if (param.name == 'gzip') { form.value.gzip = param.params[0]; + } else if (sizeKeys.includes(param.name)) { + form.value[param.name] = parseSizeParam(param.name, param.params[0]); } else { - form.value[param.name] = Number(param.params[0].match(/\d+/g)); + form.value[param.name] = Number(param.params[0].match(/\d+/g)?.[0] ?? 0); } } }; @@ -121,10 +150,10 @@ const submit = async (formEl: FormInstance | undefined) => { let params = { gzip: form.value.gzip, server_names_hash_bucket_size: String(form.value.server_names_hash_bucket_size), - client_header_buffer_size: String(form.value.client_header_buffer_size) + 'k', - client_max_body_size: String(form.value.client_max_body_size) + 'm', + client_header_buffer_size: withUnit('client_header_buffer_size', form.value.client_header_buffer_size, 'k'), + client_max_body_size: withUnit('client_max_body_size', form.value.client_max_body_size, 'm'), keepalive_timeout: String(form.value.keepalive_timeout), - gzip_min_length: String(form.value.gzip_min_length) + 'k', + gzip_min_length: withUnit('gzip_min_length', form.value.gzip_min_length, 'k'), gzip_comp_level: String(form.value.gzip_comp_level), }; updateReq.value.params = params; From 9ba509b84618fcaeefe7ac1c932162e720da1e9b Mon Sep 17 00:00:00 2001 From: Snrat Date: Wed, 26 Aug 2026 04:39:59 +0800 Subject: [PATCH 6/8] feat(website): expose brotli settings in the compression page Brotli could be enabled as a module but never configured from the panel, so its behaviour was invisible and unchangeable without editing nginx.conf by hand. The section appears only once the module is enabled and built, since the directives are rejected by nginx while the module is not loaded. Values are read from and written to the panel-managed http.d file rather than nginx.conf, so they are removed together with the module. brotli_types stays out of the form on purpose: it is kept aligned with gzip_types so both encoders cover the same content, and exposing it would invite the two lists to drift apart. Saving reuses the existing scope endpoint with a dedicated brotli scope, which keeps the managed file as the single source of truth instead of duplicating the values into nginx.conf. --- agent/app/dto/nginx.go | 8 +- agent/app/service/nginx.go | 6 + agent/app/service/nginx_http_config.go | 22 ++++ agent/app/service/nginx_module_runtime.go | 108 +++++++++++++++++- agent/i18n/lang/en.yaml | 2 + agent/i18n/lang/es-ES.yaml | 2 + agent/i18n/lang/fa.yaml | 2 + agent/i18n/lang/ja.yaml | 2 + agent/i18n/lang/ko.yaml | 2 + agent/i18n/lang/lo.yaml | 2 + agent/i18n/lang/ms.yaml | 2 + agent/i18n/lang/pt-BR.yaml | 2 + agent/i18n/lang/ru.yaml | 2 + agent/i18n/lang/tr.yaml | 2 + agent/i18n/lang/zh-Hant.yaml | 2 + agent/i18n/lang/zh.yaml | 2 + frontend/src/lang/modules/en.ts | 2 + frontend/src/lang/modules/es-es.ts | 2 + frontend/src/lang/modules/fa.ts | 2 + frontend/src/lang/modules/ja.ts | 2 + frontend/src/lang/modules/ko.ts | 2 + frontend/src/lang/modules/lo.ts | 2 + frontend/src/lang/modules/ms.ts | 2 + frontend/src/lang/modules/pt-br.ts | 2 + frontend/src/lang/modules/ru.ts | 2 + frontend/src/lang/modules/tr.ts | 2 + frontend/src/lang/modules/zh-Hant.ts | 2 + frontend/src/lang/modules/zh.ts | 2 + .../website/nginx/performance/index.vue | 79 ++++++++++++- 29 files changed, 267 insertions(+), 4 deletions(-) diff --git a/agent/app/dto/nginx.go b/agent/app/dto/nginx.go index 9c61dc45e2d7..6c4076aa8d1c 100644 --- a/agent/app/dto/nginx.go +++ b/agent/app/dto/nginx.go @@ -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 { diff --git a/agent/app/service/nginx.go b/agent/app/service/nginx.go index 063dea7af8da..c992692b803d 100644 --- a/agent/app/service/nginx.go +++ b/agent/app/service/nginx.go @@ -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 @@ -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 diff --git a/agent/app/service/nginx_http_config.go b/agent/app/service/nginx_http_config.go index 9682d7a1f6c8..6628a45123e7 100644 --- a/agent/app/service/nginx_http_config.go +++ b/agent/app/service/nginx_http_config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "regexp" "sort" "strings" @@ -65,6 +66,27 @@ func renderNginxHTTPConfig(directives []nginxHTTPDirective) []byte { return []byte(content.String()) } +var nginxHTTPDirectiveRe = regexp.MustCompile(`^[ \t]*([a-z_][a-z0-9_]*)[ \t]+([^;]*);[ \t]*$`) + +// readNginxHTTPDirectives parses a managed file back into directive values. +// A missing or unreadable file yields no directives, which makes callers fall +// back to their defaults. +func readNginxHTTPDirectives(filePath string) map[string][]string { + content, err := os.ReadFile(filePath) + if err != nil { + return nil + } + directives := make(map[string][]string) + for _, line := range strings.Split(string(content), "\n") { + match := nginxHTTPDirectiveRe.FindStringSubmatch(line) + if match == nil { + continue + } + directives[match[1]] = strings.Fields(match[2]) + } + return directives +} + // snapshotManagedNginxHTTPConfigs captures every managed file so a failed // nginx -t can be rolled back. func snapshotManagedNginxHTTPConfigs(configDir string) (nginxModuleConfigSnapshot, error) { diff --git a/agent/app/service/nginx_module_runtime.go b/agent/app/service/nginx_module_runtime.go index 956a7a4c9a83..0353550304b0 100644 --- a/agent/app/service/nginx_module_runtime.go +++ b/agent/app/service/nginx_module_runtime.go @@ -1,8 +1,13 @@ package service import ( + "path" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" ) // nginxCompressibleTypes is shared by gzip_types and brotli_types so both @@ -62,6 +67,10 @@ func nginxModuleRuntimeOrder(name string) int { // desiredNginxModuleRuntimeConfigs renders the managed http.d files for every // enabled module that has a ready build and known runtime defaults. +// +// Values the user changed through the compression settings page are read back +// from the current managed file, so reconciling after an unrelated module +// change does not silently reset them to the defaults. func desiredNginxModuleRuntimeConfigs(install model.AppInstall, modules []dto.NginxModule, target dto.NginxModuleTarget) map[string][]byte { desired := make(map[string][]byte) for _, module := range modules { @@ -74,11 +83,108 @@ func desiredNginxModuleRuntimeConfigs(install model.AppInstall, modules []dto.Ng continue } fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(module.Name), module.Name) - desired[fileName] = renderNginxHTTPConfig(directives) + current := readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName)) + desired[fileName] = renderNginxHTTPConfig(mergeNginxRuntimeDirectives(directives, current)) } return desired } +// mergeNginxRuntimeDirectives keeps the declared directive set and ordering +// while preferring values already present in the managed file. +func mergeNginxRuntimeDirectives(defaults []nginxHTTPDirective, current map[string][]string) []nginxHTTPDirective { + if len(current) == 0 { + return defaults + } + merged := make([]nginxHTTPDirective, 0, len(defaults)) + for _, directive := range defaults { + if params, ok := current[directive.Name]; ok && len(params) > 0 { + directive.Params = params + } + merged = append(merged, directive) + } + return merged +} + +// nginxBrotliModuleName is the catalog name of the brotli module. +const nginxBrotliModuleName = "ngx_brotli" + +// getNginxBrotliParams reports the brotli settings currently in effect. +// +// Brotli is served from the managed http.d file instead of nginx.conf, so the +// directives can be removed together with the module. When the module is +// disabled the declared defaults are returned, which lets the settings page +// show what would be applied once it is enabled. +func getNginxBrotliParams() ([]response.NginxParam, error) { + install, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName) + current := readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName)) + res := make([]response.NginxParam, 0, len(dto.BrotliKeys)) + for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) { + res = append(res, response.NginxParam{Name: directive.Name, Params: directive.Params}) + } + return res, nil +} + +// updateNginxBrotliParams persists brotli settings to the managed http.d file. +// +// Writing is refused unless the module is enabled and built: the directives +// would reference a module that is not loaded and nginx would fail to start. +func updateNginxBrotliParams(params []dto.NginxParam) error { + install, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + if !nginxHTTPConfigSupported(install) { + return buserr.New("ErrBrotliUnsupported") + } + modules, err := loadNginxModules(install) + if err != nil { + return err + } + values := make(map[string][]string, len(params)) + for _, param := range params { + values[param.Name] = param.Params + } + for i := range modules { + if modules[i].Name != nginxBrotliModuleName { + continue + } + if !modules[i].Enable { + return buserr.New("ErrBrotliDisabled") + } + fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName) + configDir := nginxHTTPConfigDir(install) + snapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir) + if snapErr != nil { + return snapErr + } + merged := mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], values) + desired := map[string][]byte{fileName: renderNginxHTTPConfig(merged)} + for name, content := range snapshot { + if name != fileName { + desired[name] = content + } + } + if err = applyManagedNginxHTTPConfigs(configDir, desired); err != nil { + _ = applyManagedNginxHTTPConfigs(configDir, snapshot) + return err + } + if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil { + _ = applyManagedNginxHTTPConfigs(configDir, snapshot) + return err + } + if err = opNginx(install.ContainerName, constant.NginxReload); err != nil { + _ = applyManagedNginxHTTPConfigs(configDir, snapshot) + return err + } + return nil + } + return buserr.New("ErrBrotliDisabled") +} + // nginxModuleRuntimeReady reports whether the module is actually usable. // // Dynamic modules need a ready build for the current target, otherwise the diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 9af2b721a0df..a7dd89318e24 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: 'Invalid domain format: {{ .name }}' ErrDefaultAlias: 'default is reserved; use another alias' ErrParentWebsite: 'Delete subsite {{ .name }} first' ErrBuildDirNotFound: 'The build directory does not exist' +ErrBrotliDisabled: 'The brotli module is not enabled, enable and build it first' +ErrBrotliUnsupported: 'The installed OpenResty version does not support managed brotli settings' ErrImageNotExist: 'Runtime image not found: {{ .name }}' ErrProxyIsUsed: 'Load balancer is used by reverse proxy' ErrSSLValid: 'Certificate file is invalid' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 586d6466c049..d1cb8b161a82 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: 'El formato del dominio {{ .name }} es incorrecto' ErrDefaultAlias: 'default es un código reservado, use otro' ErrParentWebsite: 'Primero debe eliminar el sub-sitio {{ .name }}' ErrBuildDirNotFound: 'El directorio de compilación no existe' +ErrBrotliDisabled: 'El módulo brotli no está habilitado, actívelo y compílelo primero' +ErrBrotliUnsupported: 'La versión instalada de OpenResty no admite configuraciones brotli gestionadas' ErrImageNotExist: 'La imagen del entorno {{ .name }} no existe, edítela de nuevo' ErrProxyIsUsed: 'El balanceo de carga ya está usado por un proxy reverso, no se puede eliminar' ErrSSLValid: 'Archivo de certificado anómalo, revise el estado del certificado' diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 32d1bb561488..b2f1b50bc35d 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: 'فرمت دامنه نامعتبر است: {{ .name }}' ErrDefaultAlias: 'default رزرو شده است؛ از نام مستعار دیگری استفاده کنید' ErrParentWebsite: 'ابتدا زیرسایت {{ .name }} را حذف کنید' ErrBuildDirNotFound: 'دایرکتوری ساخت وجود ندارد' +ErrBrotliDisabled: 'ماژول brotli فعال نیست، ابتدا آن را فعال و بیلد کنید' +ErrBrotliUnsupported: 'نسخه نصب‌شده OpenResty از تنظیمات مدیریت‌شده brotli پشتیبانی نمی‌کند' ErrImageNotExist: 'تصویر محیط اجرا یافت نشد: {{ .name }}' ErrProxyIsUsed: 'تعادل بار توسط پراکسی معکوس استفاده می‌شود' ErrSSLValid: 'فایل گواهی نامعتبر است' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index bb4d1a1a0389..10a560b5bc34 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: '{{ .name }} ドメイン名の形式が正しくありませ ErrDefaultAlias: 'デフォルトは予約済みのコードです。別のコードを使用してください' ErrParentWebsite: 'まずサブサイト {{ .name }} を削除する必要があります' ErrBuildDirNotFound: 'ビルド ディレクトリが存在しません' +ErrBrotliDisabled: 'brotli モジュールが有効になっていません。先に有効化してビルドしてください' +ErrBrotliUnsupported: 'インストールされている OpenResty のバージョンは管理された brotli 設定をサポートしていません' ErrImageNotExist: 'オペレーティング環境 {{ .name }} イメージが存在しません。オペレーティング環境を再編集してください' ErrProxyIsUsed: 'ロードバランシングはリバースプロキシによって使用されているため、削除できません' ErrSSLValid: '証明書ファイルが異常です、証明書の状態を確認してください!' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index f47378a22c45..4f47cfc4d02c 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: '{{ .name }} 도메인 이름 형식이 올바르지 않습니 ErrDefaultAlias: '기본값은 예약된 코드입니다. 다른 코드를 사용하세요' ErrParentWebsite: '먼저 하위 사이트 {{ .name }}을 삭제해야 합니다.' ErrBuildDirNotFound: '빌드 디렉토리가 존재하지 않습니다' +ErrBrotliDisabled: 'brotli 모듈이 활성화되지 않았습니다. 먼저 활성화하고 빌드하세요' +ErrBrotliUnsupported: '설치된 OpenResty 버전은 관리형 brotli 설정을 지원하지 않습니다' ErrImageNotExist: '운영 환경 {{ .name }} 이미지가 존재하지 않습니다. 운영 환경을 다시 편집하세요.' ErrProxyIsUsed: '로드 밸런싱이 역방향 프록시에 의해 사용되었으므로 삭제할 수 없습니다' ErrSSLValid: '인증서 파일에 문제가 있습니다. 인증서 상태를 확인하세요' diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index 98394f4784cd..bde0bf4004ba 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -205,6 +205,8 @@ ErrDomainFormat: 'ຮູບແບບໂດເມນບໍ່ຖືກຕ້ອ ErrDefaultAlias: 'ຊື່ default ຖືກສະຫງວນໄວ້; ກະລຸນາໃຊ້ຊື່ອື່ນ' ErrParentWebsite: 'ກະລຸນາລຶບເວັບໄຊຍ່ອຍ {{ .name }} ກ່ອນ' ErrBuildDirNotFound: 'ບໍ່ມີໂຟນເດີ Build ຢູ່' +ErrBrotliDisabled: 'ໂມດູນ brotli ຍັງບໍ່ໄດ້ເປີດໃຊ້, ກະລຸນາເປີດໃຊ້ ແລະ ສ້າງກ່ອນ' +ErrBrotliUnsupported: 'ລຸ້ນ OpenResty ທີ່ຕິດຕັ້ງບໍ່ຮອງຮັບການຕັ້ງຄ່າ brotli ແບບຈັດການ' ErrImageNotExist: 'ບໍ່ພົບຮູບພາບ runtime: {{ .name }}' ErrProxyIsUsed: 'ຕົວຈັດການການໂຫຼດ (Load balancer) ຖືກໃຊ້ງານໂດຍ reverse proxy' ErrSSLValid: 'ໄຟລ໌ໃບຮັບຮອງບໍ່ຖືກຕ້ອງ' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index cc9ac8aab68a..3bfe42c3ecec 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: 'Format nama domain {{ .name }} tidak betul' ErrDefaultAlias: 'lalai ialah kod simpanan, sila gunakan kod lain' ErrParentWebsite: 'Anda perlu memadamkan subtapak {{ .name }} dahulu' ErrBuildDirNotFound: 'Direktori binaan tidak wujud' +ErrBrotliDisabled: 'Modul brotli tidak diaktifkan, aktifkan dan bina dahulu' +ErrBrotliUnsupported: 'Versi OpenResty yang dipasang tidak menyokong tetapan brotli terurus' ErrImageNotExist: 'Imej persekitaran operasi {{ .name }} tidak wujud, sila edit semula persekitaran pengendalian' ErrProxyIsUsed: 'Pengimbang beban telah digunakan oleh pengganti terbalik, tidak boleh dipadamkan' ErrSSLValid: 'Fail sijil bermasalah, sila periksa status sijil' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 24deaf4eeecd..ae2e13cd9e36 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: 'o formato do nome de domínio {{ .name }} está incorreto' ErrDefaultAlias: 'padrão é um código reservado, use outro código' ErrParentWebsite: 'Você precisa excluir o subsite {{ .name }} primeiro' ErrBuildDirNotFound: 'O diretório de compilação não existe' +ErrBrotliDisabled: 'O módulo brotli não está ativado, ative-o e compile-o primeiro' +ErrBrotliUnsupported: 'A versão instalada do OpenResty não suporta configurações brotli gerenciadas' ErrImageNotExist: 'A imagem do ambiente operacional {{ .name }} não existe, edite novamente o ambiente operacional' ErrProxyIsUsed: 'Balanceamento de carga foi usado por proxy reverso, não pode ser excluído' ErrSSLValid: 'O arquivo do certificado está anormal, verifique o status do certificado' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index b9cabd88dfb8..94da0b8e0af4 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: 'Неверный формат доменного имени {{ ErrDefaultAlias: 'по умолчанию зарезервирован код, используйте другой код' ErrParentWebsite: 'Сначала вам необходимо удалить дочерний сайт {{ .name }}' ErrBuildDirNotFound: 'Каталог сборки не существует' +ErrBrotliDisabled: 'Модуль brotli не включён, сначала включите и соберите его' +ErrBrotliUnsupported: 'Установленная версия OpenResty не поддерживает управляемые настройки brotli' ErrImageNotExist: 'Образ операционной среды {{ .name }} не существует, пожалуйста, отредактируйте операционную среду заново' ErrProxyIsUsed: 'Балансировка нагрузки используется обратным прокси, невозможно удалить' ErrSSLValid: 'Файл сертификата аномален, проверьте статус сертификата' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index c71d97454e94..9d68eee00fda 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: '{{ .name }} alan adı formatı yanlış' ErrDefaultAlias: 'default ayrılmış bir kod, lütfen başka bir kod kullanın' ErrParentWebsite: 'Önce {{ .name }} alt sitesini silmeniz gerekiyor' ErrBuildDirNotFound: 'Yapı dizini mevcut değil' +ErrBrotliDisabled: 'brotli modülü etkin değil, önce etkinleştirin ve derleyin' +ErrBrotliUnsupported: 'Kurulu OpenResty sürümü yönetilen brotli ayarlarını desteklemiyor' ErrImageNotExist: 'İşletim ortamı {{ .name }} image mevcut değil, lütfen işletim ortamını yeniden düzenleyin' ErrProxyIsUsed: 'Yük dengeleme ters proxy tarafından kullanıldı, silinemez' ErrSSLValid: 'Sertifika dosyası anormal, lütfen sertifika durumunu kontrol edin' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 4cb9952d1097..cec7e58f2523 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: '{{ .name }} 網域格式不正確' ErrDefaultAlias: 'default 為保留代號,請使用其他代號' ErrParentWebsite: '需要先移除子網站{{ .name }}' ErrBuildDirNotFound: '建置目錄不存在' +ErrBrotliDisabled: 'brotli 模組未啟用,請先啟用並建置' +ErrBrotliUnsupported: '目前 OpenResty 版本不支援託管 brotli 設定' ErrImageNotExist: '執行環境{{ .name }} 映像不存在,請重新編輯執行環境' ErrProxyIsUsed: '負載均衡已被反向代理使用,無法刪除' ErrSSLValid: '憑證檔案異常,請檢查憑證狀態!' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 77ec11697cac..fb49cb593013 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -215,6 +215,8 @@ ErrDomainFormat: "{{ .name }} 域名格式不正确" ErrDefaultAlias: "default 为保留代号,请使用其他代号" ErrParentWebsite: "需要先删除子网站 {{ .name }}" ErrBuildDirNotFound: "构建目录不存在" +ErrBrotliDisabled: "brotli 模块未启用,请先启用并构建" +ErrBrotliUnsupported: "当前 OpenResty 版本不支持托管 brotli 配置" ErrImageNotExist: "运行环境 {{ .name }} 镜像不存在,请重新编辑运行环境" ErrProxyIsUsed: "负载均衡已被反向代理使用,无法删除" ErrSSLValid: '证书文件异常,请检查证书状态!' diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 49baa422f7c9..633d1e219d2d 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -3904,6 +3904,8 @@ const message = { gzipMinLengthHelper: 'Minimum Compressed File', gzipCompLevelHelper: 'Compression Rate', gzipHelper: 'Enable compression for transmission', + brotliHelper: 'Enable brotli compression, usually smaller than gzip', + brotliCompLevelHelper: 'Brotli compression rate, 0 to 11', connections: 'Active connections', accepts: 'Accepts', handled: 'Handled', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 5a45e87b0046..acf994011189 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -3951,6 +3951,8 @@ const message = { gzipMinLengthHelper: 'Tamaño mínimo para comprimir', gzipCompLevelHelper: 'Nivel de compresión', gzipHelper: 'Habilitar compresión para transmisión', + brotliHelper: 'Habilitar compresión brotli, normalmente menor que gzip', + brotliCompLevelHelper: 'Tasa de compresión brotli, de 0 a 11', connections: 'Conexiones activas', accepts: 'Aceptadas', handled: 'Gestionadas', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index f5dd35614745..5b488f6374bb 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -3871,6 +3871,8 @@ const message = { gzipMinLengthHelper: 'حداقل فایل فشرده', gzipCompLevelHelper: 'نرخ فشرده‌سازی', gzipHelper: 'فعال‌سازی فشرده‌سازی برای انتقال', + brotliHelper: 'فعال‌سازی فشرده‌سازی brotli، معمولاً کوچک‌تر از gzip', + brotliCompLevelHelper: 'نرخ فشرده‌سازی brotli، از 0 تا 11', connections: 'اتصال‌های فعال', accepts: 'پذیرش‌ها', handled: 'مدیریت شده', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index e7cacbc98fa3..c02edc3072ab 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -3894,6 +3894,8 @@ const message = { gzipMinLengthHelper: '最小圧縮ファイル', gzipCompLevelHelper: '圧縮率', gzipHelper: '伝送の圧縮を有効にします', + brotliHelper: 'brotli 圧縮を有効にします。通常 gzip より小さくなります', + brotliCompLevelHelper: 'brotli 圧縮率、0 から 11', connections: 'アクティブな接続', accepts: '受け入れます', handled: '処理', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 716aa7bb72dd..07d3bf83f414 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -3823,6 +3823,8 @@ const message = { gzipMinLengthHelper: '최소 압축 파일 크기', gzipCompLevelHelper: '압축률', gzipHelper: '전송을 위한 압축 활성화', + brotliHelper: 'brotli 압축 활성화, 일반적으로 gzip보다 작습니다', + brotliCompLevelHelper: 'brotli 압축률, 0에서 11까지', connections: '활성 연결', accepts: '수락', handled: '처리됨', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index ab9080d57ec8..5fee100ccb87 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -3798,6 +3798,8 @@ const message = { gzipMinLengthHelper: 'ຂະໜາດໄຟລ໌ຕ່ຳສຸດທີ່ຈະບີບອັດ', gzipCompLevelHelper: 'ອັດຕາການບີບອັດ', gzipHelper: 'ເປີດໃຊ້ການບີບອັດໃນການຮັບສົ່ງຂໍ້ມູນ', + brotliHelper: 'ເປີດໃຊ້ການບີບອັດ brotli, ປົກກະຕິນ້ອຍກວ່າ gzip', + brotliCompLevelHelper: 'ອັດຕາການບີບອັດ brotli, 0 ຫາ 11', connections: 'ການເຊື່ອມຕໍ່ທີ່ກຳລັງເຮັດວຽກ', accepts: 'ຍອມຮັບແລ້ວ', handled: 'ຈັດການແລ້ວ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 0ab85801c796..827cd6aa0138 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -3966,6 +3966,8 @@ const message = { gzipMinLengthHelper: 'Saiz minimum fail untuk pemampatan', gzipCompLevelHelper: 'Kadar mampatan', gzipHelper: 'Aktifkan pemampatan untuk penghantaran', + brotliHelper: 'Aktifkan pemampatan brotli, biasanya lebih kecil daripada gzip', + brotliCompLevelHelper: 'Kadar pemampatan brotli, 0 hingga 11', connections: 'Sambungan aktif', accepts: 'Diterima', handled: 'Diuruskan', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 33ba342f4709..bbac49543fb9 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -3986,6 +3986,8 @@ const message = { gzipMinLengthHelper: 'Tamanho mínimo para compressão', gzipCompLevelHelper: 'Nível de compressão', gzipHelper: 'Ativar compressão na transmissão', + brotliHelper: 'Ativar compressão brotli, geralmente menor que gzip', + brotliCompLevelHelper: 'Taxa de compressão brotli, de 0 a 11', connections: 'Conexões ativas', accepts: 'Accepts', handled: 'Handled', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 48e2c45b4d7c..3fde397a35e0 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -3954,6 +3954,8 @@ const message = { gzipMinLengthHelper: 'Минимальный размер сжатого файла', gzipCompLevelHelper: 'Степень сжатия', gzipHelper: 'Включить сжатие для передачи', + brotliHelper: 'Включить сжатие brotli, обычно меньше, чем gzip', + brotliCompLevelHelper: 'Степень сжатия brotli, от 0 до 11', connections: 'Активные соединения', accepts: 'Принято', handled: 'Обработано', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index bbc41c439d7f..796ec75de3c4 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -3969,6 +3969,8 @@ const message = { gzipMinLengthHelper: 'Minimum Sıkıştırılmış Dosya', gzipCompLevelHelper: 'Sıkıştırma Oranı', gzipHelper: 'İletim için sıkıştırmayı etkinleştir', + brotliHelper: 'brotli sıkıştırmayı etkinleştir, genellikle gzip ile karşılaştırıldığında daha küçüktür', + brotliCompLevelHelper: 'Brotli sıkıştırma oranı, 0 ile 11 arası', connections: 'Aktif bağlantılar', accepts: 'Kabul edilenler', handled: 'İşlenenler', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 3e571d962e03..7848724d3ece 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -3648,6 +3648,8 @@ const message = { gzipMinLengthHelper: '最小壓縮檔案', gzipCompLevelHelper: '壓縮率', gzipHelper: '是否開啟壓縮傳輸', + brotliHelper: '開啟 brotli 壓縮,通常比 gzip 體積更小', + brotliCompLevelHelper: 'brotli 壓縮率,取值 0 到 11', connections: '活動連接(Active connections)', accepts: '總連接次數(accepts)', handled: '總握手次數(handled)', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 7ce725430074..4b7fd24485c6 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -3693,6 +3693,8 @@ const message = { gzipMinLengthHelper: '最小压缩文件', gzipCompLevelHelper: '压缩率', gzipHelper: '是否开启压缩传输', + brotliHelper: '开启 brotli 压缩,通常比 gzip 体积更小', + brotliCompLevelHelper: 'brotli 压缩率,取值 0 到 11', connections: '活动连接(Active connections)', accepts: '总连接次数(accepts)', handled: '总握手次数(handled)', diff --git a/frontend/src/views/website/website/nginx/performance/index.vue b/frontend/src/views/website/website/nginx/performance/index.vue index 3d2c1434316d..46ebeb9667f0 100644 --- a/frontend/src/views/website/website/nginx/performance/index.vue +++ b/frontend/src/views/website/website/nginx/performance/index.vue @@ -48,6 +48,29 @@ + + + + + + + + {{ $t('nginx.brotliHelper') }} + + + + + + {{ $t('nginx.gzipMinLengthHelper') }} + + + + + + {{ $t('nginx.brotliCompLevelHelper') }} + + + {{ $t('commons.button.save') }} @@ -58,7 +81,7 @@