Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: re-added captcha checks #3249

Merged
merged 1 commit into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 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::{captcha_answer::CaptchaAnswer, local_site::LocalSite},
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: CaptchaAnswer = CaptchaAnswer {
answer,
uuid: uuid.clone(),
expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
};
// Stores the captcha item in the db
CaptchaAnswer::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
1 change: 1 addition & 0 deletions crates/api_crud/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ tracing = { workspace = true }
url = { workspace = true }
async-trait = { workspace = true }
webmention = "0.4.0"
chrono = { worspace = true }
18 changes: 18 additions & 0 deletions crates/api_crud/src/user/create.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::PerformCrud;
use activitypub_federation::http_signatures::generate_actor_keypair;
use actix_web::web::Data;
use chrono::NaiveDateTime;
use lemmy_api_common::{
context::LemmyContext,
person::{LoginResponse, Register},
Expand All @@ -19,6 +20,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
aggregates::structs::PersonAggregates,
source::{
captcha_answer::CaptchaAnswer,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
Expand Down Expand Up @@ -71,6 +73,22 @@ impl PerformCrud for Register {
return Err(LemmyError::from_message("passwords_dont_match"));
}

if local_site.site_setup && local_site.captcha_enabled {
let check = CaptchaAnswer::check_captcha(
context.pool(),
CaptchaAnswer {
uuid: data.captcha_uuid.clone().unwrap_or_default(),
answer: data.captcha_answer.clone().unwrap_or_default(),
// not used when checking
expires: NaiveDateTime::MIN,
},
)
.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
26 changes: 6 additions & 20 deletions crates/db_schema/src/diesel_ltree.patch
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs
index 255c6422..f2ccf5e2 100644
--- a/crates/db_schema/src/schema.rs
+++ b/crates/db_schema/src/schema.rs
@@ -2,16 +2,12 @@

pub mod sql_types {
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "listing_type_enum"))]
--- schema.rs 2023-06-21 22:25:50.252384233 +0100
+++ "schema copy.rs" 2023-06-21 22:26:50.452378651 +0100
@@ -6,10 +6,6 @@
pub struct ListingTypeEnum;

- #[derive(diesel::sql_types::SqlType)]
#[derive(diesel::sql_types::SqlType)]
- #[diesel(postgres_type(name = "ltree"))]
- pub struct Ltree;
-
#[derive(diesel::sql_types::SqlType)]
- #[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "registration_mode_enum"))]
pub struct RegistrationModeEnum;

#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "sort_type_enum"))]
@@ -67,13 +63,13 @@ diesel::table! {
when_ -> Timestamp,
}
}
@@ -78,7 +74,7 @@

diesel::table! {
use diesel::sql_types::*;
Expand All @@ -31,6 +20,3 @@ index 255c6422..f2ccf5e2 100644

comment (id) {
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
content -> Text,
164 changes: 164 additions & 0 deletions crates/db_schema/src/impls/captcha_answer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use crate::{
schema::captcha_answer,
source::captcha_answer::CaptchaAnswer,
utils::{functions::lower, get_conn, naive_now, DbPool},
};
use diesel::{
delete,
dsl::exists,
insert_into,
result::Error,
select,
ExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;

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

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

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

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

// fetch requested captcha
let captcha_exists = select(exists(
captcha_answer::dsl::captcha_answer
.filter((captcha_answer::dsl::uuid).eq(to_check.uuid.clone()))
.filter(lower(captcha_answer::dsl::answer).eq(to_check.answer.to_lowercase().clone())),
))
.get_result::<bool>(conn)
.await?;

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

Ok(captcha_exists)
}
}

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

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

let captcha_a_id = "a".to_string();

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

let result = CaptchaAnswer::check_captcha(
pool,
CaptchaAnswer {
uuid: captcha_a_id.clone(),
answer: "xyz".to_string(),
expires: chrono::NaiveDateTime::MIN,
},
)
.await;

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

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

let captcha_a_id = "a".to_string();

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

let result = CaptchaAnswer::check_captcha(
pool,
CaptchaAnswer {
uuid: captcha_a_id.clone(),
answer: "xyz".to_string(),
expires: chrono::NaiveDateTime::MIN,
},
)
.await;

let result_repeat = CaptchaAnswer::check_captcha(
pool,
CaptchaAnswer {
uuid: captcha_a_id.clone(),
answer: "xyz".to_string(),
expires: chrono::NaiveDateTime::MIN,
},
)
.await;

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

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

let expired_id = "already_expired".to_string();

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

let expired_result = CaptchaAnswer::check_captcha(
pool,
CaptchaAnswer {
uuid: expired_id.clone(),
answer: "xyz".to_string(),
expires: chrono::NaiveDateTime::MIN,
},
)
.await;

assert!(expired_result.is_ok());
assert!(!expired_result.unwrap());
}
}
1 change: 1 addition & 0 deletions crates/db_schema/src/impls/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod activity;
pub mod actor_language;
pub mod captcha_answer;
pub mod comment;
pub mod comment_reply;
pub mod comment_report;
Expand Down
9 changes: 9 additions & 0 deletions crates/db_schema/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ diesel::table! {
}
}

diesel::table! {
captcha_answer (uuid) {
uuid -> Text,
answer -> Text,
expires -> Timestamp,
}
}

diesel::table! {
use diesel::sql_types::{Bool, Int4, Nullable, Text, Timestamp, Varchar};
use diesel_ltree::sql_types::Ltree;
Expand Down Expand Up @@ -916,6 +924,7 @@ diesel::allow_tables_to_appear_in_same_query!(
admin_purge_community,
admin_purge_person,
admin_purge_post,
captcha_answer,
comment,
comment_aggregates,
comment_like,
Expand Down