This commit is contained in:
sentriz
2019-05-19 23:28:05 +01:00
parent 5c657d9630
commit ad571ed7ab
45 changed files with 787 additions and 492 deletions

43
server/handler/handler.go Normal file
View File

@@ -0,0 +1,43 @@
package handler
import (
"html/template"
"github.com/jinzhu/gorm"
"github.com/wader/gormstore"
"github.com/sentriz/gonic/model"
)
type contextKey int
const (
contextUserKey contextKey = iota
contextSessionKey
)
type Controller struct {
DB *gorm.DB
SessDB *gormstore.Store
Templates map[string]*template.Template
MusicPath string
}
func (c *Controller) GetSetting(key string) string {
var setting model.Setting
c.DB.Where("key = ?", key).First(&setting)
return setting.Value
}
func (c *Controller) SetSetting(key, value string) {
c.DB.
Where(model.Setting{Key: key}).
Assign(model.Setting{Value: value}).
FirstOrCreate(&model.Setting{})
}
func (c *Controller) GetUserFromName(name string) *model.User {
var user model.User
c.DB.Where("name = ?", name).First(&user)
return &user
}

View File

@@ -0,0 +1,245 @@
package handler
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
"github.com/jinzhu/gorm"
"github.com/sentriz/gonic/model"
"github.com/sentriz/gonic/lastfm"
)
func (c *Controller) ServeLogin(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, r, c.Templates["login"], nil)
}
func (c *Controller) ServeLoginDo(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
session.AddFlash("please provide both a username and password")
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
user := c.GetUserFromName(username)
if !(username == user.Name && password == user.Password) {
session.AddFlash("invalid username / password")
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
// put the user name into the session. future endpoints after this one
// are wrapped with WithUserSession() which will get the name from the
// session and put the row into the request context.
session.Values["user"] = user.Name
session.Save(r, w)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
session.Options.MaxAge = -1
session.Save(r, w)
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
}
func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
var data templateData
c.DB.Table("album_artists").Count(&data.ArtistCount)
c.DB.Table("albums").Count(&data.AlbumCount)
c.DB.Table("tracks").Count(&data.TrackCount)
c.DB.Find(&data.AllUsers)
data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key")
scheme := firstExisting(
"http", // fallback
r.Header.Get("X-Forwarded-Proto"),
r.Header.Get("X-Forwarded-Scheme"),
r.URL.Scheme,
)
host := firstExisting(
"localhost:7373", // fallback
r.Header.Get("X-Forwarded-Host"),
r.Host,
)
data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host)
renderTemplate(w, r, c.Templates["home"], &data)
}
func (c *Controller) ServeChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, r, c.Templates["change_own_password"], nil)
}
func (c *Controller) ServeChangeOwnPasswordDo(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
passwordOne := r.FormValue("password_one")
passwordTwo := r.FormValue("password_two")
err := validatePasswords(passwordOne, passwordTwo)
if err != nil {
session.AddFlash(err.Error())
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
user := r.Context().Value(contextUserKey).(*model.User)
user.Password = passwordOne
c.DB.Save(user)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
func (c *Controller) ServeLinkLastFMDo(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "please provide a token", 400)
return
}
sessionKey, err := lastfm.GetSession(
c.GetSetting("lastfm_api_key"),
c.GetSetting("lastfm_secret"),
token,
)
session := r.Context().Value(contextSessionKey).(*sessions.Session)
if err != nil {
session.AddFlash(err.Error())
session.Save(r, w)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
return
}
user := r.Context().Value(contextUserKey).(*model.User)
user.LastFMSession = sessionKey
c.DB.Save(&user)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
func (c *Controller) ServeUnlinkLastFMDo(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(contextUserKey).(*model.User)
user.LastFMSession = ""
c.DB.Save(&user)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("user")
if username == "" {
http.Error(w, "please provide a username", 400)
return
}
var user model.User
err := c.DB.Where("name = ?", username).First(&user).Error
if gorm.IsRecordNotFoundError(err) {
http.Error(w, "couldn't find a user with that name", 400)
return
}
var data templateData
data.SelectedUser = &user
renderTemplate(w, r, c.Templates["change_password"], &data)
}
func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
username := r.URL.Query().Get("user")
var user model.User
c.DB.Where("name = ?", username).First(&user)
passwordOne := r.FormValue("password_one")
passwordTwo := r.FormValue("password_two")
err := validatePasswords(passwordOne, passwordTwo)
if err != nil {
session.AddFlash(err.Error())
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
user.Password = passwordOne
c.DB.Save(&user)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("user")
if username == "" {
http.Error(w, "please provide a username", 400)
return
}
var user model.User
err := c.DB.Where("name = ?", username).First(&user).Error
if gorm.IsRecordNotFoundError(err) {
http.Error(w, "couldn't find a user with that name", 400)
return
}
var data templateData
data.SelectedUser = &user
renderTemplate(w, r, c.Templates["delete_user"], &data)
}
func (c *Controller) ServeDeleteUserDo(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("user")
var user model.User
c.DB.Where("name = ?", username).First(&user)
c.DB.Delete(&user)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
func (c *Controller) ServeCreateUser(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, r, c.Templates["create_user"], nil)
}
func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
username := r.FormValue("username")
err := validateUsername(username)
if err != nil {
session.AddFlash(err.Error())
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
passwordOne := r.FormValue("password_one")
passwordTwo := r.FormValue("password_two")
err = validatePasswords(passwordOne, passwordTwo)
if err != nil {
session.AddFlash(err.Error())
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
user := model.User{
Name: username,
Password: passwordOne,
}
err = c.DB.Create(&user).Error
if err != nil {
session.AddFlash(fmt.Sprintf(
"could not create user `%s`: %v", username, err,
))
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}
func (c *Controller) ServeUpdateLastFMAPIKey(w http.ResponseWriter, r *http.Request) {
var data templateData
data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key")
data.CurrentLastFMAPISecret = c.GetSetting("lastfm_secret")
renderTemplate(w, r, c.Templates["update_lastfm_api_key"], &data)
}
func (c *Controller) ServeUpdateLastFMAPIKeyDo(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
apiKey := r.FormValue("api_key")
secret := r.FormValue("secret")
err := validateAPIKey(apiKey, secret)
if err != nil {
session.AddFlash(err.Error())
session.Save(r, w)
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
c.SetSetting("lastfm_api_key", apiKey)
c.SetSetting("lastfm_secret", secret)
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
}

View File

@@ -0,0 +1,42 @@
package handler
import "fmt"
func validateUsername(username string) error {
if username == "" {
return fmt.Errorf("please enter the username")
}
return nil
}
func validatePasswords(pOne, pTwo string) error {
if pOne == "" || pTwo == "" {
return fmt.Errorf("please enter the password twice")
}
if !(pOne == pTwo) {
return fmt.Errorf("the two passwords entered were not the same")
}
return nil
}
func validateAPIKey(apiKey, secret string) error {
if apiKey == "" || secret == "" {
return fmt.Errorf("please enter both the api key and secret")
}
return nil
}
func firstExisting(or string, strings ...string) string {
current := ""
for _, s := range strings {
if s == "" {
continue
}
current = s
break
}
if current == "" {
return or
}
return current
}

View File

@@ -0,0 +1,179 @@
package handler
import (
"fmt"
"net/http"
"github.com/jinzhu/gorm"
"github.com/sentriz/gonic/model"
"github.com/sentriz/gonic/server/subsonic"
)
func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
// we are browsing by folder, but the subsonic docs show sub <artist> elements
// for this, so we're going to return root directories as "artists"
var folders []*model.Folder
c.DB.Where("parent_id = ?", 1).Find(&folders)
var indexMap = make(map[rune]*subsonic.Index)
var indexes []*subsonic.Index
for _, folder := range folders {
i := indexOf(folder.Name)
index, ok := indexMap[i]
if !ok {
index = &subsonic.Index{
Name: string(i),
Artists: []*subsonic.Artist{},
}
indexMap[i] = index
indexes = append(indexes, index)
}
index.Artists = append(index.Artists, &subsonic.Artist{
ID: folder.ID,
Name: folder.Name,
})
}
sub := subsonic.NewResponse()
sub.Indexes = &subsonic.Indexes{
LastModified: 0,
Index: indexes,
}
respond(w, r, sub)
}
func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
childrenObj := []*subsonic.Child{}
var cFolder model.Folder
c.DB.First(&cFolder, id)
//
// start looking for child folders in the current dir
var folders []*model.Folder
c.DB.
Where("parent_id = ?", id).
Find(&folders)
for _, folder := range folders {
childrenObj = append(childrenObj, &subsonic.Child{
Parent: cFolder.ID,
ID: folder.ID,
Title: folder.Name,
IsDir: true,
CoverID: folder.CoverID,
})
}
//
// start looking for child tracks in the current dir
var tracks []*model.Track
c.DB.
Where("folder_id = ?", id).
Preload("Album").
Order("track_number").
Find(&tracks)
for _, track := range tracks {
if getStrParam(r, "c") == "Jamstash" {
// jamstash thinks it can't play flacs
track.ContentType = "audio/mpeg"
track.Suffix = "mp3"
}
childrenObj = append(childrenObj, &subsonic.Child{
ID: track.ID,
Album: track.Album.Title,
Artist: track.Artist,
ContentType: track.ContentType,
CoverID: cFolder.CoverID,
Duration: 0,
IsDir: false,
Parent: cFolder.ID,
Path: track.Path,
Size: track.Size,
Suffix: track.Suffix,
Title: track.Title,
Track: track.TrackNumber,
Type: "music",
})
}
//
// respond section
sub := subsonic.NewResponse()
sub.Directory = &subsonic.Directory{
ID: cFolder.ID,
Parent: cFolder.ParentID,
Name: cFolder.Name,
Children: childrenObj,
}
respond(w, r, sub)
}
// changes to this function should be reflected in in _by_tags.go's
// getAlbumListTwo() function
func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
listType := getStrParam(r, "type")
if listType == "" {
respondError(w, r, 10, "please provide a `type` parameter")
return
}
q := c.DB
switch listType {
case "alphabeticalByArtist":
// not sure what it meant by "artist" since we're browsing by folder
// - so we'll consider the parent folder's name to be the "artist"
q = q.Joins(`
JOIN folders AS parent_folders
ON folders.parent_id = parent_folders.id`)
q = q.Order("parent_folders.name")
case "alphabeticalByName":
// not sure about "name" either, so lets use the folder's name
q = q.Order("name")
case "frequent":
user := r.Context().Value(contextUserKey).(*model.User)
q = q.Joins(`
JOIN plays
ON folders.id = plays.folder_id AND plays.user_id = ?`,
user.ID)
q = q.Order("plays.count DESC")
case "newest":
q = q.Order("updated_at DESC")
case "random":
q = q.Order(gorm.Expr("random()"))
case "recent":
user := r.Context().Value(contextUserKey).(*model.User)
q = q.Joins(`
JOIN plays
ON folders.id = plays.folder_id AND plays.user_id = ?`,
user.ID)
q = q.Order("plays.time DESC")
default:
respondError(w, r, 10, fmt.Sprintf(
"unknown value `%s` for parameter 'type'", listType,
))
return
}
var folders []*model.Folder
q.
Where("folders.has_tracks = 1").
Offset(getIntParamOr(r, "offset", 0)).
Limit(getIntParamOr(r, "size", 10)).
Preload("Parent").
Find(&folders)
listObj := []*subsonic.Album{}
for _, folder := range folders {
listObj = append(listObj, &subsonic.Album{
ID: folder.ID,
Title: folder.Name,
Album: folder.Name,
CoverID: folder.CoverID,
ParentID: folder.ParentID,
IsDir: true,
Artist: folder.Parent.Name,
})
}
sub := subsonic.NewResponse()
sub.Albums = &subsonic.Albums{
List: listObj,
}
respond(w, r, sub)
}

View File

@@ -0,0 +1,180 @@
package handler
import (
"fmt"
"net/http"
"github.com/jinzhu/gorm"
"github.com/sentriz/gonic/model"
"github.com/sentriz/gonic/server/subsonic"
)
func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
var artists []*model.AlbumArtist
c.DB.Find(&artists)
var indexMap = make(map[rune]*subsonic.Index)
var indexes subsonic.Artists
for _, artist := range artists {
i := indexOf(artist.Name)
index, ok := indexMap[i]
if !ok {
index = &subsonic.Index{
Name: string(i),
Artists: []*subsonic.Artist{},
}
indexMap[i] = index
indexes.List = append(indexes.List, index)
}
index.Artists = append(index.Artists, &subsonic.Artist{
ID: artist.ID,
Name: artist.Name,
})
}
sub := subsonic.NewResponse()
sub.Artists = &indexes
respond(w, r, sub)
}
func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var artist model.AlbumArtist
c.DB.
Preload("Albums").
First(&artist, id)
albumsObj := []*subsonic.Album{}
for _, album := range artist.Albums {
albumsObj = append(albumsObj, &subsonic.Album{
ID: album.ID,
Name: album.Title,
Created: album.CreatedAt,
Artist: artist.Name,
ArtistID: artist.ID,
CoverID: album.CoverID,
})
}
sub := subsonic.NewResponse()
sub.Artist = &subsonic.Artist{
ID: artist.ID,
Name: artist.Name,
Albums: albumsObj,
}
respond(w, r, sub)
}
func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var album model.Album
c.DB.
Preload("AlbumArtist").
Preload("Tracks").
First(&album, id)
tracksObj := []*subsonic.Track{}
for _, track := range album.Tracks {
tracksObj = append(tracksObj, &subsonic.Track{
ID: track.ID,
Title: track.Title,
Artist: track.Artist, // track artist
TrackNo: track.TrackNumber,
ContentType: track.ContentType,
Path: track.Path,
Suffix: track.Suffix,
Created: track.CreatedAt,
Size: track.Size,
Album: album.Title,
AlbumID: album.ID,
ArtistID: album.AlbumArtist.ID, // album artist
CoverID: album.CoverID,
Type: "music",
})
}
sub := subsonic.NewResponse()
sub.Album = &subsonic.Album{
ID: album.ID,
Name: album.Title,
CoverID: album.CoverID,
Created: album.CreatedAt,
Artist: album.AlbumArtist.Name,
Tracks: tracksObj,
}
respond(w, r, sub)
}
// changes to this function should be reflected in in _by_folder.go's
// getAlbumList() function
func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
listType := getStrParam(r, "type")
if listType == "" {
respondError(w, r, 10, "please provide a `type` parameter")
return
}
q := c.DB
switch listType {
case "alphabeticalByArtist":
q = q.Joins(`
JOIN album_artists
ON albums.album_artist_id = album_artists.id`)
q = q.Order("album_artists.name")
case "alphabeticalByName":
q = q.Order("title")
case "byYear":
q = q.Where(
"year BETWEEN ? AND ?",
getIntParamOr(r, "fromYear", 1800),
getIntParamOr(r, "toYear", 2200))
q = q.Order("year")
case "frequent":
user := r.Context().Value(contextUserKey).(*model.User)
q = q.Joins(`
JOIN plays
ON albums.id = plays.album_id AND plays.user_id = ?`,
user.ID)
q = q.Order("plays.count DESC")
case "newest":
q = q.Order("updated_at DESC")
case "random":
q = q.Order(gorm.Expr("random()"))
case "recent":
user := r.Context().Value(contextUserKey).(*model.User)
q = q.Joins(`
JOIN plays
ON albums.id = plays.album_id AND plays.user_id = ?`,
user.ID)
q = q.Order("plays.time DESC")
default:
respondError(w, r, 10, fmt.Sprintf(
"unknown value `%s` for parameter 'type'", listType,
))
return
}
var albums []*model.Album
q.
Offset(getIntParamOr(r, "offset", 0)).
Limit(getIntParamOr(r, "size", 10)).
Preload("AlbumArtist").
Find(&albums)
listObj := []*subsonic.Album{}
for _, album := range albums {
listObj = append(listObj, &subsonic.Album{
ID: album.ID,
Name: album.Title,
Created: album.CreatedAt,
CoverID: album.CoverID,
Artist: album.AlbumArtist.Name,
ArtistID: album.AlbumArtist.ID,
})
}
sub := subsonic.NewResponse()
sub.AlbumsTwo = &subsonic.Albums{
List: listObj,
}
respond(w, r, sub)
}

View File

@@ -0,0 +1,157 @@
package handler
import (
"fmt"
"net/http"
"os"
"sync/atomic"
"time"
"unicode"
"github.com/rainycape/unidecode"
"github.com/sentriz/gonic/lastfm"
"github.com/sentriz/gonic/model"
"github.com/sentriz/gonic/scanner"
"github.com/sentriz/gonic/server/subsonic"
)
func indexOf(s string) rune {
first := string(s[0])
c := rune(unidecode.Unidecode(first)[0])
if !unicode.IsLetter(c) {
return '#'
}
return c
}
func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var track model.Track
c.DB.
Preload("Album").
Preload("Folder").
First(&track, id)
if track.Path == "" {
respondError(w, r, 70, fmt.Sprintf("media with id `%d` was not found", id))
return
}
file, err := os.Open(track.Path)
if err != nil {
respondError(w, r, 0, fmt.Sprintf("error while streaming media: %v", err))
return
}
stat, _ := file.Stat()
http.ServeContent(w, r, track.Path, stat.ModTime(), file)
//
// after we've served the file, mark the album as played
user := r.Context().Value(contextUserKey).(*model.User)
play := model.Play{
AlbumID: track.Album.ID,
FolderID: track.Folder.ID,
UserID: user.ID,
}
c.DB.Where(play).First(&play)
play.Time = time.Now() // for getAlbumList?type=recent
play.Count++ // for getAlbumList?type=frequent
c.DB.Save(&play)
}
func (c *Controller) GetCoverArt(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
var cover model.Cover
c.DB.First(&cover, id)
w.Write(cover.Image)
}
func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) {
sub := subsonic.NewResponse()
sub.Licence = &subsonic.Licence{
Valid: true,
}
respond(w, r, sub)
}
func (c *Controller) Ping(w http.ResponseWriter, r *http.Request) {
sub := subsonic.NewResponse()
respond(w, r, sub)
}
func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
// fetch user to get lastfm session
user := r.Context().Value(contextUserKey).(*model.User)
if user.LastFMSession == "" {
respondError(w, r, 0, fmt.Sprintf("no last.fm session for this user: %v", err))
return
}
// fetch track for getting info to send to last.fm function
var track model.Track
c.DB.
Preload("Album").
Preload("AlbumArtist").
First(&track, id)
// get time from args or use now
time := getIntParamOr(r, "time", int(time.Now().Unix()))
// get submission, where the default is true. we will
// check if it's false later
submission := getStrParamOr(r, "submission", "true")
// scrobble with above info
err = lastfm.Scrobble(
c.GetSetting("lastfm_api_key"),
c.GetSetting("lastfm_secret"),
user.LastFMSession,
&track,
time,
submission != "false",
)
if err != nil {
respondError(w, r, 0, fmt.Sprintf("error when submitting: %v", err))
return
}
sub := subsonic.NewResponse()
respond(w, r, sub)
}
func (c *Controller) GetMusicFolders(w http.ResponseWriter, r *http.Request) {
folders := &subsonic.MusicFolders{}
folders.List = []*subsonic.MusicFolder{
{ID: 1, Name: "music"},
}
sub := subsonic.NewResponse()
sub.MusicFolders = folders
respond(w, r, sub)
}
func (c *Controller) StartScan(w http.ResponseWriter, r *http.Request) {
scanC := scanner.New(c.DB, c.MusicPath)
go scanC.Start()
c.GetScanStatus(w, r)
}
func (c *Controller) GetScanStatus(w http.ResponseWriter, r *http.Request) {
var trackCount int
c.DB.Model(&model.Track{}).Count(&trackCount)
sub := subsonic.NewResponse()
sub.ScanStatus = &subsonic.ScanStatus{
Scanning: atomic.LoadInt32(&scanner.IsScanning) == 1,
Count: trackCount,
}
respond(w, r, sub)
}
func (c *Controller) NotFound(w http.ResponseWriter, r *http.Request) {
respondError(w, r, 0, "unknown route")
}

View File

@@ -0,0 +1,59 @@
package handler
import (
"context"
"net/http"
"github.com/gorilla/sessions"
"github.com/sentriz/gonic/model"
)
func (c *Controller) WithSession(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, _ := c.SessDB.Get(r, "gonic")
withSession := context.WithValue(r.Context(), contextSessionKey, session)
next.ServeHTTP(w, r.WithContext(withSession))
}
}
func (c *Controller) WithUserSession(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// session exists at this point
session := r.Context().Value(contextSessionKey).(*sessions.Session)
username, ok := session.Values["user"].(string)
if !ok {
session.AddFlash("you are not authenticated")
session.Save(r, w)
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// take username from sesion and add the user row to the context
user := c.GetUserFromName(username)
if user.ID == 0 {
// the username in the client's session no longer relates to a
// user in the database (maybe the user was deleted)
session.Options.MaxAge = -1
session.Save(r, w)
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
withUser := context.WithValue(r.Context(), contextUserKey, user)
next.ServeHTTP(w, r.WithContext(withUser))
}
}
func (c *Controller) WithAdminSession(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// session and user exist at this point
session := r.Context().Value(contextSessionKey).(*sessions.Session)
user := r.Context().Value(contextUserKey).(*model.User)
if !user.IsAdmin {
session.AddFlash("you are not an admin")
session.Save(r, w)
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
}
}

View File

@@ -0,0 +1,13 @@
package handler
import (
"log"
"net/http"
)
func (c *Controller) WithLogging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("connection from `%s` for `%s`", r.RemoteAddr, r.URL)
next.ServeHTTP(w, r)
}
}

View File

@@ -0,0 +1,98 @@
package handler
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"net/url"
)
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 checkCredentialsToken(password, token, salt string) bool {
toHash := fmt.Sprintf("%s%s", password, salt)
hash := md5.Sum([]byte(toHash))
expToken := hex.EncodeToString(hash[:])
return token == expToken
}
func checkCredentialsBasic(password, givenPassword string) bool {
if givenPassword[:4] == "enc:" {
bytes, _ := hex.DecodeString(givenPassword[4:])
givenPassword = string(bytes)
}
return password == givenPassword
}
func (c *Controller) WithValidSubsonicArgs(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := checkHasAllParams(r.URL.Query())
if err != nil {
respondError(w, r, 10, err.Error())
return
}
username, password := r.URL.Query().Get("u"),
r.URL.Query().Get("p")
token, salt := r.URL.Query().Get("t"),
r.URL.Query().Get("s")
passwordAuth, tokenAuth := token == "" && salt == "",
password == ""
if tokenAuth == passwordAuth {
respondError(w, r,
10, "please provide parameters `t` and `s`, or just `p`",
)
return
}
user := c.GetUserFromName(username)
if user.ID == 0 {
// the user does not exist
respondError(w, r, 40, "invalid username")
return
}
var credsOk bool
if tokenAuth {
credsOk = checkCredentialsToken(user.Password, token, salt)
} else {
credsOk = checkCredentialsBasic(user.Password, password)
}
if !credsOk {
respondError(w, r, 40, "invalid password")
return
}
withUser := context.WithValue(r.Context(), contextUserKey, user)
next.ServeHTTP(w, r.WithContext(withUser))
}
}
func (c *Controller) WithCORS(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods",
"POST, GET, OPTIONS, PUT, DELETE",
)
w.Header().Set("Access-Control-Allow-Headers",
"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization",
)
if r.Method == "OPTIONS" {
return
}
next.ServeHTTP(w, r)
}
}

39
server/handler/parse.go Normal file
View File

@@ -0,0 +1,39 @@
package handler
import (
"fmt"
"net/http"
"strconv"
)
func getStrParam(r *http.Request, key string) string {
return r.URL.Query().Get(key)
}
func getStrParamOr(r *http.Request, key, or string) string {
val := getStrParam(r, key)
if val == "" {
return or
}
return val
}
func getIntParam(r *http.Request, key string) (int, error) {
strVal := r.URL.Query().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 getIntParamOr(r *http.Request, key string, or int) int {
val, err := getIntParam(r, key)
if err != nil {
return or
}
return val
}

View File

@@ -0,0 +1,43 @@
package handler
import (
"fmt"
"html/template"
"net/http"
"github.com/gorilla/sessions"
"github.com/sentriz/gonic/model"
)
type templateData struct {
Flashes []interface{}
User *model.User
SelectedUser *model.User
AllUsers []*model.User
ArtistCount int
AlbumCount int
TrackCount int
CurrentLastFMAPIKey string
CurrentLastFMAPISecret string
RequestRoot string
}
func renderTemplate(w http.ResponseWriter, r *http.Request,
tmpl *template.Template, data *templateData) {
session := r.Context().Value(contextSessionKey).(*sessions.Session)
if data == nil {
data = &templateData{}
}
data.Flashes = session.Flashes()
session.Save(r, w)
user, ok := r.Context().Value(contextUserKey).(*model.User)
if ok {
data.User = user
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, fmt.Sprintf("500 when executing: %v", err), 500)
return
}
}

View File

@@ -0,0 +1,56 @@
package handler
import (
"encoding/json"
"encoding/xml"
"log"
"net/http"
"github.com/sentriz/gonic/server/subsonic"
)
func respondRaw(w http.ResponseWriter, r *http.Request,
code int, sub *subsonic.Response) {
res := subsonic.MetaResponse{
Response: sub,
}
switch r.URL.Query().Get("f") {
case "json":
w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(res)
if err != nil {
log.Printf("could not marshall to json: %v\n", err)
}
w.Write(data)
case "jsonp":
w.Header().Set("Content-Type", "application/javascript")
data, err := json.Marshal(res)
if err != nil {
log.Printf("could not marshall to json: %v\n", err)
}
callback := r.URL.Query().Get("callback")
w.Write([]byte(callback))
w.Write([]byte("("))
w.Write(data)
w.Write([]byte(");"))
default:
w.Header().Set("Content-Type", "application/xml")
data, err := xml.MarshalIndent(res, "", " ")
if err != nil {
log.Printf("could not marshall to xml: %v\n", err)
}
w.Write(data)
}
}
func respond(w http.ResponseWriter, r *http.Request,
sub *subsonic.Response) {
respondRaw(w, r, http.StatusOK, sub)
}
func respondError(w http.ResponseWriter, r *http.Request,
code int, message string) {
respondRaw(w, r, http.StatusBadRequest, subsonic.NewError(
code, message,
))
}