dfcdf4d3ca
Feishin's first-login flow hits getUser to discover the authenticated
identity's roles. We never registered the route, so chi returned a
plain-text 404 — Feishin's parser treats anything non-Subsonic as a
generic auth failure ("Failed to log in"), masking the real cause.
- Implement /rest/getUser with the full role bag. Admins get every
role; non-admins get the play-music subset. Single-user M1 means
cross-user lookups by admins return the caller's roles for now;
revisit when user management lands.
- Set sub.NotFound on /rest/* to emit a Subsonic envelope (code 0,
"Method not implemented") instead of plain text. Any future client
probing an unimplemented endpoint now sees a parseable failure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
59 lines
2.2 KiB
Go
59 lines
2.2 KiB
Go
// Package subsonic implements a Subsonic-compatible REST API surface under
|
|
// /rest. It exists for third-party Subsonic clients (Symfonium, Substreamer,
|
|
// DSub, Tempo, play:Sub, …). Minstrel's own clients use the native /api/*
|
|
// surface, which has modern auth; /rest/* retains Subsonic's legacy auth for
|
|
// compatibility only. See docs/auth.md for the security posture.
|
|
package subsonic
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Mount attaches Subsonic handlers at /rest on r. Endpoints are exposed at
|
|
// both /rest/foo and /rest/foo.view because client conventions vary. Both
|
|
// GET and POST are accepted; Subsonic's auth params live in the query string
|
|
// either way.
|
|
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, cfg Config) {
|
|
b := &browseHandlers{pool: pool}
|
|
m := newMediaHandlers(pool)
|
|
r.Route("/rest", func(sub chi.Router) {
|
|
sub.Use(Middleware(pool, cfg))
|
|
register(sub, "/ping", handlePing)
|
|
register(sub, "/getLicense", handleGetLicense)
|
|
register(sub, "/getMusicFolders", b.getMusicFolders)
|
|
register(sub, "/getIndexes", b.getIndexes)
|
|
register(sub, "/getArtists", b.getArtists)
|
|
register(sub, "/getArtist", b.getArtist)
|
|
register(sub, "/getAlbum", b.getAlbum)
|
|
register(sub, "/getAlbumList2", b.getAlbumList2)
|
|
register(sub, "/getSong", b.getSong)
|
|
register(sub, "/search3", b.search3)
|
|
register(sub, "/stream", m.handleStream)
|
|
register(sub, "/download", m.handleDownload)
|
|
register(sub, "/getCoverArt", m.handleGetCoverArt)
|
|
register(sub, "/scrobble", m.handleScrobble)
|
|
register(sub, "/getUser", handleGetUser)
|
|
|
|
// Subsonic clients expect every /rest/* miss to come back as a
|
|
// Subsonic-shaped error envelope, not chi's plain-text 404. Without
|
|
// this, hitting an unimplemented method (Feishin's first-login probe
|
|
// hits getUser, getPlaylists, etc.) breaks the client's parser and
|
|
// surfaces as a generic "failed to log in".
|
|
sub.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
|
WriteFail(w, r, ErrGeneric, "Method not implemented")
|
|
})
|
|
_ = logger
|
|
})
|
|
}
|
|
|
|
func register(r chi.Router, path string, h func(http.ResponseWriter, *http.Request)) {
|
|
r.Get(path, h)
|
|
r.Post(path, h)
|
|
r.Get(path+".view", h)
|
|
r.Post(path+".view", h)
|
|
}
|