Skip to main content

tf_rust_engineio/asynchronous/client/
async_client.rs

1use std::{fmt::Debug, pin::Pin};
2
3use crate::{
4    asynchronous::{async_socket::Socket as InnerSocket, generator::StreamGenerator},
5    error::Result,
6    Packet,
7};
8use async_stream::try_stream;
9use futures_util::{Stream, StreamExt};
10
11/// An engine.io client that allows interaction with the connected engine.io
12/// server. This client provides means for connecting, disconnecting and sending
13/// packets to the server.
14///
15/// ## Note:
16/// There is no need to put this Client behind an `Arc`, as the type uses `Arc`
17/// internally and provides a shared state beyond all cloned instances.
18#[derive(Clone)]
19pub struct Client {
20    pub(super) socket: InnerSocket,
21    generator: StreamGenerator<Packet>,
22}
23
24impl Client {
25    pub(super) fn new(socket: InnerSocket) -> Self {
26        Client {
27            socket: socket.clone(),
28            generator: StreamGenerator::new(Self::stream(socket)),
29        }
30    }
31
32    pub async fn close(&self) -> Result<()> {
33        self.disconnect().await
34    }
35
36    /// Opens the connection to a specified server. The first Pong packet is sent
37    /// to the server to trigger the Ping-cycle.
38    pub async fn connect(&self) -> Result<()> {
39        self.socket.connect().await
40    }
41
42    /// Disconnects the connection.
43    pub async fn disconnect(&self) -> Result<()> {
44        let client = self.clone();
45        tokio::spawn(async move {
46            // Clear shared in-flight GET/stream ownership even while aliases remain alive.
47            client.generator.close().await;
48            client.socket.disconnect().await
49        })
50        .await
51        .map_err(std::io::Error::other)?
52    }
53
54    /// Sends a packet to the server.
55    pub async fn emit(&self, packet: Packet) -> Result<()> {
56        self.socket.emit(packet).await
57    }
58
59    /// Static method that returns a generator for each element of the stream.
60    fn stream(
61        socket: InnerSocket,
62    ) -> Pin<Box<impl Stream<Item = Result<Packet>> + 'static + Send>> {
63        Box::pin(try_stream! {
64            let socket = socket.clone();
65            for await item in socket.as_stream() {
66                let packet = item?;
67                socket.handle_incoming_packet(packet.clone()).await?;
68                yield packet;
69            }
70        })
71    }
72
73    /// Check if the underlying transport client is connected.
74    pub fn is_connected(&self) -> bool {
75        self.socket.is_connected()
76    }
77}
78
79impl Stream for Client {
80    type Item = Result<Packet>;
81
82    fn poll_next(
83        mut self: Pin<&mut Self>,
84        cx: &mut std::task::Context<'_>,
85    ) -> std::task::Poll<Option<Self::Item>> {
86        self.generator.poll_next_unpin(cx)
87    }
88}
89
90impl Debug for Client {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("Client")
93            .field("socket", &self.socket)
94            .finish()
95    }
96}
97
98#[cfg(test)]
99mod test {
100
101    use super::*;
102    use crate::{asynchronous::ClientBuilder, header::HeaderMap, packet::PacketId, Error};
103    use bytes::Bytes;
104    use futures_util::StreamExt;
105    use native_tls::TlsConnector;
106    use url::Url;
107
108    /// The purpose of this test is to check whether the Client is properly cloneable or not.
109    /// As the documentation of the engine.io client states, the object needs to maintain it's internal
110    /// state when cloned and the cloned object should reflect the same state throughout the lifetime
111    /// of both objects (initial and cloned).
112    #[tokio::test]
113    async fn test_client_cloneable() -> Result<()> {
114        let url = crate::test::engine_io_server()?;
115
116        let mut sut = builder(url).build().await?;
117        let mut cloned = sut.clone();
118
119        sut.connect().await?;
120
121        // when the underlying socket is connected, the
122        // state should also change on the cloned one
123        assert!(sut.is_connected());
124        assert!(cloned.is_connected());
125
126        // both clients should reflect the same messages.
127        assert_eq!(
128            sut.next().await.unwrap()?,
129            Packet::new(PacketId::Message, "hello client")
130        );
131
132        sut.emit(Packet::new(PacketId::Message, "respond")).await?;
133
134        assert_eq!(
135            cloned.next().await.unwrap()?,
136            Packet::new(PacketId::Message, "Roger Roger")
137        );
138
139        cloned.disconnect().await?;
140
141        // when the underlying socket is disconnected, the
142        // state should also change on the cloned one
143        assert!(!sut.is_connected());
144        assert!(!cloned.is_connected());
145
146        Ok(())
147    }
148
149    #[tokio::test]
150    async fn test_illegal_actions() -> Result<()> {
151        let url = crate::test::engine_io_server()?;
152        let mut sut = builder(url.clone()).build().await?;
153
154        assert!(sut
155            .emit(Packet::new(PacketId::Close, Bytes::new()))
156            .await
157            .is_err());
158
159        sut.connect().await?;
160
161        assert!(sut.next().await.unwrap().is_ok());
162
163        assert!(builder(Url::parse("fake://fake.fake").unwrap())
164            .build_websocket()
165            .await
166            .is_err());
167
168        sut.disconnect().await?;
169
170        Ok(())
171    }
172
173    use reqwest::header::HOST;
174
175    use crate::packet::Packet;
176
177    fn builder(url: Url) -> ClientBuilder {
178        ClientBuilder::new(url)
179            .on_open(|_| {
180                Box::pin(async {
181                    println!("Open event!");
182                })
183            })
184            .on_packet(|packet| {
185                Box::pin(async move {
186                    println!("Received packet: {:?}", packet);
187                })
188            })
189            .on_data(|data| {
190                Box::pin(async move {
191                    println!("Received data: {:?}", std::str::from_utf8(&data));
192                })
193            })
194            .on_close(|_| {
195                Box::pin(async {
196                    println!("Close event!");
197                })
198            })
199            .on_error(|error| {
200                Box::pin(async move {
201                    println!("Error {}", error);
202                })
203            })
204    }
205
206    async fn test_connection(socket: Client) -> Result<()> {
207        let mut socket = socket;
208
209        socket.connect().await.unwrap();
210
211        assert_eq!(
212            socket.next().await.unwrap()?,
213            Packet::new(PacketId::Message, "hello client")
214        );
215        println!("received msg, about to send");
216
217        socket
218            .emit(Packet::new(PacketId::Message, "respond"))
219            .await?;
220
221        println!("send msg");
222
223        assert_eq!(
224            socket.next().await.unwrap()?,
225            Packet::new(PacketId::Message, "Roger Roger")
226        );
227        println!("received 2");
228
229        socket.close().await
230    }
231
232    #[tokio::test]
233    async fn test_connection_long() -> Result<()> {
234        // Long lived socket to receive pings
235        let url = crate::test::engine_io_server()?;
236        let mut socket = builder(url).build().await?;
237
238        socket.connect().await?;
239
240        // hello client
241        assert!(matches!(
242            socket.next().await.unwrap()?,
243            Packet {
244                packet_id: PacketId::Message,
245                ..
246            }
247        ));
248        // Ping
249        assert!(matches!(
250            socket.next().await.unwrap()?,
251            Packet {
252                packet_id: PacketId::Ping,
253                ..
254            }
255        ));
256
257        socket.disconnect().await?;
258
259        assert!(!socket.is_connected());
260
261        Ok(())
262    }
263
264    #[tokio::test]
265    async fn test_connection_dynamic() -> Result<()> {
266        let url = crate::test::engine_io_server()?;
267        let socket = builder(url).build().await?;
268        test_connection(socket).await?;
269
270        let url = crate::test::engine_io_polling_server()?;
271        let socket = builder(url).build().await?;
272        test_connection(socket).await
273    }
274
275    #[tokio::test]
276    async fn test_connection_fallback() -> Result<()> {
277        let url = crate::test::engine_io_server()?;
278        let socket = builder(url).build_with_fallback().await?;
279        test_connection(socket).await?;
280
281        let url = crate::test::engine_io_polling_server()?;
282        let socket = builder(url).build_with_fallback().await?;
283        test_connection(socket).await
284    }
285
286    #[tokio::test]
287    async fn test_connection_dynamic_secure() -> Result<()> {
288        let url = crate::test::engine_io_server_secure()?;
289        let mut socket_builder = builder(url);
290        socket_builder = socket_builder.tls_config(crate::test::tls_connector()?);
291        let socket = socket_builder.build().await?;
292        test_connection(socket).await
293    }
294
295    #[tokio::test]
296    async fn test_connection_polling() -> Result<()> {
297        let url = crate::test::engine_io_server()?;
298        let socket = builder(url).build_polling().await?;
299        test_connection(socket).await
300    }
301
302    #[tokio::test]
303    async fn test_connection_wss() -> Result<()> {
304        let url = crate::test::engine_io_polling_server()?;
305        assert!(builder(url).build_websocket_with_upgrade().await.is_err());
306
307        let host =
308            std::env::var("ENGINE_IO_SECURE_HOST").unwrap_or_else(|_| "localhost".to_owned());
309        let mut url = crate::test::engine_io_server_secure()?;
310
311        let mut headers = HeaderMap::default();
312        headers.insert(HOST, host);
313        let mut builder = builder(url.clone());
314
315        builder = builder.tls_config(crate::test::tls_connector()?);
316        builder = builder.headers(headers.clone());
317        let socket = builder.clone().build_websocket_with_upgrade().await?;
318
319        test_connection(socket).await?;
320
321        let socket = builder.build_websocket().await?;
322
323        test_connection(socket).await?;
324
325        url.set_scheme("wss").unwrap();
326
327        let builder = self::builder(url)
328            .tls_config(crate::test::tls_connector()?)
329            .headers(headers);
330        let socket = builder.clone().build_websocket().await?;
331
332        test_connection(socket).await?;
333
334        assert!(builder.build_websocket_with_upgrade().await.is_err());
335
336        Ok(())
337    }
338
339    #[tokio::test]
340    async fn test_connection_ws() -> Result<()> {
341        let url = crate::test::engine_io_polling_server()?;
342        assert!(builder(url.clone()).build_websocket().await.is_err());
343        assert!(builder(url).build_websocket_with_upgrade().await.is_err());
344
345        let mut url = crate::test::engine_io_server()?;
346
347        let builder = builder(url.clone());
348        let socket = builder.clone().build_websocket().await?;
349        test_connection(socket).await?;
350
351        let socket = builder.build_websocket_with_upgrade().await?;
352        test_connection(socket).await?;
353
354        url.set_scheme("ws").unwrap();
355
356        let builder = self::builder(url);
357        let socket = builder.clone().build_websocket().await?;
358
359        test_connection(socket).await?;
360
361        assert!(builder.build_websocket_with_upgrade().await.is_err());
362
363        Ok(())
364    }
365
366    #[tokio::test]
367    async fn test_open_invariants() -> Result<()> {
368        let url = crate::test::engine_io_server()?;
369        let illegal_url = "this is illegal";
370
371        assert!(Url::parse(illegal_url).is_err());
372
373        let invalid_protocol = "file:///tmp/foo";
374        assert!(builder(Url::parse(invalid_protocol).unwrap())
375            .build()
376            .await
377            .is_err());
378
379        let sut = builder(url.clone()).build().await?;
380        let _error = sut
381            .emit(Packet::new(PacketId::Close, Bytes::new()))
382            .await
383            .expect_err("error");
384        assert!(matches!(Error::IllegalActionBeforeOpen(), _error));
385
386        // test missing match arm in socket constructor
387        let mut headers = HeaderMap::default();
388        // Use the correct Host header value including the port
389        let host =
390            std::env::var("ENGINE_IO_SECURE_HOST").unwrap_or_else(|_| "localhost:4201".to_owned());
391        headers.insert(HOST, host);
392
393        let _ = builder(url.clone())
394            .tls_config(
395                TlsConnector::builder()
396                    .danger_accept_invalid_certs(true)
397                    .build()
398                    .unwrap(),
399            )
400            .build()
401            .await?;
402        let _ = builder(url).headers(headers).build().await?;
403        Ok(())
404    }
405}