// TODO: // - Have a task that cleans up expired logins once in a while (once every day?) use tokio; use warp::{Filter, Reply, Rejection}; use argon2::{ password_hash::{ rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString }, Argon2 }; use serde::{Serialize, Deserialize}; mod constants; use constants::*; mod util; mod db; use db::DB; mod path; use path::Path; macro_rules! mk_bad_request { ($res:expr) => { Ok(Box::new(warp::reply::with_status($res, warp::http::StatusCode::BAD_REQUEST))) } } macro_rules! mk_not_found { ($res:expr) => { Ok(Box::new(warp::reply::with_status($res, warp::http::StatusCode::NOT_FOUND))) } } macro_rules! mk_unauthorized { ($msg:expr) => { Ok(Box::new(warp::reply::with_status($msg, warp::http::StatusCode::UNAUTHORIZED))) } } macro_rules! mk_server_err { () => { Ok(Box::new(warp::reply::with_status("Internal server error", warp::http::StatusCode::INTERNAL_SERVER_ERROR))) } } type Response = Result, Rejection>; async fn handle_ping(db: DB, mtoken: Option) -> Response { if let Some(token) = mtoken { match db::maybe_refresh_token(db, &token).await { Ok(()) => {}, Err(err) => { return mk_bad_request!(err); }, } } return Ok(Box::new("pong")) } #[derive(Deserialize)] struct RegisterReq { username: String, password: String, } async fn handle_register(db: DB, req: RegisterReq) -> Response { let salt = SaltString::generate(&mut OsRng); let hash = match Argon2::default().hash_password(req.password.as_bytes(), &salt) { Ok(hash) => hash.to_string(), Err(_) => return mk_bad_request!("bad request"), }; match db::register_account(db, &req.username, &hash.to_string()).await { Ok(()) => Ok(Box::new("Registered")), Err(err) => mk_bad_request!(err), } } async fn handle_login(db: DB, req: RegisterReq) -> Response { let passhash = match db::get_passhash(db.clone(), &req.username).await { Ok(passhash) => passhash, Err(()) => return mk_not_found!("User not found"), }; let parsed_hash = match PasswordHash::new(&passhash) { Ok(parsed_hash) => parsed_hash, Err(err) => { eprintln!("Could not parse password hash: {err}"); return mk_server_err!(); } }; if let Err(_) = Argon2::default().verify_password(req.password.as_bytes(), &parsed_hash) { return mk_unauthorized!("Incorrect password"); } match db::create_login_token(db, &req.username).await { Ok(token) => Ok(Box::new(token)), Err(()) => { eprintln!("Failed inserting login token for user '{0}'", &req.username); mk_server_err!() } } } async fn handle_logout(db: DB, token: String) -> Response { db::drop_token(db, &token).await; Ok(Box::new("Logged out")) } macro_rules! check_login { ($db:expr, $token:expr) => { match db::check_login($db.clone(), $token).await { Ok(user) => user, Err(()) => return mk_unauthorized!("Not logged in"), } } } #[derive(Deserialize)] struct FileCreateReq { path: String, } #[derive(Serialize)] struct FileCreateRes { id: i64, } async fn handle_file_create(db: DB, token: String, req: FileCreateReq) -> Response { let user = check_login!(db, &token); let path = match Path::split(&req.path) { Some(path) => path, None => return mk_bad_request!("Invalid path"), }; match db::file_create_empty(db.clone(), user, &path).await { Ok(id) => Ok(Box::new(warp::reply::json(&FileCreateRes { id }))), Err(err) => mk_bad_request!(err), } } macro_rules! db_handler1 { ($db:expr, $handler:ident) => { { let db2 = $db.clone(); move |a| $handler(db2.clone(), a) } } } macro_rules! db_handler2 { ($db:expr, $handler:ident) => { { let db2 = $db.clone(); move |a,b| $handler(db2.clone(), a, b) } } } #[tokio::main] async fn main() { let db: DB = db::open().await; println!("Opened database at {DB_FILE_NAME}."); let use_login_token = warp::header::("x-kaasnoot-token"); let use_optional_login_token = warp::header::optional::("x-kaasnoot-token"); let router = (warp::get().and(warp::path!("ping")) .and(use_optional_login_token) .and_then(db_handler1!(db, handle_ping))) .or(warp::post().and(warp::path!("register")) .and(warp::body::json()) .and_then(db_handler1!(db, handle_register))) .or(warp::post().and(warp::path!("login")) .and(warp::body::json()) .and_then(db_handler1!(db, handle_login))) .or(warp::post().and(warp::path!("logout")) .and(use_login_token) .and_then(db_handler1!(db, handle_logout))) .or(warp::put().and(warp::path!("file")) .and(use_login_token) .and(warp::body::json()) .and_then(db_handler2!(db, handle_file_create))); warp::serve(router) .run(([0, 0, 0, 0], 8775)) .await; }