implement bind

This commit is contained in:
cel 🌸 2024-12-02 21:50:15 +00:00
parent 859a19820d
commit be198ca15b
15 changed files with 654 additions and 11 deletions

View File

@ -2,6 +2,7 @@ use std::str::Utf8Error;
use rsasl::mechname::MechanismNameError; use rsasl::mechname::MechanismNameError;
use crate::stanza::client::error::Error as ClientError;
use crate::{jid::ParseError, stanza::sasl::Failure}; use crate::{jid::ParseError, stanza::sasl::Failure};
#[derive(Debug)] #[derive(Debug)]
@ -22,12 +23,14 @@ pub enum Error {
Negotiation, Negotiation,
TlsRequired, TlsRequired,
UnexpectedEnd, UnexpectedEnd,
UnexpectedElement, UnexpectedElement(peanuts::Element),
UnexpectedText, UnexpectedText,
XML(peanuts::Error), XML(peanuts::Error),
SASL(SASLError), SASL(SASLError),
JID(ParseError), JID(ParseError),
Authentication(Failure), Authentication(Failure),
ClientError(ClientError),
MissingError,
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -13,6 +13,9 @@ use trust_dns_resolver::proto::rr::domain::IntoLabel;
use crate::connection::{Tls, Unencrypted}; use crate::connection::{Tls, Unencrypted};
use crate::error::Error; 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::sasl::{Auth, Challenge, Mechanisms, Response, ServerResponse};
use crate::stanza::starttls::{Proceed, StartTls}; use crate::stanza::starttls::{Proceed, StartTls};
use crate::stanza::stream::{Feature, Features, Stream}; use crate::stanza::stream::{Feature, Features, Stream};
@ -147,7 +150,96 @@ where
} }
pub async fn bind(&mut self) -> Result<()> { 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] #[instrument]
@ -324,9 +416,12 @@ impl std::fmt::Debug for Jabber<Unencrypted> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::time::Duration;
use super::*; use super::*;
use crate::connection::Connection; use crate::connection::Connection;
use test_log::test; use test_log::test;
use tokio::time::sleep;
#[test(tokio::test)] #[test(tokio::test)]
async fn start_stream() { async fn start_stream() {
@ -373,4 +468,18 @@ mod tests {
Feature::Unknown => todo!(), 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
}
} }

View File

@ -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<BindType>,
}
impl FromElement for Bind {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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<Self> {
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<Self> {
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<Self> {
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())
}
}

View File

@ -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<String>,
r#type: ErrorType,
// children (sequence)
error: StanzaError,
text: Option<Text>,
}
impl FromElement for Error {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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<Self, Self::Err> {
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(),
}
}
}

124
src/stanza/client/iq.rs Normal file
View File

@ -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<JID>,
pub id: String,
pub to: Option<JID>,
pub r#type: IqType,
pub lang: Option<String>,
// children
// ##other
pub query: Option<Query>,
pub errors: Vec<Error>,
}
#[derive(Clone)]
pub enum Query {
Bind(Bind),
Unsupported,
}
impl FromElement for Query {
fn from_element(element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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<Self> {
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<Self, Self::Err> {
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(),
}
}
}

View File

@ -0,0 +1,37 @@
use crate::JID;
pub struct Message {
from: Option<JID>,
id: Option<String>,
to: Option<JID>,
r#type: Option<MessageType>,
// children
subject: Option<Subject>,
body: Option<Body>,
thread: Option<Thread>,
lang: Option<String>,
}
pub enum MessageType {
Chat,
Error,
Groupchat,
Headline,
Normal,
}
pub struct Body {
lang: Option<String>,
body: Option<String>,
}
pub struct Subject {
lang: Option<String>,
subject: Option<String>,
}
pub struct Thread {
// TODO: NOT DONE
parent: Option<String>,
thread: Option<String>,
}

6
src/stanza/client/mod.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod error;
pub mod iq;
pub mod message;
pub mod presence;
pub const XMLNS: &str = "jabber:client";

View File

@ -0,0 +1,48 @@
use peanuts::element::{FromElement, IntoElement};
use crate::JID;
use super::error::Error;
pub struct Presence {
from: Option<JID>,
id: Option<String>,
to: Option<JID>,
r#type: PresenceType,
lang: Option<String>,
// children
show: Option<Show>,
status: Option<Status>,
priority: Option<Priority>,
errors: Vec<Error>,
// ##other
// content: Vec<Box<dyn AsElement>>,
}
pub enum PresenceType {
Error,
Probe,
Subscribe,
Subscribed,
Unavailable,
Unsubscribe,
Unsubscribed,
}
pub enum Show {
Away,
Chat,
Dnd,
Xa,
}
pub struct Status {
lang: Option<String>,
status: String1024,
}
// minLength 1 maxLength 1024
pub struct String1024(String);
// xs:byte
pub struct Priority(u8);

126
src/stanza/error.rs Normal file
View File

@ -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<String>),
InternalServerError,
ItemNotFound,
JidMalformed,
NotAcceptable,
NotAllowed,
NotAuthorized,
PolicyViolation,
RecipientUnavailable,
Redirect(Option<String>),
RegistrationRequired,
RemoteServerNotFound,
RemoteServerTimeout,
ResourceConstraint,
ServiceUnavailable,
SubscriptionRequired,
UndefinedCondition,
UnexpectedRequest,
}
impl FromElement for Error {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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<String>,
text: Option<String>,
}
impl FromElement for Text {
fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
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())
}
}

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1,11 +1,12 @@
use peanuts::declaration::VersionInfo; use peanuts::declaration::VersionInfo;
pub mod bind; pub mod bind;
pub mod iq; pub mod client;
pub mod message; pub mod error;
pub mod presence;
pub mod sasl; pub mod sasl;
pub mod starttls; pub mod starttls;
pub mod stream; pub mod stream;
pub static XML_VERSION: VersionInfo = VersionInfo::One; pub static XML_VERSION: VersionInfo = VersionInfo::One;
pub use error::Error;

View File

@ -1 +0,0 @@

View File

@ -17,7 +17,7 @@ impl IntoElement for StartTls {
let mut builder = Element::builder("starttls", Some(XMLNS)); let mut builder = Element::builder("starttls", Some(XMLNS));
if self.required { if self.required {
builder = builder.push_child(Element::builder("required", Some(XMLNS))) builder = builder.push_child(Required)
} }
builder 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)] #[derive(Debug)]
pub struct Proceed; pub struct Proceed;

View File

@ -5,13 +5,14 @@ use peanuts::XML_NS;
use peanuts::{element::Name, Element}; use peanuts::{element::Name, Element};
use tracing::debug; use tracing::debug;
use crate::stanza::bind;
use crate::{Error, JID}; use crate::{Error, JID};
use super::client;
use super::sasl::{self, Mechanisms}; use super::sasl::{self, Mechanisms};
use super::starttls::{self, StartTls}; use super::starttls::{self, StartTls};
pub const XMLNS: &str = "http://etherx.jabber.org/streams"; pub const XMLNS: &str = "http://etherx.jabber.org/streams";
pub const XMLNS_CLIENT: &str = "jabber:client";
// MUST be qualified by stream namespace // MUST be qualified by stream namespace
// #[derive(XmlSerialize, XmlDeserialize)] // #[derive(XmlSerialize, XmlDeserialize)]
@ -53,7 +54,7 @@ impl IntoElement for Stream {
fn builder(&self) -> ElementBuilder { fn builder(&self) -> ElementBuilder {
Element::builder("stream", Some(XMLNS.to_string())) Element::builder("stream", Some(XMLNS.to_string()))
.push_namespace_declaration_override(Some("stream"), XMLNS) .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("to", self.to.clone())
.push_attribute_opt("from", self.from.clone()) .push_attribute_opt("from", self.from.clone())
.push_attribute_opt("id", self.id.clone()) .push_attribute_opt("id", self.id.clone())
@ -150,6 +151,10 @@ impl FromElement for Feature {
debug!("identified mechanisms"); debug!("identified mechanisms");
Ok(Feature::Sasl(Mechanisms::from_element(element)?)) Ok(Feature::Sasl(Mechanisms::from_element(element)?))
} }
(Some(bind::XMLNS), "bind") => {
debug!("identified bind");
Ok(Feature::Bind)
}
_ => { _ => {
debug!("identified unknown feature"); debug!("identified unknown feature");
Ok(Feature::Unknown) Ok(Feature::Unknown)