From 889e79c8b829cc717163d4179aa4cc41fa6fb043 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 20 Jun 2026 21:03:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9B=E5=AD=90=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E7=AB=99=E7=82=B9=E6=A0=87=E8=AF=86=E5=8F=8C=E8=BD=A8?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/index.md | 2 + internal/apps/agent/nginx/manager.go | 2 + internal/apps/agent/nginx/pow_assets.go | 14 +++- internal/apps/openflare/agent/config.go | 2 +- .../apps/openflare/config_version/helpers.go | 46 ----------- .../apps/openflare/config_version/logics.go | 7 +- .../apps/openflare/config_version/snapshot.go | 9 ++- internal/apps/openflare/flared/helpers.go | 28 ------- internal/apps/openflare/flared/logics.go | 3 +- .../apps/openflare/proxy_route/helpers.go | 72 ++++++------------ internal/apps/openflare/proxy_route/logics.go | 7 +- .../apps/openflare/proxy_route/logics_test.go | 29 +++++++ .../apps/openflare/routeidentity/identity.go | 76 +++++++++++++++++++ .../openflare/routeidentity/identity_test.go | 38 ++++++++++ internal/apps/openflare/uptimekuma/sync.go | 31 +------- pkg/render/openresty/render.go | 43 +++-------- pkg/render/openresty/render_test.go | 5 +- 17 files changed, 215 insertions(+), 199 deletions(-) create mode 100644 internal/apps/openflare/routeidentity/identity.go create mode 100644 internal/apps/openflare/routeidentity/identity_test.go diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 682fcc0b..03fa9251 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -20,6 +20,8 @@ sidebar: false - 修复 WAF 规则组 PoW 策略发布后边缘不生效:统一 WAF 绑定站点名与 OpenResty 路由 `site_name` 解析逻辑,并为所有已启用网站生成 `site_rule_groups` 条目(含仅依赖全局规则组的站点)。 +- 收敛子代理站点标识双轨逻辑:新增 `routeidentity` 统一包,`proxy_route`、`config_version`、`uptimekuma`、`flared` 与 OpenResty 渲染共用 `ResolveSiteName` / `DecodeDomains`;移除废弃 `RenderPoWConfig`;PoW Lua 与 WAF 一致仅依赖 `$openflare_waf_site`。 + - 修复全球态势板在仅有 `geo_name`(如 mmdb 的 Germany)而无经纬度时误用美国 fallback 坐标的问题;按国家名/ISO 匹配地图质心。 - 修复 Agent 心跳上报公网 IP 后节点地理位置未自动更新:进程启动时按 `GeoIPProvider` 初始化 `pkg/geoip`,`mmdb` 模式从内置 GeoLite2 种子到 `data/`,并在 Relay 心跳同步地理位置。 diff --git a/internal/apps/agent/nginx/manager.go b/internal/apps/agent/nginx/manager.go index 9fe90f5d..fe3e5fc5 100644 --- a/internal/apps/agent/nginx/manager.go +++ b/internal/apps/agent/nginx/manager.go @@ -749,6 +749,8 @@ func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error { return m.writeManagedCertFiles(certFiles) } +// writePowConfig persists legacy pow_config.json for backward compatibility. +// PoW runtime loads site config from waf_config.json; pow_config.json is deprecated. func (m *Manager) writePowConfig(supportFiles []protocol.SupportFile) error { if m.RuntimeConfigDir == "" { return nil diff --git a/internal/apps/agent/nginx/pow_assets.go b/internal/apps/agent/nginx/pow_assets.go index 04d9ec71..e206e9fc 100644 --- a/internal/apps/agent/nginx/pow_assets.go +++ b/internal/apps/agent/nginx/pow_assets.go @@ -117,7 +117,7 @@ end local site = ngx.var.openflare_waf_site or "" if site == "" then - site = host + return end local config_raw = pow_config_dict:get(site) @@ -230,7 +230,9 @@ local redir = args["redir"] or "" local site = ngx.var.openflare_waf_site or "" if site == "" then - site = host + ngx.status = 403 + ngx.say("PoW site not resolved; openflare_waf_site is required") + return end local config_raw = pow_config_dict:get(site) @@ -348,6 +350,14 @@ const openRestyPowVerifyLua = `local cjson = require "cjson.safe" local pow_challenges = ngx.shared.openflare_pow_challenges local pow_sessions = ngx.shared.openflare_pow_sessions +local site = ngx.var.openflare_waf_site or "" +if site == "" then + ngx.status = 403 + ngx.header.content_type = "application/json" + ngx.say(cjson.encode({error = "PoW site not resolved; openflare_waf_site is required"})) + return +end + local args = ngx.req.get_uri_args() local challenge_id = args["id"] or "" local response = args["response"] or "" diff --git a/internal/apps/openflare/agent/config.go b/internal/apps/openflare/agent/config.go index 7ee88edf..e2bd1daa 100644 --- a/internal/apps/openflare/agent/config.go +++ b/internal/apps/openflare/agent/config.go @@ -91,7 +91,7 @@ func sourceSupportFiles(files []SupportFile) []SupportFile { func isRuntimeGeneratedSupportFile(path string) bool { switch strings.TrimSpace(path) { - case "pow_config.json", "waf_config.json", openrestyrender.SourceConfigFileName: + case "pow_config.json", "waf_config.json", openrestyrender.SourceConfigFileName: // pow_config.json is legacy; waf_config.json is canonical return true default: return false diff --git a/internal/apps/openflare/config_version/helpers.go b/internal/apps/openflare/config_version/helpers.go index 41bc37f1..1908b4c1 100644 --- a/internal/apps/openflare/config_version/helpers.go +++ b/internal/apps/openflare/config_version/helpers.go @@ -26,52 +26,6 @@ func isUniqueConstraintError(err error) bool { return strings.Contains(strings.ToLower(err.Error()), "unique") } -func normalizeProxyRouteSiteName(route *model.ProxyRoute, raw, primaryDomain string) string { - siteName := strings.TrimSpace(raw) - if siteName != "" { - return siteName - } - if route != nil && strings.TrimSpace(route.SiteName) != "" { - return strings.TrimSpace(route.SiteName) - } - return primaryDomain -} - -func normalizeProxyRouteDomains(rawDomains []string) ([]string, error) { - normalized := make([]string, 0, len(rawDomains)) - seen := make(map[string]struct{}, len(rawDomains)) - for _, rawDomain := range rawDomains { - domain := strings.ToLower(strings.TrimSpace(rawDomain)) - if domain == "" { - continue - } - if strings.Contains(domain, "://") || strings.Contains(domain, "/") { - return nil, fmt.Errorf("domain %q is invalid", rawDomain) - } - if _, ok := seen[domain]; ok { - continue - } - seen[domain] = struct{}{} - normalized = append(normalized, domain) - } - if len(normalized) == 0 { - return nil, fmt.Errorf("domain is required") - } - return normalized, nil -} - -func decodeStoredDomains(raw string, fallbackDomain string) ([]string, error) { - text := strings.TrimSpace(raw) - if text == "" { - return normalizeProxyRouteDomains([]string{fallbackDomain}) - } - var domains []string - if err := json.Unmarshal([]byte(text), &domains); err != nil { - return nil, fmt.Errorf("domains payload is invalid") - } - return normalizeProxyRouteDomains(domains) -} - func decodeStoredUpstreams(raw string, fallbackOriginURL string) ([]string, error) { text := strings.TrimSpace(raw) if text == "" { diff --git a/internal/apps/openflare/config_version/logics.go b/internal/apps/openflare/config_version/logics.go index 8db03655..90a99eb4 100644 --- a/internal/apps/openflare/config_version/logics.go +++ b/internal/apps/openflare/config_version/logics.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/model" "gorm.io/gorm" ) @@ -321,14 +322,14 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute { return []snapshotRoute{} } for index := range routes { - normalizedDomains, err := decodeStoredDomains("", routes[index].Domain) + normalizedDomains, err := routeidentity.DecodeDomains("", routes[index].Domain) if len(routes[index].Domains) > 0 { - normalizedDomains, err = normalizeProxyRouteDomains(routes[index].Domains) + normalizedDomains, err = routeidentity.NormalizeDomains(routes[index].Domains) } if err == nil && len(normalizedDomains) > 0 { routes[index].Domains = normalizedDomains routes[index].Domain = normalizedDomains[0] - routes[index].SiteName = normalizeProxyRouteSiteName(nil, routes[index].SiteName, normalizedDomains[0]) + routes[index].SiteName = routeidentity.ResolveSiteName(nil, routes[index].SiteName, normalizedDomains[0]) } normalizedCertIDs, primaryCertID, certErr := normalizeSnapshotCertificateIDs(routes[index].CertID, routes[index].CertIDs) if certErr == nil { diff --git a/internal/apps/openflare/config_version/snapshot.go b/internal/apps/openflare/config_version/snapshot.go index 53d503bc..5915947a 100644 --- a/internal/apps/openflare/config_version/snapshot.go +++ b/internal/apps/openflare/config_version/snapshot.go @@ -11,6 +11,7 @@ import ( "sort" "strings" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/apps/openflare/waf" "github.com/Rain-kl/Wavelet/internal/model" openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty" @@ -211,7 +212,7 @@ func buildCurrentConfigBundle(ctx context.Context, requireRoutes bool) (*configB func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]snapshotRoute, error) { items := make([]snapshotRoute, 0, len(routes)) for _, route := range routes { - domains, err := decodeStoredDomains(route.Domains, route.Domain) + domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain) if err != nil { return nil, fmt.Errorf("route %s domains are invalid", route.Domain) } @@ -245,7 +246,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna } items = append(items, snapshotRoute{ ID: route.ID, - SiteName: normalizeProxyRouteSiteName(route, route.SiteName, domains[0]), + SiteName: routeidentity.ResolveSiteName(route, route.SiteName, domains[0]), Domain: domains[0], Domains: domains, OriginURL: originURL, @@ -319,11 +320,11 @@ func buildSnapshotWAFDocument(ctx context.Context, routes []*model.ProxyRoute) ( if route == nil { continue } - domains, domainErr := decodeStoredDomains(route.Domains, route.Domain) + domains, domainErr := routeidentity.DecodeDomains(route.Domains, route.Domain) if domainErr != nil { return snapshotWAFDocument{}, fmt.Errorf("route %s domains are invalid", route.Domain) } - enabledRouteSiteNames[route.ID] = normalizeProxyRouteSiteName(route, route.SiteName, domains[0]) + enabledRouteSiteNames[route.ID] = routeidentity.ResolveSiteName(route, route.SiteName, domains[0]) } rawBindings, err := model.ListOpenFlareWAFRuleGroupBindings(ctx) if err != nil { diff --git a/internal/apps/openflare/flared/helpers.go b/internal/apps/openflare/flared/helpers.go index 9ed07d48..68e4370d 100644 --- a/internal/apps/openflare/flared/helpers.go +++ b/internal/apps/openflare/flared/helpers.go @@ -5,7 +5,6 @@ package flared import ( "context" - "encoding/json" "errors" "fmt" "net" @@ -121,33 +120,6 @@ func relayClientAddress(node *model.OpenFlareNode) string { return fmt.Sprintf("%s:%d", addr, port) } -func decodeStoredDomains(raw string, fallbackDomain string) ([]string, error) { - text := strings.TrimSpace(raw) - if text == "" { - domain := strings.ToLower(strings.TrimSpace(fallbackDomain)) - if domain == "" { - return nil, errors.New("domain is required") - } - return []string{domain}, nil - } - var domains []string - if err := json.Unmarshal([]byte(text), &domains); err != nil { - return nil, errors.New("domains payload is invalid") - } - normalized := make([]string, 0, len(domains)) - for _, item := range domains { - domain := strings.ToLower(strings.TrimSpace(item)) - if domain == "" { - continue - } - normalized = append(normalized, domain) - } - if len(normalized) == 0 { - return nil, errors.New("domain is required") - } - return normalized, nil -} - func parseTunnelTargetAddr(addr string) (string, int) { addr = strings.TrimSpace(addr) if addr == "" { diff --git a/internal/apps/openflare/flared/logics.go b/internal/apps/openflare/flared/logics.go index c2ce899e..c6f69108 100644 --- a/internal/apps/openflare/flared/logics.go +++ b/internal/apps/openflare/flared/logics.go @@ -11,6 +11,7 @@ import ( "time" "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" "gorm.io/gorm" @@ -133,7 +134,7 @@ func GetTunnelConfig(ctx context.Context, node *model.OpenFlareNode) (*TunnelCon if !route.Enabled { continue } - domains, decodeErr := decodeStoredDomains(route.Domains, route.Domain) + domains, decodeErr := routeidentity.DecodeDomains(route.Domains, route.Domain) if decodeErr != nil { continue } diff --git a/internal/apps/openflare/proxy_route/helpers.go b/internal/apps/openflare/proxy_route/helpers.go index 9fc704d1..e794c95f 100644 --- a/internal/apps/openflare/proxy_route/helpers.go +++ b/internal/apps/openflare/proxy_route/helpers.go @@ -17,6 +17,7 @@ import ( "strings" "unicode" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" "gorm.io/gorm" @@ -315,45 +316,30 @@ func loadTLSCertificates(ctx context.Context, certIDs []uint) ([]*tlsCertificate return certificates, nil } -func normalizeProxyRouteSiteNameInput(route *model.ProxyRoute, raw, primaryDomain string) string { - siteName := strings.TrimSpace(raw) - if siteName != "" { - return siteName - } - if route != nil && strings.TrimSpace(route.SiteName) != "" { - return strings.TrimSpace(route.SiteName) - } - return primaryDomain -} - func normalizeProxyRouteDomainValue(raw string) string { return strings.ToLower(strings.TrimSpace(raw)) } -func normalizeProxyRouteDomains(rawDomains []string) ([]string, error) { - normalized := make([]string, 0, len(rawDomains)) - for _, rawDomain := range rawDomains { - domain := normalizeProxyRouteDomainValue(rawDomain) - if domain == "" { - continue - } - if strings.Contains(domain, "://") || strings.Contains(domain, "/") { - return nil, errors.New(errProxyRouteDomainInvalid) - } - normalized = append(normalized, domain) +func mapRouteIdentityDomainError(err error) error { + if err == nil { + return nil } - normalized = uniqueStrings(normalized) - if len(normalized) == 0 { - return nil, errors.New(errProxyRouteDomainRequired) + switch err.Error() { + case "domain is required": + return errors.New(errProxyRouteDomainRequired) + default: + if strings.Contains(err.Error(), " is invalid") { + return errors.New(errProxyRouteDomainInvalid) + } + return err } - return normalized, nil } func normalizeProxyRouteDomainsInput(route *model.ProxyRoute, rawDomain string, rawDomains []string) ([]string, error) { if len(rawDomains) > 0 { - domains, err := normalizeProxyRouteDomains(rawDomains) + domains, err := routeidentity.NormalizeDomains(rawDomains) if err != nil { - return nil, err + return nil, mapRouteIdentityDomainError(err) } domain := normalizeProxyRouteDomainValue(rawDomain) if domain != "" && domain != domains[0] { @@ -363,7 +349,7 @@ func normalizeProxyRouteDomainsInput(route *model.ProxyRoute, rawDomain string, } if route != nil { - existingDomains, err := decodeStoredDomains(route.Domains, route.Domain) + existingDomains, err := routeidentity.DecodeDomains(route.Domains, route.Domain) if err == nil && len(existingDomains) > 0 { domain := normalizeProxyRouteDomainValue(rawDomain) if domain == "" || domain == existingDomains[0] { @@ -372,7 +358,11 @@ func normalizeProxyRouteDomainsInput(route *model.ProxyRoute, rawDomain string, } } - return normalizeProxyRouteDomains([]string{rawDomain}) + domains, err := routeidentity.NormalizeDomains([]string{rawDomain}) + if err != nil { + return nil, mapRouteIdentityDomainError(err) + } + return domains, nil } func validateProxyRouteSiteName(siteName string) error { @@ -397,15 +387,13 @@ func validateProxyRouteIdentityUniqueness(ctx context.Context, route *model.Prox if item == nil || item.ID == currentID { continue } - existingSiteName := normalizeProxyRouteSiteNameInput(item, item.SiteName, item.Domain) - if existingSiteName == siteName { - return errors.New(errProxyRouteSiteNameExists) - } - - existingDomains, err := decodeStoredDomains(item.Domains, item.Domain) + existingSiteName, existingDomains, err := routeidentity.ResolveFromRoute(item) if err != nil { return fmt.Errorf("existing route %d domains are invalid: %w", item.ID, err) } + if existingSiteName == siteName { + return errors.New(errProxyRouteSiteNameExists) + } existingSet := make(map[string]struct{}, len(existingDomains)) for _, existingDomain := range existingDomains { existingSet[existingDomain] = struct{}{} @@ -789,18 +777,6 @@ func decodeStoredUpstreams(raw string, fallbackOriginURL string) ([]string, erro return normalizeUpstreams(fallbackOriginURL, upstreams) } -func decodeStoredDomains(raw string, fallbackDomain string) ([]string, error) { - text := strings.TrimSpace(raw) - if text == "" { - return normalizeProxyRouteDomains([]string{fallbackDomain}) - } - var domains []string - if err := json.Unmarshal([]byte(text), &domains); err != nil { - return nil, errors.New("domains payload is invalid") - } - return normalizeProxyRouteDomains(domains) -} - func decodeStoredCertIDs(raw string, fallbackCertID *uint) ([]uint, error) { text := strings.TrimSpace(raw) if text == "" { diff --git a/internal/apps/openflare/proxy_route/logics.go b/internal/apps/openflare/proxy_route/logics.go index 01037a78..561cfe14 100644 --- a/internal/apps/openflare/proxy_route/logics.go +++ b/internal/apps/openflare/proxy_route/logics.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/model" ) @@ -164,7 +165,7 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input) return nil, err } domain := domains[0] - siteName := normalizeProxyRouteSiteNameInput(route, input.SiteName, domain) + siteName := routeidentity.ResolveSiteName(route, input.SiteName, domain) upstreamType := normalizeUpstreamType(input.UpstreamType) _, originID, upstreams, err := resolveProxyRouteUpstreams(ctx, upstreamType, input) @@ -276,7 +277,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e if route == nil { return nil, errors.New("proxy route is nil") } - domains, err := decodeStoredDomains(route.Domains, route.Domain) + domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain) if err != nil { return nil, err } @@ -307,7 +308,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e primaryDomain := domains[0] return &View{ ID: route.ID, - SiteName: normalizeProxyRouteSiteNameInput(route, route.SiteName, primaryDomain), + SiteName: routeidentity.ResolveSiteName(route, route.SiteName, primaryDomain), Domain: primaryDomain, Domains: domains, PrimaryDomain: primaryDomain, diff --git a/internal/apps/openflare/proxy_route/logics_test.go b/internal/apps/openflare/proxy_route/logics_test.go index 78874407..4151d949 100644 --- a/internal/apps/openflare/proxy_route/logics_test.go +++ b/internal/apps/openflare/proxy_route/logics_test.go @@ -86,3 +86,32 @@ func TestListProxyRoutes(t *testing.T) { assert.Equal(t, "second.example.com", routes[0].Domain) assert.Equal(t, "first.example.com", routes[1].Domain) } + +func TestValidateProxyRouteIdentityUniquenessUsesDecodedPrimaryDomain(t *testing.T) { + cleanup := setupProxyRouteTestDB(t) + defer cleanup() + ctx := context.Background() + + existing := &model.ProxyRoute{ + SiteName: "", + Domain: "legacy.example.com", + Domains: `["primary.example.com"]`, + OriginURL: "http://origin.example.com:8080", + Upstreams: `["http://origin.example.com:8080"]`, + Enabled: true, + UpstreamType: "direct", + } + require.NoError(t, model.CreateProxyRouteRecord(ctx, existing)) + + view, err := GetProxyRoute(ctx, existing.ID) + require.NoError(t, err) + assert.Equal(t, "primary.example.com", view.SiteName) + + _, err = CreateProxyRoute(ctx, Input{ + SiteName: "primary.example.com", + Domain: "other.example.com", + OriginURL: "http://origin-b.example.com:8080", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "site_name already exists") +} diff --git a/internal/apps/openflare/routeidentity/identity.go b/internal/apps/openflare/routeidentity/identity.go new file mode 100644 index 00000000..3c7acce1 --- /dev/null +++ b/internal/apps/openflare/routeidentity/identity.go @@ -0,0 +1,76 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +// Package routeidentity resolves proxy route site names and normalized domains +// for OpenFlare control-plane and edge rendering. +package routeidentity + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/Rain-kl/Wavelet/internal/model" +) + +// NormalizeDomains lowercases, deduplicates, and validates proxy route domains. +func NormalizeDomains(rawDomains []string) ([]string, error) { + normalized := make([]string, 0, len(rawDomains)) + seen := make(map[string]struct{}, len(rawDomains)) + for _, rawDomain := range rawDomains { + domain := strings.ToLower(strings.TrimSpace(rawDomain)) + if domain == "" { + continue + } + if strings.Contains(domain, "://") || strings.Contains(domain, "/") { + return nil, fmt.Errorf("domain %q is invalid", rawDomain) + } + if _, ok := seen[domain]; ok { + continue + } + seen[domain] = struct{}{} + normalized = append(normalized, domain) + } + if len(normalized) == 0 { + return nil, errors.New("domain is required") + } + return normalized, nil +} + +// DecodeDomains parses stored domains JSON or falls back to a single domain value. +func DecodeDomains(raw string, fallbackDomain string) ([]string, error) { + text := strings.TrimSpace(raw) + if text == "" { + return NormalizeDomains([]string{fallbackDomain}) + } + var domains []string + if err := json.Unmarshal([]byte(text), &domains); err != nil { + return nil, errors.New("domains payload is invalid") + } + return NormalizeDomains(domains) +} + +// ResolveSiteName returns the runtime site identifier for a proxy route. +func ResolveSiteName(route *model.ProxyRoute, raw, primaryDomain string) string { + siteName := strings.TrimSpace(raw) + if siteName != "" { + return siteName + } + if route != nil && strings.TrimSpace(route.SiteName) != "" { + return strings.TrimSpace(route.SiteName) + } + return strings.TrimSpace(primaryDomain) +} + +// ResolveFromRoute decodes domains and resolves the site name for a stored route. +func ResolveFromRoute(route *model.ProxyRoute) (siteName string, domains []string, err error) { + if route == nil { + return "", nil, errors.New("proxy route is nil") + } + domains, err = DecodeDomains(route.Domains, route.Domain) + if err != nil { + return "", nil, err + } + return ResolveSiteName(route, route.SiteName, domains[0]), domains, nil +} diff --git a/internal/apps/openflare/routeidentity/identity_test.go b/internal/apps/openflare/routeidentity/identity_test.go new file mode 100644 index 00000000..590de1c3 --- /dev/null +++ b/internal/apps/openflare/routeidentity/identity_test.go @@ -0,0 +1,38 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package routeidentity + +import ( + "testing" + + "github.com/Rain-kl/Wavelet/internal/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecodeDomainsNormalizesCaseAndOrder(t *testing.T) { + domains, err := DecodeDomains(`["WWW.Example.COM","example.com"]`, "fallback.example.com") + require.NoError(t, err) + assert.Equal(t, []string{"www.example.com", "example.com"}, domains) +} + +func TestResolveSiteNamePrefersExplicitValue(t *testing.T) { + route := &model.ProxyRoute{SiteName: "stored-name", Domain: "example.com"} + assert.Equal(t, "custom", ResolveSiteName(route, "custom", "example.com")) + assert.Equal(t, "stored-name", ResolveSiteName(route, "", "example.com")) + + routeWithoutSiteName := &model.ProxyRoute{Domain: "example.com"} + assert.Equal(t, "example.com", ResolveSiteName(routeWithoutSiteName, "", "example.com")) +} + +func TestResolveFromRoute(t *testing.T) { + route := &model.ProxyRoute{ + Domain: "Example.COM", + Domains: `["example.com","www.example.com"]`, + } + siteName, domains, err := ResolveFromRoute(route) + require.NoError(t, err) + assert.Equal(t, "example.com", siteName) + assert.Equal(t, []string{"example.com", "www.example.com"}, domains) +} diff --git a/internal/apps/openflare/uptimekuma/sync.go b/internal/apps/openflare/uptimekuma/sync.go index dc8989ca..b428e7e0 100644 --- a/internal/apps/openflare/uptimekuma/sync.go +++ b/internal/apps/openflare/uptimekuma/sync.go @@ -5,12 +5,12 @@ package uptimekuma import ( "context" - "encoding/json" "fmt" "log/slog" "strings" "sync/atomic" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/model" ) @@ -164,42 +164,17 @@ func filterOpenFlareMonitors(monitors map[string]Monitor, openFlareTagID int) ma } func routeMonitorURL(route *model.ProxyRoute) (string, error) { - domains, err := decodeStoredDomains(route.Domains, route.Domain) + domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain) if err != nil { return "", err } - domain := route.Domain - if len(domains) > 0 { - domain = domains[0] - } + domain := domains[0] if route.EnableHTTPS { return "https://" + domain, nil } return "http://" + domain, nil } -func decodeStoredDomains(raw string, fallbackDomain string) ([]string, error) { - text := strings.TrimSpace(raw) - if text == "" { - if strings.TrimSpace(fallbackDomain) == "" { - return nil, fmt.Errorf("domain is empty") - } - return []string{fallbackDomain}, nil - } - - var domains []string - if err := json.Unmarshal([]byte(text), &domains); err != nil { - return nil, fmt.Errorf("domains payload is invalid: %w", err) - } - if len(domains) == 0 { - if strings.TrimSpace(fallbackDomain) == "" { - return nil, fmt.Errorf("domain is empty") - } - return []string{fallbackDomain}, nil - } - return domains, nil -} - func monitorPayload(id int, name, targetURL string) map[string]any { payload := map[string]any{ "type": "http", diff --git a/pkg/render/openresty/render.go b/pkg/render/openresty/render.go index 2b71cea4..55e80347 100644 --- a/pkg/render/openresty/render.go +++ b/pkg/render/openresty/render.go @@ -92,10 +92,7 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er return "", fmt.Errorf("route %s domains are invalid", route.Domain) } serverNames := renderServerNames(domains) - displayName := strings.TrimSpace(route.SiteName) - if displayName == "" { - displayName = domains[0] - } + displayName := resolveRouteSiteName(route) cacheConfig := routeCacheConfig{Enabled: route.CacheEnabled, Policy: route.CachePolicy, Rules: route.CacheRules} limitConfig := routeLimitConfig{LimitConnPerServer: route.LimitConnPerServer, LimitConnPerIP: route.LimitConnPerIP, LimitRate: route.LimitRate} powEnabled, _ := getPoWConfigForRoute(route.ID, doc.WAF) @@ -112,29 +109,6 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er return builder.String(), nil } -// RenderPoWConfig serialises the Proof-of-Work configuration for all enabled -// routes as a JSON string consumed by the OpenResty Lua runtime. -func RenderPoWConfig(doc Document) (string, error) { - type domainEntry struct { - Domains []string `json:"domains"` - Enabled bool `json:"enabled"` - Config *PoWConfig `json:"config"` - } - entries := make([]domainEntry, 0) - for _, route := range doc.Routes { - powEnabled, powConfig := getPoWConfigForRoute(route.ID, doc.WAF) - if !powEnabled { - continue - } - entries = append(entries, domainEntry{Domains: normalizedRouteDomains(route), Enabled: true, Config: powConfig}) - } - if len(entries) == 0 { - return "{}", nil - } - data, err := json.Marshal(entries) - return string(data), err -} - // RenderWAFConfig serialises the WAF runtime configuration (rule groups and // per-site bindings) as a JSON string consumed by the OpenResty Lua runtime. func RenderWAFConfig(snapshot WAFDocument) (string, error) { @@ -642,11 +616,18 @@ func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string { return builder.String() } -func buildRouteUpstreamName(route Route) string { - identity := strings.TrimSpace(route.SiteName) - if identity == "" { - identity = route.Domain +func resolveRouteSiteName(route Route) string { + if name := strings.TrimSpace(route.SiteName); name != "" { + return name } + if domains := normalizedRouteDomains(route); len(domains) > 0 { + return domains[0] + } + return strings.TrimSpace(route.Domain) +} + +func buildRouteUpstreamName(route Route) string { + identity := resolveRouteSiteName(route) sanitized := strings.Map(func(r rune) rune { switch { case r >= 'a' && r <= 'z': diff --git a/pkg/render/openresty/render_test.go b/pkg/render/openresty/render_test.go index 843820a9..ba9f28a5 100644 --- a/pkg/render/openresty/render_test.go +++ b/pkg/render/openresty/render_test.go @@ -42,10 +42,7 @@ func TestRenderWAFConfigIncludesAllRouteSiteNames(t *testing.T) { } for _, route := range doc.Routes { - siteName := strings.TrimSpace(route.SiteName) - if siteName == "" { - siteName = normalizedRouteDomains(route)[0] - } + siteName := resolveRouteSiteName(route) if _, ok := decoded.SiteRuleGroups[siteName]; !ok { t.Fatalf("site_rule_groups missing site %q, got %#v", siteName, decoded.SiteRuleGroups) }