From 8a2f76ffc96db0f609e56c37d1f1c23436ab753d Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Wed, 13 May 2026 12:03:00 +0800 Subject: [PATCH] feat: unified 'To' field for Proxy/Rewrite, support KV map configs, and auto-detect HTTP protocol for simple ports (by AI) --- config.go | 53 ++++++++++++++++++++++++++++++++++++--------------- proxy.go | 48 +++++++++++++++++++++++++++++++++++++++------- proxy_test.go | 2 +- rewrite.go | 14 +++++++++----- server.go | 8 ++++++-- service.go | 10 ++++------ 6 files changed, 99 insertions(+), 36 deletions(-) diff --git a/config.go b/config.go index 426fad9..1def31c 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package service import ( + "apigo.cc/go/cast" "path/filepath" ) @@ -66,8 +67,8 @@ type ServiceConfig struct { StopTimeout int // 停止服务的超时时间 (ms) // 从配置文件中加载的静态路由策略 (按 Host 分组,全局配置用 "" 或 "*") - Proxies map[string][]ProxyRule - Rewrites map[string][]RewriteRule + Proxies map[string]map[string]any + Rewrites map[string]map[string]any Statics map[string]map[string]string } @@ -78,31 +79,53 @@ func ApplyConfig() { hostPoliciesLock.Lock() defer hostPoliciesLock.Unlock() - // 清理旧的 file 策略 + // 1. Proxies KV 解析 fileProxies = make(map[string][]*proxyType) - fileRewrites = make(map[string][]*rewriteType) - - for host, rules := range Config.Proxies { + for host, kv := range Config.Proxies { if host == "*" { host = "" } - newProxies := make([]*proxyType, 0, len(rules)) - for _, r := range rules { - newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath)) + rules := make([]*proxyType, 0, len(kv)) + for path, val := range kv { + if to, ok := val.(string); ok { + // 极简 KV 模式: "/api/*": "user-svc/v1/*" + rules = append(rules, parseProxyRule(0, path, "", "", to)) + } else { + // 对象模式: "/api/*": {"To": "...", "Auth": 1} + m, _ := cast.ToMap[string, any](val) + rules = append(rules, parseProxyRule( + cast.Int(m["Auth"]), + path, + cast.String(m["ToApp"]), + cast.String(m["ToPath"]), + cast.String(m["To"]), + )) + } } - fileProxies[host] = newProxies + fileProxies[host] = rules rebuildProxiesUnderLock(host) } - for host, rules := range Config.Rewrites { + // 2. Rewrites KV 解析 + fileRewrites = make(map[string][]*rewriteType) + for host, kv := range Config.Rewrites { if host == "*" { host = "" } - newRewrites := make([]*rewriteType, 0, len(rules)) - for _, r := range rules { - newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath)) + rules := make([]*rewriteType, 0, len(kv)) + for path, val := range kv { + if to, ok := val.(string); ok { + rules = append(rules, parseRewriteRule(path, "", to)) + } else { + m, _ := cast.ToMap[string, any](val) + rules = append(rules, parseRewriteRule( + path, + cast.String(m["ToPath"]), + cast.String(m["To"]), + )) + } } - fileRewrites[host] = newRewrites + fileRewrites[host] = rules rebuildRewritesUnderLock(host) } diff --git a/proxy.go b/proxy.go index bb52fda..ef1b0e1 100644 --- a/proxy.go +++ b/proxy.go @@ -22,7 +22,40 @@ type proxyType struct { toPrefix string } -func parseProxyRule(authLevel int, path, toApp, toPath string) *proxyType { +func parseTo(to string) (app, path string) { + if to == "" { + return "", "/" + } + + // 查找协议 + if strings.Contains(to, "://") { + protocolPart, rest, _ := strings.Cut(to, "://") + // 协议后的第一个 / + if firstSlash := strings.Index(rest, "/"); firstSlash != -1 { + app = protocolPart + "://" + rest[:firstSlash] + path = rest[firstSlash:] + } else { + app = to + path = "/" + } + } else { + // 无协议,第一个 / 之前是 app + if firstSlash := strings.Index(to, "/"); firstSlash != -1 { + app = to[:firstSlash] + path = to[firstSlash:] + } else { + app = to + path = "/" + } + } + return +} + +func parseProxyRule(authLevel int, path, toApp, toPath string, to string) *proxyType { + if to != "" { + toApp, toPath = parseTo(to) + } + p := &proxyType{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath} if strings.ContainsRune(path, '(') { matcher, err := regexp.Compile("^" + path + "$") @@ -41,8 +74,8 @@ func parseProxyRule(authLevel int, path, toApp, toPath string) *proxyType { return p } -func (hc *HostContext) Proxy(authLevel int, path string, toApp, toPath string) *HostContext { - p := parseProxyRule(authLevel, path, toApp, toPath) +func (hc *HostContext) Proxy(authLevel int, path string, to string) *HostContext { + p := parseProxyRule(authLevel, path, "", "", to) hostPoliciesLock.Lock() defer hostPoliciesLock.Unlock() @@ -199,15 +232,16 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) { type ProxyRule struct { Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$ AuthLevel int // 所需鉴权级别 - ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换) - ToPath string // 目标路径 (可含 $1 变量替换) + To string // 目标地址,格式为 "app/path" 或 "http://url/path" (支持后缀 /* 映射) + ToApp string // [Deprecated] 目标 AppName 或完整 URL (可含 $1 变量替换) + ToPath string // [Deprecated] 目标路径 (可含 $1 变量替换) } -// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的动态代理规则,不影响通过代码或文件写入的固定规则。 +// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的动态代理规则。 func ReplaceProxies(host string, rules []ProxyRule) { newProxies := make([]*proxyType, 0, len(rules)) for _, r := range rules { - newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath)) + newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath, r.To)) } hostPoliciesLock.Lock() diff --git a/proxy_test.go b/proxy_test.go index 9ae6e26..d3ab443 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -43,7 +43,7 @@ func TestProxyDirect(t *testing.T) { defer backend.Close() // 注册代理规则 - Host("*").Proxy(0, "/proxy", backend.URL, "/hello") + Host("*").Proxy(0, "/proxy", backend.URL+"/hello") rh := &RouteHandler{} req := httptest.NewRequest("GET", "/proxy", nil) diff --git a/rewrite.go b/rewrite.go index 57df6aa..feac414 100644 --- a/rewrite.go +++ b/rewrite.go @@ -17,7 +17,10 @@ type rewriteType struct { toPrefix string } -func parseRewriteRule(fromPath, toPath string) *rewriteType { +func parseRewriteRule(fromPath, toPath, to string) *rewriteType { + if to != "" { + toPath = to + } s := &rewriteType{fromPath: fromPath, toPath: toPath} if strings.ContainsRune(fromPath, '(') { matcher, err := regexp.Compile("^" + fromPath + "$") @@ -36,8 +39,8 @@ func parseRewriteRule(fromPath, toPath string) *rewriteType { return s } -func (hc *HostContext) Rewrite(path string, toPath string) *HostContext { - s := parseRewriteRule(path, toPath) +func (hc *HostContext) Rewrite(path string, to string) *HostContext { + s := parseRewriteRule(path, "", to) hostPoliciesLock.Lock() defer hostPoliciesLock.Unlock() @@ -135,14 +138,15 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo // RewriteRule 定义了外部传递的 URL 重写规则 type RewriteRule struct { Path string // 原始路径或匹配正则,例如 ^/old/(.*)$ - ToPath string // 重写后的路径,例如 /new/$1 + To string // 目标路径或完整 URL,例如 /new/$1 + ToPath string // [Deprecated] 重写后的路径 } // ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的动态重写规则。 func ReplaceRewrites(host string, rules []RewriteRule) { newRewrites := make([]*rewriteType, 0, len(rules)) for _, r := range rules { - newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath)) + newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath, r.To)) } hostPoliciesLock.Lock() diff --git a/server.go b/server.go index b23b644..67996c8 100644 --- a/server.go +++ b/server.go @@ -55,13 +55,17 @@ func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error { part := strings.Split(listenStr, "|")[0] addr, opts, _ := strings.Cut(part, ",") - protocol := "http" + protocol := "" for _, opt := range strings.Split(opts, ",") { opt = strings.ToLower(strings.TrimSpace(opt)) - if opt == "h2c" || opt == "h2" { + if opt == "h2c" || opt == "h2" || opt == "http" || opt == "https" { protocol = opt } } + + if protocol == "" { + protocol = "http" // Default to http + } if !strings.Contains(addr, ":") { addr = ":" + addr diff --git a/service.go b/service.go index e22512b..88c98ba 100644 --- a/service.go +++ b/service.go @@ -260,16 +260,14 @@ func (gc *GroupContext) WebSocket(path string, serviceFunc any) *websocketServic return gc.hc.WebSocket(gc.prefix+path, serviceFunc) } -func (gc *GroupContext) Rewrite(path string, toPath string) *GroupContext { - gc.hc.Rewrite(gc.prefix+path, toPath) +func (gc *GroupContext) Rewrite(path string, to string) *GroupContext { + gc.hc.Rewrite(gc.prefix+path, to) return gc } - -func (gc *GroupContext) Proxy(authLevel int, path string, toApp, toPath string) *GroupContext { - gc.hc.Proxy(authLevel, gc.prefix+path, toApp, toPath) +func (gc *GroupContext) Proxy(authLevel int, path string, to string) *GroupContext { + gc.hc.Proxy(authLevel, gc.prefix+path, to) return gc } - func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType { funcType := reflect.TypeOf(serviceFunc) if funcType.Kind() != reflect.Func {