From 9582160d2cfb706ed84082ae0f63b19751066b6c Mon Sep 17 00:00:00 2001 From: JustTestingV Date: Sun, 8 Oct 2023 13:53:39 +0000 Subject: [PATCH] xmpp: logopass auth (#19) Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/19 Co-authored-by: JustTestingV Co-committed-by: JustTestingV --- Cargo.lock | 2 + Cargo.toml | 1 + crates/projection-xmpp/src/lib.rs | 51 ++++++++++++++---- crates/proto-xmpp/Cargo.toml | 4 ++ crates/proto-xmpp/src/sasl.rs | 86 +++++++++++++++++++++++++++++++ src/main.rs | 2 +- 6 files changed, 135 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13b8fca..da91e6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1324,6 +1324,8 @@ name = "proto-xmpp" version = "0.0.1-dev" dependencies = [ "anyhow", + "assert_matches", + "base64", "derive_more", "lazy_static", "quick-xml", diff --git a/Cargo.toml b/Cargo.toml index 12df833..4f850e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ clap = { version = "4.4.4", features = ["derive"] } serde = { version = "1.0.152", features = ["rc", "serde_derive"] } tracing = "0.1.37" # logging & tracing api prometheus = { version = "0.13.3", default-features = false } +base64 = "0.21.3" lavina-core = { path = "crates/lavina-core" } [package] diff --git a/crates/projection-xmpp/src/lib.rs b/crates/projection-xmpp/src/lib.rs index 7e93537..2a7e0ee 100644 --- a/crates/projection-xmpp/src/lib.rs +++ b/crates/projection-xmpp/src/lib.rs @@ -26,10 +26,12 @@ use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry}; use lavina_core::prelude::*; use lavina_core::room::{RoomId, RoomRegistry}; use lavina_core::terminator::Terminator; +use lavina_core::repo::Storage; use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; use proto_xmpp::client::{Iq, Message, MessageType, Presence}; use proto_xmpp::disco::*; use proto_xmpp::roster::RosterQuery; +use proto_xmpp::sasl::AuthBody; use proto_xmpp::session::Session; use proto_xmpp::stream::*; use proto_xmpp::xml::{Continuation, FromXml, Parser, ToXml}; @@ -60,6 +62,7 @@ pub async fn launch( players: PlayerRegistry, rooms: RoomRegistry, metrics: MetricsRegistry, + storage: Storage, ) -> Result { log::info!("Starting XMPP projection"); @@ -99,11 +102,12 @@ pub async fn launch( } let players = players.clone(); let rooms = rooms.clone(); + let storage = storage.clone(); let terminator = Terminator::spawn(|termination| { let stopped_tx = stopped_tx.clone(); let loaded_config = loaded_config.clone(); async move { - match handle_socket(loaded_config, stream, &socket_addr, players, rooms, termination).await { + match handle_socket(loaded_config, stream, &socket_addr, players, rooms, storage, termination).await { Ok(_) => log::info!("Connection terminated"), Err(err) => log::warn!("Connection failed: {err}"), } @@ -143,6 +147,7 @@ async fn handle_socket( socket_addr: &SocketAddr, mut players: PlayerRegistry, rooms: RoomRegistry, + mut storage: Storage, termination: Deferred<()>, // TODO use it to stop the connection gracefully ) -> Result<()> { log::info!("Received an XMPP connection from {socket_addr}"); @@ -167,7 +172,7 @@ async fn handle_socket( let mut xml_reader = NsReader::from_reader(BufReader::new(a)); let mut xml_writer = Writer::new(b); - let authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf).await?; + let authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf, &mut storage).await?; log::debug!("User authenticated"); let mut connection = players.connect_to_player(authenticated.player_id.clone()).await; socket_final( @@ -224,6 +229,7 @@ async fn socket_auth( xml_reader: &mut NsReader<(impl AsyncBufRead + Unpin)>, xml_writer: &mut Writer<(impl AsyncWrite + Unpin)>, reader_buf: &mut Vec, + storage: &mut Storage, ) -> Result { read_xml_header(xml_reader, reader_buf).await?; let _ = ClientStreamStart::parse(xml_reader, reader_buf).await?; @@ -248,16 +254,41 @@ async fn socket_auth( .await?; xml_writer.get_mut().flush().await?; - let _ = proto_xmpp::sasl::Auth::parse(xml_reader, reader_buf).await?; + let auth: proto_xmpp::sasl::Auth = proto_xmpp::sasl::Auth::parse(xml_reader, reader_buf).await?; proto_xmpp::sasl::Success.write_xml(xml_writer).await?; - let name: Str = "darova".into(); - Ok(Authenticated { - player_id: PlayerId::from("darova")?, - xmpp_name: Name(name.clone()), - xmpp_resource: Resource(name.clone()), - xmpp_muc_name: Resource(name), - }) + match AuthBody::from_str(&auth.body) { + Ok(logopass) => { + let name = &logopass.login; + let stored_user = storage.retrieve_user_by_name(name).await?; + + let stored_user = match stored_user { + Some(u) => u, + None => { + log::info!("User '{}' not found", name); + return Err(fail("no user found")); + } + }; + // TODO return proper XML errors to the client + + if stored_user.password.is_none() { + log::info!("Password not defined for user '{}'", name); + return Err(fail("password is not defined")); + } + if stored_user.password.as_deref() != Some(&logopass.password) { + log::info!("Incorrect password supplied for user '{}'", name); + return Err(fail("passwords do not match")); + } + + Ok(Authenticated { + player_id: PlayerId::from(name.as_str())?, + xmpp_name: Name(name.to_string().into()), + xmpp_resource: Resource(name.to_string().into()), + xmpp_muc_name: Resource(name.to_string().into()), + }) + }, + Err(e) => return Err(e) + } } async fn socket_final( diff --git a/crates/proto-xmpp/Cargo.toml b/crates/proto-xmpp/Cargo.toml index d8b28f9..df0f03c 100644 --- a/crates/proto-xmpp/Cargo.toml +++ b/crates/proto-xmpp/Cargo.toml @@ -10,3 +10,7 @@ regex.workspace = true anyhow.workspace = true tokio.workspace = true derive_more.workspace = true +base64.workspace = true + +[dev-dependencies] +assert_matches.workspace = true diff --git a/crates/proto-xmpp/src/sasl.rs b/crates/proto-xmpp/src/sasl.rs index 7068e7a..71f4945 100644 --- a/crates/proto-xmpp/src/sasl.rs +++ b/crates/proto-xmpp/src/sasl.rs @@ -5,6 +5,7 @@ use quick_xml::{ NsReader, Writer, }; use tokio::io::{AsyncBufRead, AsyncWrite}; +use base64::{Engine as _, engine::general_purpose}; use super::skip_text; use anyhow::{anyhow, Result}; @@ -12,6 +13,7 @@ use anyhow::{anyhow, Result}; pub enum Mechanism { Plain, } + impl Mechanism { pub fn to_str(&self) -> &'static str { match self { @@ -27,10 +29,93 @@ impl Mechanism { } } +#[derive(PartialEq, Debug)] +pub struct AuthBody { + pub login: String, + pub password: String, +} + +impl AuthBody { + pub fn from_str(input: &[u8]) -> Result { + match general_purpose::STANDARD.decode(input){ + Ok(decoded_body) => { + match String::from_utf8(decoded_body) { + Ok(parsed_to_string) => { + let separated_words: Vec<&str> = parsed_to_string.split("\x00").collect::>().clone(); + if separated_words.len() == 3 { + // first segment ignored (might be needed in the future) + Ok(AuthBody { login: separated_words[1].to_string(), password: separated_words[2].to_string() }) + } else { return Err(anyhow!("Incorrect auth format")) } + }, + Err(e) => return Err(anyhow!(e)) + } + }, + Err(e) => return Err(anyhow!(e)) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_returning_auth_body() { + let orig = b"\x00login\x00pass"; + let encoded = general_purpose::STANDARD.encode(orig); + let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()}; + let result = AuthBody::from_str(encoded.as_bytes()).unwrap(); + + assert_eq!(expected, result); + } + + #[test] + fn test_ignoring_first_segment() { + let orig = b"ignored\x00login\x00pass"; + let encoded = general_purpose::STANDARD.encode(orig); + let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()}; + let result = AuthBody::from_str(encoded.as_bytes()).unwrap(); + + assert_eq!(expected, result); + } + + #[test] + fn test_returning_auth_body_with_empty_strings() { + let orig = b"\x00\x00"; + let encoded = general_purpose::STANDARD.encode(orig); + let expected = AuthBody {login: "".to_string(), password: "".to_string()}; + let result = AuthBody::from_str(encoded.as_bytes()).unwrap(); + + assert_eq!(expected, result); + } + + + #[test] + fn test_fail_if_size_less_then_3() { + let orig = b"login\x00pass"; + let encoded = general_purpose::STANDARD.encode(orig); + let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()}; + let result = AuthBody::from_str(encoded.as_bytes()); + + assert!(result.is_err()); + } + + #[test] + fn test_fail_if_size_greater_then_3() { + let orig = b"first\x00login\x00pass\x00other"; + let encoded = general_purpose::STANDARD.encode(orig); + let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()}; + let result = AuthBody::from_str(encoded.as_bytes()); + + assert!(result.is_err()); + } + +} + pub struct Auth { pub mechanism: Mechanism, pub body: Vec, } + impl Auth { pub async fn parse( reader: &mut NsReader, @@ -69,6 +154,7 @@ impl Auth { } pub struct Success; + impl Success { pub async fn write_xml(&self, writer: &mut Writer) -> Result<()> { let event = BytesStart::new(r#"success xmlns="urn:ietf:params:xml:ns:xmpp-sasl""#); diff --git a/src/main.rs b/src/main.rs index 74e40ed..53d7c21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,7 +62,7 @@ async fn main() -> Result<()> { storage.clone(), ) .await?; - let xmpp = projection_xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone()).await?; + let xmpp = projection_xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?; tracing::info!("Started"); sleep.await;