From 672eb1ee75b915ae9224b27d66d15c264356c912 Mon Sep 17 00:00:00 2001 From: Shagnor Date: Tue, 27 Dec 2022 16:08:27 +1100 Subject: [PATCH] Get login working correctly. --- Cargo.lock | 61 ++++++++++++++++++- blastmud_game/Cargo.toml | 1 + blastmud_game/src/db.rs | 16 +++++ .../src/message_handler/new_session.rs | 4 +- .../src/message_handler/user_commands.rs | 15 ++--- .../message_handler/user_commands/agree.rs | 34 ++++++++--- .../src/message_handler/user_commands/help.rs | 20 +++--- .../message_handler/user_commands/login.rs | 36 +++++++++++ .../message_handler/user_commands/register.rs | 23 +++++-- blastmud_game/src/models/user.rs | 2 + 10 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/login.rs diff --git a/Cargo.lock b/Cargo.lock index 5ccc5c56..0c465b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "aliasable" version = "0.1.3" @@ -119,6 +128,7 @@ dependencies = [ "tokio-stream", "tokio-util", "uuid", + "validator", ] [[package]] @@ -692,6 +702,17 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.3.0" @@ -785,6 +806,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "md-5" version = "0.10.5" @@ -1191,6 +1218,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1792,7 +1836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna", + "idna 0.3.0", "percent-encoding", ] @@ -1812,6 +1856,21 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591" +dependencies = [ + "idna 0.2.3", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/blastmud_game/Cargo.toml b/blastmud_game/Cargo.toml index 7236e116..c06afb54 100644 --- a/blastmud_game/Cargo.toml +++ b/blastmud_game/Cargo.toml @@ -31,3 +31,4 @@ nom = "7.1.1" ouroboros = "0.15.5" chrono = { version = "0.4.23", features = ["serde"] } bcrypt = "0.13.0" +validator = "0.16.0" diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index cf954dd7..28338f54 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -9,6 +9,7 @@ use tokio_postgres::NoTls; use crate::message_handler::ListenerSession; use crate::DResult; use crate::models::{session::Session, user::User, item::Item}; +use tokio_postgres::types::ToSql; use serde_json; use futures::FutureExt; @@ -226,6 +227,21 @@ impl DBTrans { &details.username.to_lowercase()]).await?; Ok(()) } + + pub async fn attach_user_to_session(self: &Self, username: &str, + session: &ListenerSession) -> DResult<()> { + let username_l = username.to_lowercase(); + self.pg_trans()? + .execute("INSERT INTO sendqueue (session, listener, message) \ + SELECT current_session, current_listener, $1 FROM users \ + WHERE username = $2 AND current_session IS NOT NULL \ + AND current_listener IS NOT NULL", + &[&"Logged in from another session\r\n", &username_l]).await?; + self.pg_trans()? + .execute("UPDATE users SET current_session = $1, current_listener = $2 WHERE username = $3", + &[&session.session as &(dyn ToSql + Sync), &session.listener, &username_l]).await?; + Ok(()) + } pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); diff --git a/blastmud_game/src/message_handler/new_session.rs b/blastmud_game/src/message_handler/new_session.rs index b37fb855..658b5e53 100644 --- a/blastmud_game/src/message_handler/new_session.rs +++ b/blastmud_game/src/message_handler/new_session.rs @@ -11,8 +11,8 @@ pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) -> Welcome to BlastMud - a text-based post-apocalyptic \ game restricted to adults (18+)\r\n\ Some commands to get you started:\r\n\ - \tregister username> password> to register as a new user.\r\n\ - \tconnect username> password> to log in as an existing user.\r\n\ + \tregister username> password> email> to register as a new user.\r\n\ + \tlogin username> password> to log in as an existing user.\r\n\ \thelp to learn more.\r\n\ [Please note BlastMud is still under development. You are welcome to play as we \ develop it, but note it might still have bugs, unimplemented features, and \ diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index ac68224f..fafcda76 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -14,6 +14,7 @@ mod quit; mod less_explicit_mode; mod register; mod agree; +mod login; pub struct VerbContext<'l> { session: &'l ListenerSession, @@ -36,15 +37,9 @@ pub trait UserVerb { pub type UResult = Result; -impl From<&str> for CommandHandlingError { - fn from(input: &str) -> CommandHandlingError { - SystemError(Box::from(input)) - } -} - -impl From> for CommandHandlingError { - fn from(input: Box) -> CommandHandlingError { - SystemError(input) +impl From for CommandHandlingError where T: Into> { + fn from(input: T) -> CommandHandlingError { + SystemError(input.into()) } } @@ -66,6 +61,8 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! { static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "less_explicit_mode" => less_explicit_mode::VERB, "register" => register::VERB, + "login" => login::VERB, + "connect" => login::VERB, "agree" => agree::VERB }; diff --git a/blastmud_game/src/message_handler/user_commands/agree.rs b/blastmud_game/src/message_handler/user_commands/agree.rs index 670e1185..95130312 100644 --- a/blastmud_game/src/message_handler/user_commands/agree.rs +++ b/blastmud_game/src/message_handler/user_commands/agree.rs @@ -1,7 +1,8 @@ -use super::{VerbContext, UserVerb, UserVerbRef, UResult}; +use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error}; use crate::models::user::{User, UserTermData}; use async_trait::async_trait; use ansi_macro::ansi; +use chrono::Utc; pub struct Verb; @@ -23,7 +24,7 @@ static REQUIRED_AGREEMENTS: [&str;4] = [ is personally identifying information, or is objectionable or abhorrent (including, without \ limitation, any content related to sexual violence, real or fictional children under 18, bestiality, \ the promotion or glorification of proscribed drug use, or fetishes that involve degrading or \ - inflicting pain in someone for the enjoyment of others). I agree to defend, indemnify, and hold \ + inflicting pain on someone for the enjoyment of others). I agree to defend, indemnify, and hold \ harmless the creators, staff, volunteers and contributors in any matter relating to content sent \ (or re-sent) by me, in any matter arising from the game sending content to me, and in any matter \ consequential to sharing my password, using an insecure password, or otherwise allowing or taking \ @@ -68,7 +69,9 @@ fn first_outstanding_agreement(ctx: &VerbContext) -> UResult(ctx: &'a mut VerbContext<'b>) -> UResult where 'b: 'a { match first_outstanding_agreement(ctx)? { None => { - user_mut(ctx)?.terms.terms_complete = true; + let user = user_mut(ctx)?; + user.terms.terms_complete = true; + user.terms.last_presented_term = None; Ok(true) } Some((text, hash)) => { @@ -77,9 +80,9 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U user.terms.last_presented_term = Some(hash); ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!( "Please review the following:\r\n\ - {}\r\n\ - Type agree to accept. If you can't or don't agree, you \ - unfortunately can't play, so type quit to log off.\r\n"), + \t{}\r\n\ + Type agree to accept. If you can't or don't agree, you \ + unfortunately can't play, so type quit to log off.\r\n"), text))).await?; Ok(false) } @@ -88,7 +91,24 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U #[async_trait] impl UserVerb for Verb { - async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { + let user = user_mut(ctx)?; + match user.terms.last_presented_term.as_ref() { + None => { + drop(user); + user_error("There was nothing pending your agreement.".to_owned())?; + } + Some(last_term) => { + user.terms.accepted_terms.insert(last_term.to_owned(), Utc::now()); + drop(user); + if check_and_notify_accepts(ctx).await? { + ctx.trans.queue_for_session(ctx.session, Some( + ansi!("That was the last of the terms to agree to - welcome onboard!\r\n\ + Hint: Try l to look around.\r\n"))).await?; + } + } + } + ctx.trans.save_user_model(ctx.user_dat.as_ref().unwrap()).await?; Ok(()) } } diff --git a/blastmud_game/src/message_handler/user_commands/help.rs b/blastmud_game/src/message_handler/user_commands/help.rs index 9c33ef24..9f9bc4f0 100644 --- a/blastmud_game/src/message_handler/user_commands/help.rs +++ b/blastmud_game/src/message_handler/user_commands/help.rs @@ -20,19 +20,25 @@ static UNREGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! Topics of interest to unregistered users:\r\n\ \tregister\tLearn about the register command.\r\n\ \tlogin\tLearn how to log in as an existing user.\r\n"), + "register" => + ansi!("Registers a new user. You are allowed at most 5 at once.\r\n\ + \tregister username> password> email>\r\n\ + Email will be used to check you don't have too many accounts and \ + in case you need to reset your password."), + "login" => + ansi!("Logs in as an existing user.\r\n\ + \tlogin username> password") }; static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { "" => ansi!("Type help topicname> to learn about a topic. Most \ commands can be used as a topicname.\r\n\ - Topics of interest to new users:\r\n\ - \tregister\tLearn about the register command.\r\n\ - \tnewbie\tLearn how to survive as a newb."), - "" => - ansi!("You are supposed to replace topicname> with the topic you want \ - to learn about. Example:\r\n\ - \thelp register will tell you about the register command.") + Topics of interest:\r\n\ + \tnewbie\tLearn the absolute basics."), + "newbie" => + ansi!("So you've just landed in BlastMud, and want to know how to get started?\r\n\ + As we develop the game, this will eventually have some useful information for you!"), }; static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { diff --git a/blastmud_game/src/message_handler/user_commands/login.rs b/blastmud_game/src/message_handler/user_commands/login.rs new file mode 100644 index 00000000..05d90f9b --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/login.rs @@ -0,0 +1,36 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error}; +use async_trait::async_trait; +use tokio::time; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { + let (username, password) = match remaining.split_whitespace().collect::>()[..] { + [] => user_error("Too few options to login".to_owned())?, + [username, password] => (username, password), + _ => user_error("Too many options to login".to_owned())?, + }; + + match ctx.trans.find_by_username(username).await? { + None => user_error("No such user.".to_owned())?, + Some(user) => { + time::sleep(time::Duration::from_secs(5)).await; + if !bcrypt::verify(password, &user.password_hash)? { + user_error("Invalid password.".to_owned())? + } + *ctx.user_dat = Some(user); + } + } + + ctx.trans.attach_user_to_session(username, ctx.session).await?; + super::agree::check_and_notify_accepts(ctx).await?; + if let Some(user) = ctx.user_dat { + ctx.trans.save_user_model(user).await?; + } + + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/message_handler/user_commands/register.rs b/blastmud_game/src/message_handler/user_commands/register.rs index ab938b77..6f9ee04f 100644 --- a/blastmud_game/src/message_handler/user_commands/register.rs +++ b/blastmud_game/src/message_handler/user_commands/register.rs @@ -4,23 +4,30 @@ use super::{user_error, parsing::parse_username}; use crate::models::{user::User, item::Item}; use chrono::Utc; use ansi_macro::ansi; +use tokio::time; pub struct Verb; #[async_trait] impl UserVerb for Verb { async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { - let (username, mut password) = match parse_username(remaining) { + let (username, password, email) = match parse_username(remaining) { Err(e) => user_error("Invalid username: ".to_owned() + e)?, - Ok(r) => r + Ok((username, rest)) => { + match rest.split_whitespace().collect::>()[..] { + [password, email] => (username, password, email), + [] | [_] => user_error("Too few options to register - supply username, password, and email".to_owned())?, + _ => user_error("Too many options to register - supply username, password, and email".to_owned())?, + } + } }; - password = password.trim(); + if ctx.trans.find_by_username(username).await?.is_some() { user_error("Username already exists".to_owned())?; } - if password.contains(" ") || password.contains("\t") { - user_error("To avoid future confusion, password can't contain spaces / tabs".to_owned())?; - } else if password.len() < 6 { + if password.len() < 6 { user_error("Password must be 6 characters long or longer".to_owned())?; + } else if !validator::validate_email(email) { + user_error("Please supply a valid email in case you need to reset your password.".to_owned())?; } let player_item_id = ctx.trans.create_item(&Item { @@ -30,10 +37,14 @@ impl UserVerb for Verb { location: "room/chargen_room".to_owned(), ..Item::default() }).await?; + + // Force a wait to protect against abuse. + time::sleep(time::Duration::from_secs(5)).await; let password_hash = bcrypt::hash(password, 10).expect("hash not to fail"); let user_dat = User { username: username.to_owned(), password_hash: password_hash.to_owned(), + email: email.to_owned(), player_item_id, registered_at: Some(Utc::now()), ..User::default() diff --git a/blastmud_game/src/models/user.rs b/blastmud_game/src/models/user.rs index 56a7d65f..e9f62a17 100644 --- a/blastmud_game/src/models/user.rs +++ b/blastmud_game/src/models/user.rs @@ -66,6 +66,7 @@ pub enum StatType { pub struct User { pub username: String, pub password_hash: String, // bcrypted. + pub email: String, pub player_item_id: i64, pub registered_at: Option>, pub banned_until: Option>, @@ -106,6 +107,7 @@ impl Default for User { User { username: "unknown".to_owned(), password_hash: "unknown".to_owned(), + email: "unknown".to_owned(), player_item_id: 0, registered_at: None, banned_until: None,