From 1e1e87c9b2307e06ed02a52d36e56cabb78fd5c6 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 26 Jul 2020 17:24:25 -0400 Subject: [PATCH] Adding audio wav file for Captcha using espeak. --- docker/dev/Dockerfile | 3 ++ docker/prod/Dockerfile | 4 ++ docs/src/contributing_websocket_http_api.md | 1 + server/src/api/user.rs | 8 ++- server/src/lib.rs | 55 ++++++++++++++++++++- ui/src/components/login.tsx | 52 ++++++++++++++----- ui/src/components/symbols.tsx | 7 ++- ui/src/interfaces.ts | 1 + ui/translations/en.json | 3 +- 9 files changed, 116 insertions(+), 18 deletions(-) diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index b86618d85..afbdbbbe1 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -41,6 +41,9 @@ FROM alpine:3.12 # Install libpq for postgres RUN apk add libpq +# Install Espeak for captchas +RUN apk add espeak + # Copy resources COPY server/config/defaults.hjson /config/defaults.hjson COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 774387404..845df88de 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -50,6 +50,10 @@ FROM alpine:3.12 as lemmy # Install libpq for postgres RUN apk add libpq + +# Install Espeak for captchas +RUN apk add espeak + RUN addgroup -g 1000 lemmy RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index abe4e81e1..4c8d1bb19 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -428,6 +428,7 @@ These expire after 10 minutes. op: "GetCaptcha", data: { png: String, // A Base64 encoded png + wav: Option, // A Base64 encoded wav audio file uuid: String, } } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 066e16a2b..e6729a69d 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -2,6 +2,7 @@ use crate::{ api::{claims::Claims, is_admin, APIError, Oper, Perform}, apub::ApubObjectType, blocking, + captcha_espeak_wav_base64, websocket::{ server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage}, UserOperation, @@ -79,7 +80,8 @@ pub struct GetCaptcha {} #[derive(Serialize, Deserialize)] pub struct GetCaptchaResponse { - png: String, // A Base64 encoded png + png: String, // A Base64 encoded png + wav: Option, // A Base64 encoded wav audio uuid: String, } @@ -513,6 +515,8 @@ impl Perform for Oper { let uuid = uuid::Uuid::new_v4().to_string(); + let wav = captcha_espeak_wav_base64(&answer).ok(); + let captcha_item = CaptchaItem { answer, uuid: uuid.to_owned(), @@ -523,7 +527,7 @@ impl Perform for Oper { ws.chatserver.do_send(captcha_item); } - Ok(GetCaptchaResponse { png, uuid }) + Ok(GetCaptchaResponse { png, uuid, wav }) } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 3cf474e94..fb6e890ea 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -36,6 +36,7 @@ use actix_web::{client::Client, dev::ConnectionInfo}; use log::error; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use serde::Deserialize; +use std::process::Command; pub type DbPool = diesel::r2d2::Pool>; pub type ConnectionId = usize; @@ -213,9 +214,56 @@ where Ok(res) } +pub fn captcha_espeak_wav_base64(captcha: &str) -> Result { + let mut built_text = String::new(); + + // Building proper speech text for espeak + for mut c in captcha.chars() { + let new_str = if c.is_alphabetic() { + if c.is_lowercase() { + c.make_ascii_uppercase(); + format!("lower case {} ... ", c) + } else { + c.make_ascii_uppercase(); + format!("capital {} ... ", c) + } + } else { + format!("{} ...", c) + }; + + built_text.push_str(&new_str); + } + + espeak_wav_base64(&built_text) +} + +pub fn espeak_wav_base64(text: &str) -> Result { + // Make a temp file path + let uuid = uuid::Uuid::new_v4().to_string(); + let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid); + + // Write the wav file + Command::new("espeak") + .arg("-w") + .arg(&file_path) + .arg(text) + .status()?; + + // Read the wav file bytes + let bytes = std::fs::read(&file_path)?; + + // Delete the file + std::fs::remove_file(file_path)?; + + // Convert to base64 + let base64 = base64::encode(bytes); + + Ok(base64) +} + #[cfg(test)] mod tests { - use crate::is_image_content_type; + use crate::{captcha_espeak_wav_base64, is_image_content_type}; #[test] fn test_image() { @@ -230,6 +278,11 @@ mod tests { }); } + #[test] + fn test_espeak() { + assert!(captcha_espeak_wav_base64("WxRt2l").is_ok()) + } + // These helped with testing // #[test] // fn test_iframely() { diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx index 43d16a621..cefe92859 100644 --- a/ui/src/components/login.tsx +++ b/ui/src/components/login.tsx @@ -49,6 +49,7 @@ export class Login extends Component { captcha: { png: undefined, uuid: undefined, + wav: undefined, }, }; @@ -136,11 +137,7 @@ export class Login extends Component { -
- {this.state.captcha.uuid && ( - - )} -
+ {this.showCaptcha()}
{
); } + registerForm() { return (
@@ -259,11 +257,7 @@ export class Login extends Component { -
- {this.state.captcha.uuid && ( - - )} -
+ {this.showCaptcha()}
{ ); } + showCaptcha() { + return ( +
+ {this.state.captcha.uuid && ( + <> + + {this.state.captcha.wav && ( + + )} + + )} +
+ ); + } + handleLoginSubmit(i: Login, event: any) { event.preventDefault(); i.state.loginLoading = true; @@ -380,7 +402,13 @@ export class Login extends Component { WebSocketService.Instance.passwordReset(resetForm); } - captchaSrc() { + handleCaptchaPlay(i: Login) { + event.preventDefault(); + let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.wav); + snd.play(); + } + + captchaPngSrc() { return `data:image/png;base64,${this.state.captcha.png}`; } @@ -412,7 +440,7 @@ export class Login extends Component { this.state.captcha = data; this.state.loginForm.captcha_uuid = data.uuid; this.state.registerForm.captcha_uuid = data.uuid; - this.setState({ captcha: data }); + this.setState(this.state); } else if (res.op == UserOperation.PasswordReset) { toast(i18n.t('reset_password_mail_sent')); } else if (res.op == UserOperation.GetSite) { diff --git a/ui/src/components/symbols.tsx b/ui/src/components/symbols.tsx index 48d5a5b61..eb68182c8 100644 --- a/ui/src/components/symbols.tsx +++ b/ui/src/components/symbols.tsx @@ -15,6 +15,9 @@ export class Symbols extends Component { xmlnsXlink="http://www.w3.org/1999/xlink" > + + + @@ -193,10 +196,10 @@ export class Symbols extends Component { - + - + diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index fcbe6b51d..90794e6ff 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -566,6 +566,7 @@ export interface RegisterForm { export interface GetCaptchaResponse { png: string; + wav?: string; uuid: string; } diff --git a/ui/translations/en.json b/ui/translations/en.json index fb30a86b9..4ee0907a5 100644 --- a/ui/translations/en.json +++ b/ui/translations/en.json @@ -283,5 +283,6 @@ "cake_day_title": "Cake day:", "cake_day_info": "It's {{ creator_name }}'s cake day today!", "invalid_post_title": "Invalid post title", - "invalid_url": "Invalid URL." + "invalid_url": "Invalid URL.", + "play_captcha_audio": "Play Captcha Audio" }