package api import ( "crypto/rand" "encoding/base64" "net/http" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/audit" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // The Subsonic password is for clients that sign in with Subsonic's t/s // scheme (md5 of the password plus a salt). Checking that needs the password // itself, so it is stored readable (migration 0003). That is why Minstrel // generates it rather than letting the user choose one: a generated value // can never be a login password reused from somewhere else, so a leaked // users table gives up access to this server's /rest API and nothing more // (M462 #5026). const subsonicPasswordBytes = 18 // 24 base64url characters type subsonicPasswordStatusResp struct { Enabled bool `json:"enabled"` } type subsonicPasswordResp struct { Password string `json:"password"` } // handleGetMySubsonicPassword implements GET /api/me/subsonic-password. It // reports only whether one is set; the value is shown once, when generated. func (h *handlers) handleGetMySubsonicPassword(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { return } writeJSON(w, http.StatusOK, subsonicPasswordStatusResp{Enabled: user.SubsonicPassword != nil}) } // handleGenerateMySubsonicPassword implements POST /api/me/subsonic-password: // replaces any existing Subsonic password with a new random one and returns it. func (h *handlers) handleGenerateMySubsonicPassword(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { return } b := make([]byte, subsonicPasswordBytes) if _, err := rand.Read(b); err != nil { h.logger.Error("generate subsonic password: rand failed", "err", err) writeErr(w, apierror.Internal(err)) return } pw := base64.RawURLEncoding.EncodeToString(b) if err := dbq.New(h.pool).SetSubsonicPassword(r.Context(), dbq.SetSubsonicPasswordParams{ ID: user.ID, SubsonicPassword: &pw, }); err != nil { h.logger.Error("generate subsonic password: update failed", "err", err) writeErr(w, apierror.Internal(err)) return } audit.WriteOrLog(r.Context(), h.pool, h.logger, user.ID, user.ID, audit.ActionSubsonicPasswordSet, nil) writeJSON(w, http.StatusOK, subsonicPasswordResp{Password: pw}) } // handleClearMySubsonicPassword implements DELETE /api/me/subsonic-password, // which turns t/s and p= sign-in off for the account. func (h *handlers) handleClearMySubsonicPassword(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { return } if err := dbq.New(h.pool).SetSubsonicPassword(r.Context(), dbq.SetSubsonicPasswordParams{ ID: user.ID, SubsonicPassword: nil, }); err != nil { h.logger.Error("clear subsonic password: update failed", "err", err) writeErr(w, apierror.Internal(err)) return } audit.WriteOrLog(r.Context(), h.pool, h.logger, user.ID, user.ID, audit.ActionSubsonicPasswordClear, nil) w.WriteHeader(http.StatusNoContent) }