diff --git a/src/error.rs b/src/error.rs index a1f853b..b5cf446 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,7 @@ use std::str::Utf8Error; use rsasl::mechname::MechanismNameError; +use crate::stanza::client::error::Error as ClientError; use crate::{jid::ParseError, stanza::sasl::Failure}; #[derive(Debug)] @@ -22,12 +23,14 @@ pub enum Error { Negotiation, TlsRequired, UnexpectedEnd, - UnexpectedElement, + UnexpectedElement(peanuts::Element), UnexpectedText, XML(peanuts::Error), SASL(SASLError), JID(ParseError), Authentication(Failure), + ClientError(ClientError), + MissingError, } #[derive(Debug)] diff --git a/src/jabber.rs b/src/jabber.rs index 599879d..96cd73a 100644 --- a/src/jabber.rs +++ b/src/jabber.rs @@ -13,6 +13,9 @@ use trust_dns_resolver::proto::rr::domain::IntoLabel; use crate::connection::{Tls, Unencrypted}; use crate::error::Error; +use crate::stanza::bind::{Bind, BindType, FullJidType, ResourceType}; +use crate::stanza::client::error::Error as ClientError; +use crate::stanza::client::iq::{Iq, IqType, Query}; use crate::stanza::sasl::{Auth, Challenge, Mechanisms, Response, ServerResponse}; use crate::stanza::starttls::{Proceed, StartTls}; use crate::stanza::stream::{Feature, Features, Stream}; @@ -147,7 +150,96 @@ where } pub async fn bind(&mut self) -> Result<()> { - todo!() + let iq_id = nanoid::nanoid!(); + if let Some(resource) = self.jid.clone().unwrap().resourcepart { + let iq = Iq { + from: None, + id: iq_id.clone(), + to: None, + r#type: IqType::Set, + lang: None, + query: Some(Query::Bind(Bind { + r#type: Some(BindType::Resource(ResourceType(resource))), + })), + errors: Vec::new(), + }; + self.writer.write_full(&iq).await?; + let result: Iq = self.reader.read().await?; + match result { + Iq { + from: _, + id, + to: _, + r#type: IqType::Result, + lang: _, + query: + Some(Query::Bind(Bind { + r#type: Some(BindType::Jid(FullJidType(jid))), + })), + errors: _, + } if id == iq_id => { + self.jid = Some(jid); + return Ok(()); + } + Iq { + from: _, + id, + to: _, + r#type: IqType::Error, + lang: _, + query: None, + errors, + } if id == iq_id => { + return Err(Error::ClientError( + errors.first().ok_or(Error::MissingError)?.clone(), + )) + } + _ => return Err(Error::UnexpectedElement(result.into_element())), + } + } else { + let iq = Iq { + from: None, + id: iq_id.clone(), + to: None, + r#type: IqType::Set, + lang: None, + query: Some(Query::Bind(Bind { r#type: None })), + errors: Vec::new(), + }; + self.writer.write_full(&iq).await?; + let result: Iq = self.reader.read().await?; + match result { + Iq { + from: _, + id, + to: _, + r#type: IqType::Result, + lang: _, + query: + Some(Query::Bind(Bind { + r#type: Some(BindType::Jid(FullJidType(jid))), + })), + errors: _, + } if id == iq_id => { + self.jid = Some(jid); + return Ok(()); + } + Iq { + from: _, + id, + to: _, + r#type: IqType::Error, + lang: _, + query: None, + errors, + } if id == iq_id => { + return Err(Error::ClientError( + errors.first().ok_or(Error::MissingError)?.clone(), + )) + } + _ => return Err(Error::UnexpectedElement(result.into_element())), + } + } } #[instrument] @@ -324,9 +416,12 @@ impl std::fmt::Debug for Jabber { #[cfg(test)] mod tests { + use std::time::Duration; + use super::*; use crate::connection::Connection; use test_log::test; + use tokio::time::sleep; #[test(tokio::test)] async fn start_stream() { @@ -373,4 +468,18 @@ mod tests { Feature::Unknown => todo!(), } } + + #[tokio::test] + async fn negotiate() { + let jabber = Connection::connect_user("test@blos.sm", "slayed".to_string()) + .await + .unwrap() + .ensure_tls() + .await + .unwrap() + .negotiate() + .await + .unwrap(); + sleep(Duration::from_secs(5)).await + } } diff --git a/src/stanza/bind.rs b/src/stanza/bind.rs index 8b13789..0e67a83 100644 --- a/src/stanza/bind.rs +++ b/src/stanza/bind.rs @@ -1 +1,99 @@ +use peanuts::{ + element::{FromElement, IntoElement}, + Element, +}; +use crate::JID; + +pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-bind"; + +#[derive(Clone)] +pub struct Bind { + pub r#type: Option, +} + +impl FromElement for Bind { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("bind"); + element.check_name(XMLNS); + + let r#type = element.pop_child_opt()?; + + Ok(Bind { r#type }) + } +} + +impl IntoElement for Bind { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("bind", Some(XMLNS)).push_child_opt(self.r#type.clone()) + } +} + +#[derive(Clone)] +pub enum BindType { + Resource(ResourceType), + Jid(FullJidType), +} + +impl FromElement for BindType { + fn from_element(element: peanuts::Element) -> peanuts::element::DeserializeResult { + match element.identify() { + (Some(XMLNS), "resource") => { + Ok(BindType::Resource(ResourceType::from_element(element)?)) + } + (Some(XMLNS), "jid") => Ok(BindType::Jid(FullJidType::from_element(element)?)), + _ => Err(peanuts::DeserializeError::UnexpectedElement(element)), + } + } +} + +impl IntoElement for BindType { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + BindType::Resource(resource_type) => resource_type.builder(), + BindType::Jid(full_jid_type) => full_jid_type.builder(), + } + } +} + +// minLength 8 maxLength 3071 +#[derive(Clone)] +pub struct FullJidType(pub JID); + +impl FromElement for FullJidType { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("jid"); + element.check_namespace(XMLNS); + + let jid = element.pop_value()?; + + Ok(FullJidType(jid)) + } +} + +impl IntoElement for FullJidType { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("jid", Some(XMLNS)).push_text(self.0.clone()) + } +} + +// minLength 1 maxLength 1023 +#[derive(Clone)] +pub struct ResourceType(pub String); + +impl FromElement for ResourceType { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("resource")?; + element.check_namespace(XMLNS)?; + + let resource = element.pop_value()?; + + Ok(ResourceType(resource)) + } +} + +impl IntoElement for ResourceType { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("resource", Some(XMLNS)).push_text(self.0.clone()) + } +} diff --git a/src/stanza/client/error.rs b/src/stanza/client/error.rs new file mode 100644 index 0000000..fc5ed21 --- /dev/null +++ b/src/stanza/client/error.rs @@ -0,0 +1,83 @@ +use std::str::FromStr; + +use peanuts::element::{FromElement, IntoElement}; +use peanuts::{DeserializeError, Element}; + +use crate::stanza::error::Text; +use crate::stanza::Error as StanzaError; + +use super::XMLNS; + +#[derive(Clone, Debug)] +pub struct Error { + by: Option, + r#type: ErrorType, + // children (sequence) + error: StanzaError, + text: Option, +} + +impl FromElement for Error { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("error")?; + element.check_name(XMLNS)?; + + let by = element.attribute_opt("by")?; + let r#type = element.attribute("type")?; + let error = element.pop_child_one()?; + let text = element.pop_child_opt()?; + + Ok(Error { + by, + r#type, + error, + text, + }) + } +} + +impl IntoElement for Error { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("error", Some(XMLNS)) + .push_attribute_opt("by", self.by.clone()) + .push_attribute("type", self.r#type) + .push_child(self.error.clone()) + .push_child_opt(self.text.clone()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ErrorType { + Auth, + Cancel, + Continue, + Modify, + Wait, +} + +impl FromStr for ErrorType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result { + match s { + "auth" => Ok(ErrorType::Auth), + "cancel" => Ok(ErrorType::Cancel), + "continue" => Ok(ErrorType::Continue), + "modify" => Ok(ErrorType::Modify), + "wait" => Ok(ErrorType::Wait), + _ => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for ErrorType { + fn to_string(&self) -> String { + match self { + ErrorType::Auth => "auth".to_string(), + ErrorType::Cancel => "cancel".to_string(), + ErrorType::Continue => "continue".to_string(), + ErrorType::Modify => "modify".to_string(), + ErrorType::Wait => "wait".to_string(), + } + } +} diff --git a/src/stanza/client/iq.rs b/src/stanza/client/iq.rs new file mode 100644 index 0000000..b23f8b7 --- /dev/null +++ b/src/stanza/client/iq.rs @@ -0,0 +1,124 @@ +use std::str::FromStr; + +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; + +use crate::{ + stanza::{ + bind::{self, Bind}, + client::error::Error, + }, + JID, +}; + +use super::XMLNS; + +pub struct Iq { + pub from: Option, + pub id: String, + pub to: Option, + pub r#type: IqType, + pub lang: Option, + // children + // ##other + pub query: Option, + pub errors: Vec, +} + +#[derive(Clone)] +pub enum Query { + Bind(Bind), + Unsupported, +} + +impl FromElement for Query { + fn from_element(element: peanuts::Element) -> peanuts::element::DeserializeResult { + match element.identify() { + (Some(bind::XMLNS), "bind") => Ok(Query::Bind(Bind::from_element(element)?)), + _ => Ok(Query::Unsupported), + } + } +} + +impl IntoElement for Query { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + Query::Bind(bind) => bind.builder(), + // TODO: consider what to do if attempt to serialize unsupported + Query::Unsupported => todo!(), + } + } +} + +impl FromElement for Iq { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("iq")?; + element.check_namespace(XMLNS)?; + + let from = element.attribute_opt("from")?; + let id = element.attribute("id")?; + let to = element.attribute_opt("to")?; + let r#type = element.attribute("type")?; + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let query = element.pop_child_opt()?; + let errors = element.pop_children()?; + + Ok(Iq { + from, + id, + to, + r#type, + lang, + query, + errors, + }) + } +} + +impl IntoElement for Iq { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("iq", Some(XMLNS)) + .push_attribute_opt("from", self.from.clone()) + .push_attribute("id", self.id.clone()) + .push_attribute_opt("to", self.to.clone()) + .push_attribute("type", self.r#type) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_child_opt(self.query.clone()) + .push_children(self.errors.clone()) + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum IqType { + Error, + Get, + Result, + Set, +} + +impl FromStr for IqType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result { + match s { + "error" => Ok(IqType::Error), + "get" => Ok(IqType::Get), + "result" => Ok(IqType::Result), + "set" => Ok(IqType::Set), + _ => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for IqType { + fn to_string(&self) -> String { + match self { + IqType::Error => "error".to_string(), + IqType::Get => "get".to_string(), + IqType::Result => "result".to_string(), + IqType::Set => "set".to_string(), + } + } +} diff --git a/src/stanza/client/message.rs b/src/stanza/client/message.rs new file mode 100644 index 0000000..cdfda5d --- /dev/null +++ b/src/stanza/client/message.rs @@ -0,0 +1,37 @@ +use crate::JID; + +pub struct Message { + from: Option, + id: Option, + to: Option, + r#type: Option, + // children + subject: Option, + body: Option, + thread: Option, + lang: Option, +} + +pub enum MessageType { + Chat, + Error, + Groupchat, + Headline, + Normal, +} + +pub struct Body { + lang: Option, + body: Option, +} + +pub struct Subject { + lang: Option, + subject: Option, +} + +pub struct Thread { + // TODO: NOT DONE + parent: Option, + thread: Option, +} diff --git a/src/stanza/client/mod.rs b/src/stanza/client/mod.rs new file mode 100644 index 0000000..7b25b15 --- /dev/null +++ b/src/stanza/client/mod.rs @@ -0,0 +1,6 @@ +pub mod error; +pub mod iq; +pub mod message; +pub mod presence; + +pub const XMLNS: &str = "jabber:client"; diff --git a/src/stanza/client/presence.rs b/src/stanza/client/presence.rs new file mode 100644 index 0000000..46194f3 --- /dev/null +++ b/src/stanza/client/presence.rs @@ -0,0 +1,48 @@ +use peanuts::element::{FromElement, IntoElement}; + +use crate::JID; + +use super::error::Error; + +pub struct Presence { + from: Option, + id: Option, + to: Option, + r#type: PresenceType, + lang: Option, + // children + show: Option, + status: Option, + priority: Option, + errors: Vec, + // ##other + // content: Vec>, +} + +pub enum PresenceType { + Error, + Probe, + Subscribe, + Subscribed, + Unavailable, + Unsubscribe, + Unsubscribed, +} + +pub enum Show { + Away, + Chat, + Dnd, + Xa, +} + +pub struct Status { + lang: Option, + status: String1024, +} + +// minLength 1 maxLength 1024 +pub struct String1024(String); + +// xs:byte +pub struct Priority(u8); diff --git a/src/stanza/error.rs b/src/stanza/error.rs new file mode 100644 index 0000000..99c1f15 --- /dev/null +++ b/src/stanza/error.rs @@ -0,0 +1,126 @@ +// https://datatracker.ietf.org/doc/html/rfc6120#appendix-A.8 + +use peanuts::{ + element::{FromElement, IntoElement}, + Element, XML_NS, +}; + +pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas"; + +#[derive(Clone, Debug)] +pub enum Error { + BadRequest, + Conflict, + FeatureNotImplemented, + Forbidden, + Gone(Option), + InternalServerError, + ItemNotFound, + JidMalformed, + NotAcceptable, + NotAllowed, + NotAuthorized, + PolicyViolation, + RecipientUnavailable, + Redirect(Option), + RegistrationRequired, + RemoteServerNotFound, + RemoteServerTimeout, + ResourceConstraint, + ServiceUnavailable, + SubscriptionRequired, + UndefinedCondition, + UnexpectedRequest, +} + +impl FromElement for Error { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + let error; + match element.identify() { + (Some(XMLNS), "bad-request") => error = Error::BadRequest, + (Some(XMLNS), "conflict") => error = Error::Conflict, + (Some(XMLNS), "feature-not-implemented") => error = Error::FeatureNotImplemented, + (Some(XMLNS), "forbidden") => error = Error::Forbidden, + (Some(XMLNS), "gone") => return Ok(Error::Gone(element.pop_value_opt()?)), + (Some(XMLNS), "internal-server-error") => error = Error::InternalServerError, + (Some(XMLNS), "item-not-found") => error = Error::ItemNotFound, + (Some(XMLNS), "jid-malformed") => error = Error::JidMalformed, + (Some(XMLNS), "not-acceptable") => error = Error::NotAcceptable, + (Some(XMLNS), "not-allowed") => error = Error::NotAllowed, + (Some(XMLNS), "not-authorized") => error = Error::NotAuthorized, + (Some(XMLNS), "policy-violation") => error = Error::PolicyViolation, + (Some(XMLNS), "recipient-unavailable") => error = Error::RecipientUnavailable, + (Some(XMLNS), "redirect") => return Ok(Error::Redirect(element.pop_value_opt()?)), + (Some(XMLNS), "registration-required") => error = Error::RegistrationRequired, + (Some(XMLNS), "remote-server-not-found") => error = Error::RemoteServerNotFound, + (Some(XMLNS), "remote-server-timeout") => error = Error::RemoteServerTimeout, + (Some(XMLNS), "resource-constraint") => error = Error::ResourceConstraint, + (Some(XMLNS), "service-unavailable") => error = Error::ServiceUnavailable, + (Some(XMLNS), "subscription-required") => error = Error::SubscriptionRequired, + (Some(XMLNS), "undefined-condition") => error = Error::UndefinedCondition, + (Some(XMLNS), "unexpected-request") => error = Error::UnexpectedRequest, + _ => return Err(peanuts::DeserializeError::UnexpectedElement(element)), + } + element.no_more_content()?; + return Ok(error); + } +} + +impl IntoElement for Error { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + Error::BadRequest => Element::builder("bad-request", Some(XMLNS)), + Error::Conflict => Element::builder("conflict", Some(XMLNS)), + Error::FeatureNotImplemented => { + Element::builder("feature-not-implemented", Some(XMLNS)) + } + Error::Forbidden => Element::builder("forbidden", Some(XMLNS)), + Error::Gone(r) => Element::builder("gone", Some(XMLNS)).push_text_opt(r.clone()), + Error::InternalServerError => Element::builder("internal-server-error", Some(XMLNS)), + Error::ItemNotFound => Element::builder("item-not-found", Some(XMLNS)), + Error::JidMalformed => Element::builder("jid-malformed", Some(XMLNS)), + Error::NotAcceptable => Element::builder("not-acceptable", Some(XMLNS)), + Error::NotAllowed => Element::builder("not-allowed", Some(XMLNS)), + Error::NotAuthorized => Element::builder("not-authorized", Some(XMLNS)), + Error::PolicyViolation => Element::builder("policy-violation", Some(XMLNS)), + Error::RecipientUnavailable => Element::builder("recipient-unavailable", Some(XMLNS)), + Error::Redirect(r) => { + Element::builder("redirect", Some(XMLNS)).push_text_opt(r.clone()) + } + Error::RegistrationRequired => Element::builder("registration-required", Some(XMLNS)), + Error::RemoteServerNotFound => Element::builder("remote-server-not-found", Some(XMLNS)), + Error::RemoteServerTimeout => Element::builder("remote-server-timeout", Some(XMLNS)), + Error::ResourceConstraint => Element::builder("resource-constraint", Some(XMLNS)), + Error::ServiceUnavailable => Element::builder("service-unavailable", Some(XMLNS)), + Error::SubscriptionRequired => Element::builder("subscription-required", Some(XMLNS)), + Error::UndefinedCondition => Element::builder("undefined-condition", Some(XMLNS)), + Error::UnexpectedRequest => Element::builder("unexpected-request", Some(XMLNS)), + } + } +} + +#[derive(Clone, Debug)] +pub struct Text { + lang: Option, + text: Option, +} + +impl FromElement for Text { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult { + element.check_name("text")?; + element.check_name(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let text = element.pop_value_opt()?; + + Ok(Text { lang, text }) + } +} + +impl IntoElement for Text { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("text", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text_opt(self.text.clone()) + } +} diff --git a/src/stanza/iq.rs b/src/stanza/iq.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/stanza/iq.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/stanza/message.rs b/src/stanza/message.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/stanza/message.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/stanza/mod.rs b/src/stanza/mod.rs index 4f1ce48..84e80ab 100644 --- a/src/stanza/mod.rs +++ b/src/stanza/mod.rs @@ -1,11 +1,12 @@ use peanuts::declaration::VersionInfo; pub mod bind; -pub mod iq; -pub mod message; -pub mod presence; +pub mod client; +pub mod error; pub mod sasl; pub mod starttls; pub mod stream; pub static XML_VERSION: VersionInfo = VersionInfo::One; + +pub use error::Error; diff --git a/src/stanza/presence.rs b/src/stanza/presence.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/stanza/presence.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/stanza/starttls.rs b/src/stanza/starttls.rs index 33721ab..fb66711 100644 --- a/src/stanza/starttls.rs +++ b/src/stanza/starttls.rs @@ -17,7 +17,7 @@ impl IntoElement for StartTls { let mut builder = Element::builder("starttls", Some(XMLNS)); if self.required { - builder = builder.push_child(Element::builder("required", Some(XMLNS))) + builder = builder.push_child(Required) } builder @@ -52,6 +52,12 @@ impl FromElement for Required { } } +impl IntoElement for Required { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("required", Some(XMLNS)) + } +} + #[derive(Debug)] pub struct Proceed; diff --git a/src/stanza/stream.rs b/src/stanza/stream.rs index fecace5..c49a2bc 100644 --- a/src/stanza/stream.rs +++ b/src/stanza/stream.rs @@ -5,13 +5,14 @@ use peanuts::XML_NS; use peanuts::{element::Name, Element}; use tracing::debug; +use crate::stanza::bind; use crate::{Error, JID}; +use super::client; use super::sasl::{self, Mechanisms}; use super::starttls::{self, StartTls}; pub const XMLNS: &str = "http://etherx.jabber.org/streams"; -pub const XMLNS_CLIENT: &str = "jabber:client"; // MUST be qualified by stream namespace // #[derive(XmlSerialize, XmlDeserialize)] @@ -53,7 +54,7 @@ impl IntoElement for Stream { fn builder(&self) -> ElementBuilder { Element::builder("stream", Some(XMLNS.to_string())) .push_namespace_declaration_override(Some("stream"), XMLNS) - .push_namespace_declaration_override(None::<&str>, XMLNS_CLIENT) + .push_namespace_declaration_override(None::<&str>, client::XMLNS) .push_attribute_opt("to", self.to.clone()) .push_attribute_opt("from", self.from.clone()) .push_attribute_opt("id", self.id.clone()) @@ -150,6 +151,10 @@ impl FromElement for Feature { debug!("identified mechanisms"); Ok(Feature::Sasl(Mechanisms::from_element(element)?)) } + (Some(bind::XMLNS), "bind") => { + debug!("identified bind"); + Ok(Feature::Bind) + } _ => { debug!("identified unknown feature"); Ok(Feature::Unknown)