forked from lavina/lavina
xmpp: logopass auth (#19)
Reviewed-on: lavina/lavina#19 Co-authored-by: JustTestingV <JustTestingV@gmail.com> Co-committed-by: JustTestingV <JustTestingV@gmail.com>
This commit is contained in:
parent
3ff8c98f14
commit
9582160d2c
|
@ -1324,6 +1324,8 @@ name = "proto-xmpp"
|
|||
version = "0.0.1-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
"base64",
|
||||
"derive_more",
|
||||
"lazy_static",
|
||||
"quick-xml",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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<Terminator> {
|
||||
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<u8>,
|
||||
storage: &mut Storage,
|
||||
) -> Result<Authenticated> {
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<AuthBody> {
|
||||
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::<Vec<_>>().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<u8>,
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
pub async fn parse(
|
||||
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
|
||||
|
@ -69,6 +154,7 @@ impl Auth {
|
|||
}
|
||||
|
||||
pub struct Success;
|
||||
|
||||
impl Success {
|
||||
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
|
||||
let event = BytesStart::new(r#"success xmlns="urn:ietf:params:xml:ns:xmpp-sasl""#);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue