Skip to content

Commit

Permalink
feat: re-added captcha checks
Browse files Browse the repository at this point in the history
  • Loading branch information
TKilFree committed Jun 21, 2023
1 parent 3775418 commit b0809cf
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 71 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async-trait = { workspace = true }
captcha = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
chrono = { workspace = true }

[dev-dependencies]
serial_test = { workspace = true }
Expand Down
16 changes: 16 additions & 0 deletions crates/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use actix_web::web::Data;
use captcha::Captcha;
use lemmy_api_common::{context::LemmyContext, utils::local_site_to_slur_regex};
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_utils::{error::LemmyError, utils::slurs::check_slurs};
Expand All @@ -20,6 +21,21 @@ pub trait Perform {
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError>;
}

/// Converts the captcha to a base64 encoded wav audio file
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> String {
let letters = captcha.as_wav();

let mut concat_letters: Vec<u8> = Vec::new();

for letter in letters {
let bytes = letter.unwrap_or_default();
concat_letters.extend(bytes);
}

// Convert to base64
base64::encode(concat_letters)
}

/// Check size of report and remove whitespace
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
let slur_regex = &local_site_to_slur_regex(local_site);
Expand Down
53 changes: 53 additions & 0 deletions crates/api/src/local_user/get_captcha.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::{captcha_as_wav_base64, Perform};
use actix_web::web::Data;
use captcha::{gen, Difficulty};
use chrono::Duration;
use lemmy_api_common::{
context::LemmyContext,
person::{CaptchaResponse, GetCaptcha, GetCaptchaResponse},
};
use lemmy_db_schema::{
source::{local_site::LocalSite, user_captcha::UserCaptcha},
utils::naive_now,
};
use lemmy_utils::error::LemmyError;

#[async_trait::async_trait(?Send)]
impl Perform for GetCaptcha {
type Response = GetCaptchaResponse;

#[tracing::instrument(skip(context))]
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError> {
let local_site = LocalSite::read(context.pool()).await?;

if !local_site.captcha_enabled {
return Ok(GetCaptchaResponse { ok: None });
}

let captcha = gen(match local_site.captcha_difficulty.as_str() {
"easy" => Difficulty::Easy,
"hard" => Difficulty::Hard,
_ => Difficulty::Medium,
});

let answer = captcha.chars_as_string();

let png = captcha.as_base64().expect("failed to generate captcha");

let uuid = uuid::Uuid::new_v4().to_string();

let wav = captcha_as_wav_base64(&captcha);

let captcha = UserCaptcha {
answer,
uuid: uuid.clone(),
expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
};
// Stores the captcha item in the db
UserCaptcha::insert(context.pool(), &captcha).await?;

Ok(GetCaptchaResponse {
ok: Some(CaptchaResponse { png, wav, uuid }),
})
}
}
1 change: 1 addition & 0 deletions crates/api/src/local_user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod ban_person;
mod block;
mod change_password;
mod change_password_after_reset;
mod get_captcha;
mod list_banned;
mod login;
mod notifications;
Expand Down
15 changes: 15 additions & 0 deletions crates/api_crud/src/user/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use lemmy_db_schema::{
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
user_captcha::{CheckCaptcha, UserCaptcha},
},
traits::Crud,
RegistrationMode,
Expand Down Expand Up @@ -71,6 +72,20 @@ impl PerformCrud for Register {
return Err(LemmyError::from_message("passwords_dont_match"));
}

if local_site.site_setup && local_site.captcha_enabled {
let check = UserCaptcha::check_captcha(
context.pool(),
CheckCaptcha {
uuid: data.captcha_uuid.clone().unwrap_or_default(),
answer: data.captcha_answer.clone().unwrap_or_default(),
},
)
.await?;
if !check {
return Err(LemmyError::from_message("captcha_incorrect"));
}
}

let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?;
Expand Down
1 change: 1 addition & 0 deletions crates/db_schema/src/impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ pub mod registration_application;
pub mod secret;
pub mod site;
pub mod tagline;
pub mod user_captcha;
119 changes: 119 additions & 0 deletions crates/db_schema/src/impls/user_captcha.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use crate::{
schema::user_captcha,
source::user_captcha::{CheckCaptcha, UserCaptcha},
utils::{functions::lower, get_conn, naive_now, DbPool},
};
use diesel::{delete, insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;

impl UserCaptcha {
pub async fn insert(pool: &DbPool, captcha: &UserCaptcha) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;

insert_into(user_captcha::table)
.values(captcha)
.get_result::<Self>(conn)
.await
}

pub async fn check_captcha(pool: &DbPool, to_check: CheckCaptcha) -> Result<bool, Error> {
let conn = &mut get_conn(pool).await?;

// delete any expired captchas
delete(user_captcha::table.filter(user_captcha::expires.lt(&naive_now())))
.execute(conn)
.await?;

// fetch requested captcha
let captcha = user_captcha::dsl::user_captcha
.filter((user_captcha::dsl::uuid).eq(to_check.uuid.clone()))
.filter(lower(user_captcha::dsl::answer).eq(to_check.answer.to_lowercase().clone()))
.first::<Self>(conn)
.await;

let result = captcha.is_ok();

// delete checked captcha
delete(user_captcha::table.filter(user_captcha::uuid.eq(to_check.uuid.clone())))
.execute(conn)
.await?;

Ok(result)
}
}

#[cfg(test)]
mod tests {
use crate::{
source::user_captcha::{CheckCaptcha, UserCaptcha},
utils::{build_db_pool_for_tests, naive_now},
};
use chrono::Duration;
use serial_test::serial;

#[tokio::test]
#[serial]
async fn test_captcha() {
let pool = &build_db_pool_for_tests().await;

let captcha_a_id = "a".to_string();

let _ = UserCaptcha::insert(
pool,
&UserCaptcha {
uuid: captcha_a_id.clone(),
answer: "XYZ".to_string(),
expires: naive_now() + Duration::minutes(10),
},
)
.await;

let result = UserCaptcha::check_captcha(
pool,
CheckCaptcha {
uuid: captcha_a_id.clone(),
answer: "xyz".to_string(),
},
)
.await;

assert!(result.is_ok());
assert!(result.unwrap());

let result_repeat = UserCaptcha::check_captcha(
pool,
CheckCaptcha {
uuid: captcha_a_id.clone(),
answer: "xyz".to_string(),
},
)
.await;

assert!(result_repeat.is_ok());
assert!(!result_repeat.unwrap());

let expired_id = "already_expired".to_string();

let _ = UserCaptcha::insert(
pool,
&UserCaptcha {
uuid: expired_id.clone(),
answer: "xyz".to_string(),
expires: naive_now() - Duration::seconds(1),
},
)
.await;

let expired_result = UserCaptcha::check_captcha(
pool,
CheckCaptcha {
uuid: expired_id.clone(),
answer: "xyz".to_string(),
},
)
.await;

assert!(expired_result.is_ok());
assert!(!expired_result.unwrap());
}
}

0 comments on commit b0809cf

Please sign in to comment.