From 67197220ae5bf91cb6b31337018cd00f38e09c07 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Mar 2026 22:13:53 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E4=B8=8A=E6=B8=B8=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_server/service/config_version.go | 86 ++++++++++++++++--- openflare_server/service/https_phase1_test.go | 33 +++++-- 2 files changed, 98 insertions(+), 21 deletions(-) diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go index 3df3731d..9d9433cb 100644 --- a/openflare_server/service/config_version.go +++ b/openflare_server/service/config_version.go @@ -79,6 +79,13 @@ type routeCacheConfig struct { Rules []string } +type routeUpstreamConfig struct { + Name string + Scheme string + Address string + UsesNamedUpstream bool +} + type openRestyConfigSnapshot struct { WorkerProcesses string `json:"worker_processes"` WorkerConnections int `json:"worker_connections"` @@ -650,8 +657,12 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) Policy: route.CachePolicy, Rules: cacheRules, } + upstreamConfig := buildRouteUpstreamConfig(route, cfg) + if upstreamConfig.UsesNamedUpstream { + builder.WriteString(renderNamedUpstreamBlock(upstreamConfig)) + } if !route.EnableHTTPS { - builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, cfg)) + builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, upstreamConfig, cfg)) continue } if route.CertID == nil || *route.CertID == 0 { @@ -668,9 +679,9 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) if route.RedirectHTTP { builder.WriteString(renderHTTPRedirectServer(route.Domain)) } else { - builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, cfg)) + builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, upstreamConfig, cfg)) } - builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, route.OriginHost, certificate.ID, customHeaders, cacheConfig, cfg)) + builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, route.OriginHost, certificate.ID, customHeaders, cacheConfig, upstreamConfig, cfg)) } return builder.String(), dedupeSupportFiles(supportFiles), nil } @@ -807,29 +818,29 @@ func nextVersionNumber(now time.Time) (string, error) { return fmt.Sprintf("%s-%03d", prefix, count+1), nil } -func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, cfg openRestyConfigSnapshot) string { - return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, renderProxyHeaderBlock(originURL, originHost, customHeaders), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, cfg)) +func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string { + return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig, cfg)) } func renderHTTPRedirectServer(domain string) string { return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", domain) } -func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, cfg openRestyConfigSnapshot) string { +func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string { certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID)) - return fmt.Sprintf("server {\n listen 443 ssl http2;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, cfg)) + return fmt.Sprintf("server {\n listen 443 ssl http2;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s%s%s }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig, cfg)) } func renderConnectionUpgradeMap() string { - return " map $http_upgrade $connection_upgrade {\n default upgrade;\n '' close;\n }\n\n" + return " map $http_upgrade $connection_upgrade {\n default upgrade;\n '' \"\";\n }\n\n" } func renderDefaultServerBlock() string { return " server {\n listen 80 default_server;\n server_name _;\n\n return 404;\n }\n\n" } -func renderProxyHeaderBlock(originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput) string { +func renderProxyHeaderBlock(originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, upstreamConfig routeUpstreamConfig) string { var builder strings.Builder if strings.TrimSpace(originHost) != "" { builder.WriteString(fmt.Sprintf(" proxy_set_header Host %s;\n", quoteNginxHeaderValue(originHost))) @@ -843,11 +854,13 @@ func renderProxyHeaderBlock(originURL string, originHost string, customHeaders [ 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 { + if common.OpenRestyWebsocketEnabled || upstreamConfig.UsesNamedUpstream { builder.WriteString(" proxy_http_version 1.1;\n") - builder.WriteString(" proxy_set_header Upgrade $http_upgrade;\n") builder.WriteString(" proxy_set_header Connection $connection_upgrade;\n") } + if common.OpenRestyWebsocketEnabled { + builder.WriteString(" proxy_set_header Upgrade $http_upgrade;\n") + } for _, header := range customHeaders { builder.WriteString(fmt.Sprintf(" proxy_set_header %s %s;\n", header.Key, quoteNginxHeaderValue(header.Value))) } @@ -919,11 +932,14 @@ func buildPathExactMatchPattern(rules []string) string { return fmt.Sprintf("^(?:%s)$", strings.Join(parts, "|")) } -func renderProxyPassBlock(originURL string, cfg openRestyConfigSnapshot) string { +func renderProxyPassBlock(originURL string, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string { parsed, err := url.Parse(originURL) if err != nil || parsed.Host == "" || parsed.Scheme == "" { return fmt.Sprintf(" proxy_pass %s;\n", originURL) } + if upstreamConfig.UsesNamedUpstream { + return fmt.Sprintf(" proxy_pass %s://%s;\n", upstreamConfig.Scheme, upstreamConfig.Name) + } if !shouldUseRuntimeResolver(originURL, cfg.Resolvers) { return fmt.Sprintf(" proxy_pass %s;\n", originURL) } @@ -949,6 +965,52 @@ func renderProxyPassBlock(originURL string, cfg openRestyConfigSnapshot) string return builder.String() } +func buildRouteUpstreamConfig(route *model.ProxyRoute, cfg openRestyConfigSnapshot) routeUpstreamConfig { + parsed, err := url.Parse(strings.TrimSpace(route.OriginURL)) + if err != nil || parsed.Host == "" || parsed.Scheme == "" { + return routeUpstreamConfig{} + } + if shouldUseRuntimeResolver(route.OriginURL, cfg.Resolvers) { + return routeUpstreamConfig{} + } + if strings.TrimSpace(parsed.EscapedPath()) != "" && strings.TrimSpace(parsed.EscapedPath()) != "/" { + return routeUpstreamConfig{} + } + if parsed.RawQuery != "" { + return routeUpstreamConfig{} + } + return routeUpstreamConfig{ + Name: buildRouteUpstreamName(route), + Scheme: parsed.Scheme, + Address: parsed.Host, + UsesNamedUpstream: true, + } +} + +func buildRouteUpstreamName(route *model.ProxyRoute) string { + sanitized := strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z': + return r + case r >= 'A' && r <= 'Z': + return r + ('a' - 'A') + case r >= '0' && r <= '9': + return r + default: + return '_' + } + }, route.Domain) + sanitized = strings.Trim(sanitized, "_") + if sanitized == "" { + sanitized = "backend" + } + return fmt.Sprintf("backend_%s_%d", sanitized, route.ID) +} + +func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string { + return fmt.Sprintf("upstream %s {\n server %s max_fails=3 fail_timeout=10s;\n keepalive 128;\n}\n\n", upstreamConfig.Name, upstreamConfig.Address) +} + func shouldUseRuntimeResolver(originURL string, resolvers string) bool { if strings.TrimSpace(resolvers) == "" { return false diff --git a/openflare_server/service/https_phase1_test.go b/openflare_server/service/https_phase1_test.go index b15746e5..2e3e246f 100644 --- a/openflare_server/service/https_phase1_test.go +++ b/openflare_server/service/https_phase1_test.go @@ -141,8 +141,14 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) { if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Connection $connection_upgrade;") { t.Fatal("expected rendered config to use normalized websocket connection header") } - if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://origin.internal;") { - t.Fatal("expected rendered config to keep direct proxy_pass when no resolvers are configured") + if !strings.Contains(result.Version.RenderedConfig, "upstream backend_custom_example_com_1 {") { + t.Fatal("expected rendered config to define named upstream for simple origins") + } + if !strings.Contains(result.Version.RenderedConfig, "keepalive 128;") { + t.Fatal("expected rendered config to enable upstream keepalive") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_custom_example_com_1;") { + t.Fatal("expected rendered config to proxy through named upstream when no resolver is required") } if strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") { t.Fatal("expected rendered config to avoid runtime-resolved proxy_pass when no resolvers are configured") @@ -221,6 +227,9 @@ func TestPublishConfigVersionRendersRouteLevelCachePolicy(t *testing.T) { if strings.Count(result.Version.RenderedConfig, "proxy_cache openflare_cache;") != 1 { t.Fatal("expected only cache-enabled route to include proxy_cache directive") } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_static_example_com_1;") { + t.Fatal("expected cache-enabled route to proxy through named upstream") + } if !strings.Contains(result.Version.SnapshotJSON, `"cache_enabled":true`) { t.Fatal("expected snapshot to include route cache toggle") } @@ -255,8 +264,11 @@ func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) { if !strings.Contains(result.Version.RenderedConfig, `proxy_ssl_name "git.arctel.net";`) { t.Fatal("expected rendered config to set proxy ssl name from origin host override") } - if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://git.arctel.net;") { - t.Fatal("expected rendered config to keep direct proxy_pass for hostname origin when resolvers are blank") + if !strings.Contains(result.Version.RenderedConfig, "upstream backend_git_arctel_de_1 {") { + t.Fatal("expected rendered config to define named upstream for static hostname origins") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://backend_git_arctel_de_1;") { + t.Fatal("expected rendered config to proxy through named upstream for hostname origin when resolvers are blank") } if !strings.Contains(result.Version.SnapshotJSON, `"origin_host":"git.arctel.net"`) { t.Fatal("expected snapshot to include origin_host override") @@ -285,6 +297,9 @@ func TestPublishConfigVersionUsesRuntimeResolverWhenConfigured(t *testing.T) { if !strings.Contains(result.Version.MainConfig, "resolver 1.1.1.1 8.8.8.8 valid=30s ipv6=off;") { t.Fatal("expected main config to render configured resolver directive") } + if strings.Contains(result.Version.RenderedConfig, "upstream backend_resolver_example_com_1 {") { + t.Fatal("expected runtime-resolved origin to avoid named upstream block") + } if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "https://origin.internal";`) { t.Fatal("expected rendered config to use runtime upstream variable when resolvers are configured") } @@ -309,8 +324,11 @@ func TestPublishConfigVersionKeepsDirectProxyPassForIPOrigins(t *testing.T) { if err != nil { t.Fatalf("PublishConfigVersion failed: %v", err) } - if !strings.Contains(result.Version.RenderedConfig, "proxy_pass http://10.0.0.8:8080;") { - t.Fatal("expected rendered config to keep direct proxy_pass for IP origin") + if !strings.Contains(result.Version.RenderedConfig, "upstream backend_ip_origin_example_com_1 {") { + t.Fatal("expected rendered config to define named upstream for static IP origins") + } + if !strings.Contains(result.Version.RenderedConfig, "proxy_pass http://backend_ip_origin_example_com_1;") { + t.Fatal("expected rendered config to proxy through named upstream for IP origin") } if strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "http://10.0.0.8:8080"`) { t.Fatal("expected rendered config to avoid runtime resolver variables for IP origin") @@ -342,9 +360,6 @@ func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) { 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 $connection_upgrade;") { - t.Fatal("expected preview config to omit websocket connection header when disabled") - } } func TestPreviewAndDiffConfigVersion(t *testing.T) {