package service import ( gohttp "apigo.cc/go/http" "apigo.cc/go/log" "fmt" "io" "net/http" "regexp" "strings" "time" ) type proxyType struct { matcher *regexp.Regexp authLevel int fromPath string toApp string toPath string hasWildcard bool prefix string toPrefix string } 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 + "$") if err == nil { p.matcher = matcher } } else if strings.HasSuffix(path, "/*") { p.hasWildcard = true p.prefix = path[:len(path)-1] if strings.HasSuffix(toPath, "/*") { p.toPrefix = toPath[:len(toPath)-1] } else { p.toPrefix = toPath } } return p } func (hc *HostContext) Proxy(authLevel int, path string, to string) *HostContext { p := parseProxyRule(authLevel, path, "", "", to) hostPoliciesLock.Lock() defer hostPoliciesLock.Unlock() codeProxies[hc.host] = append(codeProxies[hc.host], p) rebuildProxiesUnderLock(hc.host) return hc } func rebuildProxiesUnderLock(host string) { var combined []*proxyType combined = append(combined, codeProxies[host]...) combined = append(combined, fileProxies[host]...) combined = append(combined, dynamicProxies[host]...) hostProxies[host] = combined } var httpClientPool *gohttp.Client func findProxy(request *Request) (int, *string, *string, string) { host := request.Host hostOnly, port, _ := strings.Cut(host, ":") hosts := []string{host} if port != "" { hosts = append(hosts, hostOnly, ":"+port) } hosts = append(hosts, "*") requestPath := request.RequestURI queryString := "" if pos := strings.Index(requestPath, "?"); pos != -1 { queryString = requestPath[pos:] requestPath = requestPath[:pos] } hostPoliciesLock.RLock() defer hostPoliciesLock.RUnlock() for _, h := range hosts { proxies, exists := hostProxies[h] if !exists { continue } for _, pi := range proxies { if pi.matcher != nil { finds := pi.matcher.FindAllStringSubmatch(requestPath, 1) if len(finds) > 0 { toApp := pi.toApp toPath := pi.toPath for i, part := range finds[0] { toApp = strings.ReplaceAll(toApp, fmt.Sprintf("$%d", i), part) toPath = strings.ReplaceAll(toPath, fmt.Sprintf("$%d", i), part) } toPath += queryString return pi.authLevel, &toApp, &toPath, h } } else if pi.hasWildcard { if strings.HasPrefix(requestPath, pi.prefix) { suffix := requestPath[len(pi.prefix):] toPath := pi.toPrefix + suffix + queryString toApp := pi.toApp return pi.authLevel, &toApp, &toPath, h } } else { if pi.fromPath == requestPath { toPath := pi.toPath + queryString return pi.authLevel, &pi.toApp, &toPath, h } } } } return 0, nil, nil, "" } func processProxy(request *Request, response *Response, logger *log.Logger) bool { authLevel, proxyToApp, proxyToPath, foundHost := findProxy(request) if proxyToApp == nil || proxyToPath == nil || *proxyToApp == "" || *proxyToPath == "" { return false } // 鉴权 pass, obj := checkAuthForProxy(authLevel, request, response, logger) if !pass { if !response.changed { response.WriteHeader(http.StatusForbidden) } return true } _ = obj // Currently unused in proxy app := *proxyToApp path := *proxyToPath logger.Info("proxy", "app", app, "path", path, "host", foundHost) if strings.Contains(app, "://") { // 直接 URL 代理 if httpClientPool == nil { httpClientPool = gohttp.NewClient(time.Duration(Config.RedirectTimeout) * time.Millisecond) } res := httpClientPool.ManualDoByRequest(request.Request, request.Method, app+path, request.Body) copyResponse(res, response, logger) } else { // Discover 代理 if GlobalDiscoverer == nil { logger.Error("proxy failed: GlobalDiscoverer is not initialized") response.WriteHeader(http.StatusBadGateway) return true } caller := GlobalDiscoverer.NewCaller(request.Request, logger) caller.NoBody = true res, _ := caller.ManualDoWithNode(request.Method, app, "", path, request.Body) copyResponse(res, response, logger) } return true } func checkAuthForProxy(authLevel int, request *Request, response *Response, logger *log.Logger) (bool, any) { ac := webAuthCheckers[authLevel] if ac == nil { ac = webAuthChecker } if ac == nil { return true, nil } return ac(authLevel, logger, &request.RequestURI, nil, request, response, nil) } func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) { if res.Error != nil || res.Response == nil { response.WriteHeader(http.StatusBadGateway) if res.Error != nil { _, _ = response.WriteString(res.Error.Error()) } return } for k, v := range res.Response.Header { response.Header().Set(k, v[0]) } response.WriteHeader(res.Response.StatusCode) if res.Response.Body != nil { defer res.Response.Body.Close() _, err := io.Copy(response.Writer, res.Response.Body) if err != nil { logger.Error("proxy copy body failed", "error", err.Error()) } } } // ProxyRule 定义了外部传递或 Redis 中获取的代理规则配置 type ProxyRule struct { Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$ AuthLevel int // 所需鉴权级别 To string // 目标地址,格式为 "app/path" 或 "http://url/path" (支持后缀 /* 映射) ToApp string // [Deprecated] 目标 AppName 或完整 URL (可含 $1 变量替换) ToPath string // [Deprecated] 目标路径 (可含 $1 变量替换) } // 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, r.To)) } hostPoliciesLock.Lock() defer hostPoliciesLock.Unlock() dynamicProxies[host] = newProxies rebuildProxiesUnderLock(host) }