Minimal subsonic functionality
This commit is contained in:
126
src/routes/subsonic/auth.rs
Normal file
126
src/routes/subsonic/auth.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use actix_web::HttpRequest;
|
||||
use md5::{Digest, Md5};
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
use shanty_db::entities::user::Model as User;
|
||||
use shanty_db::queries;
|
||||
|
||||
/// Subsonic authentication method.
|
||||
pub enum AuthMethod {
|
||||
/// Modern: token = md5(password + salt)
|
||||
Token { token: String, salt: String },
|
||||
/// Legacy: plaintext password
|
||||
Password(String),
|
||||
/// Legacy: hex-encoded password (p=enc:hexstring)
|
||||
HexPassword(String),
|
||||
}
|
||||
|
||||
/// Common Subsonic API parameters extracted from the query string.
|
||||
pub struct SubsonicParams {
|
||||
/// Username
|
||||
pub username: String,
|
||||
/// Authentication method + credentials
|
||||
pub auth: AuthMethod,
|
||||
/// API version requested
|
||||
#[allow(dead_code)]
|
||||
pub version: String,
|
||||
/// Client name
|
||||
#[allow(dead_code)]
|
||||
pub client: String,
|
||||
/// Response format: "xml" or "json"
|
||||
pub format: String,
|
||||
}
|
||||
|
||||
pub enum SubsonicAuthError {
|
||||
MissingParam(String),
|
||||
AuthFailed,
|
||||
}
|
||||
|
||||
impl SubsonicParams {
|
||||
/// Extract Subsonic params from the query string.
|
||||
pub fn from_request(req: &HttpRequest) -> Result<Self, SubsonicAuthError> {
|
||||
let qs = req.query_string();
|
||||
let params: Vec<(String, String)> = serde_urlencoded::from_str(qs).unwrap_or_default();
|
||||
|
||||
let get = |name: &str| -> Option<String> {
|
||||
params
|
||||
.iter()
|
||||
.find(|(k, _)| k == name)
|
||||
.map(|(_, v)| v.clone())
|
||||
};
|
||||
|
||||
let username = get("u").ok_or_else(|| SubsonicAuthError::MissingParam("u".into()))?;
|
||||
let version = get("v").unwrap_or_else(|| "1.16.1".into());
|
||||
let client = get("c").unwrap_or_else(|| "unknown".into());
|
||||
let format = get("f").unwrap_or_else(|| "xml".into());
|
||||
|
||||
// Try token auth first (modern), then legacy password
|
||||
let auth = if let (Some(token), Some(salt)) = (get("t"), get("s")) {
|
||||
AuthMethod::Token { token, salt }
|
||||
} else if let Some(p) = get("p") {
|
||||
if let Some(hex_str) = p.strip_prefix("enc:") {
|
||||
AuthMethod::HexPassword(hex_str.to_string())
|
||||
} else {
|
||||
AuthMethod::Password(p)
|
||||
}
|
||||
} else {
|
||||
return Err(SubsonicAuthError::MissingParam(
|
||||
"authentication required (t+s or p)".into(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
username,
|
||||
auth,
|
||||
version,
|
||||
client,
|
||||
format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify Subsonic authentication against the stored subsonic_password.
|
||||
pub async fn verify_auth(
|
||||
db: &DatabaseConnection,
|
||||
params: &SubsonicParams,
|
||||
) -> Result<User, SubsonicAuthError> {
|
||||
let user = queries::users::find_by_username(db, ¶ms.username)
|
||||
.await
|
||||
.map_err(|_| SubsonicAuthError::AuthFailed)?
|
||||
.ok_or(SubsonicAuthError::AuthFailed)?;
|
||||
|
||||
let subsonic_password = user
|
||||
.subsonic_password
|
||||
.as_deref()
|
||||
.ok_or(SubsonicAuthError::AuthFailed)?;
|
||||
|
||||
match ¶ms.auth {
|
||||
AuthMethod::Token { token, salt } => {
|
||||
// Compute md5(password + salt) and compare
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(subsonic_password.as_bytes());
|
||||
hasher.update(salt.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
let expected = hex::encode(result);
|
||||
if expected != *token {
|
||||
return Err(SubsonicAuthError::AuthFailed);
|
||||
}
|
||||
}
|
||||
AuthMethod::Password(password) => {
|
||||
// Direct plaintext comparison
|
||||
if password != subsonic_password {
|
||||
return Err(SubsonicAuthError::AuthFailed);
|
||||
}
|
||||
}
|
||||
AuthMethod::HexPassword(hex_str) => {
|
||||
// Decode hex to string, compare
|
||||
let decoded = hex::decode(hex_str).map_err(|_| SubsonicAuthError::AuthFailed)?;
|
||||
let password = String::from_utf8(decoded).map_err(|_| SubsonicAuthError::AuthFailed)?;
|
||||
if password != subsonic_password {
|
||||
return Err(SubsonicAuthError::AuthFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
Reference in New Issue
Block a user