diff --git a/Cargo.lock b/Cargo.lock index 5be73738..1442b7da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,6 +635,7 @@ dependencies = [ "ffprobe", "file-rotate", "jsonrpc-http-server", + "jsonwebtoken", "lettre", "log", "notify", @@ -709,7 +710,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin", + "spin 0.9.3", ] [[package]] @@ -1202,6 +1203,20 @@ dependencies = [ "unicase", ] +[[package]] +name = "jsonwebtoken" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9051c17f81bae79440afa041b3a278e1de71bfb96d32454b477fd4703ccb6f" +dependencies = [ + "base64", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1462,6 +1477,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1638,6 +1664,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +[[package]] +name = "pem" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" +dependencies = [ + "base64", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1837,6 +1872,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1998,6 +2048,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.9", +] + [[package]] name = "simplelog" version = "0.12.0" @@ -2032,6 +2094,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.3" @@ -2424,6 +2492,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" diff --git a/Cargo.toml b/Cargo.toml index 2184aeb3..f9c9d48b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ faccess = "0.2" ffprobe = "0.3" file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } jsonrpc-http-server = "18.0" +jsonwebtoken = "8" lettre = "0.10.0-rc.6" log = "0.4" notify = "4.0" @@ -30,8 +31,11 @@ serde_json = "1.0" serde_yaml = "0.8" shlex = "1.1" simplelog = { version = "^0.12", features = ["paris"] } - -sqlx = { version = "0.5", features = [ "chrono", "runtime-actix-native-tls", "sqlite" ] } +sqlx = { version = "0.5", features = [ + "chrono", + "runtime-actix-native-tls", + "sqlite" +] } time = { version = "0.3", features = ["formatting", "macros"] } walkdir = "2" diff --git a/src/api/handles.rs b/src/api/handles.rs index 3ee3fecc..400b16d4 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -1,6 +1,7 @@ use std::path::Path; use faccess::PathExt; +use rand::{distributions::Alphanumeric, Rng}; use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; @@ -20,7 +21,7 @@ pub fn db_path() -> Result> { } async fn cretea_schema() -> Result { - let pool = db_connection().await?; + let conn = db_connection().await?; let query = "PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS groups ( @@ -35,6 +36,7 @@ async fn cretea_schema() -> Result { preview_url TEXT NOT NULL, settings_path TEXT NOT NULL, extra_extensions TEXT NOT NULL, + secret TEXT NOT NULL, UNIQUE(channel_name) ); CREATE TABLE IF NOT EXISTS user @@ -48,8 +50,8 @@ async fn cretea_schema() -> Result { FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE SET NULL ON DELETE SET NULL, UNIQUE(email, username) );"; - let result = sqlx::query(query).execute(&pool).await; - pool.close().await; + let result = sqlx::query(query).execute(&conn).await; + conn.close().await; result } @@ -64,14 +66,19 @@ pub async fn db_init() -> Result<&'static str, Box> { Err(e) => panic!("{e}"), } } + let secret: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(80) + .map(char::from) + .collect(); + let instances = db_connection().await?; let query = "INSERT INTO groups(name) VALUES('admin'), ('user'); - INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions) + INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions, secret) VALUES('Channel 1', 'http://localhost/live/preview.m3u8', - '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');"; - sqlx::query(query).execute(&instances).await?; - + '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', $1);"; + sqlx::query(query).bind(secret).execute(&instances).await?; instances.close().await; Ok("Database initialized!") @@ -79,20 +86,19 @@ pub async fn db_init() -> Result<&'static str, Box> { pub async fn db_connection() -> Result, sqlx::Error> { let db_path = db_path().unwrap(); + let conn = SqlitePool::connect(&db_path).await?; - let pool = SqlitePool::connect(&db_path).await?; - - Ok(pool) + Ok(conn) } pub async fn add_user( - instances: &SqlitePool, mail: &str, user: &str, pass: &str, salt: &str, group: &i64, ) -> Result { + let conn = db_connection().await?; let query = "INSERT INTO user (email, username, password, salt, group_id) VALUES($1, $2, $3, $4, $5)"; let result = sqlx::query(query) @@ -101,32 +107,18 @@ pub async fn add_user( .bind(pass) .bind(salt) .bind(group) - .execute(instances) + .execute(&conn) .await?; + conn.close().await; Ok(result) } -pub async fn get_users( - instances: &SqlitePool, - index: Option, -) -> Result, sqlx::Error> { - let query = match index { - Some(i) => format!("SELECT id, email, username FROM user WHERE id = {i}"), - None => "SELECT id, email, username FROM user".to_string(), - }; - - let result: Vec = sqlx::query_as(&query).fetch_all(instances).await?; - instances.close().await; - - Ok(result) -} - -pub async fn get_login(user: &str) -> Result, sqlx::Error> { - let pool = db_connection().await?; - let query = "SELECT id, username, password, salt FROM user WHERE username = $1"; - let result: Vec = sqlx::query_as(query).bind(user).fetch_all(&pool).await?; - pool.close().await; +pub async fn get_login(user: &str) -> Result { + let conn = db_connection().await?; + let query = "SELECT id, email, username, password, salt FROM user WHERE username = $1"; + let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?; + conn.close().await; Ok(result) } diff --git a/src/api/models.rs b/src/api/models.rs index 026baa2e..3bf15f1d 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -2,23 +2,30 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct User { + #[sqlx(default)] pub id: Option, #[sqlx(default)] pub email: Option, pub username: String, #[sqlx(default)] + #[serde(skip_serializing)] pub password: String, #[sqlx(default)] + #[serde(skip_serializing)] pub salt: Option, #[sqlx(default)] + #[serde(skip_serializing)] pub group_id: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct Settings { pub id: i64, pub channel_name: String, pub preview_url: String, pub settings_path: String, pub extra_extensions: String, + #[sqlx(default)] + #[serde(skip_serializing)] + pub secret: String, } diff --git a/src/api/routes.rs b/src/api/routes.rs index 993d85c4..1b3a0c45 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,81 +1,59 @@ -use crate::api::{ - handles::{db_connection, get_login, get_users}, - models::User, -}; use actix_web::{get, post, web, Responder}; use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; +use serde::Serialize; use simplelog::*; +use crate::api::{handles::get_login, models::User}; + #[get("/hello/{name}")] async fn greet(name: web::Path) -> impl Responder { format!("Hello {name}!") } -// /// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123", "email":"user@example.org" }' http://127.0.0.1:8080/api/user/ -// #[post("/api/user/")] -// pub async fn user(user: web::Json) -> impl Responder { -// let params = Sha512Params::new(10_000).expect("RandomError!"); -// let hashed_password = sha512_simple(&user.password, ¶ms).expect("Should not fail"); - -// // // Verifying a stored password -// // assert!(sha512_check("Not so secure password", &hashed_password).is_ok()); - -// if let Ok(pool) = db_connection().await { -// if let Err(e) = add_user( -// &pool, -// &user.email.clone().unwrap(), -// &user.username, -// &hashed_password, -// &user.group_id.unwrap(), -// ) -// .await -// { -// pool.close().await; -// return e.to_string(); -// }; - -// pool.close().await; -// } - -// format!("User {} added", user.username) -// } - -/// curl -X GET http://127.0.0.1:8080/api/user/1 -#[get("/api/user/{id}")] -pub async fn get_user(id: web::Path) -> impl Responder { - if let Ok(pool) = db_connection().await { - match get_users(&pool, Some(*id)).await { - Ok(r) => { - return web::Json(r); - } - Err(_) => { - return web::Json(vec![]); - } - }; - } - - web::Json(vec![]) +#[derive(Serialize)] +struct ResponseObj { + message: String, + status: i32, + data: Option, } + /// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' http://127.0.0.1:8080/auth/login/ #[post("/auth/login/")] pub async fn login(credentials: web::Json) -> impl Responder { - if let Ok(u) = get_login(&credentials.username).await { - if u.is_empty() { - return "User not found"; - } - let pass = u[0].password.clone(); + match get_login(&credentials.username).await { + Ok(mut user) => { + let pass = user.password.clone(); + user.password = "".into(); + user.salt = None; - if let Ok(hash) = PasswordHash::new(&pass) { + let hash = PasswordHash::new(&pass).unwrap(); if Argon2::default() .verify_password(credentials.password.as_bytes(), &hash) .is_ok() { info!("user {} login", credentials.username); - return "login correct!"; - } - }; - }; - error!("Login {} failed!", credentials.username); - "Login failed!" + web::Json(ResponseObj { + message: "login correct!".into(), + status: 200, + data: Some(user), + }) + } else { + error!("Wrong password for {}!", credentials.username); + web::Json(ResponseObj { + message: "Wrong password!".into(), + status: 401, + data: None, + }) + } + } + Err(e) => { + error!("Login {} failed! {e}", credentials.username); + return web::Json(ResponseObj { + message: format!("Login {} failed!", credentials.username), + status: 404, + data: None, + }); + } + } } diff --git a/src/api/utils.rs b/src/api/utils.rs index 1ed94a47..bc5b522d 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -6,7 +6,7 @@ use simplelog::*; use crate::api::{ args_parse::Args, - handles::{add_user, db_connection, db_init}, + handles::{add_user, db_init}, }; pub async fn run_args(args: Args) -> Result<(), i32> { @@ -42,33 +42,22 @@ pub async fn run_args(args: Args) -> Result<(), i32> { } }; - match db_connection().await { - Ok(pool) => { - if let Err(e) = add_user( - &pool, - &args.email.unwrap(), - &username, - &password_hash.to_string(), - &salt.to_string(), - &1, - ) - .await - { - pool.close().await; - error!("{e}"); - return Err(1); - }; + if let Err(e) = add_user( + &args.email.unwrap(), + &username, + &password_hash.to_string(), + &salt.to_string(), + &1, + ) + .await + { + error!("{e}"); + return Err(1); + }; - pool.close().await; - info!("Create admin user \"{username}\" done..."); + info!("Create admin user \"{username}\" done..."); - return Err(0); - } - Err(e) => { - error!("Add admin user failed! Did you init the database?"); - panic!("{e}") - } - } + return Err(0); } Ok(()) diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index cc12d75d..7bb75d55 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -5,11 +5,7 @@ use clap::Parser; use simplelog::*; use ffplayout_engine::{ - api::{ - args_parse::Args, - routes::{get_user, login}, - utils::run_args, - }, + api::{args_parse::Args, routes::login, utils::run_args}, utils::{init_logging, GlobalConfig}, }; @@ -35,7 +31,7 @@ async fn main() -> std::io::Result<()> { let port = ip_port[1].parse::().unwrap(); info!("running ffplayout API, listen on {conn}"); - HttpServer::new(|| App::new().service(get_user).service(login)) + HttpServer::new(|| App::new().service(login)) .bind((addr, port))? .run() .await