Add framework for captchas

This commit is contained in:
Elias Projahn 2021-01-30 20:59:57 +01:00
parent 2066b9c423
commit 59acc460f8
5 changed files with 174 additions and 19 deletions

View file

@ -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"] }

View file

@ -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(())
}

View file

@ -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<String>,
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<DbPool>,
captcha_manager: web::Data<CaptchaManager>,
data: web::Json<UserRegistration>,
) -> Result<HttpResponse, ServerError> {
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

133
src/routes/captcha.rs Normal file
View file

@ -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<Question> = 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<HashMap<String, &'static Question>>,
}
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<Captcha> {
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<bool> {
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<CaptchaManager>) -> Result<HttpResponse, ServerError> {
let manager = manager.into_inner();
let captcha = manager.generate_captcha()?;
Ok(HttpResponse::Ok().json(captcha))
}

View file

@ -1,6 +1,9 @@
pub mod auth;
pub use auth::*;
pub mod captcha;
pub use captcha::*;
pub mod ensembles;
pub use ensembles::*;