diff --git a/Cargo.lock b/Cargo.lock index fe1252f..89ddaec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -62,15 +71,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" @@ -80,9 +89,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -131,9 +140,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "2163a0e204a148662b6b6816d4b5d5668a5f2f8df498ccbd5cd0e864e78fecba" dependencies = [ "powerfmt", ] @@ -245,35 +254,35 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", - "pin-utils", + "slab", ] [[package]] @@ -386,9 +395,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" dependencies = [ "once_cell", "wasm-bindgen", @@ -402,9 +411,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libsqlite3-sys" @@ -431,12 +440,27 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + [[package]] name = "memchr" version = "2.8.0" @@ -572,6 +596,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -700,6 +741,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -723,9 +770,11 @@ dependencies = [ "axum", "diesel", "dotenvy", + "md5", "serde", "serde_json", "tokio", + "tower-http", "tracing-subscriber", ] @@ -749,9 +798,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -868,6 +917,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -918,19 +983,23 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "valuable" @@ -952,9 +1021,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" dependencies = [ "cfg-if", "once_cell", @@ -965,9 +1034,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -975,9 +1044,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" dependencies = [ "bumpalo", "proc-macro2", @@ -988,9 +1057,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" dependencies = [ "unicode-ident", ] @@ -1086,6 +1155,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5f67220..c8b223d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,6 @@ axum = "0.8.8" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" tokio = { version = "1.49.0", features = ["full"] } -tracing-subscriber = "0.3.22" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +md5 = "0.8.0" +tower-http = {version = "0.6.8", features = ["trace"]} diff --git a/src/main.rs b/src/main.rs index 9afce55..696d6be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,11 @@ mod routes; mod schema; mod state; mod types; -use std::env; - use axum::Router; use dotenvy::dotenv; +use std::env; +use tower_http::trace::TraceLayer; +use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; use crate::routes::system_router::system_routers; use crate::routes::user_management_router::user_management_routs; @@ -18,9 +19,16 @@ async fn main() { let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let state = AppState::new(&database_url) .unwrap_or_else(|_| panic!("Error creating pool for {}", database_url)); - tracing_subscriber::fmt::init(); + tracing_subscriber::registry() + .with( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "tower_http=debug,axum=debug".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); let app = Router::new() .nest("/rest", system_routers().merge(user_management_routs())) + .layer(TraceLayer::new_for_http()) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3311").await.unwrap(); diff --git a/src/routes/system_router.rs b/src/routes/system_router.rs index 461d765..f9d51a0 100644 --- a/src/routes/system_router.rs +++ b/src/routes/system_router.rs @@ -1,17 +1,51 @@ -use crate::state::AppState; +use crate::schema::users; use crate::types::types::OpenSubSonicResponses; +use crate::{state::AppState, types::types::OpenSubsonicAuth}; +use axum::extract::State; use axum::{Json, Router, http::StatusCode, routing::get}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; pub fn system_routers() -> Router { - Router::new().route( - "/getOpenSubsonicExtensions", - get(get_opensubsonic_extentions).post(get_opensubsonic_extentions), + Router::new() + .route( + "/getOpenSubsonicExtensions.view", + get(get_opensubsonic_extensions).post(get_opensubsonic_extensions), + ) + .route("/ping.view", get(ping).post(ping)) + .route("/license.view", get(license).post(license)) +} + +async fn license( + State(db_con): State, + user: OpenSubsonicAuth, +) -> (StatusCode, Json) { + let conn = &mut db_con.pool.get().unwrap(); + let result = users::table + .select(users::email) + .filter(users::username.eq(user.username)) + .first::(conn); + match result { + Ok(e) => ( + StatusCode::OK, + Json(OpenSubSonicResponses::license_response(Some(e))), + ), + Err(_) => ( + StatusCode::OK, + Json(OpenSubSonicResponses::license_response(None)), + ), + } +} + +async fn ping(_user: OpenSubsonicAuth) -> (StatusCode, Json) { + ( + StatusCode::OK, + Json(OpenSubSonicResponses::open_subsonic_response()), ) } -async fn get_opensubsonic_extentions() -> (StatusCode, Json) { +async fn get_opensubsonic_extensions() -> (StatusCode, Json) { ( StatusCode::OK, - Json(OpenSubSonicResponses::open_subsonic_extentions()), + Json(OpenSubSonicResponses::open_subsonic_extensions()), ) } diff --git a/src/routes/user_management_router.rs b/src/routes/user_management_router.rs index 8018576..620fab4 100644 --- a/src/routes/user_management_router.rs +++ b/src/routes/user_management_router.rs @@ -13,8 +13,7 @@ use axum::{ use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, insert_into}; pub fn user_management_routs() -> Router { - let userm_routs = Router::new().route("/createUser.view", get(create_user).post(create_user)); - return userm_routs; + Router::new().route("/createUser.view", get(create_user).post(create_user)) } fn check_if_user_exist(user_name: &String, db_con: &AppState) -> bool { @@ -38,7 +37,9 @@ async fn create_user( } else { return ( StatusCode::BAD_REQUEST, - Json(OpenSubSonicResponses::open_subsonic_response()), + Json(OpenSubSonicResponses::open_subsonic_response_error( + OpenSubsonicErrorCode::MissingParameter, + )), ); }; if check_if_user_exist(¶ms.username, &db_con) { diff --git a/src/types/system.rs b/src/types/system.rs index e949ac0..7f2b09d 100644 --- a/src/types/system.rs +++ b/src/types/system.rs @@ -10,3 +10,14 @@ pub struct OpenSubSonicExtension { pub struct OpenSubSonicExtensions { pub openSubsonicExtensions: Vec, } + +#[derive(Serialize, Deserialize)] +pub struct LicenseBody { + pub valid: bool, + pub email: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct LicenseResponse { + pub license: LicenseBody, +} diff --git a/src/types/types.rs b/src/types/types.rs index 3acfe78..c2780a7 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -1,6 +1,17 @@ +use axum::{ + Json, RequestPartsExt, + extract::{FromRequestParts, Query, RawForm}, + http::{StatusCode, request::Parts}, + middleware::{from_extractor, from_extractor_with_state}, +}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use serde::{Deserialize, Serialize}; -use crate::types::system::{OpenSubSonicExtension, OpenSubSonicExtensions}; +use crate::{ + schema::users, + state::AppState, + types::system::{LicenseBody, LicenseResponse, OpenSubSonicExtension, OpenSubSonicExtensions}, +}; #[derive(Debug, Clone, Copy)] pub enum OpenSubsonicErrorCode { @@ -47,6 +58,118 @@ impl OpenSubsonicErrorCode { } } +#[derive(Deserialize)] +pub struct OpenSubsonicBaseQuery { + pub u: String, + pub p: Option, + pub t: Option, + pub s: Option, + pub apiKey: Option, + pub v: String, + pub c: String, + pub f: String, +} + +#[derive(Deserialize)] +pub struct OpenSubsonicAuth { + pub username: String, + pub client: String, + pub version: String, + pub format: String, +} + +impl FromRequestParts for OpenSubsonicAuth { + type Rejection = (StatusCode, Json); + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { + // ---- 1. Parse query safely ---- + let Query(query) = Query::::from_request_parts(parts, state) + .await + .map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(OpenSubSonicResponses::open_subsonic_response_error( + OpenSubsonicErrorCode::MissingParameter, + )), + ) + })?; + + // ---- 2. Require token + salt ---- + let (token, salt) = match (query.t.as_ref(), query.s.as_ref()) { + (Some(t), Some(s)) => (t, s), + _ => { + return Err(( + StatusCode::BAD_REQUEST, + Json(OpenSubSonicResponses::open_subsonic_response_error( + OpenSubsonicErrorCode::AuthMechanismNotSupported, + )), + )); + } + }; + + // ---- 3. DB lookup (blocking -> spawn_blocking) ---- + let username = query.u.clone(); + let pool = state.pool.clone(); + + let password = tokio::task::spawn_blocking( + move || -> Result)> { + let mut conn = pool.get().map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(OpenSubSonicResponses::open_subsonic_response_error( + OpenSubsonicErrorCode::Generic, + )), + ) + })?; + + users::table + .select(users::password) + .filter(users::username.eq(username)) + .first::(&mut conn) + .map_err(|_| { + ( + StatusCode::UNAUTHORIZED, + Json(OpenSubSonicResponses::open_subsonic_response_error( + OpenSubsonicErrorCode::WrongUsernameOrPassword, + )), + ) + }) + }, + ) + .await + .map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(OpenSubSonicResponses::open_subsonic_response_error( + OpenSubsonicErrorCode::Generic, + )), + ) + })??; + + // ---- 4. verify token ---- + let expected_token = format!("{:x}", md5::compute(format!("{}{}", password, salt))); + + if &expected_token != token { + return Err(( + StatusCode::UNAUTHORIZED, + Json(OpenSubSonicResponses::open_subsonic_response_error( + OpenSubsonicErrorCode::NotAuthorized, + )), + )); + } + + Ok(OpenSubsonicAuth { + username: query.u, + client: query.c, + version: query.v, + format: query.f, + }) + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BaseResponse { @@ -62,11 +185,17 @@ pub struct BaseResponse { #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ErrorResponse { +struct ErrorResponseBody { code: u8, message: String, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ErrorResponse { + error: ErrorResponseBody, +} + #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum OpenSubSonicResponses { @@ -83,6 +212,10 @@ pub enum OpenSubSonicResponses { subsonic_response: BaseResponse, // openSubSonicExtensions: Vec, }, + License { + #[serde(rename = "subsonic-response")] + subsonic_response: BaseResponse, + }, } impl OpenSubSonicResponses { @@ -97,6 +230,27 @@ impl OpenSubSonicResponses { } } + pub fn license_response(email: Option) -> Self { + match email { + Some(_) => Self::License { + subsonic_response: Self::base_response(Some(LicenseResponse { + license: LicenseBody { + valid: true, + email: email, + }, + })), + }, + None => Self::License { + subsonic_response: Self::base_response(Some(LicenseResponse { + license: LicenseBody { + valid: true, + email: None, + }, + })), + }, + } + } + pub fn open_subsonic_response_error(error_code: OpenSubsonicErrorCode) -> Self { Self::OpenSubSonicResponseError { subsonic_response: BaseResponse { @@ -106,8 +260,10 @@ impl OpenSubSonicResponses { server_version: "1.16.1".to_string(), open_subsonic: true, data: Some(ErrorResponse { - code: error_code as u8, - message: error_code.description().to_string(), + error: ErrorResponseBody { + code: error_code as u8, + message: error_code.description().to_string(), + }, }), }, } @@ -119,7 +275,7 @@ impl OpenSubSonicResponses { } } - pub fn open_subsonic_extentions() -> Self { + pub fn open_subsonic_extensions() -> Self { let mut extension: Vec = Vec::new(); extension.push(OpenSubSonicExtension { name: "apiKeyAuthentication".to_string(),