Implement Mihomo external-controller support in configuration and gateway
- Added optional Mihomo configuration fields in `config.compose.yaml` and `config.example.yaml` for enhanced integration with the Mihomo external-controller. - Updated the `Gateway` to handle Mihomo API requests, including proxying and error handling for Mihomo-specific endpoints. - Enhanced the documentation in `GATEWAY_RUN.md` to guide users on configuring Mihomo integration. - Introduced new utility functions in the web client for interacting with Mihomo API endpoints, improving the overall user experience. - Updated the sidebar in the Svelte components to include a link to the Mihomo section, enhancing navigation.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewMihomoForward proxies /api/{alias}/mihomo/... to Mihomo external-controller (REST + WebSocket).
|
||||
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{
|
||||
Director: func(req *http.Request) {
|
||||
NormalizeRequestURLPath(req)
|
||||
p := req.URL.Path
|
||||
if !strings.HasPrefix(p, stripPrefix) {
|
||||
return
|
||||
}
|
||||
rest := strings.TrimPrefix(p, stripPrefix)
|
||||
rest = strings.TrimPrefix(rest, "/")
|
||||
q := req.URL.RawQuery
|
||||
dest := JoinPathPrefix(target, "/", rest)
|
||||
req.URL.Scheme = dest.Scheme
|
||||
req.URL.Host = dest.Host
|
||||
req.URL.Path = dest.Path
|
||||
req.URL.RawQuery = q
|
||||
req.Host = dest.Host
|
||||
req.Header.Del("Authorization")
|
||||
if auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
},
|
||||
Transport: rt,
|
||||
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},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMihomoForwardRewritesPathAndAuth(t *testing.T) {
|
||||
target, err := url.Parse("http://127.0.0.1:9090")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cap := &captureTransport{}
|
||||
h := NewMihomoForward(target, "/api/mtg/mihomo", "Bearer testsecret", cap, nil)
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://gw/api/mtg/mihomo/proxies", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer client-should-not-forward")
|
||||
h.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
if cap.got == nil {
|
||||
t.Fatal("no outgoing request captured")
|
||||
}
|
||||
if got := cap.got.Header.Get("Authorization"); got != "Bearer testsecret" {
|
||||
t.Fatalf("Authorization: got %q want Bearer testsecret", got)
|
||||
}
|
||||
want, err := url.Parse("http://127.0.0.1:9090/proxies")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSameURL(t, cap.got.URL, want)
|
||||
}
|
||||
Reference in New Issue
Block a user