Refactor captcha solutions and AppState
This commit is contained in:
parent
db0ad404a1
commit
a0c2edba56
5 changed files with 104 additions and 94 deletions
|
@ -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.
|
||||||
|
unsafe {
|
||||||
|
*self.solutions.get_unchecked_mut(self.last_id as usize) = Some(sol);
|
||||||
|
}
|
||||||
|
|
||||||
sols.push(sol)
|
self.last_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_answer(&self, id: u16, answer: &str) -> bool {
|
/// Checks an answer and returns if it matches the solution with the given id.
|
||||||
let mut sols = self.lock().unwrap();
|
///
|
||||||
|
/// 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) = sols.get(id) {
|
if let Some(sol) = sol_ref {
|
||||||
if sol == answer.trim() {
|
if sol == answer {
|
||||||
sols.set(id, None);
|
*sol_ref = None;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue