Phase 2: Subsonic REST API framework (auth, response formatting, error handling, routing) plus user permission model
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package subsonic
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
vsauth "git.noroutes.net/NoRoutes/vibetunes/internal/auth"
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
||||
)
|
||||
|
||||
// authenticate implements original Subsonic authentication: either
|
||||
// token/salt (t = md5(password + salt), the modern recommended mode) or
|
||||
// password (p=<plaintext> or p=enc:<hex>, kept for older clients).
|
||||
//
|
||||
// This never touches VibeSonic's Argon2id-hashed password directly for
|
||||
// token auth — that hash is one-way by design. Token auth is instead
|
||||
// verified against a separately maintained, AES-GCM-encrypted copy of
|
||||
// the password (see internal/auth/subsonic_secret.go); the /rest/*
|
||||
// surface never reintroduces MD5 or plaintext as VibeSonic's own storage
|
||||
// format, it only speaks it on the wire where the protocol requires it.
|
||||
func authenticate(db *sql.DB, encryptionKey []byte, p params) (*database.User, *apiError) {
|
||||
username, aerr := p.Required("u")
|
||||
if aerr != nil {
|
||||
return nil, aerr
|
||||
}
|
||||
|
||||
user, err := database.GetUserByUsername(db, username)
|
||||
if err != nil || user.Disabled {
|
||||
return nil, errWrongCredentials
|
||||
}
|
||||
|
||||
token := p.Get("t")
|
||||
salt := p.Get("s")
|
||||
password := p.Get("p")
|
||||
|
||||
switch {
|
||||
case token != "" && salt != "":
|
||||
if !user.SubsonicAuthSecret.Valid {
|
||||
return nil, errTokenAuthNotSupported
|
||||
}
|
||||
plaintext, derr := vsauth.DecryptPassword(encryptionKey, user.SubsonicAuthSecret.String)
|
||||
if derr != nil {
|
||||
return nil, errWrongCredentials
|
||||
}
|
||||
sum := md5.Sum([]byte(plaintext + salt))
|
||||
expected := hex.EncodeToString(sum[:])
|
||||
if !strings.EqualFold(expected, token) {
|
||||
return nil, errWrongCredentials
|
||||
}
|
||||
|
||||
case password != "":
|
||||
plaintext := password
|
||||
if strings.HasPrefix(password, "enc:") {
|
||||
decoded, herr := hex.DecodeString(strings.TrimPrefix(password, "enc:"))
|
||||
if herr != nil {
|
||||
return nil, errWrongCredentials
|
||||
}
|
||||
plaintext = string(decoded)
|
||||
}
|
||||
if !user.PasswordHash.Valid {
|
||||
return nil, errWrongCredentials
|
||||
}
|
||||
ok, verr := vsauth.VerifyPassword(plaintext, user.PasswordHash.String)
|
||||
if verr != nil || !ok {
|
||||
return nil, errWrongCredentials
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, errMissingParameter("p or t/s")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// checkVersion rejects clients whose declared protocol version is newer
|
||||
// than what this server implements. A missing or unparsable v is
|
||||
// tolerated (treated as compatible) rather than rejected outright, to
|
||||
// stay lenient with tooling that omits it — the protocol is additive, so
|
||||
// there is no reason to reject an old client either.
|
||||
func checkVersion(v string) *apiError {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
client, ok := parseVersion(v)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
server, _ := parseVersion(ServerVersion)
|
||||
if versionGreater(client, server) {
|
||||
return errClientMustUpgrade
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type semver struct{ major, minor, patch int }
|
||||
|
||||
func parseVersion(v string) (semver, bool) {
|
||||
parts := strings.SplitN(v, ".", 3)
|
||||
if len(parts) == 0 {
|
||||
return semver{}, false
|
||||
}
|
||||
var s semver
|
||||
var err error
|
||||
s.major, err = strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return semver{}, false
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
s.minor, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
s.patch, _ = strconv.Atoi(parts[2])
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
func versionGreater(a, b semver) bool {
|
||||
if a.major != b.major {
|
||||
return a.major > b.major
|
||||
}
|
||||
if a.minor != b.minor {
|
||||
return a.minor > b.minor
|
||||
}
|
||||
return a.patch > b.patch
|
||||
}
|
||||
Reference in New Issue
Block a user