diff --git a/atsf_server/controller/option.go b/atsf_server/controller/option.go index eba822b0..3f296ff7 100644 --- a/atsf_server/controller/option.go +++ b/atsf_server/controller/option.go @@ -102,17 +102,18 @@ func validateOpenRestyOption(key string, value string) error { return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key) } case "OpenRestyEventsMultiAcceptEnabled", + "OpenRestyProxyRequestBufferingEnabled", "OpenRestyProxyBufferingEnabled", "OpenRestyGzipEnabled", "OpenRestyCacheEnabled", "OpenRestyCacheLockEnabled": return validateBooleanOption(key, trimmed) - case "OpenRestyProxyBuffers": + case "OpenRestyProxyBuffers", "OpenRestyLargeClientHeaderBuffers": if openRestyProxyBuffersPattern.MatchString(trimmed) { return nil } return fmt.Errorf("%s 格式必须类似 \"16 16k\"", key) - case "OpenRestyProxyBufferSize", "OpenRestyProxyBusyBuffersSize", "OpenRestyCacheMaxSize": + case "OpenRestyProxyBufferSize", "OpenRestyProxyBusyBuffersSize", "OpenRestyCacheMaxSize", "OpenRestyClientMaxBodySize": if openRestySizePattern.MatchString(trimmed) { return nil } diff --git a/atsf_server/controller/option_test.go b/atsf_server/controller/option_test.go index 8e3ca522..8f809b1e 100644 --- a/atsf_server/controller/option_test.go +++ b/atsf_server/controller/option_test.go @@ -18,6 +18,12 @@ func TestValidateOpenRestyOption(t *testing.T) { {name: "proxy buffers invalid", key: "OpenRestyProxyBuffers", value: "16x16k", wantErr: true}, {name: "cache max size valid", key: "OpenRestyCacheMaxSize", value: "2g"}, {name: "cache max size invalid", key: "OpenRestyCacheMaxSize", value: "2gb", wantErr: true}, + {name: "client max body size valid", key: "OpenRestyClientMaxBodySize", value: "64m"}, + {name: "client max body size invalid", key: "OpenRestyClientMaxBodySize", value: "64mb", wantErr: true}, + {name: "large client header buffers valid", key: "OpenRestyLargeClientHeaderBuffers", value: "4 16k"}, + {name: "large client header buffers invalid", key: "OpenRestyLargeClientHeaderBuffers", value: "4x16k", wantErr: true}, + {name: "proxy request buffering valid", key: "OpenRestyProxyRequestBufferingEnabled", value: "true"}, + {name: "proxy request buffering invalid", key: "OpenRestyProxyRequestBufferingEnabled", value: "on", wantErr: true}, {name: "cache inactive valid", key: "OpenRestyCacheInactive", value: "30m"}, {name: "cache inactive invalid", key: "OpenRestyCacheInactive", value: "30", wantErr: true}, {name: "cache use stale valid", key: "OpenRestyCacheUseStale", value: "error timeout http_500"}, diff --git a/atsf_server/service/config_version.go b/atsf_server/service/config_version.go index ff159be4..5addc492 100644 --- a/atsf_server/service/config_version.go +++ b/atsf_server/service/config_version.go @@ -72,10 +72,13 @@ type openRestyConfigSnapshot struct { KeepaliveRequests int `json:"keepalive_requests"` ClientHeaderTimeout int `json:"client_header_timeout"` ClientBodyTimeout int `json:"client_body_timeout"` + ClientMaxBodySize string `json:"client_max_body_size"` + LargeClientHeaderBuffers string `json:"large_client_header_buffers"` SendTimeout int `json:"send_timeout"` ProxyConnectTimeout int `json:"proxy_connect_timeout"` ProxySendTimeout int `json:"proxy_send_timeout"` ProxyReadTimeout int `json:"proxy_read_timeout"` + ProxyRequestBuffering bool `json:"proxy_request_buffering"` ProxyBufferingEnabled bool `json:"proxy_buffering_enabled"` ProxyBuffers string `json:"proxy_buffers"` ProxyBufferSize string `json:"proxy_buffer_size"` @@ -126,10 +129,13 @@ var requiredMainConfigTemplatePlaceholders = []string{ "{{OpenRestyKeepaliveRequests}}", "{{OpenRestyClientHeaderTimeout}}", "{{OpenRestyClientBodyTimeout}}", + "{{OpenRestyClientMaxBodySize}}", + "{{OpenRestyLargeClientHeaderBuffers}}", "{{OpenRestySendTimeout}}", "{{OpenRestyProxyConnectTimeout}}", "{{OpenRestyProxySendTimeout}}", "{{OpenRestyProxyReadTimeout}}", + "{{OpenRestyProxyRequestBuffering}}", "{{OpenRestyProxyBuffering}}", "{{OpenRestyProxyBuffers}}", "{{OpenRestyProxyBufferSize}}", @@ -437,10 +443,13 @@ func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot { KeepaliveRequests: common.OpenRestyKeepaliveRequests, ClientHeaderTimeout: common.OpenRestyClientHeaderTimeout, ClientBodyTimeout: common.OpenRestyClientBodyTimeout, + ClientMaxBodySize: common.OpenRestyClientMaxBodySize, + LargeClientHeaderBuffers: common.OpenRestyLargeClientHeaderBuffers, SendTimeout: common.OpenRestySendTimeout, ProxyConnectTimeout: common.OpenRestyProxyConnectTimeout, ProxySendTimeout: common.OpenRestyProxySendTimeout, ProxyReadTimeout: common.OpenRestyProxyReadTimeout, + ProxyRequestBuffering: common.OpenRestyProxyRequestBufferingEnabled, ProxyBufferingEnabled: common.OpenRestyProxyBufferingEnabled, ProxyBuffers: common.OpenRestyProxyBuffers, ProxyBufferSize: common.OpenRestyProxyBufferSize, @@ -494,10 +503,13 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon appendIfChanged("OpenRestyKeepaliveRequests", fmt.Sprintf("%d", left.KeepaliveRequests), fmt.Sprintf("%d", right.KeepaliveRequests)) appendIfChanged("OpenRestyClientHeaderTimeout", fmt.Sprintf("%d", left.ClientHeaderTimeout), fmt.Sprintf("%d", right.ClientHeaderTimeout)) appendIfChanged("OpenRestyClientBodyTimeout", fmt.Sprintf("%d", left.ClientBodyTimeout), fmt.Sprintf("%d", right.ClientBodyTimeout)) + appendIfChanged("OpenRestyClientMaxBodySize", left.ClientMaxBodySize, right.ClientMaxBodySize) + appendIfChanged("OpenRestyLargeClientHeaderBuffers", left.LargeClientHeaderBuffers, right.LargeClientHeaderBuffers) appendIfChanged("OpenRestySendTimeout", fmt.Sprintf("%d", left.SendTimeout), fmt.Sprintf("%d", right.SendTimeout)) appendIfChanged("OpenRestyProxyConnectTimeout", fmt.Sprintf("%d", left.ProxyConnectTimeout), fmt.Sprintf("%d", right.ProxyConnectTimeout)) appendIfChanged("OpenRestyProxySendTimeout", fmt.Sprintf("%d", left.ProxySendTimeout), fmt.Sprintf("%d", right.ProxySendTimeout)) appendIfChanged("OpenRestyProxyReadTimeout", fmt.Sprintf("%d", left.ProxyReadTimeout), fmt.Sprintf("%d", right.ProxyReadTimeout)) + appendIfChanged("OpenRestyProxyRequestBufferingEnabled", fmt.Sprintf("%t", left.ProxyRequestBuffering), fmt.Sprintf("%t", right.ProxyRequestBuffering)) appendIfChanged("OpenRestyProxyBufferingEnabled", fmt.Sprintf("%t", left.ProxyBufferingEnabled), fmt.Sprintf("%t", right.ProxyBufferingEnabled)) appendIfChanged("OpenRestyProxyBuffers", left.ProxyBuffers, right.ProxyBuffers) appendIfChanged("OpenRestyProxyBufferSize", left.ProxyBufferSize, right.ProxyBufferSize) @@ -536,10 +548,13 @@ func openRestyOptionKeys() []string { "OpenRestyKeepaliveRequests", "OpenRestyClientHeaderTimeout", "OpenRestyClientBodyTimeout", + "OpenRestyClientMaxBodySize", + "OpenRestyLargeClientHeaderBuffers", "OpenRestySendTimeout", "OpenRestyProxyConnectTimeout", "OpenRestyProxySendTimeout", "OpenRestyProxyReadTimeout", + "OpenRestyProxyRequestBufferingEnabled", "OpenRestyProxyBufferingEnabled", "OpenRestyProxyBuffers", "OpenRestyProxyBufferSize", @@ -629,10 +644,13 @@ func renderMainConfigTemplate(templateText string, cfg openRestyConfigSnapshot) "{{OpenRestyKeepaliveRequests}}", fmt.Sprintf("%d", cfg.KeepaliveRequests), "{{OpenRestyClientHeaderTimeout}}", fmt.Sprintf("%d", cfg.ClientHeaderTimeout), "{{OpenRestyClientBodyTimeout}}", fmt.Sprintf("%d", cfg.ClientBodyTimeout), + "{{OpenRestyClientMaxBodySize}}", cfg.ClientMaxBodySize, + "{{OpenRestyLargeClientHeaderBuffers}}", cfg.LargeClientHeaderBuffers, "{{OpenRestySendTimeout}}", fmt.Sprintf("%d", cfg.SendTimeout), "{{OpenRestyProxyConnectTimeout}}", fmt.Sprintf("%d", cfg.ProxyConnectTimeout), "{{OpenRestyProxySendTimeout}}", fmt.Sprintf("%d", cfg.ProxySendTimeout), "{{OpenRestyProxyReadTimeout}}", fmt.Sprintf("%d", cfg.ProxyReadTimeout), + "{{OpenRestyProxyRequestBuffering}}", onOff(cfg.ProxyRequestBuffering), "{{OpenRestyProxyBuffering}}", onOff(cfg.ProxyBufferingEnabled), "{{OpenRestyProxyBuffers}}", cfg.ProxyBuffers, "{{OpenRestyProxyBufferSize}}", cfg.ProxyBufferSize, diff --git a/atsf_server/service/https_phase1_test.go b/atsf_server/service/https_phase1_test.go index fa91cfbd..03decc2a 100644 --- a/atsf_server/service/https_phase1_test.go +++ b/atsf_server/service/https_phase1_test.go @@ -290,6 +290,34 @@ func TestOpenRestyMainConfigTemplateRenderAndValidate(t *testing.T) { } } +func TestOpenRestyCommonRequestOptionsRender(t *testing.T) { + setupServiceTestDB(t) + + if err := model.UpdateOption("OpenRestyClientMaxBodySize", "128m"); err != nil { + t.Fatalf("UpdateOption OpenRestyClientMaxBodySize failed: %v", err) + } + if err := model.UpdateOption("OpenRestyLargeClientHeaderBuffers", "8 32k"); err != nil { + t.Fatalf("UpdateOption OpenRestyLargeClientHeaderBuffers failed: %v", err) + } + if err := model.UpdateOption("OpenRestyProxyRequestBufferingEnabled", "false"); err != nil { + t.Fatalf("UpdateOption OpenRestyProxyRequestBufferingEnabled failed: %v", err) + } + + preview, err := PreviewConfigVersion() + if err != nil { + t.Fatalf("PreviewConfigVersion failed: %v", err) + } + if !strings.Contains(preview.MainConfig, "client_max_body_size 128m;") { + t.Fatal("expected preview main config to include client_max_body_size") + } + if !strings.Contains(preview.MainConfig, "large_client_header_buffers 8 32k;") { + t.Fatal("expected preview main config to include large_client_header_buffers") + } + if !strings.Contains(preview.MainConfig, "proxy_request_buffering off;") { + t.Fatal("expected preview main config to include proxy_request_buffering off") + } +} + func setupServiceTestDB(t *testing.T) { t.Helper() common.SQLitePath = filepath.Join(t.TempDir(), "service.db") diff --git a/atsf_server/web/features/performance/components/performance-page.tsx b/atsf_server/web/features/performance/components/performance-page.tsx index 7172b713..bc9ec6a0 100644 --- a/atsf_server/web/features/performance/components/performance-page.tsx +++ b/atsf_server/web/features/performance/components/performance-page.tsx @@ -38,10 +38,13 @@ const defaultPerformanceFields = { OpenRestyKeepaliveRequests: '1000', OpenRestyClientHeaderTimeout: '15', OpenRestyClientBodyTimeout: '15', + OpenRestyClientMaxBodySize: '64m', + OpenRestyLargeClientHeaderBuffers: '4 16k', OpenRestySendTimeout: '30', OpenRestyProxyConnectTimeout: '5', OpenRestyProxySendTimeout: '60', OpenRestyProxyReadTimeout: '60', + OpenRestyProxyRequestBufferingEnabled: true, OpenRestyProxyBufferingEnabled: true, OpenRestyProxyBuffers: '16 16k', OpenRestyProxyBufferSize: '8k', @@ -76,10 +79,16 @@ const performanceFieldTooltips: Record = { keepalive_requests: '单个长连接允许复用的最大请求数。', client_header_timeout: '读取客户端请求头的超时时间,单位秒。', client_body_timeout: '读取客户端请求体的超时时间,单位秒。', + client_max_body_size: + '限制客户端请求体大小,常用于上传文件大小控制,例如 64m、128m。', + large_client_header_buffers: + '控制大请求头使用的缓冲区数量和大小,例如 4 16k。', send_timeout: '向客户端发送响应时的超时时间,单位秒。', proxy_connect_timeout: '连接上游源站的超时时间,单位秒。', proxy_send_timeout: '向上游发送请求的超时时间,单位秒。', proxy_read_timeout: '等待上游返回响应的超时时间,单位秒。', + proxy_request_buffering: + '控制请求体是否先在 Nginx 侧缓冲后再转发给上游,上传和流式场景经常会用到。', proxy_buffering: '控制是否启用代理响应缓冲。开启后通常有更平滑的吞吐,但会增加内存占用。', proxy_buffers: '设置代理响应缓冲区的数量和大小,例如 16 16k。', @@ -207,11 +216,18 @@ export function PerformancePage() { OpenRestyClientHeaderTimeout: optionMap.OpenRestyClientHeaderTimeout ?? '15', OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15', + OpenRestyClientMaxBodySize: optionMap.OpenRestyClientMaxBodySize ?? '64m', + OpenRestyLargeClientHeaderBuffers: + optionMap.OpenRestyLargeClientHeaderBuffers ?? '4 16k', OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30', OpenRestyProxyConnectTimeout: optionMap.OpenRestyProxyConnectTimeout ?? '5', OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60', OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60', + OpenRestyProxyRequestBufferingEnabled: toBoolean( + optionMap.OpenRestyProxyRequestBufferingEnabled, + true, + ), OpenRestyProxyBufferingEnabled: toBoolean( optionMap.OpenRestyProxyBufferingEnabled, true, @@ -298,6 +314,18 @@ export function PerformancePage() { throw new Error(`${label} 必须为大于 0 的整数。`); } } + if (!isSizeValue(performanceFields.OpenRestyClientMaxBodySize)) { + throw new Error( + 'client_max_body_size 必须为整数或带 k/m/g 单位的大小值。', + ); + } + if ( + !isProxyBuffersValue( + performanceFields.OpenRestyLargeClientHeaderBuffers, + ) + ) { + throw new Error('large_client_header_buffers 格式必须类似 "4 16k"。'); + } await saveOptionEntries( [ @@ -334,6 +362,14 @@ export function PerformancePage() { 'OpenRestyClientBodyTimeout', performanceFields.OpenRestyClientBodyTimeout.trim(), ], + [ + 'OpenRestyClientMaxBodySize', + performanceFields.OpenRestyClientMaxBodySize.trim(), + ], + [ + 'OpenRestyLargeClientHeaderBuffers', + performanceFields.OpenRestyLargeClientHeaderBuffers.trim(), + ], [ 'OpenRestySendTimeout', performanceFields.OpenRestySendTimeout.trim(), @@ -357,6 +393,7 @@ export function PerformancePage() { throw new Error('代理超时参数必须为大于 0 的整数秒。'); } } + if (!isProxyBuffersValue(performanceFields.OpenRestyProxyBuffers)) { throw new Error('proxy_buffers 格式必须类似 "16 16k"。'); } @@ -381,6 +418,10 @@ export function PerformancePage() { 'OpenRestyProxyReadTimeout', performanceFields.OpenRestyProxyReadTimeout.trim(), ], + [ + 'OpenRestyProxyRequestBufferingEnabled', + String(performanceFields.OpenRestyProxyRequestBufferingEnabled), + ], [ 'OpenRestyProxyBufferingEnabled', String(performanceFields.OpenRestyProxyBufferingEnabled), @@ -788,6 +829,36 @@ export function PerformancePage() { } /> + + + setPerformanceFields((previous) => ({ + ...previous, + OpenRestyClientMaxBodySize: event.target.value, + })) + } + placeholder="64m" + /> + + + + setPerformanceFields((previous) => ({ + ...previous, + OpenRestyLargeClientHeaderBuffers: event.target.value, + })) + } + placeholder="4 16k" + /> + + + setPerformanceFields((previous) => ({ + ...previous, + OpenRestyProxyRequestBufferingEnabled: checked, + })) + } + />