add param abstraction to request context

This commit is contained in:
sentriz
2020-01-17 20:09:08 +00:00
parent 4af1e43389
commit 8e5d397082
13 changed files with 207 additions and 184 deletions

View File

@@ -10,8 +10,16 @@ import (
"github.com/pkg/errors"
"senan.xyz/g/gonic/server/ctrlbase"
"senan.xyz/g/gonic/server/ctrlsubsonic/params"
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
"senan.xyz/g/gonic/server/parsing"
)
type CtxKey int
const (
CtxUser CtxKey = iota
CtxSession
CtxParams
)
type Controller struct {
@@ -42,12 +50,10 @@ func (ew *errWriter) write(buf []byte) {
}
func writeResp(w http.ResponseWriter, r *http.Request, resp *spec.Response) error {
if resp.Error != nil {
w.WriteHeader(http.StatusBadRequest)
}
res := metaResponse{Response: resp}
params := r.Context().Value(CtxParams).(params.Params)
ew := &errWriter{w: w}
switch parsing.GetStrParam(r, "f") {
switch params.Get("f") {
case "json":
w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(res)
@@ -62,7 +68,7 @@ func writeResp(w http.ResponseWriter, r *http.Request, resp *spec.Response) erro
return errors.Wrap(err, "marshal to jsonp")
}
// TODO: error if no callback provided instead of using a default
pCall := parsing.GetStrParamOr(r, "callback", "cb")
pCall := params.GetOr("callback", "cb")
ew.write([]byte(pCall))
ew.write([]byte("("))
ew.write(data)

View File

@@ -9,9 +9,8 @@ import (
"github.com/jinzhu/gorm"
"senan.xyz/g/gonic/model"
"senan.xyz/g/gonic/server/ctrlsubsonic/params"
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
"senan.xyz/g/gonic/server/key"
"senan.xyz/g/gonic/server/parsing"
)
// the subsonic spec metions "artist" a lot when talking about the
@@ -60,7 +59,8 @@ func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
}
func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
id, err := parsing.GetIntParam(r, "id")
params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetInt("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
@@ -86,7 +86,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
Find(&childTracks)
for _, c := range childTracks {
toAppend := spec.NewTCTrackByFolder(c, folder)
if parsing.GetStrParam(r, "c") == "Jamstash" {
if params.Get("c") == "Jamstash" {
// jamstash thinks it can't play flacs
toAppend.ContentType = "audio/mpeg"
toAppend.Suffix = "mp3"
@@ -103,7 +103,8 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
// changes to this function should be reflected in in _by_tags.go's
// getAlbumListTwo() function
func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
listType := parsing.GetStrParam(r, "type")
params := r.Context().Value(CtxParams).(params.Params)
listType := params.Get("type")
if listType == "" {
return spec.NewError(10, "please provide a `type` parameter")
}
@@ -117,7 +118,7 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
case "alphabeticalByName":
q = q.Order("right_path")
case "frequent":
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
q = q.Joins(`
JOIN plays
ON albums.id = plays.album_id AND plays.user_id = ?`,
@@ -128,7 +129,7 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
case "random":
q = q.Order(gorm.Expr("random()"))
case "recent":
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
q = q.Joins(`
JOIN plays
ON albums.id = plays.album_id AND plays.user_id = ?`,
@@ -140,8 +141,8 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
var folders []*model.Album
q.
Where("albums.tag_artist_id IS NOT NULL").
Offset(parsing.GetIntParamOr(r, "offset", 0)).
Limit(parsing.GetIntParamOr(r, "size", 10)).
Offset(params.GetIntOr("offset", 0)).
Limit(params.GetIntOr("size", 10)).
Preload("Parent").
Find(&folders)
sub := spec.NewResponse()
@@ -155,7 +156,8 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
}
func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
query := parsing.GetStrParam(r, "query")
params := r.Context().Value(CtxParams).(params.Params)
query := params.Get("query")
if query == "" {
return spec.NewError(10, "please provide a `query` parameter")
}
@@ -170,8 +172,8 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
AND (right_path LIKE ? OR
right_path_u_dec LIKE ?)
`, query, query).
Offset(parsing.GetIntParamOr(r, "artistOffset", 0)).
Limit(parsing.GetIntParamOr(r, "artistCount", 20)).
Offset(params.GetIntOr("artistOffset", 0)).
Limit(params.GetIntOr("artistCount", 20)).
Find(&artists)
for _, a := range artists {
results.Artists = append(results.Artists,
@@ -186,8 +188,8 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
AND (right_path LIKE ? OR
right_path_u_dec LIKE ?)
`, query, query).
Offset(parsing.GetIntParamOr(r, "albumOffset", 0)).
Limit(parsing.GetIntParamOr(r, "albumCount", 20)).
Offset(params.GetIntOr("albumOffset", 0)).
Limit(params.GetIntOr("albumCount", 20)).
Find(&albums)
for _, a := range albums {
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
@@ -201,8 +203,8 @@ func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
filename LIKE ? OR
filename_u_dec LIKE ?
`, query, query).
Offset(parsing.GetIntParamOr(r, "songOffset", 0)).
Limit(parsing.GetIntParamOr(r, "songCount", 20)).
Offset(params.GetIntOr("songOffset", 0)).
Limit(params.GetIntOr("songCount", 20)).
Find(&tracks)
for _, t := range tracks {
results.Tracks = append(results.Tracks,

View File

@@ -9,9 +9,8 @@ import (
"github.com/jinzhu/gorm"
"senan.xyz/g/gonic/model"
"senan.xyz/g/gonic/server/ctrlsubsonic/params"
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
"senan.xyz/g/gonic/server/key"
"senan.xyz/g/gonic/server/parsing"
)
func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
@@ -52,7 +51,8 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
}
func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
id, err := parsing.GetIntParam(r, "id")
params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetInt("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
@@ -70,7 +70,8 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
}
func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
id, err := parsing.GetIntParam(r, "id")
params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetInt("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
@@ -97,7 +98,8 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
// changes to this function should be reflected in in _by_folder.go's
// getAlbumList() function
func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
listType := parsing.GetStrParam(r, "type")
params := r.Context().Value(CtxParams).(params.Params)
listType := params.Get("type")
if listType == "" {
return spec.NewError(10, "please provide a `type` parameter")
}
@@ -113,11 +115,11 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
case "byYear":
q = q.Where(
"tag_year BETWEEN ? AND ?",
parsing.GetIntParamOr(r, "fromYear", 1800),
parsing.GetIntParamOr(r, "toYear", 2200))
params.GetIntOr("fromYear", 1800),
params.GetIntOr("toYear", 2200))
q = q.Order("tag_year")
case "frequent":
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
q = q.Joins(`
JOIN plays
ON albums.id = plays.album_id AND plays.user_id = ?`,
@@ -128,7 +130,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
case "random":
q = q.Order(gorm.Expr("random()"))
case "recent":
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
q = q.Joins(`
JOIN plays
ON albums.id = plays.album_id AND plays.user_id = ?`,
@@ -140,8 +142,8 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
var albums []*model.Album
q.
Where("albums.tag_artist_id IS NOT NULL").
Offset(parsing.GetIntParamOr(r, "offset", 0)).
Limit(parsing.GetIntParamOr(r, "size", 10)).
Offset(params.GetIntOr("offset", 0)).
Limit(params.GetIntOr("size", 10)).
Preload("TagArtist").
Find(&albums)
sub := spec.NewResponse()
@@ -155,7 +157,8 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
}
func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
query := parsing.GetStrParam(r, "query")
params := r.Context().Value(CtxParams).(params.Params)
query := params.Get("query")
if query == "" {
return spec.NewError(10, "please provide a `query` parameter")
}
@@ -170,8 +173,8 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
name LIKE ? OR
name_u_dec LIKE ?
`, query, query).
Offset(parsing.GetIntParamOr(r, "artistOffset", 0)).
Limit(parsing.GetIntParamOr(r, "artistCount", 20)).
Offset(params.GetIntOr("artistOffset", 0)).
Limit(params.GetIntOr("artistCount", 20)).
Find(&artists)
for _, a := range artists {
results.Artists = append(results.Artists,
@@ -186,8 +189,8 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
tag_title LIKE ? OR
tag_title_u_dec LIKE ?
`, query, query).
Offset(parsing.GetIntParamOr(r, "albumOffset", 0)).
Limit(parsing.GetIntParamOr(r, "albumCount", 20)).
Offset(params.GetIntOr("albumOffset", 0)).
Limit(params.GetIntOr("albumCount", 20)).
Find(&albums)
for _, a := range albums {
results.Albums = append(results.Albums,
@@ -202,8 +205,8 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
tag_title LIKE ? OR
tag_title_u_dec LIKE ?
`, query, query).
Offset(parsing.GetIntParamOr(r, "songOffset", 0)).
Limit(parsing.GetIntParamOr(r, "songCount", 20)).
Offset(params.GetIntOr("songOffset", 0)).
Limit(params.GetIntOr("songCount", 20)).
Find(&tracks)
for _, t := range tracks {
results.Tracks = append(results.Tracks,

View File

@@ -11,10 +11,9 @@ import (
"senan.xyz/g/gonic/model"
"senan.xyz/g/gonic/scanner"
"senan.xyz/g/gonic/server/ctrlsubsonic/params"
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
"senan.xyz/g/gonic/server/key"
"senan.xyz/g/gonic/server/lastfm"
"senan.xyz/g/gonic/server/parsing"
)
func lowerUDecOrHash(in string) string {
@@ -38,12 +37,13 @@ func (c *Controller) ServePing(r *http.Request) *spec.Response {
}
func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
id, err := parsing.GetIntParam(r, "id")
params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetInt("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
// fetch user to get lastfm session
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
if user.LastFMSession == "" {
return spec.NewError(0, "you don't have a last.fm session")
}
@@ -61,8 +61,8 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
track,
// clients will provide time in miliseconds, so use that or
// instead convert UnixNano to miliseconds
parsing.GetIntParamOr(r, "time", int(time.Now().UnixNano()/1e6)),
parsing.GetStrParamOr(r, "submission", "true") != "false",
params.GetIntOr("time", int(time.Now().UnixNano()/1e6)),
params.GetOr("submission", "true") != "false",
)
if err != nil {
return spec.NewError(0, "error when submitting: %v", err)
@@ -103,7 +103,7 @@ func (c *Controller) ServeGetScanStatus(r *http.Request) *spec.Response {
}
func (c *Controller) ServeGetUser(r *http.Request) *spec.Response {
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
sub := spec.NewResponse()
sub.User = &spec.User{
Username: user.Name,
@@ -119,7 +119,7 @@ func (c *Controller) ServeNotFound(r *http.Request) *spec.Response {
}
func (c *Controller) ServeGetPlaylists(r *http.Request) *spec.Response {
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
var playlists []*model.Playlist
c.DB.
Where("user_id = ?", user.ID).
@@ -136,7 +136,8 @@ func (c *Controller) ServeGetPlaylists(r *http.Request) *spec.Response {
}
func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
playlistID, err := parsing.GetIntParam(r, "id")
params := r.Context().Value(CtxParams).(params.Params)
playlistID, err := params.GetInt("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
@@ -159,7 +160,7 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
Order("playlist_items.created_at").
Preload("Album").
Find(&tracks)
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
sub := spec.NewResponse()
sub.Playlist = spec.NewPlaylist(&playlist)
sub.Playlist.Owner = user.Name
@@ -171,23 +172,32 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
}
func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
playlistID, _ := parsing.GetFirstIntParamOf(r, "id", "playlistId")
params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*model.User)
var playlistID int
for _, key := range []string{"id", "playlistId"} {
if val, err := params.GetInt(key); err != nil {
playlistID = val
}
}
// begin updating meta
// playlist ID may still be 0 here, if so it's okay,
// we get a new playlist
playlist := &model.Playlist{}
c.DB.
Where("id = ?", playlistID).
First(playlist)
user := r.Context().Value(key.User).(*model.User)
playlist.UserID = user.ID
if name := parsing.GetStrParam(r, "name"); name != "" {
playlist.Name = name
if val := params.Get("name"); val != "" {
playlist.Name = val
}
if comment := parsing.GetStrParam(r, "comment"); comment != "" {
playlist.Comment = comment
if val := params.Get("comment"); val != "" {
playlist.Comment = val
}
c.DB.Save(playlist)
// begin delete tracks
if indexes, ok := r.URL.Query()["songIndexToRemove"]; ok {
indexes, ok := params.GetList("songIndexToRemove")
if ok {
trackIDs := []int{}
c.DB.
Order("created_at").
@@ -204,24 +214,30 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
}
}
// begin add tracks
if toAdd := parsing.GetFirstParamOf(r, "songId", "songIdToAdd"); toAdd != nil {
for _, trackIDStr := range toAdd {
trackID, err := strconv.Atoi(trackIDStr)
if err != nil {
continue
}
c.DB.Save(&model.PlaylistItem{
PlaylistID: playlist.ID,
TrackID: trackID,
})
var toAdd []string
for _, val := range []string{"songId", "songIdToAdd"} {
toAdd, ok := params.GetList(val)
if ok {
break
}
}
for _, trackIDStr := range toAdd {
trackID, err := strconv.Atoi(trackIDStr)
if err != nil {
continue
}
c.DB.Save(&model.PlaylistItem{
PlaylistID: playlist.ID,
TrackID: trackID,
})
}
return spec.NewResponse()
}
func (c *Controller) ServeDeletePlaylist(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
c.DB.
Where("id = ?", parsing.GetIntParamOr(r, "id", 0)).
Where("id = ?", params.GetIntOr("id", 0)).
Delete(&model.Playlist{})
return spec.NewResponse()
}

View File

@@ -8,9 +8,8 @@ import (
"github.com/jinzhu/gorm"
"senan.xyz/g/gonic/model"
"senan.xyz/g/gonic/server/ctrlsubsonic/params"
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
"senan.xyz/g/gonic/server/key"
"senan.xyz/g/gonic/server/parsing"
)
// "raw" handlers are ones that don't always return a spec response.
@@ -20,7 +19,8 @@ import (
// _but not both_
func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response {
id, err := parsing.GetIntParam(r, "id")
params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetInt("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
@@ -46,7 +46,8 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s
}
func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response {
id, err := parsing.GetIntParam(r, "id")
params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetInt("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
@@ -67,7 +68,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
http.ServeFile(w, r, absPath)
//
// after we've served the file, mark the album as played
user := r.Context().Value(key.User).(*model.User)
user := r.Context().Value(CtxUser).(*model.User)
play := model.Play{
AlbumID: track.Album.ID,
UserID: user.ID,

View File

@@ -6,28 +6,11 @@ import (
"encoding/hex"
"fmt"
"net/http"
"net/url"
"senan.xyz/g/gonic/server/ctrlsubsonic/params"
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
"senan.xyz/g/gonic/server/key"
"senan.xyz/g/gonic/server/parsing"
)
var requiredParameters = []string{
"u", "v", "c",
}
func checkHasAllParams(params url.Values) error {
for _, req := range requiredParameters {
param := params.Get(req)
if param != "" {
continue
}
return fmt.Errorf("please provide a `%s` parameter", req)
}
return nil
}
func checkCredsToken(password, token, salt string) bool {
toHash := fmt.Sprintf("%s%s", password, salt)
hash := md5.Sum([]byte(toHash))
@@ -43,16 +26,34 @@ func checkCredsBasic(password, given string) bool {
return password == given
}
func (c *Controller) WithValidSubsonicArgs(next http.Handler) http.Handler {
func (c *Controller) WithParams(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := checkHasAllParams(r.URL.Query()); err != nil {
writeResp(w, r, spec.NewError(10, err.Error()))
params := params.New(r)
withParams := context.WithValue(r.Context(), CtxParams, params)
next.ServeHTTP(w, r.WithContext(withParams))
})
}
func (c *Controller) WithUser(next http.Handler) http.Handler {
requiredParameters := []string{
"u", "v", "c",
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
params := r.Context().Value(CtxParams).(params.Params)
for _, req := range requiredParameters {
param := params.Get(req)
if param != "" {
continue
}
writeResp(w, r, spec.NewError(10, "please provide a `%s` parameter", req))
return
}
username := parsing.GetStrParam(r, "u")
password := parsing.GetStrParam(r, "p")
token := parsing.GetStrParam(r, "t")
salt := parsing.GetStrParam(r, "s")
//
username := params.Get("u")
password := params.Get("p")
token := params.Get("t")
salt := params.Get("s")
//
passwordAuth := token == "" && salt == ""
tokenAuth := password == ""
if tokenAuth == passwordAuth {
@@ -74,7 +75,7 @@ func (c *Controller) WithValidSubsonicArgs(next http.Handler) http.Handler {
writeResp(w, r, spec.NewError(40, "invalid password"))
return
}
withUser := context.WithValue(r.Context(), key.User, user)
withUser := context.WithValue(r.Context(), CtxUser, user)
next.ServeHTTP(w, r.WithContext(withUser))
})
}

View File

@@ -0,0 +1,57 @@
package params
import (
"fmt"
"net/http"
"net/url"
"strconv"
)
type Params struct {
values url.Values
}
func New(r *http.Request) Params {
// first load params from the url
params := r.URL.Query()
// also if there's any in the post body, use those too
if err := r.ParseForm(); err != nil {
return Params{params}
}
for k, v := range r.Form {
params[k] = v
}
return Params{params}
}
func (p Params) Get(key string) string {
return p.values.Get(key)
}
func (p Params) GetOr(key, or string) string {
val := p.Get(key)
if val == "" {
return or
}
return val
}
func (p Params) GetInt(key string) (int, error) {
strVal := p.values.Get(key)
if strVal == "" {
return 0, fmt.Errorf("no param with key `%s`", key)
}
val, err := strconv.Atoi(strVal)
if err != nil {
return 0, fmt.Errorf("not an int `%s`", strVal)
}
return val, nil
}
func (p Params) GetIntOr(key string, or int) int {
val, err := p.GetInt(key)
if err != nil {
return or
}
return val
}