Skip to main content

tf_rust_engineio/
lib.rs

1//! # Rust-engineio-client
2//!
3//! An implementation of a engine.io client written in the rust programming language. This implementation currently
4//! supports revision 4 of the engine.io protocol. If you have any connection issues with this client,
5//! make sure the server uses at least revision 4 of the engine.io protocol.
6//!
7//! ## Example usage
8//!
9//! ``` rust
10//! use tf_rust_engineio::{ClientBuilder, Client, packet::{Packet, PacketId}};
11//! use url::Url;
12//! use bytes::Bytes;
13//!
14//! // get a client with an `on_open` callback
15//! let client: Client = ClientBuilder::new(Url::parse("http://localhost:4201").unwrap())
16//!      .on_open(|_| println!("Connection opened!"))
17//!      .build()
18//!      .expect("Creating client failed");
19//!
20//! // connect to the server
21//! client.connect().expect("Connection failed");
22//!
23//! // create a packet, in this case a message packet and emit it
24//! let packet = Packet::new(PacketId::Message, Bytes::from_static(b"Hello World"));
25//! client.emit(packet).expect("Server unreachable");
26//!
27//! // disconnect from the server
28//! client.disconnect().expect("Disconnect failed")
29//! ```
30//!
31//! The main entry point for using this crate is the [`ClientBuilder`] (or [`asynchronous::ClientBuilder`] respectively)
32//! which provides the opportunity to define how you want to connect to a certain endpoint.
33//! The following connection methods are available:
34//! * `build`: Build websocket if allowed, if not fall back to polling. Standard configuration.
35//! * `build_polling`: enforces a `polling` transport.
36//! * `build_websocket_with_upgrade`: Build socket with a polling transport then upgrade to websocket transport (if possible).
37//! * `build_websocket`: Build socket with only a websocket transport, crashes when websockets are not allowed.
38//!
39//!
40//! ## Current features
41//!
42//! This implementation now supports all of the features of the engine.io protocol mentioned [here](https://github.com/socketio/engine.io-protocol).
43//! This includes various transport options, the possibility of sending engine.io packets and registering the
44//! common engine.io event callbacks:
45//! * on_open
46//! * on_close
47//! * on_data
48//! * on_error
49//! * on_packet
50//!
51//! It is also possible to pass in custom tls configurations via the `TlsConnector` as well
52//! as custom headers for the opening request.
53//!
54//! ## Async version
55//!
56//! The crate also ships with an asynchronous version that can be enabled with a feature flag.
57//! The async version implements the same features mentioned above.
58//! The asynchronous version has a similar API, just with async functions. Currently the futures
59//! can only be executed with [`tokio`](https://tokio.rs). In the first benchmarks the async version
60//! showed improvements of up to 93% in speed.
61//! To make use of the async version, import the crate as follows:
62//! ```toml
63//! [dependencies]
64//! tf-rust-engineio = { version = "0.6.1", features = ["async"] }
65//! ```
66//!
67#![allow(clippy::rc_buffer)]
68#![warn(clippy::complexity)]
69#![warn(clippy::style)]
70#![warn(clippy::perf)]
71#![warn(clippy::correctness)]
72// Allow large error types - Error enum contains rich context (HTTP responses, URLs, etc.)
73// which is intentional for debugging. Boxing would add indirection overhead.
74#![allow(clippy::result_large_err)]
75/// A small macro that spawns a scoped thread. Used for calling the callback
76/// functions.
77macro_rules! spawn_scoped {
78    ($e:expr) => {
79        std::thread::scope(|s| {
80            s.spawn(|| $e);
81        });
82    };
83}
84
85pub mod asynchronous;
86mod callback;
87pub mod client;
88/// Generic header map
89pub mod header;
90pub mod packet;
91mod socket;
92pub mod transport;
93pub mod transports;
94
95pub const ENGINE_IO_VERSION: i32 = 4;
96
97/// Contains the error type which will be returned with every result in this
98/// crate. Handles all kinds of errors.
99pub mod error;
100
101pub use client::{Client, ClientBuilder};
102pub use error::Error;
103pub use packet::{Packet, PacketId};
104
105#[cfg(test)]
106pub(crate) mod test {
107    use super::*;
108    use native_tls::TlsConnector;
109    const CERT_PATH: &str = "../ci/cert/ca.crt";
110    use native_tls::Certificate;
111    use std::fs::File;
112    use std::io::Read;
113
114    pub(crate) fn tls_connector() -> error::Result<TlsConnector> {
115        let cert_path = std::env::var("CA_CERT_PATH").unwrap_or_else(|_| CERT_PATH.to_owned());
116        let mut cert_file = File::open(cert_path)?;
117        let mut buf = vec![];
118        cert_file.read_to_end(&mut buf)?;
119        let cert: Certificate = Certificate::from_pem(&buf[..]).unwrap();
120        Ok(TlsConnector::builder()
121            // ONLY USE FOR TESTING!
122            .danger_accept_invalid_hostnames(true)
123            .add_root_certificate(cert)
124            .build()
125            .unwrap())
126    }
127    /// The `engine.io` server for testing runs on port 4201
128    const SERVER_URL: &str = "http://localhost:4201";
129    /// The `engine.io` server that refuses upgrades runs on port 4203
130    const SERVER_POLLING_URL: &str = "http://localhost:4203";
131    const SERVER_URL_SECURE: &str = "https://localhost:4202";
132    use url::Url;
133
134    pub(crate) fn engine_io_server() -> crate::error::Result<Url> {
135        let url = std::env::var("ENGINE_IO_SERVER").unwrap_or_else(|_| SERVER_URL.to_owned());
136        Ok(Url::parse(&url)?)
137    }
138
139    pub(crate) fn engine_io_polling_server() -> crate::error::Result<Url> {
140        let url = std::env::var("ENGINE_IO_POLLING_SERVER")
141            .unwrap_or_else(|_| SERVER_POLLING_URL.to_owned());
142        Ok(Url::parse(&url)?)
143    }
144
145    pub(crate) fn engine_io_server_secure() -> crate::error::Result<Url> {
146        let url = std::env::var("ENGINE_IO_SECURE_SERVER")
147            .unwrap_or_else(|_| SERVER_URL_SECURE.to_owned());
148        Ok(Url::parse(&url)?)
149    }
150
151    /// Spawns a minimal one-shot HTTP server on a random local port that always
152    /// replies with the given status code and body. Used for verifying that the
153    /// polling transport surfaces server-side error bodies via
154    /// [`crate::Error::HttpErrorWithBody`]. Accepts a small number of
155    /// connections so a single test can drive both GET and POST paths.
156    pub(crate) fn spawn_http_error_mock(status: u16, body: &'static str) -> Url {
157        use std::io::{Read, Write};
158        use std::net::TcpListener;
159
160        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
161        let port = listener.local_addr().unwrap().port();
162
163        std::thread::spawn(move || {
164            for _ in 0..8 {
165                let Ok((mut stream, _)) = listener.accept() else {
166                    break;
167                };
168                let mut buf = [0u8; 2048];
169                let _ = stream.read(&mut buf);
170                let response = format!(
171                    "HTTP/1.1 {status} ERR\r\nContent-Type: application/json\r\nContent-Length: {len}\r\nConnection: close\r\n\r\n{body}",
172                    status = status,
173                    len = body.len(),
174                    body = body
175                );
176                let _ = stream.write_all(response.as_bytes());
177            }
178        });
179
180        Url::parse(&format!("http://127.0.0.1:{}/", port)).unwrap()
181    }
182}