2020-05-29 22:07:36 +00:00
use futures ::{ sink ::SinkExt , task ::Poll , Future , Sink , Stream } ;
use sasl ::common ::{ ChannelBinding , Credentials } ;
use std ::mem ::replace ;
use std ::pin ::Pin ;
use std ::task ::Context ;
use tokio ::net ::TcpStream ;
use tokio ::task ::JoinHandle ;
2023-06-01 09:58:04 +00:00
use xmpp_parsers ::{ ns , Element , Jid } ;
2020-05-29 22:07:36 +00:00
use super ::auth ::auth ;
use super ::bind ::bind ;
use crate ::event ::Event ;
2020-12-06 14:23:29 +00:00
use crate ::happy_eyeballs ::{ connect_to_host , connect_with_srv } ;
2020-05-29 22:07:36 +00:00
use crate ::starttls ::starttls ;
use crate ::xmpp_codec ::Packet ;
2023-12-29 06:09:33 +00:00
use crate ::xmpp_stream ::{ self , add_stanza_id , XMPPStream } ;
2020-05-29 22:07:36 +00:00
use crate ::{ Error , ProtocolError } ;
2023-12-29 06:09:33 +00:00
#[ cfg(feature = " tls-native " ) ]
use tokio_native_tls ::TlsStream ;
#[ cfg(all(feature = " tls-rust " , not(feature = " tls-native " ))) ]
use tokio_rustls ::client ::TlsStream ;
2020-05-29 22:07:36 +00:00
/// XMPP client connection and state
///
/// It is able to reconnect. TODO: implement session management.
///
/// This implements the `futures` crate's [`Stream`](#impl-Stream) and
/// [`Sink`](#impl-Sink<Packet>) traits.
2023-12-29 06:09:33 +00:00
pub struct Client < C : ServerConnector > {
config : Config < C > ,
state : ClientState < C ::Stream > ,
2020-09-11 15:39:12 +00:00
reconnect : bool ,
// TODO: tls_required=true
}
2023-12-29 06:09:33 +00:00
/// XMPP client configuration
#[ derive(Clone, Debug) ]
pub struct Config < C > {
/// jid of the account
pub jid : Jid ,
/// password of the account
pub password : String ,
/// server configuration for the account
pub server : C ,
}
/// Trait called to connect to an XMPP server, perhaps called multiple times
pub trait ServerConnector : Clone + core ::fmt ::Debug + Send + Unpin + 'static {
/// The type of Stream this ServerConnector produces
type Stream : AsyncReadAndWrite ;
/// This must return the connection ready to login, ie if starttls is involved, after TLS has been started, and then after the <stream headers are exchanged
fn connect (
& self ,
jid : & Jid ,
) -> impl std ::future ::Future < Output = Result < XMPPStream < Self ::Stream > , Error > > + Send ;
}
2020-12-06 14:23:29 +00:00
/// XMPP server connection configuration
2023-03-20 16:27:13 +00:00
#[ derive(Clone, Debug) ]
2020-12-06 14:23:29 +00:00
pub enum ServerConfig {
2023-03-20 16:27:13 +00:00
/// Use SRV record to find server host
2020-12-06 14:23:29 +00:00
UseSrv ,
#[ allow(unused) ]
2023-03-20 16:27:13 +00:00
/// Manually define server host and port
2020-12-06 14:23:29 +00:00
Manual {
2023-03-20 16:27:13 +00:00
/// Server host name
2020-12-06 14:23:29 +00:00
host : String ,
2023-03-20 16:27:13 +00:00
/// Server port
2020-12-06 14:23:29 +00:00
port : u16 ,
} ,
}
2023-12-29 06:09:33 +00:00
impl ServerConnector for ServerConfig {
type Stream = TlsStream < TcpStream > ;
async fn connect ( & self , jid : & Jid ) -> Result < XMPPStream < Self ::Stream > , Error > {
// TCP connection
let tcp_stream = match self {
ServerConfig ::UseSrv = > {
connect_with_srv ( jid . domain_str ( ) , " _xmpp-client._tcp " , 5222 ) . await ?
}
ServerConfig ::Manual { host , port } = > connect_to_host ( host . as_str ( ) , * port ) . await ? ,
} ;
// Unencryped XMPPStream
let xmpp_stream =
xmpp_stream ::XMPPStream ::start ( tcp_stream , jid . clone ( ) , ns ::JABBER_CLIENT . to_owned ( ) )
. await ? ;
if xmpp_stream . stream_features . can_starttls ( ) {
// TlsStream
let tls_stream = starttls ( xmpp_stream ) . await ? ;
// Encrypted XMPPStream
xmpp_stream ::XMPPStream ::start ( tls_stream , jid . clone ( ) , ns ::JABBER_CLIENT . to_owned ( ) )
. await
} else {
return Err ( Error ::Protocol ( ProtocolError ::NoTls ) ) ;
}
}
2020-05-29 22:07:36 +00:00
}
2023-12-29 06:09:33 +00:00
/// trait used by XMPPStream type
pub trait AsyncReadAndWrite : tokio ::io ::AsyncRead + tokio ::io ::AsyncWrite + Unpin + Send { }
impl < T : tokio ::io ::AsyncRead + tokio ::io ::AsyncWrite + Unpin + Send > AsyncReadAndWrite for T { }
2020-05-29 22:07:36 +00:00
2023-12-29 06:09:33 +00:00
enum ClientState < S : AsyncReadAndWrite > {
2020-05-29 22:07:36 +00:00
Invalid ,
Disconnected ,
2023-12-29 06:09:33 +00:00
Connecting ( JoinHandle < Result < XMPPStream < S > , Error > > ) ,
Connected ( XMPPStream < S > ) ,
2020-05-29 22:07:36 +00:00
}
2023-12-29 06:09:33 +00:00
impl Client < ServerConfig > {
2020-05-29 22:07:36 +00:00
/// Start a new XMPP client
///
/// Start polling the returned instance so that it will connect
/// and yield events.
2023-06-01 09:58:04 +00:00
pub fn new < J : Into < Jid > , P : Into < String > > ( jid : J , password : P ) -> Self {
2020-09-11 15:39:12 +00:00
let config = Config {
2023-06-01 09:58:04 +00:00
jid : jid . into ( ) ,
2020-09-11 15:39:12 +00:00
password : password . into ( ) ,
2020-12-06 14:23:29 +00:00
server : ServerConfig ::UseSrv ,
2020-09-11 15:39:12 +00:00
} ;
2023-06-01 09:58:04 +00:00
Self ::new_with_config ( config )
2020-05-29 22:07:36 +00:00
}
2023-12-29 06:09:33 +00:00
}
2020-05-29 22:07:36 +00:00
2023-12-29 06:09:33 +00:00
impl < C : ServerConnector > Client < C > {
2020-05-29 22:07:36 +00:00
/// Start a new client given that the JID is already parsed.
2023-12-29 06:09:33 +00:00
pub fn new_with_config ( config : Config < C > ) -> Self {
2022-03-24 02:26:30 +00:00
let connect = tokio ::spawn ( Self ::connect (
2020-09-11 15:39:12 +00:00
config . server . clone ( ) ,
config . jid . clone ( ) ,
config . password . clone ( ) ,
2020-09-11 15:05:49 +00:00
) ) ;
2020-05-29 22:07:36 +00:00
let client = Client {
2020-12-06 13:28:57 +00:00
config ,
2022-03-24 02:26:30 +00:00
state : ClientState ::Connecting ( connect ) ,
2020-05-29 22:07:36 +00:00
reconnect : false ,
} ;
client
}
/// Set whether to reconnect (`true`) or let the stream end
/// (`false`) when a connection to the server has ended.
pub fn set_reconnect ( & mut self , reconnect : bool ) -> & mut Self {
self . reconnect = reconnect ;
self
}
2020-09-11 15:05:49 +00:00
async fn connect (
2023-12-29 06:09:33 +00:00
server : C ,
2020-09-11 15:05:49 +00:00
jid : Jid ,
password : String ,
2023-12-29 06:09:33 +00:00
) -> Result < XMPPStream < C ::Stream > , Error > {
2023-06-21 16:30:25 +00:00
let username = jid . node_str ( ) . unwrap ( ) ;
2020-05-29 22:07:36 +00:00
let password = password ;
2023-12-29 06:09:33 +00:00
let xmpp_stream = server . connect ( & jid ) . await ? ;
2020-05-29 22:07:36 +00:00
let creds = Credentials ::default ( )
. with_username ( username )
. with_password ( password )
. with_channel_binding ( ChannelBinding ::None ) ;
// Authenticated (unspecified) stream
let stream = auth ( xmpp_stream , creds ) . await ? ;
// Authenticated XMPPStream
2020-05-29 22:14:32 +00:00
let xmpp_stream =
2020-05-29 23:19:06 +00:00
xmpp_stream ::XMPPStream ::start ( stream , jid , ns ::JABBER_CLIENT . to_owned ( ) ) . await ? ;
2020-05-29 22:07:36 +00:00
// XMPPStream bound to user session
let xmpp_stream = bind ( xmpp_stream ) . await ? ;
Ok ( xmpp_stream )
}
/// Get the client's bound JID (the one reported by the XMPP
/// server).
pub fn bound_jid ( & self ) -> Option < & Jid > {
match self . state {
ClientState ::Connected ( ref stream ) = > Some ( & stream . jid ) ,
_ = > None ,
}
}
/// Send stanza
pub async fn send_stanza ( & mut self , stanza : Element ) -> Result < ( ) , Error > {
2023-06-04 17:27:46 +00:00
self . send ( Packet ::Stanza ( add_stanza_id ( stanza , ns ::JABBER_CLIENT ) ) )
. await
2020-05-29 22:07:36 +00:00
}
/// End connection by sending `</stream:stream>`
///
/// You may expect the server to respond with the same. This
/// client will then drop its connection.
///
/// Make sure to disable reconnect.
pub async fn send_end ( & mut self ) -> Result < ( ) , Error > {
self . send ( Packet ::StreamEnd ) . await
}
}
/// Incoming XMPP events
///
/// In an `async fn` you may want to use this with `use
/// futures::stream::StreamExt;`
2023-12-29 06:09:33 +00:00
impl < C : ServerConnector > Stream for Client < C > {
2020-05-29 22:07:36 +00:00
type Item = Event ;
/// Low-level read on the XMPP stream, allowing the underlying
/// machinery to:
///
/// * connect,
/// * starttls,
/// * authenticate,
/// * bind a session, and finally
/// * receive stanzas
///
/// ...for your client
fn poll_next ( mut self : Pin < & mut Self > , cx : & mut Context ) -> Poll < Option < Self ::Item > > {
let state = replace ( & mut self . state , ClientState ::Invalid ) ;
match state {
ClientState ::Invalid = > panic! ( " Invalid client state " ) ,
ClientState ::Disconnected if self . reconnect = > {
// TODO: add timeout
2022-03-24 02:26:30 +00:00
let connect = tokio ::spawn ( Self ::connect (
2020-09-11 15:39:12 +00:00
self . config . server . clone ( ) ,
self . config . jid . clone ( ) ,
self . config . password . clone ( ) ,
2020-09-11 15:05:49 +00:00
) ) ;
2022-03-24 02:26:30 +00:00
self . state = ClientState ::Connecting ( connect ) ;
2020-05-29 22:07:36 +00:00
self . poll_next ( cx )
}
ClientState ::Disconnected = > Poll ::Ready ( None ) ,
2022-03-22 22:29:25 +00:00
ClientState ::Connecting ( mut connect ) = > match Pin ::new ( & mut connect ) . poll ( cx ) {
Poll ::Ready ( Ok ( Ok ( stream ) ) ) = > {
let bound_jid = stream . jid . clone ( ) ;
self . state = ClientState ::Connected ( stream ) ;
Poll ::Ready ( Some ( Event ::Online {
bound_jid ,
resumed : false ,
} ) )
2020-05-29 22:07:36 +00:00
}
2022-03-22 22:29:25 +00:00
Poll ::Ready ( Ok ( Err ( e ) ) ) = > {
self . state = ClientState ::Disconnected ;
return Poll ::Ready ( Some ( Event ::Disconnected ( e . into ( ) ) ) ) ;
}
Poll ::Ready ( Err ( e ) ) = > {
self . state = ClientState ::Disconnected ;
panic! ( " connect task: {} " , e ) ;
}
Poll ::Pending = > {
self . state = ClientState ::Connecting ( connect ) ;
Poll ::Pending
}
} ,
2020-05-29 22:07:36 +00:00
ClientState ::Connected ( mut stream ) = > {
// Poll sink
match Pin ::new ( & mut stream ) . poll_ready ( cx ) {
Poll ::Pending = > ( ) ,
Poll ::Ready ( Ok ( ( ) ) ) = > ( ) ,
Poll ::Ready ( Err ( e ) ) = > {
self . state = ClientState ::Disconnected ;
return Poll ::Ready ( Some ( Event ::Disconnected ( e . into ( ) ) ) ) ;
}
} ;
// Poll stream
2023-06-21 11:43:24 +00:00
//
// This needs to be a loop in order to ignore packets we don’ t care about, or those
// we want to handle elsewhere. Returning something isn’ t correct in those two
// cases because it would signal to tokio that the XMPPStream is also done, while
// there could be additional packets waiting for us.
//
// The proper solution is thus a loop which we exit once we have something to
// return.
loop {
match Pin ::new ( & mut stream ) . poll_next ( cx ) {
Poll ::Ready ( None ) = > {
// EOF
self . state = ClientState ::Disconnected ;
return Poll ::Ready ( Some ( Event ::Disconnected ( Error ::Disconnected ) ) ) ;
}
Poll ::Ready ( Some ( Ok ( Packet ::Stanza ( stanza ) ) ) ) = > {
// Receive stanza
self . state = ClientState ::Connected ( stream ) ;
return Poll ::Ready ( Some ( Event ::Stanza ( stanza ) ) ) ;
}
Poll ::Ready ( Some ( Ok ( Packet ::Text ( _ ) ) ) ) = > {
// Ignore text between stanzas
}
Poll ::Ready ( Some ( Ok ( Packet ::StreamStart ( _ ) ) ) ) = > {
// <stream:stream>
self . state = ClientState ::Disconnected ;
return Poll ::Ready ( Some ( Event ::Disconnected (
ProtocolError ::InvalidStreamStart . into ( ) ,
) ) ) ;
}
Poll ::Ready ( Some ( Ok ( Packet ::StreamEnd ) ) ) = > {
// End of stream: </stream:stream>
self . state = ClientState ::Disconnected ;
return Poll ::Ready ( Some ( Event ::Disconnected ( Error ::Disconnected ) ) ) ;
}
Poll ::Pending = > {
// Try again later
self . state = ClientState ::Connected ( stream ) ;
return Poll ::Pending ;
}
Poll ::Ready ( Some ( Err ( e ) ) ) = > {
self . state = ClientState ::Disconnected ;
return Poll ::Ready ( Some ( Event ::Disconnected ( e . into ( ) ) ) ) ;
}
2020-05-29 22:07:36 +00:00
}
}
}
}
}
}
/// Outgoing XMPP packets
///
/// See `send_stanza()` for an `async fn`
2023-12-29 06:09:33 +00:00
impl < C : ServerConnector > Sink < Packet > for Client < C > {
2020-05-29 22:07:36 +00:00
type Error = Error ;
fn start_send ( mut self : Pin < & mut Self > , item : Packet ) -> Result < ( ) , Self ::Error > {
match self . state {
ClientState ::Connected ( ref mut stream ) = > {
Pin ::new ( stream ) . start_send ( item ) . map_err ( | e | e . into ( ) )
}
_ = > Err ( Error ::InvalidState ) ,
}
}
fn poll_ready ( mut self : Pin < & mut Self > , cx : & mut Context ) -> Poll < Result < ( ) , Self ::Error > > {
match self . state {
ClientState ::Connected ( ref mut stream ) = > {
Pin ::new ( stream ) . poll_ready ( cx ) . map_err ( | e | e . into ( ) )
}
_ = > Poll ::Pending ,
}
}
fn poll_flush ( mut self : Pin < & mut Self > , cx : & mut Context ) -> Poll < Result < ( ) , Self ::Error > > {
match self . state {
ClientState ::Connected ( ref mut stream ) = > {
Pin ::new ( stream ) . poll_flush ( cx ) . map_err ( | e | e . into ( ) )
}
_ = > Poll ::Pending ,
}
}
fn poll_close ( mut self : Pin < & mut Self > , cx : & mut Context ) -> Poll < Result < ( ) , Self ::Error > > {
match self . state {
ClientState ::Connected ( ref mut stream ) = > {
Pin ::new ( stream ) . poll_close ( cx ) . map_err ( | e | e . into ( ) )
}
_ = > Poll ::Pending ,
}
}
}