package jellyfin
import (
"net"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
)
func throttleStreams(limit int) func(http.Handler) http.Handler {
if limit <= 0 {
return func(next http.Handler) http.Handler { return next }
}
return middleware.ThrottleBacklog(limit, consts.RequestThrottleBacklogLimit, consts.RequestThrottleBacklogTimeout)
}
func caseInsensitivePaths(r chi.Router) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if rctx := chi.RouteContext(req.Context()); rctx != nil && rctx.RoutePath != "" {
rctx.RoutePath = strings.ToLower(rctx.RoutePath)
} else {
req.URL.Path = strings.ToLower(req.URL.Path)
}
r.ServeHTTP(w, req)
})
}
func normalizeQueryKeys(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
folded := make(url.Values, len(q))
changed := false
for k, vs := range q {
lk := strings.ToLower(k)
folded[lk] = append(folded[lk], vs...)
if lk != k {
changed = true
}
}
if changed {
r2 := *r
u := *r.URL
u.RawQuery = folded.Encode()
r2.URL = &u
r = &r2
}
next.ServeHTTP(w, r)
})
}
type mediaBrowserAuth struct {
Client, Device, DeviceId, Version, Token string
}
var mediaBrowserAuthField = regexp.MustCompile(`(\w+)="([^"]*)"`)
func parseMediaBrowserAuth(r *http.Request) mediaBrowserAuth {
if a, ok := parseAuthHeader(r.Header.Get("Authorization")); ok {
return a
}
a, _ := parseAuthHeader(r.Header.Get("X-Emby-Authorization"))
return a
}
func parseAuthHeader(h string) (mediaBrowserAuth, bool) {
var a mediaBrowserAuth
scheme, params, found := strings.Cut(h, " ")
if !found || (!strings.EqualFold(scheme, "MediaBrowser") && !strings.EqualFold(scheme, "Emby")) {
return a, false
}
for _, m := range mediaBrowserAuthField.FindAllStringSubmatch(params, -1) {
switch m[1] {
case "Client":
a.Client = unescapeField(m[2])
case "Device":
a.Device = unescapeField(m[2])
case "DeviceId":
a.DeviceId = unescapeField(m[2])
case "Version":
a.Version = unescapeField(m[2])
case "Token":
a.Token = unescapeField(m[2])
}
}
return a, true
}
func unescapeField(v string) string {
if decoded, err := url.PathUnescape(v); err == nil {
return decoded
}
return v
}
func tokenFromRequest(r *http.Request) string {
if t := parseMediaBrowserAuth(r).Token; t != "" {
return t
}
if t := r.Header.Get("X-Emby-Token"); t != "" {
return t
}
if t := r.Header.Get("X-MediaBrowser-Token"); t != "" {
return t
}
if t := r.URL.Query().Get("api_key"); t != "" {
return t
}
return r.URL.Query().Get("apikey")
}
func (api *Router) userFromToken(r *http.Request) (model.User, bool) {
token := tokenFromRequest(r)
if token == "" {
return model.User{}, false
}
claims, err := auth.Validate(token)
if err != nil || claims.Subject == "" {
return model.User{}, false
}
usr, err := api.ds.User().FindByUsername(r.Context(), claims.Subject)
if err != nil {
log.Warn(r.Context(), "Jellyfin API: token subject not found", "user", claims.Subject, err)
return model.User{}, false
}
if err := auth.CheckClaims(claims, *usr, auth.AudienceJellyfin); err != nil {
log.Warn(r.Context(), "Jellyfin API: rejected token", "user", claims.Subject, err)
return model.User{}, false
}
return *usr, true
}
func (api *Router) authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
usr, ok := api.userFromToken(r)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := request.WithUser(r.Context(), usr)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (api *Router) withPlayer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if api.players == nil {
next.ServeHTTP(w, r)
return
}
ctx := r.Context()
a := parseMediaBrowserAuth(r)
if a.Client == "" && a.DeviceId == "" {
next.ServeHTTP(w, r)
return
}
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
player, trc, err := api.players.Register(ctx, a.DeviceId, a.Client, a.Device, ip)
if err != nil {
log.Warn(ctx, "Jellyfin API: could not register player", "client", a.Client, "device", a.Device, err)
next.ServeHTTP(w, r)
return
}
ctx = request.WithPlayer(ctx, *player)
if trc != nil {
ctx = request.WithTranscoding(ctx, *trc)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}