diff --git a/src/db/board.rs b/src/db/board.rs index 89cf34f..b1805cb 100755 --- a/src/db/board.rs +++ b/src/db/board.rs @@ -1,11 +1,9 @@ -use captcha::{gen, Difficulty}; use redis::{cmd, AsyncCommands, JsonAsyncCommands}; -use sha256::digest; use sqlx::{query, query_as, types::Json}; use std::collections::HashMap; use super::models::{Board, File}; -use crate::{cfg::BoardCfg, ctx::Ctx, error::NekrochanError, CAPTCHA}; +use crate::{cfg::BoardCfg, ctx::Ctx, error::NekrochanError}; impl Board { pub async fn create( @@ -216,52 +214,6 @@ impl Board { } } -impl Board { - pub fn thread_captcha(&self) -> Option<(String, String)> { - let captcha = match self.config.thread_captcha.as_str() { - "easy" => gen(Difficulty::Easy), - "medium" => gen(Difficulty::Medium), - "hard" => gen(Difficulty::Hard), - _ => return None, - }; - - let base64 = captcha.as_base64()?; - - let board = self.id.clone(); - let difficulty = self.config.thread_captcha.clone(); - let id = digest(base64.as_bytes()); - - let key = (board, difficulty, id.clone()); - let solution = captcha.chars_as_string(); - - CAPTCHA.write().ok()?.insert(key, solution); - - Some((id, base64)) - } - - pub fn reply_captcha(&self) -> Option<(String, String)> { - let captcha = match self.config.reply_captcha.as_str() { - "easy" => gen(Difficulty::Easy), - "medium" => gen(Difficulty::Medium), - "hard" => gen(Difficulty::Hard), - _ => return None, - }; - - let base64 = captcha.as_base64()?; - - let board = self.id.clone(); - let difficulty = self.config.thread_captcha.clone(); - let id = digest(base64.as_bytes()); - - let key = (board, difficulty, id.clone()); - let solution = captcha.chars_as_string(); - - CAPTCHA.write().ok()?.insert(key, solution); - - Some((id, base64)) - } -} - async fn update_overboard(ctx: &Ctx, boards: Vec) -> Result<(), NekrochanError> { query("DROP VIEW IF EXISTS overboard") .execute(ctx.db()) diff --git a/src/error.rs b/src/error.rs index 715860b..a3f1db5 100755 --- a/src/error.rs +++ b/src/error.rs @@ -54,12 +54,16 @@ pub enum NekrochanError { InternalError, #[error("Neplatný autentizační token. Vymaž soubory cookie.")] InvalidAuthError, + #[error("Tato CAPTCHA vypršela nebo neexistuje.")] + InvalidCaptchaError, #[error("Neplatná strana.")] InvalidPageError, #[error("Obsah musí mít 1-20000 znaků.")] NewsContentFormatError, #[error("Titulek musí mít 1-100 znaků.")] NewsTitleFormatError, + #[error("Tato nástěnka nevyžaduje CAPTCHA.")] + NoCaptchaError, #[error("Příspěvek musí mít obsah.")] NoContentError, #[error("Příspěvek musí mít soubor.")] @@ -86,8 +90,6 @@ pub enum NekrochanError { ReplyReplyError, #[error("Na této nástěnce se musí vyplnit CAPTCHA.")] RequiredCaptchaError, - #[error("Tato CAPTCHA neexistuje nebo už byla vyřešena.")] - SolvedCaptchaError, #[error("Toto vlákno je uzamčené.")] ThreadLockError, #[error("Tento ban nelze odvolat.")] @@ -237,9 +239,11 @@ impl ResponseError for NekrochanError { NekrochanError::InsufficientPermissionError => StatusCode::FORBIDDEN, NekrochanError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, NekrochanError::InvalidAuthError => StatusCode::UNAUTHORIZED, + NekrochanError::InvalidCaptchaError => StatusCode::BAD_REQUEST, NekrochanError::InvalidPageError => StatusCode::BAD_REQUEST, NekrochanError::NewsContentFormatError => StatusCode::BAD_REQUEST, NekrochanError::NewsTitleFormatError => StatusCode::BAD_REQUEST, + NekrochanError::NoCaptchaError => StatusCode::NOT_FOUND, NekrochanError::NoContentError => StatusCode::BAD_REQUEST, NekrochanError::NoFileError => StatusCode::BAD_REQUEST, NekrochanError::NoPostsError => StatusCode::BAD_REQUEST, @@ -253,7 +257,6 @@ impl ResponseError for NekrochanError { NekrochanError::ReplyReplyError => StatusCode::BAD_REQUEST, NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST, NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED, - NekrochanError::SolvedCaptchaError => StatusCode::BAD_REQUEST, NekrochanError::ThreadLockError => StatusCode::FORBIDDEN, NekrochanError::UnappealableError => StatusCode::BAD_REQUEST, NekrochanError::UsernameFormatError => StatusCode::BAD_REQUEST, diff --git a/src/lib.rs b/src/lib.rs index a0bbe6e..ed3a340 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,4 @@ use error::NekrochanError; -use lazy_static::lazy_static; -use std::{collections::HashMap, sync::RwLock}; - -lazy_static! { - pub static ref CAPTCHA: RwLock> = - RwLock::new(HashMap::new()); -} const GENERIC_PAGE_SIZE: i64 = 15; diff --git a/src/main.rs b/src/main.rs index db5aa1e..fc5583f 100755 --- a/src/main.rs +++ b/src/main.rs @@ -63,8 +63,9 @@ async fn run() -> Result<(), Error> { .service(web::board::board) .service(web::board_catalog::board_catalog) .service(web::index::index) - .service(web::ip_posts::ip_posts) + .service(web::captcha::captcha) .service(web::edit_posts::edit_posts) + .service(web::ip_posts::ip_posts) .service(web::login::login_get) .service(web::login::login_post) .service(web::logout::logout) diff --git a/src/web/actions/create_post.rs b/src/web/actions/create_post.rs index 8e8dfc8..58ccf89 100644 --- a/src/web/actions/create_post.rs +++ b/src/web/actions/create_post.rs @@ -19,7 +19,6 @@ use crate::{ ban_response, tcx::{account_from_auth_opt, ip_from_req}, }, - CAPTCHA, }; #[derive(MultipartForm)] @@ -95,36 +94,23 @@ pub async fn create_post( None => None, }; - let difficulties = ["easy", "medium", "hard"]; - - let difficulty = if thread.is_none() { - if difficulties.contains(&board.config.0.thread_captcha.as_str()) { - Some(board.config.0.thread_captcha.clone()) - } else { - None - } - } else if difficulties.contains(&board.config.0.reply_captcha.as_str()) { - Some(board.config.0.reply_captcha.clone()) - } else { - None - }; - - if let Some(difficulty) = difficulty { + if (thread.is_none() && board.config.0.thread_captcha != "off") + || (thread.is_some() && board.config.0.reply_captcha != "off") + { let board = board.id.clone(); + let id = form .captcha_id .ok_or(NekrochanError::RequiredCaptchaError)? .0; - let key = (board, difficulty, id); + let key = format!("captcha:{board}:{id}"); + let solution = form .captcha_solution .ok_or(NekrochanError::RequiredCaptchaError)?; - let actual_solution = CAPTCHA - .write()? - .remove(&key) - .ok_or(NekrochanError::SolvedCaptchaError)?; + let actual_solution: String = ctx.cache().get_del(key).await?; if solution.trim() != actual_solution { return Err(NekrochanError::IncorrectCaptchaError); diff --git a/src/web/captcha.rs b/src/web/captcha.rs new file mode 100644 index 0000000..d6a197c --- /dev/null +++ b/src/web/captcha.rs @@ -0,0 +1,55 @@ +use ::captcha::{gen, Difficulty}; +use actix_web::{ + get, + web::{Data, Json, Query}, +}; +use redis::AsyncCommands; +use serde::{Deserialize, Serialize}; +use sha256::digest; + +use crate::{ctx::Ctx, db::models::Board, error::NekrochanError}; + +#[derive(Deserialize)] +pub struct CaptchaQuery { + pub board: String, + pub reply: bool, +} + +#[derive(Serialize)] +pub struct CaptchaResponse { + pub png: String, + pub id: String, +} + +#[get("/captcha")] +pub async fn captcha( + ctx: Data, + Query(query): Query, +) -> Result, NekrochanError> { + let board = Board::read(&ctx, query.board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(query.board))?; + + let captcha = match board.config.thread_captcha.as_str() { + "easy" => gen(Difficulty::Easy), + "medium" => gen(Difficulty::Medium), + "hard" => gen(Difficulty::Hard), + _ => return Err(NekrochanError::NoCaptchaError), + }; + + // >NOOOOOOOO YOU NEED TO MAKE A NEW ERROR TYPE FOR THIS ERROR THAT CAN ONLY HAPPEN ONCE IN THE CODE NOOOOOOOOOO + let png = captcha.as_base64().ok_or(NekrochanError::NoCaptchaError)?; + + let board = board.id; + let id = digest(png.as_bytes()); + + let key = format!("captcha:{board}:{id}"); + let solution = captcha.chars_as_string(); + + ctx.cache().set(&key, solution).await?; + ctx.cache().expire(&key, 600).await?; + + let res = CaptchaResponse { png, id }; + + Ok(Json(res)) +} diff --git a/src/web/login.rs b/src/web/login.rs index f717a7e..d650fbb 100755 --- a/src/web/login.rs +++ b/src/web/login.rs @@ -32,6 +32,7 @@ pub async fn login_get(ctx: Data, req: HttpRequest) -> Result, pub perms: PermissionWrapper, pub name: Option, - pub password: String, pub ip: IpAddr, pub yous: HashSet, } @@ -35,16 +33,6 @@ impl TemplateCtx { }; let name = req.cookie("name").map(|cookie| cookie.value().into()); - let password_cookie = req.cookie("password").map(|cookie| cookie.value().into()); - - let password: String = match password_cookie { - Some(password) => password, - None => thread_rng() - .sample_iter(&Alphanumeric) - .take(8) - .map(char::from) - .collect(), - }; let (ip, _) = ip_from_req(req)?; let yous = ctx.cache().zrange(format!("by_ip:{ip}"), 0, -1).await?; @@ -56,7 +44,6 @@ impl TemplateCtx { boards, perms, name, - password, ip, yous, account, diff --git a/static/js/captcha.js b/static/js/captcha.js new file mode 100644 index 0000000..79e3886 --- /dev/null +++ b/static/js/captcha.js @@ -0,0 +1,25 @@ +$(function () { + $("#get-captcha").click(function () { + let btn = $(this); + + let board = btn.attr("data-board"); + let reply = btn.attr("data-reply"); + let req_url = `/captcha?board=${board}&reply=${reply}`; + + btn.text("Získat CAPTCHA"); + btn.prop("disabled", true); + btn.addClass("loading"); + + $.get(req_url, function (data, _) { + try { + $("#captcha").replaceWith(``); + $("#captcha-id").prop("value", data.id); + } catch { + btn.append(" [Chyba]"); + } + + btn.prop("disabled", false); + btn.removeClass("loading"); + }); + }); +}); diff --git a/static/js/password.js b/static/js/password.js new file mode 100644 index 0000000..4e0bba7 --- /dev/null +++ b/static/js/password.js @@ -0,0 +1,48 @@ +$(function () { + let password = get_cookie("password"); + + if (password === "") { + password = generate_password(); + set_cookie("password", password); + } + + $('input[name="password"]').prop("value", password); +}); + +function generate_password() { + let chars = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let password_length = 8; + let password = ""; + + for (let i = 0; i <= password_length; i++) { + let random_number = Math.floor(Math.random() * chars.length); + password += chars.substring(random_number, random_number + 1); + } + + return password; +} + +function get_cookie(cname) { + let name = cname + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(";"); + + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + + while (c.charAt(0) == " ") { + c = c.substring(1); + } + + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + + return ""; +} + +function set_cookie(cname, cvalue) { + document.cookie = `${cname}=${cvalue};path=/`; +} diff --git a/static/style.css b/static/style.css index 949f00d..a6936fb 100755 --- a/static/style.css +++ b/static/style.css @@ -160,12 +160,6 @@ summary { padding: 8px; } -.captcha { - width: 100%; - image-rendering: pixelated; - border: 1px solid var(--input-border); -} - .main { margin: 8px; } @@ -223,7 +217,6 @@ summary { .form-table .button, .full-width { - display: block; width: 100%; } @@ -479,4 +472,17 @@ summary { .loading { opacity: 0.5; + cursor: wait; +} + +img#captcha { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + display: block; + width: 100%; + image-rendering: pixelated; + border: 1px solid var(--input-border); + margin: 0px auto; } diff --git a/templates/base.html b/templates/base.html index a6a7bd4..250472b 100755 --- a/templates/base.html +++ b/templates/base.html @@ -11,9 +11,11 @@ + {% block scripts %}{% endblock %} - + +
{% block content %}{% endblock %} - +
diff --git a/templates/board.html b/templates/board.html index 9ba1611..e19c431 100755 --- a/templates/board.html +++ b/templates/board.html @@ -7,6 +7,7 @@ {% block theme %}{{ board.config.0.board_theme }}{% endblock %} {% block title %}/{{ board.id }}/ - {{ board.name }}{% endblock %} +{% block scripts %}{% endblock %} {% block content %}
diff --git a/templates/login.html b/templates/login.html index 7f4d12a..609c7c0 100755 --- a/templates/login.html +++ b/templates/login.html @@ -13,7 +13,7 @@ Heslo - + diff --git a/templates/macros/post-actions.html b/templates/macros/post-actions.html index 9b63ad4..3292c8f 100644 --- a/templates/macros/post-actions.html +++ b/templates/macros/post-actions.html @@ -29,14 +29,7 @@ Heslo - - - + diff --git a/templates/macros/post-form.html b/templates/macros/post-form.html index 445beaa..69a6f47 100644 --- a/templates/macros/post-form.html +++ b/templates/macros/post-form.html @@ -52,33 +52,40 @@ Heslo (na odstranění) - - - + {% if !(tcx.perms.bypass_captcha() || tcx.perms.owner()) %} + {% let difficulty %} {% if reply %} - {% if let Some((id, base64)) = board.reply_captcha() %} - - CAPTCHA - - - - - - - {% endif %} + {% let difficulty = board.config.0.reply_captcha.as_str() %} {% else %} - {% if let Some((id, base64)) = board.thread_captcha() %} + {% let difficulty = board.config.0.thread_captcha.as_str() %} + {% endif %} + + {% if (!reply && board.config.0.thread_captcha != "off") || (reply && board.config.0.reply_captcha != "off") %} + + + + CAPTCHA
+ (vyprší za 10 min.) + + +
+ + + + + +
+
+ + {% endif %} {% endif %} diff --git a/templates/staff/accounts.html b/templates/staff/accounts.html index 4afeb26..85dc506 100755 --- a/templates/staff/accounts.html +++ b/templates/staff/accounts.html @@ -45,7 +45,7 @@ Heslo - + diff --git a/templates/staff/board-config.html b/templates/staff/board-config.html index 0c58b97..7c3a243 100755 --- a/templates/staff/board-config.html +++ b/templates/staff/board-config.html @@ -76,10 +76,10 @@ - - - -