feat: unified 'To' field for Proxy/Rewrite, support KV map configs, and auto-detect HTTP protocol for simple ports (by AI)
This commit is contained in:
parent
1bf819281a
commit
8a2f76ffc9
53
config.go
53
config.go
@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apigo.cc/go/cast"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,8 +67,8 @@ type ServiceConfig struct {
|
|||||||
StopTimeout int // 停止服务的超时时间 (ms)
|
StopTimeout int // 停止服务的超时时间 (ms)
|
||||||
|
|
||||||
// 从配置文件中加载的静态路由策略 (按 Host 分组,全局配置用 "" 或 "*")
|
// 从配置文件中加载的静态路由策略 (按 Host 分组,全局配置用 "" 或 "*")
|
||||||
Proxies map[string][]ProxyRule
|
Proxies map[string]map[string]any
|
||||||
Rewrites map[string][]RewriteRule
|
Rewrites map[string]map[string]any
|
||||||
Statics map[string]map[string]string
|
Statics map[string]map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,31 +79,53 @@ func ApplyConfig() {
|
|||||||
hostPoliciesLock.Lock()
|
hostPoliciesLock.Lock()
|
||||||
defer hostPoliciesLock.Unlock()
|
defer hostPoliciesLock.Unlock()
|
||||||
|
|
||||||
// 清理旧的 file 策略
|
// 1. Proxies KV 解析
|
||||||
fileProxies = make(map[string][]*proxyType)
|
fileProxies = make(map[string][]*proxyType)
|
||||||
fileRewrites = make(map[string][]*rewriteType)
|
for host, kv := range Config.Proxies {
|
||||||
|
|
||||||
for host, rules := range Config.Proxies {
|
|
||||||
if host == "*" {
|
if host == "*" {
|
||||||
host = ""
|
host = ""
|
||||||
}
|
}
|
||||||
newProxies := make([]*proxyType, 0, len(rules))
|
rules := make([]*proxyType, 0, len(kv))
|
||||||
for _, r := range rules {
|
for path, val := range kv {
|
||||||
newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath))
|
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)
|
rebuildProxiesUnderLock(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
for host, rules := range Config.Rewrites {
|
// 2. Rewrites KV 解析
|
||||||
|
fileRewrites = make(map[string][]*rewriteType)
|
||||||
|
for host, kv := range Config.Rewrites {
|
||||||
if host == "*" {
|
if host == "*" {
|
||||||
host = ""
|
host = ""
|
||||||
}
|
}
|
||||||
newRewrites := make([]*rewriteType, 0, len(rules))
|
rules := make([]*rewriteType, 0, len(kv))
|
||||||
for _, r := range rules {
|
for path, val := range kv {
|
||||||
newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath))
|
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)
|
rebuildRewritesUnderLock(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
proxy.go
48
proxy.go
@ -22,7 +22,40 @@ type proxyType struct {
|
|||||||
toPrefix string
|
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}
|
p := &proxyType{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath}
|
||||||
if strings.ContainsRune(path, '(') {
|
if strings.ContainsRune(path, '(') {
|
||||||
matcher, err := regexp.Compile("^" + path + "$")
|
matcher, err := regexp.Compile("^" + path + "$")
|
||||||
@ -41,8 +74,8 @@ func parseProxyRule(authLevel int, path, toApp, toPath string) *proxyType {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HostContext) Proxy(authLevel int, path string, toApp, toPath string) *HostContext {
|
func (hc *HostContext) Proxy(authLevel int, path string, to string) *HostContext {
|
||||||
p := parseProxyRule(authLevel, path, toApp, toPath)
|
p := parseProxyRule(authLevel, path, "", "", to)
|
||||||
|
|
||||||
hostPoliciesLock.Lock()
|
hostPoliciesLock.Lock()
|
||||||
defer hostPoliciesLock.Unlock()
|
defer hostPoliciesLock.Unlock()
|
||||||
@ -199,15 +232,16 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) {
|
|||||||
type ProxyRule struct {
|
type ProxyRule struct {
|
||||||
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
|
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
|
||||||
AuthLevel int // 所需鉴权级别
|
AuthLevel int // 所需鉴权级别
|
||||||
ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换)
|
To string // 目标地址,格式为 "app/path" 或 "http://url/path" (支持后缀 /* 映射)
|
||||||
ToPath string // 目标路径 (可含 $1 变量替换)
|
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) {
|
func ReplaceProxies(host string, rules []ProxyRule) {
|
||||||
newProxies := make([]*proxyType, 0, len(rules))
|
newProxies := make([]*proxyType, 0, len(rules))
|
||||||
for _, r := range 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()
|
hostPoliciesLock.Lock()
|
||||||
|
|||||||
@ -43,7 +43,7 @@ func TestProxyDirect(t *testing.T) {
|
|||||||
defer backend.Close()
|
defer backend.Close()
|
||||||
|
|
||||||
// 注册代理规则
|
// 注册代理规则
|
||||||
Host("*").Proxy(0, "/proxy", backend.URL, "/hello")
|
Host("*").Proxy(0, "/proxy", backend.URL+"/hello")
|
||||||
|
|
||||||
rh := &RouteHandler{}
|
rh := &RouteHandler{}
|
||||||
req := httptest.NewRequest("GET", "/proxy", nil)
|
req := httptest.NewRequest("GET", "/proxy", nil)
|
||||||
|
|||||||
14
rewrite.go
14
rewrite.go
@ -17,7 +17,10 @@ type rewriteType struct {
|
|||||||
toPrefix string
|
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}
|
s := &rewriteType{fromPath: fromPath, toPath: toPath}
|
||||||
if strings.ContainsRune(fromPath, '(') {
|
if strings.ContainsRune(fromPath, '(') {
|
||||||
matcher, err := regexp.Compile("^" + fromPath + "$")
|
matcher, err := regexp.Compile("^" + fromPath + "$")
|
||||||
@ -36,8 +39,8 @@ func parseRewriteRule(fromPath, toPath string) *rewriteType {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HostContext) Rewrite(path string, toPath string) *HostContext {
|
func (hc *HostContext) Rewrite(path string, to string) *HostContext {
|
||||||
s := parseRewriteRule(path, toPath)
|
s := parseRewriteRule(path, "", to)
|
||||||
|
|
||||||
hostPoliciesLock.Lock()
|
hostPoliciesLock.Lock()
|
||||||
defer hostPoliciesLock.Unlock()
|
defer hostPoliciesLock.Unlock()
|
||||||
@ -135,14 +138,15 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
|
|||||||
// RewriteRule 定义了外部传递的 URL 重写规则
|
// RewriteRule 定义了外部传递的 URL 重写规则
|
||||||
type RewriteRule struct {
|
type RewriteRule struct {
|
||||||
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
|
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
|
||||||
ToPath string // 重写后的路径,例如 /new/$1
|
To string // 目标路径或完整 URL,例如 /new/$1
|
||||||
|
ToPath string // [Deprecated] 重写后的路径
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的动态重写规则。
|
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的动态重写规则。
|
||||||
func ReplaceRewrites(host string, rules []RewriteRule) {
|
func ReplaceRewrites(host string, rules []RewriteRule) {
|
||||||
newRewrites := make([]*rewriteType, 0, len(rules))
|
newRewrites := make([]*rewriteType, 0, len(rules))
|
||||||
for _, r := range 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()
|
hostPoliciesLock.Lock()
|
||||||
|
|||||||
@ -55,14 +55,18 @@ func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
|
|||||||
part := strings.Split(listenStr, "|")[0]
|
part := strings.Split(listenStr, "|")[0]
|
||||||
addr, opts, _ := strings.Cut(part, ",")
|
addr, opts, _ := strings.Cut(part, ",")
|
||||||
|
|
||||||
protocol := "http"
|
protocol := ""
|
||||||
for _, opt := range strings.Split(opts, ",") {
|
for _, opt := range strings.Split(opts, ",") {
|
||||||
opt = strings.ToLower(strings.TrimSpace(opt))
|
opt = strings.ToLower(strings.TrimSpace(opt))
|
||||||
if opt == "h2c" || opt == "h2" {
|
if opt == "h2c" || opt == "h2" || opt == "http" || opt == "https" {
|
||||||
protocol = opt
|
protocol = opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if protocol == "" {
|
||||||
|
protocol = "http" // Default to http
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.Contains(addr, ":") {
|
if !strings.Contains(addr, ":") {
|
||||||
addr = ":" + addr
|
addr = ":" + addr
|
||||||
}
|
}
|
||||||
|
|||||||
10
service.go
10
service.go
@ -260,16 +260,14 @@ func (gc *GroupContext) WebSocket(path string, serviceFunc any) *websocketServic
|
|||||||
return gc.hc.WebSocket(gc.prefix+path, serviceFunc)
|
return gc.hc.WebSocket(gc.prefix+path, serviceFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GroupContext) Rewrite(path string, toPath string) *GroupContext {
|
func (gc *GroupContext) Rewrite(path string, to string) *GroupContext {
|
||||||
gc.hc.Rewrite(gc.prefix+path, toPath)
|
gc.hc.Rewrite(gc.prefix+path, to)
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
func (gc *GroupContext) Proxy(authLevel int, path string, to string) *GroupContext {
|
||||||
func (gc *GroupContext) Proxy(authLevel int, path string, toApp, toPath string) *GroupContext {
|
gc.hc.Proxy(authLevel, gc.prefix+path, to)
|
||||||
gc.hc.Proxy(authLevel, gc.prefix+path, toApp, toPath)
|
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType {
|
func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType {
|
||||||
funcType := reflect.TypeOf(serviceFunc)
|
funcType := reflect.TypeOf(serviceFunc)
|
||||||
if funcType.Kind() != reflect.Func {
|
if funcType.Kind() != reflect.Func {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user