Implement Mihomo external-controller support in configuration and gateway
Publish telemt-api gateway Docker image / test (push) Successful in 34s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m55s

- 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:
Denozordec
2026-03-31 00:32:19 +07:00
parent bbd9619290
commit 2d7b06260e
17 changed files with 1093 additions and 9 deletions
+43 -3
View File
@@ -24,6 +24,7 @@ import (
type Gateway struct {
parsed *config.Parsed
proxies map[string]http.Handler
mihomo map[string]http.Handler
agg *aggregate.Handler
geo *geoip.Service
log *slog.Logger
@@ -45,6 +46,7 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa
g := &Gateway{
parsed: p,
proxies: make(map[string]http.Handler),
mihomo: make(map[string]http.Handler),
geo: geo,
log: log,
transport: t,
@@ -68,6 +70,19 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa
})
})
}
for alias, m := range p.MihomoByAlias {
a := alias
strip := "/api/" + a + "/mihomo"
g.mihomo[a] = proxy.NewMihomoForward(m.Base, strip, m.Auth, t, func(w http.ResponseWriter, r *http.Request, err error) {
log.Error("mihomo upstream error", "alias", a, "err", err)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadGateway)
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": false,
"error": map[string]string{"code": "bad_gateway", "message": "mihomo upstream unreachable"},
})
})
}
var aggCacheTTL time.Duration
if p.Config.Aggregate != nil && p.Config.Aggregate.CacheTTLMs > 0 {
aggCacheTTL = time.Duration(p.Config.Aggregate.CacheTTLMs) * time.Millisecond
@@ -230,7 +245,11 @@ func routeEndpoint(path string) string {
if i < 0 {
return "proxy_root"
}
return "proxy_" + rest[i+1:]
sub := rest[i+1:]
if strings.HasPrefix(sub, "mihomo/") {
return "mihomo_" + sub[len("mihomo/"):]
}
return "proxy_" + sub
}
return "ui"
}
@@ -285,7 +304,7 @@ func (g *Gateway) serve(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
rp, ok := g.proxies[alias]
_, ok := g.proxies[alias]
if !ok {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
@@ -295,7 +314,28 @@ func (g *Gateway) serve(w http.ResponseWriter, r *http.Request) {
})
return
}
rp.ServeHTTP(w, r)
mihomoPfx := "/api/" + alias + "/mihomo"
if strings.HasPrefix(r.URL.Path, mihomoPfx) {
mh, have := g.mihomo[alias]
if !have {
proxy.MihomoJSONError(w, "mihomo_not_configured", "mihomo is not configured for this server")
return
}
if r.URL.Path == mihomoPfx+"/meta" && r.Method == http.MethodGet {
mu := g.parsed.MihomoByAlias[alias]
if mu == nil {
proxy.MihomoJSONError(w, "mihomo_not_configured", "mihomo is not configured for this server")
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(proxy.MihomoMetaJSON(mu.Base))
return
}
mh.ServeHTTP(w, r)
return
}
g.proxies[alias].ServeHTTP(w, r)
}
func (g *Gateway) serveLiveEvents(w http.ResponseWriter, r *http.Request) {