Files
gronod 2d5763d6c6 feat: slim tool responses, drop lyrics search, rename module (1.0.11)
Item-shaped results now omit empty fields and drop low-value metadata
(overview, lyrics, file_path, creation_date, premiere_date, bitrate);
absent numeric fields drop the key instead of emitting "". Player
sessions report hh:mm:ss times only; playlists drop overview/date_created.
The lyrics_or_description search parameter is removed, along with the
unused DTO fields, and upstream Fields requests are slimmed to match.
Module path renamed to git.i3omb.com/gronod/emby-mcp.
2026-09-21 23:54:07 +01:00

124 lines
3.2 KiB
Go

package mcphttp
import (
"net"
"net/http"
"strings"
"git.i3omb.com/gronod/emby-mcp/internal/applog"
"git.i3omb.com/gronod/emby-mcp/internal/config"
)
// Home Assistant Supervisor networks. Traffic from Core and other add-ons
// comes from these ranges when the add-on is not on host_network.
var localNets = mustCIDRs(
"127.0.0.0/8",
"::1/128",
"172.30.32.0/23", // hassio
"172.30.33.0/24", // add-ons
)
func mustCIDRs(cidrs ...string) []*net.IPNet {
out := make([]*net.IPNet, 0, len(cidrs))
for _, c := range cidrs {
_, n, err := net.ParseCIDR(c)
if err != nil {
panic(err)
}
out = append(out, n)
}
return out
}
func isLocalPeer(r *http.Request) bool {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip.IsLoopback() {
return true
}
for _, n := range localNets {
if n.Contains(ip) {
return true
}
}
return false
}
// restrictLocalhost allows loopback and the Home Assistant container
// networks only. LAN clients (192.168/10/100) are rejected.
type statusWriter struct {
http.ResponseWriter
code int
}
func (w *statusWriter) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}
func accessLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &statusWriter{ResponseWriter: w, code: http.StatusOK}
next.ServeHTTP(sw, r)
applog.Infof("rest %s %s from %s -> %d", r.Method, r.URL.Path, r.RemoteAddr, sw.code)
})
}
func restrictLocalhost(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isLocalPeer(r) {
applog.Warnf("rest rejected %s %s from %s (not HA-local)", r.Method, r.URL.Path, r.RemoteAddr)
http.Error(w, "forbidden: add-on is restricted to Home Assistant local network", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// injectConfiguredAuth applies add-on credentials when the client sent none.
func injectConfiguredAuth(cfg *config.Config, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
if cfg.Username != "" && cfg.Password != "" {
r.SetBasicAuth(cfg.Username, cfg.Password)
} else if cfg.APIKey != "" {
r.Header.Set("Authorization", "Bearer "+cfg.APIKey)
if cfg.UserID != "" {
r.Header.Set(HeaderUserID, cfg.UserID)
}
if cfg.Username != "" {
r.Header.Set(HeaderUsername, cfg.Username)
}
}
}
next.ServeHTTP(w, r)
})
}
// WrapAccess applies local-network restriction and configured-credential
// injection around the HTTP mux.
func WrapAccess(cfg *config.Config, next http.Handler) http.Handler {
h := next
if cfg.RestrictToLocalhost && (cfg.Username != "" || cfg.APIKey != "") {
h = injectConfiguredAuth(cfg, h)
}
if cfg.RestrictToLocalhost {
h = restrictLocalhost(h)
}
return accessLog(h)
}
// HasConfiguredCreds reports whether the add-on has usable stored credentials.
func HasConfiguredCreds(cfg *config.Config) bool {
if cfg.Username != "" && cfg.Password != "" {
return true
}
return strings.TrimSpace(cfg.APIKey) != ""
}