diff --git a/internal/proxy/forward.go b/internal/proxy/forward.go index 2240650..cfd8847 100644 --- a/internal/proxy/forward.go +++ b/internal/proxy/forward.go @@ -17,6 +17,18 @@ func NewAliasForward( stripPrefix, pathPrefix, auth string, rt http.RoundTripper, errHandler func(http.ResponseWriter, *http.Request, error), +) http.Handler { + return newAliasForward(target, stripPrefix, pathPrefix, auth, rt, errHandler, true) +} + +// newAliasForward is the shared HTTP forwarder. remapUsers maps top-level "users" → "stats/users" +// for Telemt; Mihomo must pass false so /users is not rewritten. +func newAliasForward( + target *url.URL, + stripPrefix, pathPrefix, auth string, + rt http.RoundTripper, + errHandler func(http.ResponseWriter, *http.Request, error), + remapUsers bool, ) http.Handler { if errHandler == nil { errHandler = defaultForwardErrorHandler @@ -35,7 +47,7 @@ func NewAliasForward( } rest := strings.TrimPrefix(p, stripPrefix) rest = strings.TrimPrefix(rest, "/") - if (r.Method == http.MethodGet || r.Method == http.MethodHead) && rest == "users" { + if remapUsers && (r.Method == http.MethodGet || r.Method == http.MethodHead) && rest == "users" { rest = "stats/users" } diff --git a/internal/proxy/mihomo.go b/internal/proxy/mihomo.go index 19c55a1..10ad7ae 100644 --- a/internal/proxy/mihomo.go +++ b/internal/proxy/mihomo.go @@ -8,15 +8,42 @@ import ( "strings" ) -// NewMihomoForward proxies /api/{alias}/mihomo/... to Mihomo external-controller (REST + WebSocket). -// Используется Rewrite (Go 1.20+): очищается RequestURI и задаётся абсолютный URL — иначе строгий upstream -// и WebSocket upgrade могут отвечать 400 (см. аналогично Telemt в forward.go). +// NewMihomoForward proxies /api/{alias}/mihomo/... to Mihomo external-controller. +// +// REST и обычные GET/POST идут тем же путём, что и NewAliasForward (http.NewRequest + RoundTrip): +// httputil.ReverseProxy в таком режиме часто даёт 400 на строгих upstream (в т.ч. Mihomo), хотя curl к ним работает. +// Только WebSocket (traffic, memory, …) остаётся на ReverseProxy + Rewrite. func NewMihomoForward( target *url.URL, stripPrefix string, auth string, rt http.RoundTripper, errHandler func(http.ResponseWriter, *http.Request, error), +) http.Handler { + httpH := newAliasForward(target, stripPrefix, "", auth, rt, errHandler, false) + wsH := newMihomoWebSocketReverseProxy(target, stripPrefix, auth, rt, errHandler) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isWebSocketUpgrade(r) { + wsH.ServeHTTP(w, r) + return + } + httpH.ServeHTTP(w, r) + }) +} + +func isWebSocketUpgrade(r *http.Request) bool { + if r == nil { + return false + } + return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") +} + +func newMihomoWebSocketReverseProxy( + target *url.URL, + stripPrefix string, + auth string, + rt http.RoundTripper, + errHandler func(http.ResponseWriter, *http.Request, error), ) http.Handler { if errHandler == nil { errHandler = defaultForwardErrorHandler @@ -26,7 +53,6 @@ func NewMihomoForward( } rp := &httputil.ReverseProxy{ Rewrite: func(pr *httputil.ProxyRequest) { - // Нормализуем Out, не In (контракт httputil.ProxyRequest: In не трогать). NormalizeRequestURLPath(pr.Out) p := pr.Out.URL.Path if !strings.HasPrefix(p, stripPrefix) { @@ -38,8 +64,6 @@ func NewMihomoForward( du.RawQuery = pr.Out.URL.RawQuery out := pr.Out out.URL = &du - // Как в NewAliasForward: не оставлять Host клиента ни в поле, ни в Header — - // иначе строгий upstream (в т.ч. Mihomo) часто отвечает 400. out.Header.Del("Host") out.Host = du.Host out.RequestURI = "" @@ -51,8 +75,8 @@ func NewMihomoForward( out.Header.Set("Authorization", auth) } }, - Transport: rt, - FlushInterval: -1, + Transport: rt, + FlushInterval: -1, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { errHandler(w, r, err) },