From 59acc460f85acfd19139af4a5adb2d34d06ea6bd Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 30 Jan 2021 20:59:57 +0100 Subject: [PATCH] Add framework for captchas --- Cargo.toml | 2 + src/main.rs | 17 ++++-- src/routes/auth.rs | 38 +++++++----- src/routes/captcha.rs | 133 ++++++++++++++++++++++++++++++++++++++++++ src/routes/mod.rs | 3 + 5 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 src/routes/captcha.rs diff --git a/Cargo.toml b/Cargo.toml index 393f2f0..4588de9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,10 @@ diesel_migrations = "1.4.0" dotenv = "0.15.0" env_logger = "0.8.1" jsonwebtoken = "7.2.0" +lazy_static = "1.4.0" r2d2 = "0.8.9" rand = "0.7.3" serde = { version = "1.0.117", features = ["derive"] } serde_json = "1.0.59" sodiumoxide = "0.2.6" +uuid = { version = "0.8", features = ["v4"] } diff --git a/src/main.rs b/src/main.rs index 8fb060c..48e1d56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,8 @@ extern crate diesel; #[macro_use] extern crate diesel_migrations; -use actix_web::{App, HttpServer}; +use actix_web::{web, App, HttpServer}; +use anyhow::Result; mod database; mod error; @@ -15,18 +16,22 @@ mod routes; use routes::*; #[actix_web::main] -async fn main() -> std::io::Result<()> { +async fn main() -> Result<()> { dotenv::dotenv().ok(); env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); sodiumoxide::init().expect("Failed to init crypto library!"); - let db_pool = database::connect().expect("Failed to create database interface!"); + + let db_pool = web::Data::new(database::connect()?); + let captcha_manager = web::Data::new(CaptchaManager::new()); let server = HttpServer::new(move || { App::new() - .data(db_pool.clone()) + .app_data(db_pool.clone()) + .app_data(captcha_manager.clone()) .wrap(actix_web::middleware::Logger::new( "%t: %r -> %s; %b B; %D ms", )) + .service(get_captcha) .service(register_user) .service(login_user) .service(put_user) @@ -58,5 +63,7 @@ async fn main() -> std::io::Result<()> { .service(delete_medium) }); - server.bind("127.0.0.1:8087")?.run().await + server.bind("127.0.0.1:8087")?.run().await?; + + Ok(()) } diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 7bc0f6c..b3a4fd6 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,3 +1,4 @@ +use super::CaptchaManager; use crate::database; use crate::database::{DbConn, DbPool, User, UserInsertion}; use crate::error::ServerError; @@ -14,6 +15,8 @@ pub struct UserRegistration { pub username: String, pub password: String, pub email: Option, + pub captcha_id: String, + pub answer: String, } /// Request body data for user login. @@ -52,24 +55,31 @@ struct Claims { #[post("/users")] pub async fn register_user( db: web::Data, + captcha_manager: web::Data, data: web::Json, ) -> Result { - web::block(move || { - let conn = db.into_inner().get().or(Err(ServerError::Internal))?; + let captcha_manager = captcha_manager.into_inner(); - database::insert_user( - &conn, - &data.username, - &UserInsertion { - password_hash: hash_password(&data.password).or(Err(ServerError::Internal))?, - email: data.email.clone(), - }, - ) - .or(Err(ServerError::Internal)) - }) - .await?; + if captcha_manager.check_captcha(&data.captcha_id, &data.answer)? { + web::block(move || { + let conn = db.into_inner().get().or(Err(ServerError::Internal))?; - Ok(HttpResponse::Ok().finish()) + database::insert_user( + &conn, + &data.username, + &UserInsertion { + password_hash: hash_password(&data.password).or(Err(ServerError::Internal))?, + email: data.email.clone(), + }, + ) + .or(Err(ServerError::Internal)) + }) + .await?; + + Ok(HttpResponse::Ok().finish()) + } else { + Err(ServerError::Forbidden) + } } /// Update an existing user. This doesn't use a JWT for authentication but requires the client to diff --git a/src/routes/captcha.rs b/src/routes/captcha.rs new file mode 100644 index 0000000..1f3172e --- /dev/null +++ b/src/routes/captcha.rs @@ -0,0 +1,133 @@ +use crate::error::ServerError; +use actix_web::{get, web, HttpResponse}; +use anyhow::{anyhow, Result}; +use lazy_static::lazy_static; +use rand::seq::SliceRandom; +use serde::Serialize; +use std::collections::HashMap; +use std::sync::Mutex; + +// TODO/INFO: These hardcoded questions are a placeholder for a future mechanism to autogenerate +// questions from the database. This will require a easily accissible web interface for Musicus. +// There may also be another, better solution. However, the current framework of question-answer +// pairs with randomly generated identifiers will most likely stay in place. + +/// A question to identify users as human. +#[derive(Clone, Debug)] +struct Question { + /// The question that will be sent to the client. + pub question: &'static str, + + /// The answer that the client has to provide. + pub answer: &'static str, +} + +lazy_static! { + /// All available captcha questions. + static ref QUESTIONS: Vec = vec![ + Question { + question: "In welchem Jahr wurde Johannes Brahms geboren?", + answer: "1833", + }, + Question { + question: "In welchem Jahr ist Johannes Brahms gestorben?", + answer: "1897", + }, + Question { + question: "In welchem Jahr wurde Ludwig van Beethoven geboren?", + answer: "1770", + }, + Question { + question: "In welchem Jahr ist Ludwig van Beethoven gestorben?", + answer: "1827", + }, + Question { + question: "In welchem Jahr wurde Claude Debussy geboren?", + answer: "1862", + }, + Question { + question: "In welchem Jahr ist Claude Debussy gestorben?", + answer: "1918", + }, + Question { + question: "In welchem Jahr wurde Sergei Rachmaninow geboren?", + answer: "1873", + }, + Question { + question: "In welchem Jahr ist Sergei Rachmaninow gestorben?", + answer: "1943", + }, + ]; +} + +/// Response body data for captcha requests. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Captcha { + pub id: String, + pub question: String, +} + +/// A generator and manager for captchas. This will keep track of the captchas that where created +/// for clients and delete them, once the client has tried to solve them. +pub struct CaptchaManager { + captchas: Mutex>, +} + +impl CaptchaManager { + /// Create a new captcha manager. + pub fn new() -> Self { + Self { + captchas: Mutex::new(HashMap::new()), + } + } + + /// Create a new captcha with a random ID. + pub fn generate_captcha(&self) -> Result { + let mut buffer = uuid::Uuid::encode_buffer(); + let id = uuid::Uuid::new_v4().to_simple().encode_lower(&mut buffer).to_owned(); + + let question = QUESTIONS.choose(&mut rand::thread_rng()) + .ok_or_else(|| anyhow!("Failed to get random question!"))?; + + let captchas = &mut self.captchas.lock() + .or_else(|_| Err(anyhow!("Failed to aquire lock!")))?; + + captchas.insert(id.clone(), question); + + let captcha = Captcha { + id, + question: question.question.to_owned(), + }; + + Ok(captcha) + } + + /// Check whether the provided answer is correct and delete the captcha eitherway. + pub fn check_captcha(&self, id: &str, answer: &str) -> Result { + let captchas = &mut self.captchas.lock() + .or_else(|_| Err(anyhow!("Failed to aquire lock!")))?; + + let question = captchas.get(id); + + let result = if let Some(question) = question { + let result = answer == question.answer; + captchas.remove(id); + result + } else { + false + }; + + Ok(result) + } +} + +/// Request a new captcha. +#[get("/captcha")] +pub async fn get_captcha(manager: web::Data) -> Result { + let manager = manager.into_inner(); + let captcha = manager.generate_captcha()?; + + Ok(HttpResponse::Ok().json(captcha)) +} + diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 45ff9a4..02f66a6 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,6 +1,9 @@ pub mod auth; pub use auth::*; +pub mod captcha; +pub use captcha::*; + pub mod ensembles; pub use ensembles::*;