Authenticate Subsonic API calls using the DB
This commit is contained in:
@@ -24,15 +24,16 @@ type Router struct {
|
||||
Ratings engine.Ratings
|
||||
Scrobbler engine.Scrobbler
|
||||
Search engine.Search
|
||||
Users engine.Users
|
||||
|
||||
mux http.Handler
|
||||
}
|
||||
|
||||
func New(browser engine.Browser, cover engine.Cover, listGenerator engine.ListGenerator,
|
||||
func New(browser engine.Browser, cover engine.Cover, listGenerator engine.ListGenerator, users engine.Users,
|
||||
playlists engine.Playlists, ratings engine.Ratings, scrobbler engine.Scrobbler, search engine.Search) *Router {
|
||||
|
||||
r := &Router{Browser: browser, Cover: cover, ListGenerator: listGenerator, Playlists: playlists,
|
||||
Ratings: ratings, Scrobbler: scrobbler, Search: search}
|
||||
Ratings: ratings, Scrobbler: scrobbler, Search: search, Users: users}
|
||||
r.mux = r.routes()
|
||||
return r
|
||||
}
|
||||
@@ -48,7 +49,7 @@ func (api *Router) routes() http.Handler {
|
||||
|
||||
// Add validation middleware if not disabled
|
||||
if !conf.Sonic.DevDisableAuthentication {
|
||||
r.Use(authenticate)
|
||||
r.Use(authenticate(api.Users))
|
||||
// TODO Validate version
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ package subsonic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudsonic/sonic-server/conf"
|
||||
"github.com/cloudsonic/sonic-server/engine"
|
||||
"github.com/cloudsonic/sonic-server/log"
|
||||
"github.com/cloudsonic/sonic-server/model"
|
||||
"github.com/cloudsonic/sonic-server/server/subsonic/responses"
|
||||
)
|
||||
|
||||
@@ -44,36 +42,30 @@ func checkRequiredParameters(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func authenticate(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
password := conf.Sonic.Password
|
||||
user := ParamString(r, "u")
|
||||
pass := ParamString(r, "p")
|
||||
salt := ParamString(r, "s")
|
||||
token := ParamString(r, "t")
|
||||
valid := false
|
||||
func authenticate(users engine.Users) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := ParamString(r, "u")
|
||||
pass := ParamString(r, "p")
|
||||
token := ParamString(r, "t")
|
||||
salt := ParamString(r, "s")
|
||||
|
||||
switch {
|
||||
case pass != "":
|
||||
if strings.HasPrefix(pass, "enc:") {
|
||||
if dec, err := hex.DecodeString(pass[4:]); err == nil {
|
||||
pass = string(dec)
|
||||
}
|
||||
_, err := users.Authenticate(user, pass, token, salt)
|
||||
if err == model.ErrInvalidAuth {
|
||||
log.Warn(r, "Invalid login", "user", user, err)
|
||||
} else if err != nil {
|
||||
log.Error(r, "Error authenticating user", "user", user, err)
|
||||
}
|
||||
valid = pass == password
|
||||
case token != "":
|
||||
t := fmt.Sprintf("%x", md5.Sum([]byte(password+salt)))
|
||||
valid = t == token
|
||||
}
|
||||
|
||||
if user != conf.Sonic.User || !valid {
|
||||
log.Warn(r, "Invalid login", "user", user)
|
||||
SendError(w, r, NewError(responses.ErrorAuthenticationFail))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Warn(r, "Invalid login", "user", user)
|
||||
SendError(w, r, NewError(responses.ErrorAuthenticationFail))
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func requiredParams(params ...string) func(next http.Handler) http.Handler {
|
||||
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudsonic/sonic-server/conf"
|
||||
"github.com/cloudsonic/sonic-server/engine"
|
||||
"github.com/cloudsonic/sonic-server/log"
|
||||
"github.com/cloudsonic/sonic-server/model"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@@ -67,60 +68,31 @@ var _ = Describe("Middlewares", func() {
|
||||
})
|
||||
|
||||
Describe("Authenticate", func() {
|
||||
var mockedUser *mockUsers
|
||||
BeforeEach(func() {
|
||||
conf.Sonic.User = "admin"
|
||||
conf.Sonic.Password = "wordpass"
|
||||
conf.Sonic.DevDisableAuthentication = false
|
||||
mockedUser = &mockUsers{}
|
||||
})
|
||||
|
||||
Context("Plaintext password", func() {
|
||||
It("authenticates with plaintext password ", func() {
|
||||
r := newTestRequest("u=admin", "p=wordpass")
|
||||
cp := authenticate(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
It("passes all parameters to users.Authenticate ", func() {
|
||||
r := newTestRequest("u=valid", "p=password", "t=token", "s=salt")
|
||||
cp := authenticate(mockedUser)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.called).To(BeTrue())
|
||||
})
|
||||
|
||||
It("fails authentication with wrong password", func() {
|
||||
r := newTestRequest("u=admin", "p=INVALID")
|
||||
cp := authenticate(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(w.Body.String()).To(ContainSubstring(`code="40"`))
|
||||
Expect(next.called).To(BeFalse())
|
||||
})
|
||||
Expect(mockedUser.username).To(Equal("valid"))
|
||||
Expect(mockedUser.password).To(Equal("password"))
|
||||
Expect(mockedUser.token).To(Equal("token"))
|
||||
Expect(mockedUser.salt).To(Equal("salt"))
|
||||
Expect(next.called).To(BeTrue())
|
||||
})
|
||||
|
||||
Context("Encoded password", func() {
|
||||
It("authenticates with simple encoded password ", func() {
|
||||
r := newTestRequest("u=admin", "p=enc:776f726470617373")
|
||||
cp := authenticate(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
It("fails authentication with wrong password", func() {
|
||||
r := newTestRequest("u=invalid", "", "", "")
|
||||
cp := authenticate(mockedUser)(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.called).To(BeTrue())
|
||||
})
|
||||
Expect(w.Body.String()).To(ContainSubstring(`code="40"`))
|
||||
Expect(next.called).To(BeFalse())
|
||||
})
|
||||
|
||||
Context("Token based authentication", func() {
|
||||
It("authenticates with token based authentication", func() {
|
||||
r := newTestRequest("u=admin", "t=23b342970e25c7928831c3317edd0b67", "s=retnlmjetrymazgkt")
|
||||
cp := authenticate(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(next.called).To(BeTrue())
|
||||
})
|
||||
|
||||
It("fails if salt is missing", func() {
|
||||
r := newTestRequest("u=admin", "t=23b342970e25c7928831c3317edd0b67")
|
||||
cp := authenticate(next)
|
||||
cp.ServeHTTP(w, r)
|
||||
|
||||
Expect(w.Body.String()).To(ContainSubstring(`code="40"`))
|
||||
Expect(next.called).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
@@ -133,3 +105,19 @@ func (mh *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
mh.req = r
|
||||
mh.called = true
|
||||
}
|
||||
|
||||
type mockUsers struct {
|
||||
engine.Users
|
||||
username, password, token, salt string
|
||||
}
|
||||
|
||||
func (m *mockUsers) Authenticate(username, password, token, salt string) (*model.User, error) {
|
||||
m.username = username
|
||||
m.password = password
|
||||
m.token = token
|
||||
m.salt = salt
|
||||
if username == "valid" {
|
||||
return &model.User{UserName: username, Password: password}, nil
|
||||
}
|
||||
return nil, model.ErrInvalidAuth
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user