diff --git a/Cargo.lock b/Cargo.lock index 60395bce..05799478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,31 @@ dependencies = [ "zstd", ] +[[package]] +name = "actix-http-test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061d27c2a6fea968fdaca0961ff429d23a4ec878c4f68f5d08626663ade69c80" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "awc", + "bytes", + "futures-core", + "http 0.2.12", + "log", + "serde", + "serde_json", + "serde_urlencoded", + "slab", + "socket2", + "tokio", +] + [[package]] name = "actix-macros" version = "0.2.4" @@ -150,6 +175,7 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ + "actix-macros", "futures-core", "tokio", ] @@ -182,6 +208,48 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-test" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439022b5a7b5dac10798465029a9566e8e0cca7a6014541ed277b695691fac5f" +dependencies = [ + "actix-codec", + "actix-http", + "actix-http-test", + "actix-rt", + "actix-service", + "actix-utils", + "actix-web", + "awc", + "futures-core", + "futures-util", + "log", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "impl-more", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -524,6 +592,39 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "awc" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79049b2461279b886e46f1107efc347ebecc7b88d74d023dda010551a124967b" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "base64 0.22.1", + "bytes", + "cfg-if", + "cookie", + "derive_more 0.99.18", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "itoa", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -3441,6 +3542,11 @@ dependencies = [ name = "tests" version = "0.24.0-beta2" dependencies = [ + "actix-rt", + "actix-test", + "actix-web", + "actix-web-grants", + "actix-web-httpauth", "chrono", "crossbeam-channel", "ffplayout", diff --git a/ffplayout/src/api/routes.rs b/ffplayout/src/api/routes.rs index a0938c88..3c184fc2 100644 --- a/ffplayout/src/api/routes.rs +++ b/ffplayout/src/api/routes.rs @@ -160,26 +160,27 @@ struct ProgramItem { /// } /// ``` #[post("/auth/login/")] -pub async fn login(pool: web::Data>, credentials: web::Json) -> impl Responder { +pub async fn login( + pool: web::Data>, + credentials: web::Json, +) -> Result { let username = credentials.username.clone(); let password = credentials.password.clone(); match handles::select_login(&pool, &username).await { Ok(mut user) => { - let role = handles::select_role(&pool, &user.role_id.unwrap_or_default()) - .await - .unwrap_or(Role::Guest); + let role = handles::select_role(&pool, &user.role_id.unwrap_or_default()).await?; - let pass = user.password.clone(); - let password_clone = password.clone(); + let pass_hash = user.password.clone(); + let cred_password = password.clone(); user.password = "".into(); let verified_password = web::block(move || { - let hash = PasswordHash::new(&pass).unwrap(); - Argon2::default().verify_password(password_clone.as_bytes(), &hash) + let hash = PasswordHash::new(&pass_hash)?; + Argon2::default().verify_password(cred_password.as_bytes(), &hash) }) - .await; + .await?; if verified_password.is_ok() { let claims = Claims::new( @@ -195,31 +196,31 @@ pub async fn login(pool: web::Data>, credentials: web::Json) info!("user {} login, with role: {role}", username); - web::Json(UserObj { + Ok(web::Json(UserObj { message: "login correct!".into(), user: Some(user), }) .customize() - .with_status(StatusCode::OK) + .with_status(StatusCode::OK)) } else { error!("Wrong password for {username}!"); - web::Json(UserObj { + Ok(web::Json(UserObj { message: "Wrong password!".into(), user: None, }) .customize() - .with_status(StatusCode::FORBIDDEN) + .with_status(StatusCode::FORBIDDEN)) } } Err(e) => { error!("Login {username} failed! {e}"); - web::Json(UserObj { + Ok(web::Json(UserObj { message: format!("Login {username} failed!"), user: None, }) .customize() - .with_status(StatusCode::BAD_REQUEST) + .with_status(StatusCode::BAD_REQUEST)) } } } diff --git a/ffplayout/src/db/models.rs b/ffplayout/src/db/models.rs index 14876141..4d730a22 100644 --- a/ffplayout/src/db/models.rs +++ b/ffplayout/src/db/models.rs @@ -54,7 +54,7 @@ pub async fn init_globales(conn: &Pool) { } // #[serde_as] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct User { #[serde(skip_deserializing)] pub id: i32, diff --git a/ffplayout/src/lib.rs b/ffplayout/src/lib.rs index 525e151a..d2bb205a 100644 --- a/ffplayout/src/lib.rs +++ b/ffplayout/src/lib.rs @@ -1,5 +1,8 @@ use std::sync::{Arc, Mutex}; +use actix_web::{dev::ServiceRequest, Error, HttpMessage}; +use actix_web_grants::authorities::AttachAuthorities; +use actix_web_httpauth::extractors::bearer::BearerAuth; use clap::Parser; use lazy_static::lazy_static; use sysinfo::{Disks, Networks, System}; @@ -11,6 +14,8 @@ pub mod player; pub mod sse; pub mod utils; +use api::auth; +use db::models::UserMeta; use utils::advanced_config::AdvancedConfig; use utils::args_parse::Args; @@ -22,3 +27,21 @@ lazy_static! { Arc::new(Mutex::new(Networks::new_with_refreshed_list())); pub static ref SYS: Arc> = Arc::new(Mutex::new(System::new_all())); } + +pub async fn validator( + req: ServiceRequest, + credentials: BearerAuth, +) -> Result { + // We just get permissions from JWT + match auth::decode_jwt(credentials.token()).await { + Ok(claims) => { + req.attach(vec![claims.role]); + + req.extensions_mut() + .insert(UserMeta::new(claims.id, claims.channels)); + + Ok(req) + } + Err(e) => Err((e, req)), + } +} diff --git a/ffplayout/src/main.rs b/ffplayout/src/main.rs index 78cec9e3..1f494ba4 100644 --- a/ffplayout/src/main.rs +++ b/ffplayout/src/main.rs @@ -9,11 +9,9 @@ use std::{ }; use actix_files::Files; -use actix_web::{ - dev::ServiceRequest, middleware::Logger, web, App, Error, HttpMessage, HttpServer, -}; -use actix_web_grants::authorities::AttachAuthorities; -use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; +use actix_web::{middleware::Logger, web, App, HttpServer}; + +use actix_web_httpauth::middleware::HttpAuthentication; #[cfg(all(not(debug_assertions), feature = "embed_frontend"))] use actix_web_static_files::ResourceFiles; @@ -22,11 +20,8 @@ use log::*; use path_clean::PathClean; use ffplayout::{ - api::{auth, routes::*}, - db::{ - db_drop, db_pool, handles, - models::{init_globales, UserMeta}, - }, + api::routes::*, + db::{db_drop, db_pool, handles, models::init_globales}, player::{ controller::{ChannelController, ChannelManager}, utils::{get_date, is_remote, json_validate::validate_playlist, JsonPlaylist}, @@ -38,7 +33,7 @@ use ffplayout::{ logging::{init_logging, MailQueue}, playlist::generate_playlist, }, - ARGS, + validator, ARGS, }; #[cfg(any(debug_assertions, not(feature = "embed_frontend")))] @@ -55,24 +50,6 @@ fn thread_counter() -> usize { (available_threads / 2).max(2) } -async fn validator( - req: ServiceRequest, - credentials: BearerAuth, -) -> Result { - // We just get permissions from JWT - match auth::decode_jwt(credentials.token()).await { - Ok(claims) => { - req.attach(vec![claims.role]); - - req.extensions_mut() - .insert(UserMeta::new(claims.id, claims.channels)); - - Ok(req) - } - Err(e) => Err((e, req)), - } -} - #[actix_web::main] async fn main() -> std::io::Result<()> { let mail_queues = Arc::new(Mutex::new(vec![])); diff --git a/ffplayout/src/utils/channels.rs b/ffplayout/src/utils/channels.rs index 5c7f35c7..696e047f 100644 --- a/ffplayout/src/utils/channels.rs +++ b/ffplayout/src/utils/channels.rs @@ -67,15 +67,15 @@ pub async fn create_channel( channel.preview_url = preview_url(&channel.preview_url, channel.id); if global.shared_storage { - channel.hls_path = Path::new(&channel.hls_path) + channel.hls_path = Path::new(&global.public_root) .join(channel.id.to_string()) .to_string_lossy() .to_string(); - channel.playlist_path = Path::new(&channel.playlist_path) + channel.playlist_path = Path::new(&global.playlist_root) .join(channel.id.to_string()) .to_string_lossy() .to_string(); - channel.storage_path = Path::new(&channel.storage_path) + channel.storage_path = Path::new(&global.storage_root) .join(channel.id.to_string()) .to_string_lossy() .to_string(); diff --git a/frontend b/frontend index fd85411c..0b1e083c 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit fd85411c773f86cd3cef02fdd8c3fd041d9af1a8 +Subproject commit 0b1e083ce5b1818589b899d2fe0cc04f4100df32 diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 5aa87925..3b428929 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -10,6 +10,11 @@ publish = false [dev-dependencies] ffplayout= { path = "../ffplayout" } +actix-web = "4" +actix-web-grants = "4" +actix-web-httpauth = "0.8" +actix-rt = "2.10" +actix-test = "0.1" chrono = "0.4" crossbeam-channel = "0.5" ffprobe = "0.4" @@ -29,6 +34,10 @@ tokio = { version = "1.29", features = ["full"] } toml_edit = {version ="0.22", features = ["serde"]} walkdir = "2" +[[test]] +name = "api_routes" +path = "src/api_routes.rs" + [[test]] name = "lib_utils" path = "src/lib_utils.rs" diff --git a/tests/src/api_routes.rs b/tests/src/api_routes.rs new file mode 100644 index 00000000..59b6697a --- /dev/null +++ b/tests/src/api_routes.rs @@ -0,0 +1,95 @@ +use actix_web::{get, web, App, Error, HttpResponse, Responder}; +// use actix_web_httpauth::extractors::bearer::BearerAuth; + +use serde_json::json; +use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite}; + +use ffplayout::api::routes::login; +use ffplayout::db::{ + handles, + models::{init_globales, User}, +}; +use ffplayout::player::controller::ChannelManager; +use ffplayout::utils::config::PlayoutConfig; +// use ffplayout::validator; + +async fn prepare_config() -> (PlayoutConfig, ChannelManager, Pool) { + let pool = SqlitePoolOptions::new() + .connect("sqlite::memory:") + .await + .unwrap(); + handles::db_migrate(&pool).await.unwrap(); + + sqlx::query( + r#" + UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage"; + UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage"; + "#, + ) + .execute(&pool) + .await + .unwrap(); + + let user = User { + id: 0, + mail: Some("admin@mail.com".to_string()), + username: "admin".to_string(), + password: "admin".to_string(), + role_id: Some(1), + channel_ids: Some(vec![1]), + token: None, + }; + + handles::insert_user(&pool, user.clone()).await.unwrap(); + + let config = PlayoutConfig::new(&pool, 1).await; + let channel = handles::select_channel(&pool, &1).await.unwrap(); + let manager = ChannelManager::new(Some(pool.clone()), channel, config.clone()); + + (config, manager, pool) +} + +#[get("/")] +async fn get_handler() -> Result { + Ok(HttpResponse::Ok()) +} + +#[actix_rt::test] +async fn test_get() { + let srv = actix_test::start(|| App::new().service(get_handler)); + + let req = srv.get("/"); + let res = req.send().await.unwrap(); + + assert!(res.status().is_success()); +} + +#[actix_rt::test] +async fn test_login() { + let (_, _, pool) = prepare_config().await; + + init_globales(&pool).await; + + let srv = actix_test::start(move || { + let db_pool = web::Data::new(pool.clone()); + App::new().app_data(db_pool).service(login) + }); + + let payload = json!({"username": "admin", "password": "admin"}); + + let res = srv.post("/auth/login/").send_json(&payload).await.unwrap(); + + assert!(res.status().is_success()); + + let payload = json!({"username": "admin", "password": "1234"}); + + let res = srv.post("/auth/login/").send_json(&payload).await.unwrap(); + + assert_eq!(res.status().as_u16(), 403); + + let payload = json!({"username": "aaa", "password": "1234"}); + + let res = srv.post("/auth/login/").send_json(&payload).await.unwrap(); + + assert_eq!(res.status().as_u16(), 400); +}