Refactor captcha solutions and AppState

This commit is contained in:
Mo 2023-02-23 16:55:27 +01:00
parent db0ad404a1
commit a0c2edba56
5 changed files with 104 additions and 94 deletions

View file

@ -1,67 +1,56 @@
use std::{ops::Deref, sync::Mutex}; /// Stores captcha solutions in a wrapping way.
/// If the capacity is exeeded, old values are overwritten.
///
/// The wrapping prevents attacks that keep filling the memory with solutions.
/// The maximum number of solutions before wrapping is `u16::MAX as usize + 1` which is `65536`.
///
/// If you have more than 65536 persons trying to use the contact form at the same time, contact me!
///
/// Anyway, if the solution is overwritten, you get a new id in the contact form and can just submit
/// a new captcha answer without loosing the content of the other fields.
pub struct CaptchaSolutions { pub struct CaptchaSolutions {
/// The id of the last value stored.
last_id: u16, last_id: u16,
/// The vector of solutions.
solutions: Vec<Option<String>>, solutions: Vec<Option<String>>,
} }
impl CaptchaSolutions { impl Default for CaptchaSolutions {
fn get(&self, id: u16) -> &Option<String> { /// Default with `None` values.
&self.solutions[id as usize]
}
fn set(&mut self, id: u16, sol: Option<String>) {
self.solutions[id as usize] = sol;
}
#[must_use = "The new id has to be stored for future verification of an answer!"]
fn push(&mut self, sol: String) -> u16 {
self.last_id = self.last_id.wrapping_add(1);
self.set(self.last_id, Some(sol));
self.last_id
}
}
pub struct SharedCaptchaSolutions {
inner: Mutex<CaptchaSolutions>,
}
impl Default for SharedCaptchaSolutions {
fn default() -> Self { fn default() -> Self {
// Wrap the index around u16.
let max_size = u16::MAX as usize + 1; let max_size = u16::MAX as usize + 1;
Self { Self {
inner: Mutex::new(CaptchaSolutions {
last_id: 0, last_id: 0,
solutions: vec![None; max_size], solutions: vec![None; max_size],
}),
} }
} }
} }
impl Deref for SharedCaptchaSolutions { impl CaptchaSolutions {
type Target = Mutex<CaptchaSolutions>; /// Pushes a solution and returns its id.
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl SharedCaptchaSolutions {
#[must_use = "The new id has to be stored for future verification of an answer!"] #[must_use = "The new id has to be stored for future verification of an answer!"]
pub fn store_solution(&self, sol: String) -> u16 { pub fn push(&mut self, sol: String) -> u16 {
let mut sols = self.lock().unwrap(); self.last_id = self.last_id.wrapping_add(1);
// Safety: Inbounds because of u16 and wrapping_add.
sols.push(sol) unsafe {
*self.solutions.get_unchecked_mut(self.last_id as usize) = Some(sol);
} }
pub fn check_answer(&self, id: u16, answer: &str) -> bool { self.last_id
let mut sols = self.lock().unwrap(); }
if let Some(sol) = sols.get(id) { /// Checks an answer and returns if it matches the solution with the given id.
if sol == answer.trim() { ///
sols.set(id, None); /// The solution is removed in the case of a match to prevent using a true answer more than once.
pub fn check_answer(&mut self, id: u16, answer: &str) -> bool {
// Safety: Inbounds because of u16.
let sol_ref = unsafe { self.solutions.get_unchecked_mut(id as usize) };
if let Some(sol) = sol_ref {
if sol == answer {
*sol_ref = None;
return true; return true;
} }
} }

View file

@ -5,7 +5,7 @@ use lettre::{
Message, SmtpTransport, Transport, Message, SmtpTransport, Transport,
}; };
use crate::config; use crate::config::Config;
pub struct Mailer { pub struct Mailer {
mailer: SmtpTransport, mailer: SmtpTransport,
@ -13,7 +13,7 @@ pub struct Mailer {
} }
impl Mailer { impl Mailer {
pub fn new(config: &mut config::Config) -> Result<Self> { pub fn new(config: &Config) -> Result<Self> {
let creds = Credentials::new( let creds = Credentials::new(
config.email_server.email.clone(), config.email_server.email.clone(),
config.email_server.password.clone(), config.email_server.password.clone(),

View file

@ -14,35 +14,29 @@ use axum::{
routing::{get, get_service, Router}, routing::{get, get_service, Router},
Server, Server,
}; };
use std::{future::ready, net::SocketAddr, process, sync::Arc}; use std::{future::ready, net::SocketAddr, process};
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::info; use tracing::info;
async fn init() -> Result<()> { use crate::states::AppState;
let mut config = config::Config::new()?;
let path_prefix = config.path_prefix.clone();
let mailer = Arc::new(mailer::Mailer::new(&mut config)?);
let socket_address = config async fn init() -> Result<()> {
let app_state = AppState::build()?;
let path_prefix = app_state.config.path_prefix.clone();
let socket_address = app_state
.config
.socket_address .socket_address
.parse::<SocketAddr>() .parse::<SocketAddr>()
.context("Failed to parse the socket address: {e:?}")?; .context("Failed to parse the socket address: {e:?}")?;
logging::init_logger( logging::init_logger(
&config.log_file, &app_state.config.log_file,
config.utc_offset.hours, app_state.config.utc_offset.hours,
config.utc_offset.minutes, app_state.config.utc_offset.minutes,
)?; )?;
let config = Arc::new(config);
let captcha_solutions = Arc::new(captcha_solutions::SharedCaptchaSolutions::default());
let app_state = states::AppState {
config,
mailer,
captcha_solutions,
};
let static_service = let static_service =
get_service(ServeDir::new("static")).handle_error(|_| ready(StatusCode::NOT_FOUND)); get_service(ServeDir::new("static")).handle_error(|_| ready(StatusCode::NOT_FOUND));
@ -61,7 +55,7 @@ async fn init() -> Result<()> {
Ok(()) Ok(())
} }
/// Single thread /// Single thread.
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
if let Err(e) = init().await { if let Err(e) = init().await {

View file

@ -1,15 +1,20 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use askama_axum::IntoResponse; use askama_axum::IntoResponse;
use axum::extract::{Form, State}; use axum::{
use axum::response::Response; extract::{Form, State},
use std::sync::Arc; response::Response,
};
use std::sync::{Arc, Mutex};
use tracing::{error, info}; use tracing::{error, info};
use crate::{captcha_solutions, config, errors, forms, mailer, templates}; use crate::{
captcha_solutions::CaptchaSolutions, config::Config, errors::AppError, forms::ContactForm,
mailer::Mailer, templates,
};
pub struct IndexParams<'a> { pub struct IndexParams<'a> {
config: Arc<config::Config>, config: Arc<Config>,
captcha_solutions: Arc<captcha_solutions::SharedCaptchaSolutions>, captcha_solutions: Arc<Mutex<CaptchaSolutions>>,
was_validated: bool, was_validated: bool,
name: Option<String>, name: Option<String>,
email: Option<String>, email: Option<String>,
@ -19,9 +24,9 @@ pub struct IndexParams<'a> {
} }
pub async fn index( pub async fn index(
State(config): State<Arc<config::Config>>, State(config): State<Arc<Config>>,
State(captcha_solutions): State<Arc<captcha_solutions::SharedCaptchaSolutions>>, State(captcha_solutions): State<Arc<Mutex<CaptchaSolutions>>>,
) -> Result<Response, errors::AppError> { ) -> Result<Response, AppError> {
info!("Visited get(index)"); info!("Visited get(index)");
render_contact_form(IndexParams { render_contact_form(IndexParams {
@ -37,13 +42,13 @@ pub async fn index(
.await .await
} }
pub async fn render_contact_form(params: IndexParams<'_>) -> Result<Response, errors::AppError> { pub async fn render_contact_form(params: IndexParams<'_>) -> Result<Response, AppError> {
let captcha = captcha::by_name(captcha::Difficulty::Easy, captcha::CaptchaName::Lucy); let captcha = captcha::by_name(captcha::Difficulty::Easy, captcha::CaptchaName::Lucy);
let captcha_base64 = captcha.as_base64().context("Failed to create a captcha!")?; let captcha_base64 = captcha.as_base64().context("Failed to create a captcha!")?;
let solution = captcha.chars_as_string(); let solution = captcha.chars_as_string();
let id = params.captcha_solutions.store_solution(solution); let id = params.captcha_solutions.lock().unwrap().push(solution);
let template = templates::ContactForm { let template = templates::ContactForm {
base: templates::Base { base: templates::Base {
@ -65,11 +70,11 @@ pub async fn render_contact_form(params: IndexParams<'_>) -> Result<Response, er
} }
async fn failed_submission( async fn failed_submission(
config: Arc<config::Config>, config: Arc<Config>,
captcha_solutions: Arc<captcha_solutions::SharedCaptchaSolutions>, captcha_solutions: Arc<Mutex<CaptchaSolutions>>,
error_message: &str, error_message: &str,
form: forms::ContactForm, form: ContactForm,
) -> Result<Response, errors::AppError> { ) -> Result<Response, AppError> {
let params = IndexParams { let params = IndexParams {
config, config,
captcha_solutions, captcha_solutions,
@ -85,12 +90,17 @@ async fn failed_submission(
} }
pub async fn submit( pub async fn submit(
State(config): State<Arc<config::Config>>, State(config): State<Arc<Config>>,
State(captcha_solutions): State<Arc<captcha_solutions::SharedCaptchaSolutions>>, State(captcha_solutions): State<Arc<Mutex<CaptchaSolutions>>>,
State(mailer): State<Arc<mailer::Mailer>>, State(mailer): State<Arc<Mailer>>,
Form(form): Form<forms::ContactForm>, Form(form): Form<ContactForm>,
) -> Result<Response, errors::AppError> { ) -> Result<Response, AppError> {
if !captcha_solutions.check_answer(form.id, &form.captcha_answer) { let right_captcha_answer = captcha_solutions
.lock()
.unwrap()
.check_answer(form.id, form.captcha_answer.trim());
if !right_captcha_answer {
info!("Wrong CAPTCHA"); info!("Wrong CAPTCHA");
return failed_submission( return failed_submission(
@ -120,7 +130,7 @@ pub async fn submit(
success(config).await success(config).await
} }
pub async fn success(config: Arc<config::Config>) -> Result<Response, errors::AppError> { pub async fn success(config: Arc<Config>) -> Result<Response, AppError> {
info!("Successful contact form submission"); info!("Successful contact form submission");
let template = templates::Success { let template = templates::Success {

View file

@ -1,11 +1,28 @@
use anyhow::Result;
use axum::extract::FromRef; use axum::extract::FromRef;
use std::sync::Arc; use std::sync::{Arc, Mutex};
use crate::{captcha_solutions, config, mailer}; use crate::{captcha_solutions::CaptchaSolutions, config::Config, mailer::Mailer};
#[derive(Clone, FromRef)] #[derive(Clone, FromRef)]
pub struct AppState { pub struct AppState {
pub config: Arc<config::Config>, pub config: Arc<Config>,
pub mailer: Arc<mailer::Mailer>, pub mailer: Arc<Mailer>,
pub captcha_solutions: Arc<captcha_solutions::SharedCaptchaSolutions>, pub captcha_solutions: Arc<Mutex<CaptchaSolutions>>,
}
impl AppState {
pub fn build() -> Result<Self> {
let config = Config::new()?;
let mailer = Arc::new(Mailer::new(&config)?);
let config = Arc::new(config);
let captcha_solutions = Arc::new(Mutex::new(CaptchaSolutions::default()));
Ok(Self {
config,
mailer,
captcha_solutions,
})
}
} }