package proxy import ( "encoding/json" "net/http" "net/http/httputil" "net/url" "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). func NewMihomoForward( target *url.URL, stripPrefix string, auth string, rt http.RoundTripper, errHandler func(http.ResponseWriter, *http.Request, error), ) http.Handler { if errHandler == nil { errHandler = defaultForwardErrorHandler } if rt == nil { rt = http.DefaultTransport } 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) { return } rest := strings.TrimPrefix(strings.TrimPrefix(p, stripPrefix), "/") dest := JoinPathPrefix(target, "/", rest) du := *dest 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 = "" out.Proto = "HTTP/1.1" out.ProtoMajor = 1 out.ProtoMinor = 1 out.Header.Del("Authorization") if auth != "" { out.Header.Set("Authorization", auth) } }, Transport: rt, FlushInterval: -1, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { errHandler(w, r, err) }, } return rp } // MihomoMetaJSON returns a JSON body for GET .../mihomo/meta (display URL without credentials). func MihomoMetaJSON(target *url.URL) []byte { display := strings.TrimSuffix(target.String(), "/") b, _ := json.Marshal(map[string]any{ "ok": true, "controller_base": display, }) return b } // MihomoJSONError writes a JSON error for Mihomo routes when proxy is not configured. func MihomoJSONError(w http.ResponseWriter, code, msg string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusNotFound) _ = json.NewEncoder(w).Encode(map[string]any{ "ok": false, "error": map[string]string{"code": code, "message": msg}, }) }