- Updated the Mihomo proxy implementation to ensure the client Host header is removed from outgoing requests, preventing strict upstream servers from returning errors. - Revised the test for Mihomo forwarding to verify that the Host header is correctly set and not forwarded to the upstream server, improving test reliability and coverage.
82 lines
2.5 KiB
Go
82 lines
2.5 KiB
Go
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},
|
|
})
|
|
}
|