From 1f4e6f64511223fae8f45ee43c46aa6c49f2acff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Sat, 24 Sep 2022 01:41:32 +0200 Subject: [PATCH] Partial XEP-0410 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- src/error.rs | 10 +++++- src/handlers.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++-- src/room.rs | 8 +++++ src/tests/iq.rs | 37 ++++++++++++++++++++- 4 files changed, 136 insertions(+), 5 deletions(-) diff --git a/src/error.rs b/src/error.rs index 9486a3b..203a5f7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,7 +17,7 @@ use std::error::Error as StdError; use std::fmt; use tokio_xmpp::Error as TokioXMPPError; -use xmpp_parsers::{FullJid, Jid}; +use xmpp_parsers::{Error as ParserError, FullJid, Jid}; #[derive(Debug)] pub enum Error { @@ -26,6 +26,7 @@ pub enum Error { NonexistantSession(FullJid), SessionAlreadyExists(FullJid), XMPPError(TokioXMPPError), + ParserError(ParserError), } impl StdError for Error {} @@ -38,6 +39,7 @@ impl fmt::Display for Error { Error::NonexistantSession(err) => write!(f, "Session doesn't exist: {}", err), Error::SessionAlreadyExists(err) => write!(f, "Session already exist: {}", err), Error::XMPPError(err) => write!(f, "XMPP error: {}", err), + Error::ParserError(err) => write!(f, "Parser error: {}", err), } } } @@ -47,3 +49,9 @@ impl From for Error { Error::XMPPError(err) } } + +impl From for Error { + fn from(err: ParserError) -> Error { + Error::ParserError(err) + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 8e5636d..6034f82 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -17,8 +17,9 @@ use crate::component::ComponentTrait; use crate::error::Error; use crate::room::{Nick, Room}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::ops::ControlFlow; +use std::str::FromStr; use futures::stream::StreamExt; use log::{debug, error}; @@ -28,6 +29,7 @@ use xmpp_parsers::{ message::Message, muc::Muc, ns, + ping::Ping, presence::{Presence, Type as PresenceType}, stanza_error::{DefinedCondition, ErrorType, StanzaError}, BareJid, Element, Jid, @@ -43,6 +45,7 @@ async fn handle_iq_disco( let identities = vec![Identity::new("conference", "text", "en", "Hanabi")]; let features = vec![ Feature::new("http://jabber.org/protocol/disco#info"), + Feature::new("http://jabber.org/protocol/muc#self-ping-optimization"), Feature::new("xmpp:bouah.net:hanabi:muc:0"), ]; let extensions = Vec::new(); @@ -75,11 +78,88 @@ async fn handle_iq_disco( Ok(()) } -async fn handle_iq(component: &mut C, iq: Iq) -> Result<(), Error> { +async fn handle_iq_ping( + component: &mut C, + iq: Iq, + payload: Element, + rooms: &mut HashMap, +) -> Result<(), Error> { + match Ping::try_from(payload) { + Ok(_) => { + let success = Iq::empty_result(iq.from.as_ref().unwrap().clone(), iq.id.clone()); + let from = iq.from.unwrap(); + let to = iq.to.unwrap(); + + // Pinging a participant + if let Jid::Full(participant) = to.clone() { + // TODO: Reply from participant if joined and nick is correct + let bare = BareJid::from(to.clone()); + if let Some(room) = rooms.get(&bare) { + if room.is_joined(&participant) && + let Some(occupant) = room.occupants.get(&participant.resource) { + if occupant.contains(&from) { + let success = success.clone().with_from(Jid::Full(participant)); + component.send_stanza(success).await?; + return Ok(()); + } + } + + return Ok(()); + } + } else if let Jid::Bare(to) = to.clone() { + // Pinging the component + if to.node.is_none() { + let domain = BareJid::from_str(&to.domain).unwrap(); + let success = success.clone().with_from(Jid::Bare(domain)); + component.send_stanza(success).await?; + // Pinging a room + } else { + if let Some(room) = rooms.get(&to) { + if room.is_joined(&from) { + let success = success.clone().with_from(Jid::Bare(to)); + component.send_stanza(success).await?; + return Ok(()); + } + } + let success = success.clone().with_from(Jid::Bare(to)); + component.send_stanza(success).await?; + } + + return Ok(()); + } + + let error = Iq::from_error( + iq.id, + StanzaError { + type_: ErrorType::Modify, + defined_condition: DefinedCondition::NotAcceptable, + by: Some(Jid::Bare(BareJid::from(to))), + texts: BTreeMap::new(), + other: None, + }, + ); + component.send_stanza(error).await?; + + Ok(()) + } + Err(err) => { + error!("Failed to parse iq as ping: {}", err); + return Err(err.into()); + } + } +} + +async fn handle_iq( + component: &mut C, + iq: Iq, + rooms: &mut HashMap, +) -> Result<(), Error> { match iq.payload { IqType::Get(ref payload) => { if payload.is("query", ns::DISCO_INFO) { handle_iq_disco(component, iq.clone(), payload.clone()).await? + } else if payload.is("ping", ns::PING) { + handle_iq_ping(component, iq.clone(), payload.clone(), rooms).await? } else { // We MUST answer unhandled get iqs with a service-unavailable error. let error = StanzaError::new( @@ -209,7 +289,7 @@ pub async fn handle_stanza( debug!("RECV {}", String::from(&elem)); if elem.is("iq", ns::COMPONENT_ACCEPT) { let iq = Iq::try_from(elem).unwrap(); - handle_iq(component, iq).await?; + handle_iq(component, iq, rooms).await?; } else if elem.is("message", ns::COMPONENT_ACCEPT) { let message = Message::try_from(elem).unwrap(); handle_message(component, message).await?; diff --git a/src/room.rs b/src/room.rs index fe1f898..ac8830d 100644 --- a/src/room.rs +++ b/src/room.rs @@ -310,6 +310,10 @@ impl Room { Ok(()) } + + pub fn is_joined>(&self, _jid: &J) -> bool { + true + } } #[derive(Debug, Clone, PartialEq)] @@ -367,6 +371,10 @@ impl Occupant { Err(Error::NonexistantSession(real)) } } + + pub fn contains>(&self, _jid: &J) -> bool { + true + } } impl IntoIterator for Occupant { diff --git a/src/tests/iq.rs b/src/tests/iq.rs index afbb2bf..4e2dcf7 100644 --- a/src/tests/iq.rs +++ b/src/tests/iq.rs @@ -16,12 +16,16 @@ use crate::component::TestComponent; use crate::handlers::handle_stanza; use crate::room::Room; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::str::FromStr; use lazy_static::lazy_static; use xmpp_parsers::{ + disco::{DiscoInfoQuery, DiscoInfoResult, Feature}, iq::{Iq, IqType}, + muc::Muc, + ping::Ping, + presence::{Presence, Type as PresenceType}, stanza_error::{DefinedCondition, ErrorType, StanzaError}, BareJid, Element, FullJid, Jid, }; @@ -62,3 +66,34 @@ async fn test_iq_unimplemented() { component.expect(reply); handle_stanza(&mut component, &mut rooms).await.unwrap(); } + +#[tokio::test] +async fn test_self_ping_participant_non_existing() { + let realjid1 = Jid::Full(FullJid::from_str("foo@bar/qxx").unwrap()); + let roomjid = COMPONENT_JID.clone().with_node("room"); + let participant1 = roomjid.clone().with_resource("nick1"); + + let ping: Element = Iq { + from: Some(realjid1.clone()), + to: Some(Jid::Full(participant1.clone())), + id: String::from("ping"), + payload: IqType::Get(Ping {}.into()), + } + .into(); + + let mut component = TestComponent::new(vec![ping]); + let mut rooms: HashMap = HashMap::new(); + + component.expect(Into::::into(Iq::from_error( + "ping", + StanzaError { + type_: ErrorType::Modify, + defined_condition: DefinedCondition::NotAcceptable, + by: Some(Jid::Bare(roomjid)), + texts: BTreeMap::new(), + other: None, + }, + ))); + + handle_stanza(&mut component, &mut rooms).await.unwrap(); +}