🏡 index : quine-global/quintodrome.git

package jellyfin

import (
	"encoding/json"
	"errors"
	"net/http"

	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/core/quickconnect"
	"github.com/navidrome/navidrome/log"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/request"
	"github.com/navidrome/navidrome/server/jellyfin/dto"
)

func requireQuickConnect(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !conf.Server.Jellyfin.QuickConnect {
			http.Error(w, "Quick connect is disabled", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (api *Router) quickConnectEnabled(w http.ResponseWriter, r *http.Request) {
	api.ok(w, r, conf.Server.Jellyfin.QuickConnect)
}

// Initiate is unauthenticated and its fields are kept for minutes, so their size must be bounded.
const maxQuickConnectField = 512

func (api *Router) quickConnectInitiate(w http.ResponseWriter, r *http.Request) {
	a := parseMediaBrowserAuth(r)
	if a.Client == "" || a.Device == "" || a.DeviceId == "" || a.Version == "" ||
		max(len(a.Client), len(a.Device), len(a.DeviceId), len(a.Version)) > maxQuickConnectField {
		http.Error(w, "Client, Device, DeviceId and Version are required", http.StatusBadRequest)
		return
	}
	req, err := api.quickConnect.Initiate(quickconnect.Device{
		ID: a.DeviceId, Name: a.Device, App: a.Client, AppVersion: a.Version,
	})
	if errors.Is(err, quickconnect.ErrTooManyRequests) {
		http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
		return
	}
	if err != nil {
		api.internalError(w, r, err)
		return
	}
	api.ok(w, r, quickConnectResult(req))
}

func (api *Router) quickConnectConnect(w http.ResponseWriter, r *http.Request) {
	req, err := api.quickConnect.Status(r.URL.Query().Get("secret"))
	if err != nil {
		http.Error(w, "Unknown secret", http.StatusNotFound)
		return
	}
	api.ok(w, r, quickConnectResult(req))
}

func (api *Router) quickConnectAuthorize(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	caller, _ := request.UserFrom(ctx)
	userID := caller.ID
	if encoded := r.URL.Query().Get("userid"); encoded != "" {
		var ok bool
		if userID, ok = dto.DecodeID(encoded); !ok {
			http.Error(w, "Invalid userId", http.StatusBadRequest)
			return
		}
	}
	target := caller
	if userID != caller.ID {
		if !caller.IsAdmin {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}
		usr, err := api.ds.User().Get(ctx, userID)
		if errors.Is(err, model.ErrNotFound) {
			http.Error(w, "Unknown user", http.StatusNotFound)
			return
		}
		if err != nil {
			api.internalError(w, r, err)
			return
		}
		target = *usr
	}

	req, err := api.quickConnect.Authorize(r.URL.Query().Get("code"), target.ID)
	switch {
	case errors.Is(err, model.ErrNotFound):
		http.Error(w, "Unknown code", http.StatusNotFound)
	case errors.Is(err, quickconnect.ErrAlreadyAuthorized):
		http.Error(w, "Code already used", http.StatusConflict)
	case err != nil:
		api.internalError(w, r, err)
	default:
		log.Info(ctx, "Jellyfin API: Quick Connect sign-in approved", "username", target.UserName,
			"approvedBy", caller.UserName, "client", req.Device.App, "device", req.Device.Name)
		api.ok(w, r, true)
	}
}

func (api *Router) authenticateWithQuickConnect(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	var body struct {
		Secret string `json:"Secret"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Secret == "" {
		http.Error(w, "Bad Request", http.StatusBadRequest)
		return
	}
	userID, err := api.quickConnect.Redeem(body.Secret)
	if err != nil {
		http.Error(w, "Unknown secret", http.StatusNotFound)
		return
	}
	usr, err := api.ds.User().Get(ctx, userID)
	if errors.Is(err, model.ErrNotFound) {
		log.Warn(ctx, "Jellyfin API: Quick Connect user not found", "userID", userID)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}
	if err != nil {
		api.internalError(w, r, err)
		return
	}
	api.signIn(w, r, usr)
}

func quickConnectResult(req quickconnect.Request) dto.QuickConnectResult {
	return dto.QuickConnectResult{
		Authenticated: req.Authorized(),
		Secret:        req.Secret,
		Code:          req.Code,
		DeviceId:      req.Device.ID,
		DeviceName:    req.Device.Name,
		AppName:       req.Device.App,
		AppVersion:    req.Device.AppVersion,
		DateAdded:     dto.JellyfinDate(&req.DateAdded),
	}
}