mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 08:06:37 +08:00
fix: 收敛子代理站点标识双轨逻辑
This commit is contained in:
@@ -20,6 +20,8 @@ sidebar: false
|
|||||||
|
|
||||||
- 修复 WAF 规则组 PoW 策略发布后边缘不生效:统一 WAF 绑定站点名与 OpenResty 路由 `site_name` 解析逻辑,并为所有已启用网站生成 `site_rule_groups` 条目(含仅依赖全局规则组的站点)。
|
- 修复 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 匹配地图质心。
|
- 修复全球态势板在仅有 `geo_name`(如 mmdb 的 Germany)而无经纬度时误用美国 fallback 坐标的问题;按国家名/ISO 匹配地图质心。
|
||||||
|
|
||||||
- 修复 Agent 心跳上报公网 IP 后节点地理位置未自动更新:进程启动时按 `GeoIPProvider` 初始化 `pkg/geoip`,`mmdb` 模式从内置 GeoLite2 种子到 `data/`,并在 Relay 心跳同步地理位置。
|
- 修复 Agent 心跳上报公网 IP 后节点地理位置未自动更新:进程启动时按 `GeoIPProvider` 初始化 `pkg/geoip`,`mmdb` 模式从内置 GeoLite2 种子到 `data/`,并在 Relay 心跳同步地理位置。
|
||||||
|
|||||||
@@ -749,6 +749,8 @@ func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error {
|
|||||||
return m.writeManagedCertFiles(certFiles)
|
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 {
|
func (m *Manager) writePowConfig(supportFiles []protocol.SupportFile) error {
|
||||||
if m.RuntimeConfigDir == "" {
|
if m.RuntimeConfigDir == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ end
|
|||||||
|
|
||||||
local site = ngx.var.openflare_waf_site or ""
|
local site = ngx.var.openflare_waf_site or ""
|
||||||
if site == "" then
|
if site == "" then
|
||||||
site = host
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local config_raw = pow_config_dict:get(site)
|
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 ""
|
local site = ngx.var.openflare_waf_site or ""
|
||||||
if site == "" then
|
if site == "" then
|
||||||
site = host
|
ngx.status = 403
|
||||||
|
ngx.say("PoW site not resolved; openflare_waf_site is required")
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local config_raw = pow_config_dict:get(site)
|
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_challenges = ngx.shared.openflare_pow_challenges
|
||||||
local pow_sessions = ngx.shared.openflare_pow_sessions
|
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 args = ngx.req.get_uri_args()
|
||||||
local challenge_id = args["id"] or ""
|
local challenge_id = args["id"] or ""
|
||||||
local response = args["response"] or ""
|
local response = args["response"] or ""
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func sourceSupportFiles(files []SupportFile) []SupportFile {
|
|||||||
|
|
||||||
func isRuntimeGeneratedSupportFile(path string) bool {
|
func isRuntimeGeneratedSupportFile(path string) bool {
|
||||||
switch strings.TrimSpace(path) {
|
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
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -26,52 +26,6 @@ func isUniqueConstraintError(err error) bool {
|
|||||||
return strings.Contains(strings.ToLower(err.Error()), "unique")
|
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) {
|
func decodeStoredUpstreams(raw string, fallbackOriginURL string) ([]string, error) {
|
||||||
text := strings.TrimSpace(raw)
|
text := strings.TrimSpace(raw)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -321,14 +322,14 @@ func normalizeSnapshotRoutes(routes []snapshotRoute) []snapshotRoute {
|
|||||||
return []snapshotRoute{}
|
return []snapshotRoute{}
|
||||||
}
|
}
|
||||||
for index := range routes {
|
for index := range routes {
|
||||||
normalizedDomains, err := decodeStoredDomains("", routes[index].Domain)
|
normalizedDomains, err := routeidentity.DecodeDomains("", routes[index].Domain)
|
||||||
if len(routes[index].Domains) > 0 {
|
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 {
|
if err == nil && len(normalizedDomains) > 0 {
|
||||||
routes[index].Domains = normalizedDomains
|
routes[index].Domains = normalizedDomains
|
||||||
routes[index].Domain = normalizedDomains[0]
|
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)
|
normalizedCertIDs, primaryCertID, certErr := normalizeSnapshotCertificateIDs(routes[index].CertID, routes[index].CertIDs)
|
||||||
if certErr == nil {
|
if certErr == nil {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"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/apps/openflare/waf"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
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) {
|
func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]snapshotRoute, error) {
|
||||||
items := make([]snapshotRoute, 0, len(routes))
|
items := make([]snapshotRoute, 0, len(routes))
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
domains, err := decodeStoredDomains(route.Domains, route.Domain)
|
domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("route %s domains are invalid", route.Domain)
|
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{
|
items = append(items, snapshotRoute{
|
||||||
ID: route.ID,
|
ID: route.ID,
|
||||||
SiteName: normalizeProxyRouteSiteName(route, route.SiteName, domains[0]),
|
SiteName: routeidentity.ResolveSiteName(route, route.SiteName, domains[0]),
|
||||||
Domain: domains[0],
|
Domain: domains[0],
|
||||||
Domains: domains,
|
Domains: domains,
|
||||||
OriginURL: originURL,
|
OriginURL: originURL,
|
||||||
@@ -319,11 +320,11 @@ func buildSnapshotWAFDocument(ctx context.Context, routes []*model.ProxyRoute) (
|
|||||||
if route == nil {
|
if route == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains, domainErr := decodeStoredDomains(route.Domains, route.Domain)
|
domains, domainErr := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
||||||
if domainErr != nil {
|
if domainErr != nil {
|
||||||
return snapshotWAFDocument{}, fmt.Errorf("route %s domains are invalid", route.Domain)
|
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)
|
rawBindings, err := model.ListOpenFlareWAFRuleGroupBindings(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package flared
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -121,33 +120,6 @@ func relayClientAddress(node *model.OpenFlareNode) string {
|
|||||||
return fmt.Sprintf("%s:%d", addr, port)
|
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) {
|
func parseTunnelTargetAddr(addr string) (string, int) {
|
||||||
addr = strings.TrimSpace(addr)
|
addr = strings.TrimSpace(addr)
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
"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/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -133,7 +134,7 @@ func GetTunnelConfig(ctx context.Context, node *model.OpenFlareNode) (*TunnelCon
|
|||||||
if !route.Enabled {
|
if !route.Enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains, decodeErr := decodeStoredDomains(route.Domains, route.Domain)
|
domains, decodeErr := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
||||||
if decodeErr != nil {
|
if decodeErr != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -315,45 +316,30 @@ func loadTLSCertificates(ctx context.Context, certIDs []uint) ([]*tlsCertificate
|
|||||||
return certificates, nil
|
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 {
|
func normalizeProxyRouteDomainValue(raw string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(raw))
|
return strings.ToLower(strings.TrimSpace(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeProxyRouteDomains(rawDomains []string) ([]string, error) {
|
func mapRouteIdentityDomainError(err error) error {
|
||||||
normalized := make([]string, 0, len(rawDomains))
|
if err == nil {
|
||||||
for _, rawDomain := range rawDomains {
|
return nil
|
||||||
domain := normalizeProxyRouteDomainValue(rawDomain)
|
|
||||||
if domain == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
|
|
||||||
return nil, errors.New(errProxyRouteDomainInvalid)
|
|
||||||
}
|
|
||||||
normalized = append(normalized, domain)
|
|
||||||
}
|
}
|
||||||
normalized = uniqueStrings(normalized)
|
switch err.Error() {
|
||||||
if len(normalized) == 0 {
|
case "domain is required":
|
||||||
return nil, errors.New(errProxyRouteDomainRequired)
|
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) {
|
func normalizeProxyRouteDomainsInput(route *model.ProxyRoute, rawDomain string, rawDomains []string) ([]string, error) {
|
||||||
if len(rawDomains) > 0 {
|
if len(rawDomains) > 0 {
|
||||||
domains, err := normalizeProxyRouteDomains(rawDomains)
|
domains, err := routeidentity.NormalizeDomains(rawDomains)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, mapRouteIdentityDomainError(err)
|
||||||
}
|
}
|
||||||
domain := normalizeProxyRouteDomainValue(rawDomain)
|
domain := normalizeProxyRouteDomainValue(rawDomain)
|
||||||
if domain != "" && domain != domains[0] {
|
if domain != "" && domain != domains[0] {
|
||||||
@@ -363,7 +349,7 @@ func normalizeProxyRouteDomainsInput(route *model.ProxyRoute, rawDomain string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if route != nil {
|
if route != nil {
|
||||||
existingDomains, err := decodeStoredDomains(route.Domains, route.Domain)
|
existingDomains, err := routeidentity.DecodeDomains(route.Domains, route.Domain)
|
||||||
if err == nil && len(existingDomains) > 0 {
|
if err == nil && len(existingDomains) > 0 {
|
||||||
domain := normalizeProxyRouteDomainValue(rawDomain)
|
domain := normalizeProxyRouteDomainValue(rawDomain)
|
||||||
if domain == "" || domain == existingDomains[0] {
|
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 {
|
func validateProxyRouteSiteName(siteName string) error {
|
||||||
@@ -397,15 +387,13 @@ func validateProxyRouteIdentityUniqueness(ctx context.Context, route *model.Prox
|
|||||||
if item == nil || item.ID == currentID {
|
if item == nil || item.ID == currentID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
existingSiteName := normalizeProxyRouteSiteNameInput(item, item.SiteName, item.Domain)
|
existingSiteName, existingDomains, err := routeidentity.ResolveFromRoute(item)
|
||||||
if existingSiteName == siteName {
|
|
||||||
return errors.New(errProxyRouteSiteNameExists)
|
|
||||||
}
|
|
||||||
|
|
||||||
existingDomains, err := decodeStoredDomains(item.Domains, item.Domain)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("existing route %d domains are invalid: %w", item.ID, err)
|
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))
|
existingSet := make(map[string]struct{}, len(existingDomains))
|
||||||
for _, existingDomain := range existingDomains {
|
for _, existingDomain := range existingDomains {
|
||||||
existingSet[existingDomain] = struct{}{}
|
existingSet[existingDomain] = struct{}{}
|
||||||
@@ -789,18 +777,6 @@ func decodeStoredUpstreams(raw string, fallbackOriginURL string) ([]string, erro
|
|||||||
return normalizeUpstreams(fallbackOriginURL, upstreams)
|
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) {
|
func decodeStoredCertIDs(raw string, fallbackCertID *uint) ([]uint, error) {
|
||||||
text := strings.TrimSpace(raw)
|
text := strings.TrimSpace(raw)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
domain := domains[0]
|
domain := domains[0]
|
||||||
siteName := normalizeProxyRouteSiteNameInput(route, input.SiteName, domain)
|
siteName := routeidentity.ResolveSiteName(route, input.SiteName, domain)
|
||||||
|
|
||||||
upstreamType := normalizeUpstreamType(input.UpstreamType)
|
upstreamType := normalizeUpstreamType(input.UpstreamType)
|
||||||
_, originID, upstreams, err := resolveProxyRouteUpstreams(ctx, upstreamType, input)
|
_, originID, upstreams, err := resolveProxyRouteUpstreams(ctx, upstreamType, input)
|
||||||
@@ -276,7 +277,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e
|
|||||||
if route == nil {
|
if route == nil {
|
||||||
return nil, errors.New("proxy route is 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -307,7 +308,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e
|
|||||||
primaryDomain := domains[0]
|
primaryDomain := domains[0]
|
||||||
return &View{
|
return &View{
|
||||||
ID: route.ID,
|
ID: route.ID,
|
||||||
SiteName: normalizeProxyRouteSiteNameInput(route, route.SiteName, primaryDomain),
|
SiteName: routeidentity.ResolveSiteName(route, route.SiteName, primaryDomain),
|
||||||
Domain: primaryDomain,
|
Domain: primaryDomain,
|
||||||
Domains: domains,
|
Domains: domains,
|
||||||
PrimaryDomain: primaryDomain,
|
PrimaryDomain: primaryDomain,
|
||||||
|
|||||||
@@ -86,3 +86,32 @@ func TestListProxyRoutes(t *testing.T) {
|
|||||||
assert.Equal(t, "second.example.com", routes[0].Domain)
|
assert.Equal(t, "second.example.com", routes[0].Domain)
|
||||||
assert.Equal(t, "first.example.com", routes[1].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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ package uptimekuma
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"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) {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
domain := route.Domain
|
domain := domains[0]
|
||||||
if len(domains) > 0 {
|
|
||||||
domain = domains[0]
|
|
||||||
}
|
|
||||||
if route.EnableHTTPS {
|
if route.EnableHTTPS {
|
||||||
return "https://" + domain, nil
|
return "https://" + domain, nil
|
||||||
}
|
}
|
||||||
return "http://" + 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 {
|
func monitorPayload(id int, name, targetURL string) map[string]any {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"type": "http",
|
"type": "http",
|
||||||
|
|||||||
@@ -92,10 +92,7 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er
|
|||||||
return "", fmt.Errorf("route %s domains are invalid", route.Domain)
|
return "", fmt.Errorf("route %s domains are invalid", route.Domain)
|
||||||
}
|
}
|
||||||
serverNames := renderServerNames(domains)
|
serverNames := renderServerNames(domains)
|
||||||
displayName := strings.TrimSpace(route.SiteName)
|
displayName := resolveRouteSiteName(route)
|
||||||
if displayName == "" {
|
|
||||||
displayName = domains[0]
|
|
||||||
}
|
|
||||||
cacheConfig := routeCacheConfig{Enabled: route.CacheEnabled, Policy: route.CachePolicy, Rules: route.CacheRules}
|
cacheConfig := routeCacheConfig{Enabled: route.CacheEnabled, Policy: route.CachePolicy, Rules: route.CacheRules}
|
||||||
limitConfig := routeLimitConfig{LimitConnPerServer: route.LimitConnPerServer, LimitConnPerIP: route.LimitConnPerIP, LimitRate: route.LimitRate}
|
limitConfig := routeLimitConfig{LimitConnPerServer: route.LimitConnPerServer, LimitConnPerIP: route.LimitConnPerIP, LimitRate: route.LimitRate}
|
||||||
powEnabled, _ := getPoWConfigForRoute(route.ID, doc.WAF)
|
powEnabled, _ := getPoWConfigForRoute(route.ID, doc.WAF)
|
||||||
@@ -112,29 +109,6 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er
|
|||||||
return builder.String(), nil
|
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
|
// RenderWAFConfig serialises the WAF runtime configuration (rule groups and
|
||||||
// per-site bindings) as a JSON string consumed by the OpenResty Lua runtime.
|
// per-site bindings) as a JSON string consumed by the OpenResty Lua runtime.
|
||||||
func RenderWAFConfig(snapshot WAFDocument) (string, error) {
|
func RenderWAFConfig(snapshot WAFDocument) (string, error) {
|
||||||
@@ -642,11 +616,18 @@ func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string {
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRouteUpstreamName(route Route) string {
|
func resolveRouteSiteName(route Route) string {
|
||||||
identity := strings.TrimSpace(route.SiteName)
|
if name := strings.TrimSpace(route.SiteName); name != "" {
|
||||||
if identity == "" {
|
return name
|
||||||
identity = route.Domain
|
|
||||||
}
|
}
|
||||||
|
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 {
|
sanitized := strings.Map(func(r rune) rune {
|
||||||
switch {
|
switch {
|
||||||
case r >= 'a' && r <= 'z':
|
case r >= 'a' && r <= 'z':
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ func TestRenderWAFConfigIncludesAllRouteSiteNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, route := range doc.Routes {
|
for _, route := range doc.Routes {
|
||||||
siteName := strings.TrimSpace(route.SiteName)
|
siteName := resolveRouteSiteName(route)
|
||||||
if siteName == "" {
|
|
||||||
siteName = normalizedRouteDomains(route)[0]
|
|
||||||
}
|
|
||||||
if _, ok := decoded.SiteRuleGroups[siteName]; !ok {
|
if _, ok := decoded.SiteRuleGroups[siteName]; !ok {
|
||||||
t.Fatalf("site_rule_groups missing site %q, got %#v", siteName, decoded.SiteRuleGroups)
|
t.Fatalf("site_rule_groups missing site %q, got %#v", siteName, decoded.SiteRuleGroups)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user