diff --git a/Cargo.lock b/Cargo.lock index f298ebb..433db94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -802,7 +802,7 @@ dependencies = [ [[package]] name = "websocketz" -version = "0.1.0" +version = "0.1.1" dependencies = [ "base64 0.22.1", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 2a1c8cd..e4e85a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "websocketz" -version = "0.1.0" +version = "0.1.1" edition = "2024" rust-version = "1.85.1" authors = ["Jad K. Haddad "] diff --git a/README.md b/README.md index 7c22244..ac835fd 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ `zerocopy`, `async`, `no_std` and [`autobahn`](https://github.com/crossbario/autobahn-testsuite) compliant `websockets` implementation. +Please refer to the [Documentation](https://docs.rs/websocketz) for more information. + ## License Licensed under either of diff --git a/src/close_code.rs b/src/close_code.rs index 686e7c8..d6cb579 100644 --- a/src/close_code.rs +++ b/src/close_code.rs @@ -1,4 +1,8 @@ +/// A WebSocket Close code. +/// +/// Indicate why an endpoint is closing the WebSocket connection. #[repr(u16)] +#[non_exhaustive] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum CloseCode { /// Indicates a normal closure, meaning that the purpose for @@ -59,10 +63,15 @@ pub enum CloseCode { /// to a different IP (when multiple targets exist), or reconnect to the same IP /// when a user has performed an action. Again = 1013, + #[doc(hidden)] Tls = 1015, + #[doc(hidden)] Reserved(u16), + #[doc(hidden)] Iana(u16), + #[doc(hidden)] Library(u16), + #[doc(hidden)] Bad(u16), } diff --git a/src/close_frame.rs b/src/close_frame.rs index 217a052..99c9841 100644 --- a/src/close_frame.rs +++ b/src/close_frame.rs @@ -1,5 +1,6 @@ use crate::CloseCode; +/// A WebSocket Close frame. #[derive(Debug)] pub struct CloseFrame<'a> { /// The reason as a code. diff --git a/src/error.rs b/src/error.rs index ee33ddd..9f5ea11 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,34 +1,56 @@ +//! Crate's error module. +//! +//! Contains all error types used throughout the crate. + use core::convert::Infallible; +/// Error decoding a WebSocket frame. #[derive(Debug, thiserror::Error)] pub enum FrameDecodeError { + /// Reserved bits are not zero. #[error("Reserved bits must be zero")] ReservedBitsNotZero, + /// Unmasked frame received from client. + /// /// The server must close the connection when an unmasked frame is received. #[error("Received an unmasked frame from client")] UnmaskedFrameFromClient, + /// Masked frame received from server. + /// /// The client must close the connection when a masked frame is received. #[error("Received a masked frame from server")] MaskedFrameFromServer, + /// Invalid opcode. #[error("Invalid opcode")] InvalidOpCode, - // The payload length comes as a u64, converting it to usize might fail on 32-bit systems + /// Payload length is too large. + // XXX: The payload length comes as a u64, converting it to usize might fail on 32-bit systems #[error("Payload too large")] PayloadTooLarge, + /// Control frame fragmented. + /// + /// Control frames must not be fragmented. #[error("Control frame fragmented")] ControlFrameFragmented, + /// Control frame too large. + /// + /// Control frames must have a payload length of 125 bytes or less. #[error("Control frame too large")] ControlFrameTooLarge, } +/// Error encoding a WebSocket frame. #[derive(Debug, thiserror::Error)] pub enum FrameEncodeError { + /// Write buffer is too small to hold the encoded frame. #[error("Buffer too small")] BufferTooSmall, } +/// Error decoding an HTTP request/response. #[derive(Debug, thiserror::Error)] pub enum HttpDecodeError { + /// Error parsing the HTTP request/response. #[error("Parse error: {0}")] Parse(httparse::Error), } @@ -39,63 +61,83 @@ impl From for HttpDecodeError { } } +/// Error encoding an HTTP request/response. #[derive(Debug, thiserror::Error)] pub enum HttpEncodeError { + /// Write buffer is too small to hold the encoded HTTP request/response. #[error("Buffer too small")] BufferTooSmall, } +/// Protocol specific errors/violations. #[derive(Debug, thiserror::Error)] pub enum ProtocolError { + /// Close frame is invalid. #[error("Invalid close frame")] InvalidCloseFrame, + /// Close code is invalid. #[error("Invalid close code")] InvalidCloseCode, + /// Text message contains invalid UTF-8. #[error("Invalid UTF-8")] InvalidUTF8, + /// Fragment is invalid. + /// + /// This happens when a final fragment is received without any prior fragments. #[error("Invalid fragment")] InvalidFragment, + /// Continuation frame is invalid. + /// + /// This happens when a continuation frame is received without an ongoing fragmented message. #[error("Invalid continuation frame")] InvalidContinuationFrame, } +/// Error reading from a WebSocket connection. #[derive(Debug, thiserror::Error)] pub enum ReadError { + /// Error reading a WebSocket frame from the underlying I/O. #[error("Read frame error: {0}")] ReadFrame( #[source] #[from] framez::ReadError, ), + /// Error reading an HTTP request/response from the underlying I/O. #[error("Read http error: {0}")] ReadHttp( #[source] #[from] framez::ReadError, ), + /// Protocol error. #[error("Protocol error: {0}")] Protocol( #[source] #[from] ProtocolError, ), + /// Fragments buffer is too small to read a frame. #[error("Fragments buffer too small to read a frame")] FragmentsBufferTooSmall, } +/// Error writing to a WebSocket connection. #[derive(Debug, thiserror::Error)] pub enum WriteError { /// Websocket connection is closed. /// - /// To close the TCP connection, you should drop the [`WebSocket`](crate::WebSocket) instance. + /// To close the TCP connection, you should drop/close the underlying I/O instance. #[error("Connection closed")] ConnectionClosed, + /// Error writing a WebSocket frame to the underlying I/O. #[error("Write frame error: {0}")] WriteFrame( #[source] #[from] framez::WriteError, ), + /// Error writing an HTTP request/response to the underlying I/O. #[error("Write http error: {0}")] WriteHttp( #[source] @@ -104,6 +146,10 @@ pub enum WriteError { ), } +/// Error establishing a WebSocket handshake. +/// +/// # Generic Parameters +/// `E`: User-defined error type for custom errors during the handshake. #[derive(Debug, thiserror::Error)] pub enum HandshakeError { /// Use of the wrong HTTP method (the WebSocket protocol requires the GET method to be used). @@ -112,52 +158,75 @@ pub enum HandshakeError { /// Wrong HTTP version used (the WebSocket protocol requires version 1.1 or higher). #[error("HTTP version must be 1.1 or higher")] WrongHttpVersion, + /// Connection was closed during the handshake. #[error("Connection closed during handshake")] ConnectionClosed, + /// Invalid status code. (Should be 101 for switching protocols.) #[error("Invalid status code")] InvalidStatusCode, + /// Missing or invalid (`Upgrade`: `websocket`) header. #[error("Missing or invalid upgrade header")] MissingOrInvalidUpgrade, + /// Missing or invalid (`Connection`: `upgrade`) header. #[error("Missing or invalid connection header")] MissingOrInvalidConnection, + /// Missing or invalid (`Sec-WebSocket-Accept`) header. #[error("Missing or invalid sec websocket accept header")] MissingOrInvalidAccept, + /// Missing or invalid (`Sec-WebSocket-Version`) header. #[error("Missing or invalid sec websocket version header")] MissingOrInvalidSecVersion, + /// Missing (`Sec-WebSocket-Key`) header. #[error("Missing sec websocket key header")] MissingSecKey, + /// Other error. + /// + /// User-defined error type. #[error("Other: {0}")] Other(E), } +/// Fragmentation error. #[derive(Debug, thiserror::Error)] pub enum FragmentationError { + /// Fragment size is zero. #[error("Fragment size must be greater than 0")] InvalidFragmentSize, + /// Error indicating that a message type that cannot be fragmented was attempted to be fragmented. + /// + /// Only text and binary messages can be fragmented. #[error("Only text and binary messages can be fragmented")] CanNotBeFragmented, } +/// General WebSocket error type. +/// +/// # Generic Parameters +/// `E`: User-defined error type for custom errors during the handshake. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Error reading from the WebSocket connection. #[error("Read error: {0}")] Read( #[from] #[source] ReadError, ), + /// Error writing to the WebSocket connection. #[error("Write error: {0}")] Write( #[from] #[source] WriteError, ), + /// Handshake error. #[error("Handshake error: {0}")] Handshake( #[from] #[source] HandshakeError, ), + /// Fragmentation error. #[error("Fragment error: {0}")] Fragmentation( #[from] diff --git a/src/examples.rs b/src/examples.rs new file mode 100644 index 0000000..144049a --- /dev/null +++ b/src/examples.rs @@ -0,0 +1,296 @@ +//! A collection of examples to be used in the documentation. + +mod lib { + + #[tokio::test] + #[ignore = "Example"] + async fn client() { + use crate::mock::Noop; + use crate::{Message, WebSocket, http::Header, next, options::ConnectOptions}; + + // An already connected stream. + // Impl embedded_io_async Read + Write. + let stream = Noop; + + let read_buffer = &mut [0u8; 1024]; + let write_buffer = &mut [0u8; 1024]; + let fragments_buffer = &mut [0u8; 1024]; + + // Impl rand_core RngCore. + let rng = Noop; + + // Perform a WebSocket handshake as a client. + // 16 is the max number of headers to allocate space for. + let mut websocketz = WebSocket::connect::<16>( + // Set the connection options. + // The path for the WebSocket endpoint as well as any additional HTTP headers. + ConnectOptions::default() + .with_path("/ws") + .expect("Valid path") + .with_headers(&[ + Header { + name: "Host", + value: b"example.com", + }, + Header { + name: "User-Agent", + value: b"WebSocketz", + }, + ]), + stream, + rng, + read_buffer, + write_buffer, + fragments_buffer, + ) + .await + .expect("Handshake failed"); + + // Send a text message. + websocketz + .send(Message::Text("Hello, WebSocket!")) + .await + .expect("Failed to send message"); + + // Receive messages in a loop. + loop { + match next!(websocketz) { + None => { + // Connection closed. + break; + } + Some(Ok(msg)) => { + // Handle received message. + let _ = msg; + } + Some(Err(err)) => { + // Handle error. + let _ = err; + + break; + } + } + } + } + + #[tokio::test] + #[ignore = "Example"] + async fn server() { + use crate::mock::Noop; + use crate::{Message, WebSocket, http::Header, next, options::AcceptOptions}; + + // An already connected stream. + // Impl embedded_io_async Read + Write. + let stream = Noop; + + let read_buffer = &mut [0u8; 1024]; + let write_buffer = &mut [0u8; 1024]; + let fragments_buffer = &mut [0u8; 1024]; + + // Impl rand_core RngCore. + let rng: Noop = Noop; + + // Perform a WebSocket handshake as a server. + // 16 is the max number of headers to allocate space for. + let mut websocketz = WebSocket::accept::<16>( + // Set the acceptance options. + // Any additional HTTP headers. + AcceptOptions::default().with_headers(&[Header { + name: "Server", + value: b"WebSocketz", + }]), + stream, + rng, + read_buffer, + write_buffer, + fragments_buffer, + ) + .await + .expect("Handshake failed"); + + // Receive messages in a loop. + loop { + match next!(websocketz) { + None => { + // Connection closed. + break; + } + Some(Ok(msg)) => { + // Handle received message. + let _ = msg; + + // Send a binary message. + if let Err(err) = websocketz.send(Message::Binary(b"Hello, WebSocket!")).await { + let _ = err; + + break; + } + } + Some(Err(err)) => { + // Handle error. + let _ = err; + + break; + } + } + } + } + + #[tokio::test] + #[ignore = "Example"] + async fn next_macro() { + use crate::mock::Noop; + use crate::{WebSocket, next, options::ConnectOptions}; + + let stream = Noop; + let read_buffer = &mut [0u8; 1024]; + let write_buffer = &mut [0u8; 1024]; + let fragments_buffer = &mut [0u8; 1024]; + let rng = Noop; + + let websocketz = WebSocket::connect::<16>( + ConnectOptions::default() + .with_path("/ws") + .expect("Valid path"), + stream, + rng, + read_buffer, + write_buffer, + fragments_buffer, + ) + .await + .expect("Handshake failed"); + + let existing_websocket = || websocketz; + + let mut websocketz = existing_websocket(); + + while let Some(Ok(msg)) = next!(websocketz) { + // Messages hold references to the websocket buffers. + let _ = msg; + } + } + + #[tokio::test] + #[ignore = "Example"] + async fn send_method_no_compile() { + use crate::mock::Noop; + use crate::{WebSocket, next, options::ConnectOptions}; + + let stream = Noop; + let read_buffer = &mut [0u8; 1024]; + let write_buffer = &mut [0u8; 1024]; + let fragments_buffer = &mut [0u8; 1024]; + let rng = Noop; + + let websocketz = WebSocket::connect::<16>( + ConnectOptions::default() + .with_path("/ws") + .expect("Valid path"), + stream, + rng, + read_buffer, + write_buffer, + fragments_buffer, + ) + .await + .expect("Handshake failed"); + + let existing_websocket = || websocketz; + + let mut websocketz = existing_websocket(); + + while let Some(Ok(_msg)) = next!(websocketz) { + // Messages hold references to the websocket buffers. + // So this will not compile: + // cannot borrow `websocketz` as mutable more than once at a time. + // websocketz.send(msg).await.expect("Failed to send message"); + } + } + + #[tokio::test] + #[ignore = "Example"] + async fn send_macro() { + use crate::mock::Noop; + use crate::{WebSocket, next, options::ConnectOptions, send}; + + let stream = Noop; + let read_buffer = &mut [0u8; 1024]; + let write_buffer = &mut [0u8; 1024]; + let fragments_buffer = &mut [0u8; 1024]; + let rng = Noop; + + let websocketz = WebSocket::connect::<16>( + ConnectOptions::default() + .with_path("/ws") + .expect("Valid path"), + stream, + rng, + read_buffer, + write_buffer, + fragments_buffer, + ) + .await + .expect("Handshake failed"); + + let existing_websocket = || websocketz; + + let mut websocketz = existing_websocket(); + + while let Some(Ok(msg)) = next!(websocketz) { + send!(websocketz, msg).expect("Failed to send message"); + } + } + + #[tokio::test] + #[ignore = "Example"] + async fn split() { + use crate::mock::Noop; + use crate::{Message, WebSocket, next, options::ConnectOptions}; + + let stream = Noop; + let read_buffer = &mut [0u8; 1024]; + let write_buffer = &mut [0u8; 1024]; + let fragments_buffer = &mut [0u8; 1024]; + let rng = Noop; + + let websocketz = WebSocket::connect::<16>( + ConnectOptions::default() + .with_path("/ws") + .expect("Valid path"), + stream, + rng, + read_buffer, + write_buffer, + fragments_buffer, + ) + .await + .expect("Handshake failed"); + + let existing_websocket = || websocketz; + + fn split(_stream: Noop) -> (Noop, Noop) { + // Let's assume we split the stream here. + + (Noop, Noop) + } + + let websocketz = existing_websocket(); + + let (mut websocketz_read, mut websocketz_write) = websocketz.split_with(split); + + websocketz_write + .send(Message::Text("Hello, WebSocket!")) + .await + .expect("Failed to send message"); + + while let Some(Ok(msg)) = next!(websocketz_read) { + // `send` method works here, + // because `websocketz_read` and `websocketz_write` do not hold the same references. + websocketz_write + .send(msg) + .await + .expect("Failed to send message"); + } + } +} diff --git a/src/http.rs b/src/http.rs index c597d85..5197e12 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,3 +1,7 @@ +//! HTTP request and response types. +//! +//! The [`Header`] type is re-exported from the [`httparse`] crate. + use framez::{decode::Decoder, encode::Encoder}; pub use httparse::Header; use httparse::Status; @@ -95,6 +99,7 @@ impl Encoder> for OutResponseCodec { } } +/// An HTTP response. #[derive(Debug)] pub struct Response<'buf, const N: usize> { /// The response minor version, such as `1` for `HTTP/1.1`. @@ -110,6 +115,7 @@ pub struct Response<'buf, const N: usize> { } impl<'buf, const N: usize> Response<'buf, N> { + /// Creates a new [`Response`]. pub const fn new( version: u8, code: u16, @@ -124,18 +130,22 @@ impl<'buf, const N: usize> Response<'buf, N> { } } + /// Returns the HTTP version. pub const fn version(&self) -> u8 { self.version } + /// Returns the response code. pub const fn code(&self) -> u16 { self.code } + /// Returns the reason-phrase. pub const fn reason(&self) -> &'buf str { self.reason } + /// Returns the headers. pub const fn headers(&self) -> &[Header<'buf>] { &self.headers } @@ -252,6 +262,7 @@ impl Encoder> for OutRequestCodec { } } +/// An HTTP request. #[derive(Debug)] pub struct Request<'buf, const N: usize> { /// The request method, such as `GET`. @@ -265,6 +276,7 @@ pub struct Request<'buf, const N: usize> { } impl<'buf, const N: usize> Request<'buf, N> { + /// Creates a new [`Request`]. pub const fn new( method: &'buf str, path: &'buf str, @@ -279,18 +291,22 @@ impl<'buf, const N: usize> Request<'buf, N> { } } + /// Returns the request method. pub const fn method(&self) -> &'buf str { self.method } + /// Returns the request path. pub const fn path(&self) -> &'buf str { self.path } + /// Returns the HTTP version. pub const fn version(&self) -> u8 { self.version } + /// Returns the headers. pub const fn headers(&self) -> &[Header<'buf>] { &self.headers } diff --git a/src/lib.rs b/src/lib.rs index 163f614..fc2105c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,346 @@ //! `zerocopy`, `async`, `no_std` and [`autobahn`](https://github.com/crossbario/autobahn-testsuite) compliant `websockets` implementation. +//! +//! # Traits +//! +//! This library is based on [`embedded_io_async`](https://docs.rs/embedded-io-async/latest/embedded_io_async/)'s +//! [`Read`](https://docs.rs/embedded-io-async/latest/embedded_io_async/trait.Read.html) and [`Write`](https://docs.rs/embedded-io-async/latest/embedded_io_async/trait.Write.html) and [`rand_core`](https://docs.rs/rand_core/latest/rand_core/)'s [`RngCore`](https://docs.rs/rand_core/latest/rand_core/trait.RngCore.html) traits. +//! +//! It's recommended to use [`embedded_io_adapters`](https://docs.rs/embedded-io-adapters/0.6.1/embedded_io_adapters/) if you are using other async `Read` and `Write` traits like [`tokio`](https://docs.rs/tokio/latest/tokio/index.html)'s [`AsyncRead`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncRead.html) and [`AsyncWrite`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncWrite.html). +//! +//! See the examples folder for more information. +//! +//! # Examples +//! +//! In the following examples, `Noop` is a mock type that implements the required traits for using a [`WebSocket`]. +//! - A `stream` is anything that implements [`embedded_io_async::Read`] + [`embedded_io_async::Write`]. +//! - An `rng` is anything that implements [`rand_core::RngCore`]. +//! +//! ### Client +//! ``` +//! # async fn client() { +//! # use websocketz::mock::Noop; +//! use websocketz::{Message, WebSocket, http::Header, next, options::ConnectOptions}; +//! +//! // An already connected stream. +//! // Impl embedded_io_async Read + Write. +//! let stream = Noop; +//! +//! let read_buffer = &mut [0u8; 1024]; +//! let write_buffer = &mut [0u8; 1024]; +//! let fragments_buffer = &mut [0u8; 1024]; +//! +//! // Impl rand_core RngCore. +//! let rng = Noop; +//! +//! // Perform a WebSocket handshake as a client. +//! // 16 is the max number of headers to allocate space for. +//! let mut websocketz = WebSocket::connect::<16>( +//! // Set the connection options. +//! // The path for the WebSocket endpoint as well as any additional HTTP headers. +//! ConnectOptions::default() +//! .with_path("/ws") +//! .expect("Valid path") +//! .with_headers(&[ +//! Header { +//! name: "Host", +//! value: b"example.com", +//! }, +//! Header { +//! name: "User-Agent", +//! value: b"WebSocketz", +//! }, +//! ]), +//! stream, +//! rng, +//! read_buffer, +//! write_buffer, +//! fragments_buffer, +//! ) +//! .await +//! .expect("Handshake failed"); +//! +//! // Send a text message. +//! websocketz +//! .send(Message::Text("Hello, WebSocket!")) +//! .await +//! .expect("Failed to send message"); +//! +//! // Receive messages in a loop. +//! loop { +//! match next!(websocketz) { +//! None => { +//! // Connection closed. +//! break; +//! } +//! Some(Ok(msg)) => { +//! // Handle received message. +//! let _ = msg; +//! } +//! Some(Err(err)) => { +//! // Handle error. +//! let _ = err; +//! +//! break; +//! } +//! } +//! } +//! # } +//! ``` +//! +//! ### Server +//! ``` +//! # async fn server() { +//! # use websocketz::mock::Noop; +//! use websocketz::{Message, WebSocket, http::Header, next, options::AcceptOptions}; +//! +//! // An already connected stream. +//! // Impl embedded_io_async Read + Write. +//! let stream = Noop; +//! +//! let read_buffer = &mut [0u8; 1024]; +//! let write_buffer = &mut [0u8; 1024]; +//! let fragments_buffer = &mut [0u8; 1024]; +//! +//! // Impl rand_core RngCore. +//! let rng: Noop = Noop; +//! +//! // Perform a WebSocket handshake as a server. +//! // 16 is the max number of headers to allocate space for. +//! let mut websocketz = WebSocket::accept::<16>( +//! // Set the acceptance options. +//! // Any additional HTTP headers. +//! AcceptOptions::default().with_headers(&[Header { +//! name: "Server", +//! value: b"WebSocketz", +//! }]), +//! stream, +//! rng, +//! read_buffer, +//! write_buffer, +//! fragments_buffer, +//! ) +//! .await +//! .expect("Handshake failed"); +//! +//! // Receive messages in a loop. +//! loop { +//! match next!(websocketz) { +//! None => { +//! // Connection closed. +//! break; +//! } +//! Some(Ok(msg)) => { +//! // Handle received message. +//! let _ = msg; +//! +//! // Send a binary message. +//! if let Err(err) = websocketz.send(Message::Binary(b"Hello, WebSocket!")).await { +//! let _ = err; +//! +//! break; +//! } +//! } +//! Some(Err(err)) => { +//! // Handle error. +//! let _ = err; +//! +//! break; +//! } +//! } +//! } +//! # } +//! ``` +//! +//! # Laziness +//! +//! This library is `lazy`, meaning that the WebSocket connection is managed as long as you read from the connection. +//! +//! Managing the connection consists of two parts: +//! - Sending [Message::Pong] messages in response to [Message::Ping] messages. +//! - Responding to [Message::Close] messages by sending the appropriate [Message::Close] response and closing the connection. +//! +//! `auto_pong` and `auto_close` are enabled by default, but can be set using [`WebSocket::with_auto_pong`] and [`WebSocket::with_auto_close`] respectively. +//! +//! # Reading from the connection +//! +//! This library allocates nothing. It only uses exclusive references and stack memory. It is quite challenging to offer a clean API while adhering to rust's borrowing rules. +//! That's why a [`WebSocket`] does not offer any method to read messages directly. +//! +//! Instead, you can use the [`next!`] macro to read messages from the connection. +//! +//! [`next!`] unpacks the internal `private` structure of the [`WebSocket`] to obtain mutable references and perform reads. +//! +//! ``` +//! # async fn next_macro() { +//! # use websocketz::mock::Noop; +//! # use websocketz::{WebSocket, next, options::ConnectOptions}; +//! # +//! # let stream = Noop; +//! # let read_buffer = &mut [0u8; 1024]; +//! # let write_buffer = &mut [0u8; 1024]; +//! # let fragments_buffer = &mut [0u8; 1024]; +//! # let rng = Noop; +//! # +//! # let websocketz = WebSocket::connect::<16>( +//! # ConnectOptions::default() +//! # .with_path("/ws") +//! # .expect("Valid path"), +//! # stream, +//! # rng, +//! # read_buffer, +//! # write_buffer, +//! # fragments_buffer, +//! # ) +//! # .await +//! # .expect("Handshake failed"); +//! # +//! # let existing_websocket = || websocketz; +//! let mut websocketz = existing_websocket(); +//! +//! while let Some(Ok(msg)) = next!(websocketz) { +//! // Messages hold references to the websocket buffers. +//! let _ = msg; +//! } +//! # } +//! ``` +//! +//! # Writing to the connection +//! +//! [`WebSocket`] offers two methods to send messages, [`WebSocket::send`] and [`WebSocket::send_fragmented`]. +//! These methods take `&mut self`, which might be problematic in some situations. E.g., echoing back a received message. +//! ```compile_fail +//! # async fn send_method_no_compile() { +//! # use crate::mock::Noop; +//! # use crate::{WebSocket, next, options::ConnectOptions}; +//! # +//! # let stream = Noop; +//! # let read_buffer = &mut [0u8; 1024]; +//! # let write_buffer = &mut [0u8; 1024]; +//! # let fragments_buffer = &mut [0u8; 1024]; +//! # let rng = Noop; +//! # +//! # let websocketz = WebSocket::connect::<16>( +//! # ConnectOptions::default() +//! # .with_path("/ws") +//! # .expect("Valid path"), +//! # stream, +//! # rng, +//! # read_buffer, +//! # write_buffer, +//! # fragments_buffer, +//! # ) +//! # .await +//! # .expect("Handshake failed"); +//! # +//! # let existing_websocket = || websocketz; +//! let mut websocketz = existing_websocket(); +//! +//! while let Some(Ok(msg)) = next!(websocketz) { +//! // Messages hold references to the websocket buffers. +//! // So this will not compile: +//! // cannot borrow `websocketz` as mutable more than once at a time. +//! websocketz.send(msg).await.expect("Failed to send message"); +//! } +//! # } +//! ``` +//! +//! To work around this limitation, the library offers the [`send!`] and [`send_fragmented!`] macros, which work similarly to the [`next!`] macro by unpacking the internal `private` structure of the [`WebSocket`]. +//! +//! ``` +//! # async fn send_macro() { +//! # use websocketz::mock::Noop; +//! # use websocketz::{WebSocket, next, options::ConnectOptions, send}; +//! # +//! # let stream = Noop; +//! # let read_buffer = &mut [0u8; 1024]; +//! # let write_buffer = &mut [0u8; 1024]; +//! # let fragments_buffer = &mut [0u8; 1024]; +//! # let rng = Noop; +//! # +//! # let websocketz = WebSocket::connect::<16>( +//! # ConnectOptions::default() +//! # .with_path("/ws") +//! # .expect("Valid path"), +//! # stream, +//! # rng, +//! # read_buffer, +//! # write_buffer, +//! # fragments_buffer, +//! # ) +//! # .await +//! # .expect("Handshake failed"); +//! # +//! # let existing_websocket = || websocketz; +//! let mut websocketz = existing_websocket(); +//! +//! while let Some(Ok(msg)) = next!(websocketz) { +//! send!(websocketz, msg).expect("Failed to send message"); +//! } +//! # } +//!``` +//! +//! # Splitting the connection +//! +//! In some cases, you might want to split the WebSocket connection into a read half and a write half. +//! This can be achieved using the [`WebSocket::split_with`] method, which returns a [`WebSocketRead`] and [`WebSocketWrite`] tuple. +//! +//! ### Note +//! +//! Due to the `lazy` nature of the library, splitting the connection will sacrifice the automatic handling of `Ping` and `Close` messages. +//! ``` +//! # async fn split() { +//! # use websocketz::mock::Noop; +//! # use websocketz::{Message, WebSocket, next, options::ConnectOptions}; +//! # +//! # let stream = Noop; +//! # let read_buffer = &mut [0u8; 1024]; +//! # let write_buffer = &mut [0u8; 1024]; +//! # let fragments_buffer = &mut [0u8; 1024]; +//! # let rng = Noop; +//! # +//! # let websocketz = WebSocket::connect::<16>( +//! # ConnectOptions::default() +//! # .with_path("/ws") +//! # .expect("Valid path"), +//! # stream, +//! # rng, +//! # read_buffer, +//! # write_buffer, +//! # fragments_buffer, +//! # ) +//! # .await +//! # .expect("Handshake failed"); +//! # +//! # let existing_websocket = || websocketz; +//! fn split(stream: Noop) -> (Noop, Noop) { +//! // Let's assume we split the stream here. +//! +//! (Noop, Noop) +//! } +//! +//! let websocketz = existing_websocket(); +//! +//! let (mut websocketz_read, mut websocketz_write) = websocketz.split_with(split); +//! +//! websocketz_write +//! .send(Message::Text("Hello, WebSocket!")) +//! .await +//! .expect("Failed to send message"); +//! +//! while let Some(Ok(msg)) = next!(websocketz_read) { +//! // `send` method works here, +//! // because `websocketz_read` and `websocketz_write` do not hold the same references. +//! websocketz_write +//! .send(msg) +//! .await +//! .expect("Failed to send message"); +//! } +//! # } +//!``` #![no_std] #![deny(missing_debug_implementations)] -// #![deny(missing_docs)] +#![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] mod close_code; @@ -50,5 +388,8 @@ pub use websocket::{WebSocket, WebSocketRead, WebSocketWrite}; #[cfg(test)] mod tests; +#[cfg(test)] +mod examples; + #[cfg(test)] extern crate std; diff --git a/src/macros.rs b/src/macros.rs index 386aae5..4fe0114 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,3 +1,13 @@ +/// Read a [`Message`](crate::Message) from a [`WebSocket`](crate::WebSocket) or [`WebSocketRead`](crate::WebSocketRead). +/// +/// # Parameters +/// +/// - `$websocketz`: The WebSocket instance to read from. +/// +/// # Return +/// - `Some(Ok(Message))`: A message was successfully read. +/// - `Some(Err(Error))`: An error occurred while reading a message. The caller should stop reading. +/// - `None`: The WebSocket connection has been closed (EOF). The caller should stop reading. #[macro_export] macro_rules! next { ($websocketz:expr) => {{ @@ -24,6 +34,11 @@ macro_rules! next { }}; } +/// Send a [`Message`](crate::Message) through a [`WebSocket`](crate::WebSocket) or [`WebSocketWrite`](crate::WebSocketWrite). +/// +/// # Parameters +/// - `$websocketz`: The WebSocket instance to send the message through. +/// - `$message`: The message to send. #[macro_export] macro_rules! send { ($websocketz:expr, $message:expr) => {{ @@ -38,6 +53,12 @@ macro_rules! send { }}; } +/// Send a fragmented [`Message`](crate::Message) through a [`WebSocket`](crate::WebSocket) or [`WebSocketWrite`](crate::WebSocketWrite). +/// +/// # Parameters +/// - `$websocketz`: The WebSocket instance to send the message through. +/// - `$message`: The message to send. +/// - `$fragment_size`: The size of each fragment. #[macro_export] macro_rules! send_fragmented { ($websocketz:expr, $message:expr, $fragment_size:expr) => {{ diff --git a/src/message.rs b/src/message.rs index 76be5a3..5ba1ac6 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,7 +1,9 @@ use crate::{CloseFrame, Frame, OpCode, error::FragmentationError, fragments::FragmentsIterator}; +/// A WebSocket message. #[derive(Debug)] pub enum Message<'a> { + /// A text WebSocket message Text(&'a str), /// A binary WebSocket message Binary(&'a [u8]), diff --git a/src/options.rs b/src/options.rs index 8b9e64e..391d33c 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,16 +1,24 @@ +//! Options for establishing and accepting WebSocket connections. + use crate::http::Header; +/// Errors that can occur when creating [`ConnectOptions`]. #[derive(Debug, thiserror::Error)] pub enum ConnectOptionsError { + /// The path must not be empty. #[error("path must not be empty")] EmptyPath, } +/// Options for establishing a WebSocket connection as a client. #[derive(Debug)] #[non_exhaustive] pub struct ConnectOptions<'a, 'b> { - /// Must not be empty + /// The request path for the WebSocket handshake. + /// + /// Must not be empty. pub(crate) path: &'a str, + /// Additional HTTP headers to include in the handshake request. pub headers: &'a [Header<'b>], } @@ -74,9 +82,11 @@ impl<'a, 'b> ConnectOptions<'a, 'b> { } } +/// Options for accepting a WebSocket connection as a server. #[derive(Debug, Default)] #[non_exhaustive] pub struct AcceptOptions<'a, 'b> { + /// Additional HTTP headers to include in the handshake response. pub headers: &'a [Header<'b>], } diff --git a/src/websocket.rs b/src/websocket.rs index f727fc8..fb3b7d6 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -12,6 +12,12 @@ use crate::{ options::{AcceptOptions, ConnectOptions}, }; +/// A WebSocket connection. +/// +/// # Defaults: +/// +/// - `auto_pong`: `true` +/// - `auto_close`: `true` #[derive(Debug)] pub struct WebSocket<'buf, RW, Rng> { #[doc(hidden)] @@ -58,6 +64,9 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { } /// Creates a new [`WebSocket`] client and performs the handshake. + /// + /// # Generic Parameters + /// `N`: The maximum number of headers to accept in the handshake response. pub async fn connect( options: ConnectOptions<'_, '_>, inner: RW, @@ -83,6 +92,10 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { .0) } + /// Creates a new [`WebSocket`] client and performs the handshake with a custom response handler. + /// + /// # Generic Parameters + /// `N`: The maximum number of headers to accept in the handshake response. pub async fn connect_with( options: ConnectOptions<'_, '_>, inner: RW, @@ -103,6 +116,9 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { } /// Creates a new [`WebSocket`] server and performs the handshake. + /// + /// # Generic Parameters + /// `N`: The maximum number of headers to accept in the handshake request. pub async fn accept( options: AcceptOptions<'_, '_>, inner: RW, @@ -127,6 +143,10 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { .0) } + /// Creates a new [`WebSocket`] server and performs the handshake with a custom request handler. + /// + /// # Generic Parameters + /// `N`: The maximum number of headers to accept in the handshake request. pub async fn accept_with( options: AcceptOptions<'_, '_>, inner: RW, @@ -145,12 +165,14 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { .await } + /// Sets whether to automatically send a Pong response. #[inline] pub const fn with_auto_pong(mut self, auto_pong: bool) -> Self { self.core.set_auto_pong(auto_pong); self } + /// Sets whether to automatically close the connection on receiving a Close frame. #[inline] pub const fn with_auto_close(mut self, auto_close: bool) -> Self { self.core.set_auto_close(auto_close); @@ -216,6 +238,7 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { Ok((Self { core }, custom)) } + /// Sends a WebSocket message. pub async fn send(&mut self, message: Message<'_>) -> Result<(), Error> where RW: Write, @@ -224,6 +247,7 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { self.core.send(message).await } + /// Sends a fragmented WebSocket message. pub async fn send_fragmented( &mut self, message: Message<'_>, @@ -284,6 +308,7 @@ impl<'buf, RW, Rng> WebSocket<'buf, RW, Rng> { } } +/// Read half of a WebSocket connection. #[derive(Debug)] pub struct WebSocketRead<'buf, RW> { #[doc(hidden)] @@ -367,6 +392,7 @@ impl<'buf, RW> WebSocketRead<'buf, RW> { } } +/// Write half of a WebSocket connection. #[derive(Debug)] pub struct WebSocketWrite<'buf, RW, Rng> { #[doc(hidden)] @@ -412,6 +438,7 @@ impl<'buf, RW, Rng> WebSocketWrite<'buf, RW, Rng> { self.core.into_inner() } + /// Sends a WebSocket message. pub async fn send(&mut self, message: Message<'_>) -> Result<(), Error> where RW: Write, @@ -420,6 +447,7 @@ impl<'buf, RW, Rng> WebSocketWrite<'buf, RW, Rng> { self.core.send(message).await } + /// Sends a fragmented WebSocket message. pub async fn send_fragmented( &mut self, message: Message<'_>,