From 6e1eac2c864fc3d97f4bb490edb392726e93ef31 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 15 Mar 2026 13:29:19 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E6=B7=BB=E5=8A=A0=20Web?= =?UTF-8?q?Socket=20=E5=8D=87=E7=BA=A7=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=85=8D=E7=BD=AE=E5=92=8C=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- atsf_server/common/constants.go | 1 + atsf_server/controller/option.go | 1 + atsf_server/controller/option_test.go | 2 + atsf_server/model/option.go | 3 + atsf_server/service/config_version.go | 9 +++ atsf_server/service/https_phase1_test.go | 58 +++++++++++++++++++ .../components/performance-page.tsx | 22 +++++++ .../components/version-upgrade-modal.tsx | 46 +++++++++++---- docs/app-config.md | 3 +- 9 files changed, 134 insertions(+), 11 deletions(-) diff --git a/atsf_server/common/constants.go b/atsf_server/common/constants.go index 0310a143..3d652208 100644 --- a/atsf_server/common/constants.go +++ b/atsf_server/common/constants.go @@ -71,6 +71,7 @@ var OpenRestySendTimeout = 30 var OpenRestyProxyConnectTimeout = 5 var OpenRestyProxySendTimeout = 60 var OpenRestyProxyReadTimeout = 60 +var OpenRestyWebsocketEnabled = true var OpenRestyProxyRequestBufferingEnabled = false var OpenRestyProxyBufferingEnabled = true var OpenRestyProxyBuffers = "16 16k" diff --git a/atsf_server/controller/option.go b/atsf_server/controller/option.go index c1af66f5..fbcacfad 100644 --- a/atsf_server/controller/option.go +++ b/atsf_server/controller/option.go @@ -114,6 +114,7 @@ func validateOpenRestyOption(key string, value string) error { return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key) } case "OpenRestyEventsMultiAcceptEnabled", + "OpenRestyWebsocketEnabled", "OpenRestyProxyRequestBufferingEnabled", "OpenRestyProxyBufferingEnabled", "OpenRestyGzipEnabled", diff --git a/atsf_server/controller/option_test.go b/atsf_server/controller/option_test.go index 8f809b1e..6364beb3 100644 --- a/atsf_server/controller/option_test.go +++ b/atsf_server/controller/option_test.go @@ -24,6 +24,8 @@ func TestValidateOpenRestyOption(t *testing.T) { {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: "websocket valid", key: "OpenRestyWebsocketEnabled", value: "false"}, + {name: "websocket invalid", key: "OpenRestyWebsocketEnabled", value: "off", 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/model/option.go b/atsf_server/model/option.go index 4c753a02..7a55518a 100644 --- a/atsf_server/model/option.go +++ b/atsf_server/model/option.go @@ -71,6 +71,7 @@ func InitOptionMap() { common.OptionMap["OpenRestyProxyConnectTimeout"] = strconv.Itoa(common.OpenRestyProxyConnectTimeout) common.OptionMap["OpenRestyProxySendTimeout"] = strconv.Itoa(common.OpenRestyProxySendTimeout) common.OptionMap["OpenRestyProxyReadTimeout"] = strconv.Itoa(common.OpenRestyProxyReadTimeout) + common.OptionMap["OpenRestyWebsocketEnabled"] = strconv.FormatBool(common.OpenRestyWebsocketEnabled) common.OptionMap["OpenRestyProxyRequestBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyRequestBufferingEnabled) common.OptionMap["OpenRestyProxyBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyBufferingEnabled) common.OptionMap["OpenRestyProxyBuffers"] = common.OpenRestyProxyBuffers @@ -269,6 +270,8 @@ func updateOptionMap(key string, value string) { if v, err := strconv.Atoi(value); err == nil && v > 0 { common.OpenRestyProxyReadTimeout = v } + case "OpenRestyWebsocketEnabled": + common.OpenRestyWebsocketEnabled = value == "true" case "OpenRestyProxyRequestBufferingEnabled": common.OpenRestyProxyRequestBufferingEnabled = value == "true" case "OpenRestyProxyBufferingEnabled": diff --git a/atsf_server/service/config_version.go b/atsf_server/service/config_version.go index 6035786a..bc401a4d 100644 --- a/atsf_server/service/config_version.go +++ b/atsf_server/service/config_version.go @@ -78,6 +78,7 @@ type openRestyConfigSnapshot struct { ProxyConnectTimeout int `json:"proxy_connect_timeout"` ProxySendTimeout int `json:"proxy_send_timeout"` ProxyReadTimeout int `json:"proxy_read_timeout"` + WebsocketEnabled bool `json:"websocket_enabled"` ProxyRequestBuffering bool `json:"proxy_request_buffering"` ProxyBufferingEnabled bool `json:"proxy_buffering_enabled"` ProxyBuffers string `json:"proxy_buffers"` @@ -454,6 +455,7 @@ func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot { ProxyConnectTimeout: common.OpenRestyProxyConnectTimeout, ProxySendTimeout: common.OpenRestyProxySendTimeout, ProxyReadTimeout: common.OpenRestyProxyReadTimeout, + WebsocketEnabled: common.OpenRestyWebsocketEnabled, ProxyRequestBuffering: common.OpenRestyProxyRequestBufferingEnabled, ProxyBufferingEnabled: common.OpenRestyProxyBufferingEnabled, ProxyBuffers: common.OpenRestyProxyBuffers, @@ -514,6 +516,7 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon 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("OpenRestyWebsocketEnabled", fmt.Sprintf("%t", left.WebsocketEnabled), fmt.Sprintf("%t", right.WebsocketEnabled)) 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) @@ -559,6 +562,7 @@ func openRestyOptionKeys() []string { "OpenRestyProxyConnectTimeout", "OpenRestyProxySendTimeout", "OpenRestyProxyReadTimeout", + "OpenRestyWebsocketEnabled", "OpenRestyProxyRequestBufferingEnabled", "OpenRestyProxyBufferingEnabled", "OpenRestyProxyBuffers", @@ -762,6 +766,11 @@ func renderProxyHeaderBlock(customHeaders []ProxyRouteCustomHeaderInput) string builder.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n") builder.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n") builder.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n") + if common.OpenRestyWebsocketEnabled { + builder.WriteString(" proxy_http_version 1.1;\n") + builder.WriteString(" proxy_set_header Upgrade $http_upgrade;\n") + builder.WriteString(" proxy_set_header Connection $http_connection;\n") + } for _, header := range customHeaders { builder.WriteString(fmt.Sprintf(" proxy_set_header %s %s;\n", header.Key, quoteNginxHeaderValue(header.Value))) } diff --git a/atsf_server/service/https_phase1_test.go b/atsf_server/service/https_phase1_test.go index 9a9c3116..37d82072 100644 --- a/atsf_server/service/https_phase1_test.go +++ b/atsf_server/service/https_phase1_test.go @@ -96,6 +96,9 @@ func TestCreateProxyRouteRejectsHTTPSWithoutCertificate(t *testing.T) { func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) { setupServiceTestDB(t) + if err := model.UpdateOption("OpenRestyWebsocketEnabled", "true"); err != nil { + t.Fatalf("UpdateOption OpenRestyWebsocketEnabled failed: %v", err) + } _, err := CreateProxyRoute(ProxyRouteInput{ Domain: "custom.example.com", @@ -123,10 +126,52 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) { if !strings.Contains(result.Version.SnapshotJSON, "custom_headers") { t.Fatal("expected snapshot to include custom headers") } + if !strings.Contains(result.Version.RenderedConfig, "proxy_http_version 1.1;") { + t.Fatal("expected rendered config to enable HTTP/1.1 proxying for websocket upgrades") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Upgrade $http_upgrade;") { + t.Fatal("expected rendered config to forward websocket upgrade header") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Connection $http_connection;") { + t.Fatal("expected rendered config to forward websocket connection header") + } +} + +func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) { + setupServiceTestDB(t) + + _, err := CreateProxyRoute(ProxyRouteInput{ + Domain: "ws-off.example.com", + OriginURL: "https://origin.internal", + Enabled: true, + }) + if err != nil { + t.Fatalf("CreateProxyRoute failed: %v", err) + } + if err := model.UpdateOption("OpenRestyWebsocketEnabled", "false"); err != nil { + t.Fatalf("UpdateOption OpenRestyWebsocketEnabled failed: %v", err) + } + + preview, err := PreviewConfigVersion() + if err != nil { + t.Fatalf("PreviewConfigVersion failed: %v", err) + } + if strings.Contains(preview.RenderedConfig, "proxy_http_version 1.1;") { + t.Fatal("expected preview config to omit websocket proxy_http_version when disabled") + } + if strings.Contains(preview.RenderedConfig, "proxy_set_header Upgrade $http_upgrade;") { + t.Fatal("expected preview config to omit websocket upgrade header when disabled") + } + if strings.Contains(preview.RenderedConfig, "proxy_set_header Connection $http_connection;") { + t.Fatal("expected preview config to omit websocket connection header when disabled") + } } func TestPreviewAndDiffConfigVersion(t *testing.T) { setupServiceTestDB(t) + if err := model.UpdateOption("OpenRestyWebsocketEnabled", "true"); err != nil { + t.Fatalf("UpdateOption OpenRestyWebsocketEnabled failed: %v", err) + } stableRoute, err := CreateProxyRoute(ProxyRouteInput{ Domain: "stable.example.com", @@ -226,6 +271,9 @@ func TestPreviewAndDiffConfigVersion(t *testing.T) { if err = model.UpdateOption("OpenRestyProxyReadTimeout", "120"); err != nil { t.Fatalf("UpdateOption failed: %v", err) } + if err = model.UpdateOption("OpenRestyWebsocketEnabled", "false"); err != nil { + t.Fatalf("UpdateOption OpenRestyWebsocketEnabled failed: %v", err) + } diff, err = DiffConfigVersion() if err != nil { t.Fatalf("DiffConfigVersion after option change failed: %v", err) @@ -240,6 +288,7 @@ func TestPreviewAndDiffConfigVersion(t *testing.T) { t.Fatal("expected changed OpenResty option details to be reported") } found := false + foundWebsocket := false for _, item := range diff.ChangedOptionDetails { if item.Key == "OpenRestyProxyReadTimeout" { found = true @@ -247,10 +296,19 @@ func TestPreviewAndDiffConfigVersion(t *testing.T) { t.Fatalf("unexpected option diff values: %+v", item) } } + if item.Key == "OpenRestyWebsocketEnabled" { + foundWebsocket = true + if item.PreviousValue != "true" || item.CurrentValue != "false" { + t.Fatalf("unexpected websocket option diff values: %+v", item) + } + } } if !found { t.Fatal("expected OpenRestyProxyReadTimeout diff detail") } + if !foundWebsocket { + t.Fatal("expected OpenRestyWebsocketEnabled diff detail") + } } func TestCreateTLSCertificateRejectsInvalidPEM(t *testing.T) { diff --git a/atsf_server/web/features/performance/components/performance-page.tsx b/atsf_server/web/features/performance/components/performance-page.tsx index de4f37d5..cef79990 100644 --- a/atsf_server/web/features/performance/components/performance-page.tsx +++ b/atsf_server/web/features/performance/components/performance-page.tsx @@ -46,6 +46,7 @@ const defaultPerformanceFields = { OpenRestyProxyConnectTimeout: '5', OpenRestyProxySendTimeout: '60', OpenRestyProxyReadTimeout: '60', + OpenRestyWebsocketEnabled: true, OpenRestyProxyRequestBufferingEnabled: false, OpenRestyProxyBufferingEnabled: true, OpenRestyProxyBuffers: '16 16k', @@ -89,6 +90,8 @@ const performanceFieldTooltips: Record = { proxy_connect_timeout: '连接上游源站的超时时间,单位秒。', proxy_send_timeout: '向上游发送请求的超时时间,单位秒。', proxy_read_timeout: '等待上游返回响应的超时时间,单位秒。', + websocket: + '控制是否为反向代理规则自动注入 WebSocket 升级所需的 HTTP/1.1、Upgrade 和 Connection 头。', proxy_request_buffering: '控制请求体是否先在 Nginx 侧缓冲后再转发给上游,上传和流式场景经常会用到。', proxy_buffering: @@ -219,6 +222,10 @@ export function PerformancePage() { optionMap.OpenRestyProxyConnectTimeout ?? '5', OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60', OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60', + OpenRestyWebsocketEnabled: toBoolean( + optionMap.OpenRestyWebsocketEnabled, + true, + ), OpenRestyProxyRequestBufferingEnabled: toBoolean( optionMap.OpenRestyProxyRequestBufferingEnabled, false, @@ -413,6 +420,10 @@ export function PerformancePage() { 'OpenRestyProxyReadTimeout', performanceFields.OpenRestyProxyReadTimeout.trim(), ], + [ + 'OpenRestyWebsocketEnabled', + String(performanceFields.OpenRestyWebsocketEnabled), + ], [ 'OpenRestyProxyRequestBufferingEnabled', String(performanceFields.OpenRestyProxyRequestBufferingEnabled), @@ -927,6 +938,17 @@ export function PerformancePage() { } /> + + setPerformanceFields((previous) => ({ + ...previous, + OpenRestyWebsocketEnabled: checked, + })) + } + /> -
+
{upgradeLogs.map((log, index) => ( -
-
- {formatDateTime(log.created_at)} - {log.level || 'info'} -
-

+ + {formatLogTimestamp(log.created_at)} + + | + + {formatLogLevel(log.level)} + + | + {log.message} -

-
+ + ))}
diff --git a/docs/app-config.md b/docs/app-config.md index 78d350b3..fdd77f6e 100644 --- a/docs/app-config.md +++ b/docs/app-config.md @@ -140,9 +140,10 @@ volumes: | `OpenRestyClientHeaderTimeout` | `client_header_timeout` 秒数 | `15` | | `OpenRestyClientBodyTimeout` | `client_body_timeout` 秒数 | `15` | | `OpenRestySendTimeout` | `send_timeout` 秒数 | `30` | -| `OpenRestyProxyConnectTimeout` | `proxy_connect_timeout` 秒数 | `5` | +| `OpenRestyProxyConnectTimeout` | `proxy_connect_timeout` 秒数 | `5` | | `OpenRestyProxySendTimeout` | `proxy_send_timeout` 秒数 | `60` | | `OpenRestyProxyReadTimeout` | `proxy_read_timeout` 秒数 | `60` | +| `OpenRestyWebsocketEnabled` | 是否自动注入 WebSocket 升级所需代理头 | `true` | | `OpenRestyProxyRequestBufferingEnabled` | 是否启用 `proxy_request_buffering` | `false` | | `OpenRestyProxyBufferingEnabled` | 是否启用 `proxy_buffering` | `true` | | `OpenRestyProxyBuffers` | `proxy_buffers` 组合值,例如 `16 16k` | `16 16k` |