//! Handling of all client2server iq stanzas use quick_xml::events::Event; use lavina_core::room::{RoomId, RoomRegistry}; use proto_xmpp::bind::{BindRequest, BindResponse, Jid, Name, Server}; use proto_xmpp::client::{Iq, IqError, IqErrorType, IqType}; use proto_xmpp::disco::{Feature, Identity, InfoQuery, Item, ItemQuery}; use proto_xmpp::mam::{Fin, Set}; use proto_xmpp::roster::RosterQuery; use proto_xmpp::session::Session; use proto_xmpp::xml::ToXml; use crate::proto::IqClientBody; use crate::XmppConnection; impl<'a> XmppConnection<'a> { pub async fn handle_iq(&self, output: &mut Vec>, iq: Iq) { match iq.body { IqClientBody::Bind(req) => { let req = Iq { from: None, id: iq.id, to: None, r#type: IqType::Result, body: self.bind(&req).await, }; req.serialize(output); } IqClientBody::Session(_) => { let req = Iq { from: None, id: iq.id, to: None, r#type: IqType::Result, body: Session, }; req.serialize(output); } IqClientBody::Roster(_) => { let req = Iq { from: None, id: iq.id, to: None, r#type: IqType::Result, body: RosterQuery, }; req.serialize(output); } IqClientBody::DiscoInfo(info) => { let response = self.disco_info(iq.to.as_ref(), &info).await; match response { Ok(response) => { let req = Iq { from: iq.to, id: iq.id, to: None, r#type: IqType::Result, body: response, }; req.serialize(output); } Err(response) => { let req = Iq { from: iq.to, id: iq.id, to: None, r#type: IqType::Error, body: response, }; req.serialize(output); } } } IqClientBody::DiscoItem(item) => { let response = self.disco_items(iq.to.as_ref(), &item, self.rooms).await; let req = Iq { from: iq.to, id: iq.id, to: None, r#type: IqType::Result, body: response, }; req.serialize(output); } IqClientBody::MessageArchiveRequest(_) => { let response = Iq { from: iq.to, id: iq.id, to: None, r#type: IqType::Result, body: Fin { set: Set { count: Some(0) }, }, }; response.serialize(output); } _ => { let req = Iq { from: None, id: iq.id, to: None, r#type: IqType::Error, body: IqError { r#type: IqErrorType::Cancel, }, }; req.serialize(output); } } } async fn bind(&self, req: &BindRequest) -> BindResponse { BindResponse(Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }) } async fn disco_info(&self, to: Option<&Jid>, req: &InfoQuery) -> Result { let identity; let feature; match to { Some(Jid { name: None, server, resource: None, }) if server.0 == self.hostname => { identity = vec![Identity { category: "server".into(), name: None, r#type: "im".into(), }]; feature = vec![ Feature::new("http://jabber.org/protocol/disco#info"), Feature::new("http://jabber.org/protocol/disco#items"), Feature::new("iq"), Feature::new("presence"), ] } Some(Jid { name: None, server, resource: None, }) if server.0 == self.hostname_rooms => { identity = vec![Identity { category: "conference".into(), name: Some("Chat rooms".into()), r#type: "text".into(), }]; feature = vec![ Feature::new("http://jabber.org/protocol/disco#info"), Feature::new("http://jabber.org/protocol/disco#items"), Feature::new("http://jabber.org/protocol/muc"), ] } Some(Jid { name: Some(room_name), server, resource: None, }) if server.0 == self.hostname_rooms => { let room_id = RoomId::from(room_name.0.clone()).unwrap(); let Some(_) = self.rooms.get_room(&room_id).await else { // TODO should return item-not-found // example: // // // Conference room does not exist // return Err(IqError { r#type: IqErrorType::Cancel, }); }; identity = vec![Identity { category: "conference".into(), name: Some(room_id.into_inner().to_string()), r#type: "text".into(), }]; feature = vec![ Feature::new("http://jabber.org/protocol/disco#info"), Feature::new("http://jabber.org/protocol/disco#items"), Feature::new("http://jabber.org/protocol/muc"), ] } _ => { identity = vec![]; feature = vec![]; } }; Ok(InfoQuery { node: None, identity, feature, }) } async fn disco_items(&self, to: Option<&Jid>, req: &ItemQuery, rooms: &RoomRegistry) -> ItemQuery { let item = match to { Some(Jid { name: None, server, resource: None, }) if server.0 == self.hostname => { vec![Item { jid: Jid { name: None, server: Server(self.hostname_rooms.clone()), resource: None, }, name: None, node: None, }] } Some(Jid { name: None, server, resource: None, }) if server.0 == self.hostname_rooms => { let room_list = rooms.get_all_rooms().await; room_list .into_iter() .map(|room_info| Item { jid: Jid { name: Some(Name(room_info.id.into_inner())), server: Server(self.hostname_rooms.clone()), resource: None, }, name: None, node: None, }) .collect() } _ => vec![], }; ItemQuery { item } } } #[cfg(test)] mod tests { use super::*; use crate::{launch, Authenticated, RunningServer, ServerConfig}; use lavina_core::player::PlayerId; use lavina_core::repo::{Storage, StorageConfig}; use lavina_core::LavinaCore; use prometheus::Registry as MetricsRegistry; use proto_xmpp::bind::{BindRequest, Resource}; use quick_xml::{Reader, Writer}; use std::collections::HashMap; use std::io::Cursor; struct TestServer { metrics: MetricsRegistry, storage: Storage, core: LavinaCore, } impl TestServer { async fn start() -> anyhow::Result { let _ = tracing_subscriber::fmt::try_init(); let metrics = MetricsRegistry::new(); let storage = Storage::open(StorageConfig { db_path: ":memory:".into(), }) .await?; let core = LavinaCore::new(metrics.clone(), storage.clone()).await?; Ok(TestServer { metrics, storage, core }) } async fn shutdown(self) -> anyhow::Result<()> { self.core.shutdown().await?; self.storage.close().await?; Ok(()) } } #[tokio::test] async fn test_handle_iq_bind() { let server = TestServer::start().await.unwrap(); server.storage.create_user("tester").await.unwrap(); let player_id = PlayerId::from("tester").unwrap(); let mut conn = server.core.players.connect_to_player(&player_id).await; let user = Authenticated { player_id, xmpp_name: Name("tester".into()), xmpp_resource: Resource("tester".into()), xmpp_muc_name: Resource("tester".into()), }; let conn = XmppConnection { user: &user, user_handle: &mut conn, rooms: &server.core.rooms, hostname: "localhost".into(), hostname_rooms: "rooms.localhost".into(), }; let result = conn.bind(&BindRequest(Resource("whatever".into()))).await; let expected = BindResponse(Jid { name: Some(Name("tester".into())), server: Server("localhost".into()), resource: Some(Resource("tester".into())), }); assert_eq!(expected, result); server.shutdown().await.unwrap(); } }