diff --git a/Cargo.lock b/Cargo.lock index 329c8938..5b50a20e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "engineioxide" -version = "0.17.6" +version = "0.17.7" dependencies = [ "axum", "bytes", @@ -2588,7 +2588,7 @@ dependencies = [ [[package]] name = "socketioxide" -version = "0.18.5" +version = "0.18.7" dependencies = [ "axum", "bytes", @@ -2622,7 +2622,7 @@ dependencies = [ [[package]] name = "socketioxide-core" -version = "0.18.2" +version = "0.19.0" dependencies = [ "arbitrary", "bytes", @@ -2655,7 +2655,7 @@ dependencies = [ [[package]] name = "socketioxide-mongodb" -version = "0.1.3" +version = "0.1.4" dependencies = [ "bson 3.1.0", "bytes", @@ -2677,7 +2677,7 @@ dependencies = [ [[package]] name = "socketioxide-parser-common" -version = "0.17.1" +version = "0.17.2" dependencies = [ "bytes", "codspeed-criterion-compat", @@ -2690,7 +2690,7 @@ dependencies = [ [[package]] name = "socketioxide-parser-msgpack" -version = "0.17.1" +version = "0.17.3" dependencies = [ "bytes", "codspeed-criterion-compat", @@ -2703,7 +2703,7 @@ dependencies = [ [[package]] name = "socketioxide-postgres" -version = "0.1.0" +version = "0.1.1" dependencies = [ "bytes", "engineioxide", @@ -2727,7 +2727,7 @@ dependencies = [ [[package]] name = "socketioxide-redis" -version = "0.4.1" +version = "0.4.2" dependencies = [ "bytes", "engineioxide", diff --git a/crates/engineioxide/CHANGELOG.md b/crates/engineioxide/CHANGELOG.md index e729ef7d..3ddb142c 100644 --- a/crates/engineioxide/CHANGELOG.md +++ b/crates/engineioxide/CHANGELOG.md @@ -1,6 +1,8 @@ # engineioxide 0.17.7 -* chore(deps): bump `tokio_tungstenite` to v0.30 -* +* feat(security): add `ws_max_message_size`/`ws_max_frame_size` config options to bound the size of incoming +websocket messages and frames, preventing memory exhaustion by malicious clients. Defaults to 64 MiB/16 MiB. +See [Github Advisory](https://github.com/Totodore/socketioxide/security/advisories/GHSA-55mf-67qm-4wpg). + # engineioxide 0.17.6 * feat: add `emit_{binary,many}_volatile` methods to `Socket` to emit volatile packets. diff --git a/crates/engineioxide/Cargo.toml b/crates/engineioxide/Cargo.toml index f6ee4096..0e9a8e63 100644 --- a/crates/engineioxide/Cargo.toml +++ b/crates/engineioxide/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "engineioxide" description = "Engine IO server implementation as a Tower Service." -version = "0.17.6" +version = "0.17.7" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/crates/engineioxide/src/config.rs b/crates/engineioxide/src/config.rs index 2cd9f603..959add7b 100644 --- a/crates/engineioxide/src/config.rs +++ b/crates/engineioxide/src/config.rs @@ -71,6 +71,19 @@ pub struct EngineIoConfig { /// but will consume more memory. pub ws_read_buffer_size: usize, + /// The maximum size of an incoming message + /// + /// The default value is 64 MiB which should be reasonably big for all normal use-cases but small enough + /// to prevent memory eating by a malicious user. + pub ws_max_message_size: usize, + + /// The maximum size of a single incoming message frame. + /// The limit is for frame payload NOT including the frame header. + /// + /// The default value is 16 MiB which should be reasonably big for all normal use-cases but small enough + /// to prevent memory eating by a malicious user. + pub ws_max_frame_size: usize, + /// Allowed transports on this server /// It is represented as a bitfield to allow to combine any number of transports easily pub transports: u8, @@ -86,6 +99,8 @@ impl Default for EngineIoConfig { max_buffer_size: 128, max_payload: 1e5 as u64, // 100kb ws_read_buffer_size: 4096, + ws_max_message_size: 64 << 20, + ws_max_frame_size: 16 << 20, transports: TransportType::Polling as u8 | TransportType::Websocket as u8, } } @@ -205,6 +220,25 @@ impl EngineIoConfigBuilder { self } + /// The maximum size of an incoming message + /// + /// The default value is 64 MiB which should be reasonably big for all normal use-cases but small enough + /// to prevent memory eating by a malicious user. + pub fn ws_max_message_size(mut self, ws_max_message_size: usize) -> Self { + self.config.ws_max_message_size = ws_max_message_size; + self + } + + /// The maximum size of a single incoming message frame. + /// The limit is for frame payload NOT including the frame header. + /// + /// The default value is 16 MiB which should be reasonably big for all normal use-cases but small enough + /// to prevent memory eating by a malicious user. + pub fn ws_max_frame_size(mut self, ws_max_frame_size: usize) -> Self { + self.config.ws_max_frame_size = ws_max_frame_size; + self + } + /// Allowed transports on this server /// /// The `transports` array should have a size of 1 or 2 diff --git a/crates/engineioxide/src/transport/ws.rs b/crates/engineioxide/src/transport/ws.rs index ea7ad418..23ed725d 100644 --- a/crates/engineioxide/src/transport/ws.rs +++ b/crates/engineioxide/src/transport/ws.rs @@ -117,7 +117,11 @@ pub async fn on_init( where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, { - let ws_config = WebSocketConfig::default().read_buffer_size(engine.config.ws_read_buffer_size); + let ws_config = WebSocketConfig::default() + .read_buffer_size(engine.config.ws_read_buffer_size) + .max_message_size(Some(engine.config.ws_max_message_size)) + .max_frame_size(Some(engine.config.ws_max_frame_size)); + let ws_init = move || WebSocketStream::from_raw_socket(conn, Role::Server, Some(ws_config)); let (socket, ws) = if let Some(sid) = sid { match engine.get_socket(sid) { diff --git a/crates/parser-common/CHANGELOG.md b/crates/parser-common/CHANGELOG.md index 8e160bed..ba079ee8 100644 --- a/crates/parser-common/CHANGELOG.md +++ b/crates/parser-common/CHANGELOG.md @@ -1,5 +1,11 @@ +# socketioxide-parser-common 0.17.2 +* fix(security): implement `ParseConfig` rules to fix unlimited binary data attachments which could lead +to denial of service. See [Github Advisory](https://github.com/Totodore/socketioxide/security/advisories/GHSA-55mf-67qm-4wpg). +* deps: bump `socketioxide-core` to 0.19 + # socketioxide-parser-common 0.17.1 * deps: bump `socketioxide-core` to 0.18 + # socketioxide-parser-common 0.17 * deps: bump `socketioxide-core` to 0.17 * MSRV: rust-version is now 1.86 with edition 2024 diff --git a/crates/parser-common/Cargo.toml b/crates/parser-common/Cargo.toml index d1ad4de0..d182e4fd 100644 --- a/crates/parser-common/Cargo.toml +++ b/crates/parser-common/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "socketioxide-parser-common" description = "Common parser for the socketioxide protocol" -version = "0.17.1" +version = "0.17.2" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -17,7 +17,7 @@ bytes.workspace = true itoa.workspace = true serde.workspace = true serde_json.workspace = true -socketioxide-core = { version = "0.18", path = "../socketioxide-core" } +socketioxide-core = { version = "0.19", path = "../socketioxide-core" } [dev-dependencies] criterion.workspace = true diff --git a/crates/parser-common/benches/packet_decode.rs b/crates/parser-common/benches/packet_decode.rs index 0c8728de..a31f3f4d 100644 --- a/crates/parser-common/benches/packet_decode.rs +++ b/crates/parser-common/benches/packet_decode.rs @@ -3,7 +3,7 @@ use criterion::{BatchSize, Criterion, black_box, criterion_group, criterion_main use socketioxide_core::{ Sid, Value, packet::{ConnectPacket, Packet, PacketData}, - parser::Parse, + parser::{Parse, ParseConfig, ParserState}, }; use socketioxide_parser_common::CommonParser; @@ -15,7 +15,11 @@ fn encode(packet: Packet) -> String { } fn decode(value: String) -> Option { CommonParser - .decode_str(&Default::default(), black_box(value.into())) + .decode_str( + &ParseConfig::default(), + &ParserState::default(), + black_box(value.into()), + ) .ok() } diff --git a/crates/parser-common/fuzz/fuzz_targets/decode_packet_bin.rs b/crates/parser-common/fuzz/fuzz_targets/decode_packet_bin.rs index f46e340b..3a0092ee 100644 --- a/crates/parser-common/fuzz/fuzz_targets/decode_packet_bin.rs +++ b/crates/parser-common/fuzz/fuzz_targets/decode_packet_bin.rs @@ -6,6 +6,6 @@ use socketioxide_core::parser::Parse; use socketioxide_parser_common::CommonParser; fuzz_target!(|data: &[u8]| { CommonParser - .decode_bin(&Default::default(), Bytes::copy_from_slice(data)) + .decode_bin(&Default::default(), &Default::default(), Bytes::copy_from_slice(data)) .ok(); }); diff --git a/crates/parser-common/fuzz/fuzz_targets/decode_packet_str.rs b/crates/parser-common/fuzz/fuzz_targets/decode_packet_str.rs index e992370b..62f1f142 100644 --- a/crates/parser-common/fuzz/fuzz_targets/decode_packet_str.rs +++ b/crates/parser-common/fuzz/fuzz_targets/decode_packet_str.rs @@ -7,5 +7,5 @@ use socketioxide_parser_common::CommonParser; fuzz_target!(|data: &[u8]| { let data = unsafe { Str::from_bytes_unchecked(Bytes::copy_from_slice(data)) }; - CommonParser.decode_str(&Default::default(), data).ok(); + CommonParser.decode_str(&Default::default(), &Default::default(), data).ok(); }); diff --git a/crates/parser-common/src/lib.rs b/crates/parser-common/src/lib.rs index f3f0c351..18e6c07a 100644 --- a/crates/parser-common/src/lib.rs +++ b/crates/parser-common/src/lib.rs @@ -8,7 +8,7 @@ //! [<# of binary attachments>-][,][][JSON-stringified payload without binary] //! + binary attachments extracted //! ``` -use std::{collections::VecDeque, sync::atomic::Ordering}; +use std::{collections::VecDeque, sync::atomic::Ordering, time::Instant}; use bytes::Bytes; @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; use socketioxide_core::{ Str, Value, packet::{Packet, PacketData}, - parser::{Parse, ParseError, ParserError, ParserState}, + parser::{Parse, ParseConfig, ParseError, ParserError, ParserState, PartialBinPacket}, }; mod de; @@ -33,12 +33,24 @@ impl Parse for CommonParser { ser::serialize_packet(packet) } - fn decode_str(self, state: &ParserState, value: Str) -> Result { + fn decode_str( + self, + config: &ParseConfig, + state: &ParserState, + value: Str, + ) -> Result { let (packet, incoming_binary_cnt) = de::deserialize_packet(value)?; if packet.inner.is_binary() { let incoming_binary_cnt = incoming_binary_cnt.ok_or(ParseError::InvalidAttachments)?; + if incoming_binary_cnt > config.max_incoming_binaries { + return Err(ParseError::TooManyAttachments { + got: incoming_binary_cnt, + max: config.max_incoming_binaries, + }); + } + if !is_bin_packet_complete(&packet.inner, incoming_binary_cnt) { - *state.partial_bin_packet.lock().unwrap() = Some(packet); + *state.partial_bin_packet.lock().unwrap() = Some(PartialBinPacket::new(packet)); state .incoming_binary_cnt .store(incoming_binary_cnt, Ordering::Release); @@ -51,23 +63,44 @@ impl Parse for CommonParser { } } - fn decode_bin(self, state: &ParserState, data: Bytes) -> Result { + fn decode_bin( + self, + config: &ParseConfig, + state: &ParserState, + data: Bytes, + ) -> Result { let packet = &mut *state.partial_bin_packet.lock().unwrap(); match packet { - Some(Packet { - inner: - PacketData::BinaryEvent(Value::Str(_, binaries), _) - | PacketData::BinaryAck(Value::Str(_, binaries), _), - .. + Some(PartialBinPacket { + initial_packet_recv, + packet: + Packet { + inner: + PacketData::BinaryEvent(Value::Str(_, binaries), _) + | PacketData::BinaryAck(Value::Str(_, binaries), _), + .. + }, }) => { + if Instant::now() - *initial_packet_recv > config.packet_completion_timeout { + return Err(ParseError::TimeoutWhileWaitingAttachments); + } + let binaries = binaries.get_or_insert(VecDeque::new()); + let bin_buf_size = binaries.iter().map(Bytes::len).sum::() + data.len(); + if bin_buf_size > config.max_incoming_binary_buf_size { + return Err(ParseError::AttachmentsTooHeavy { + current: bin_buf_size, + max: config.max_incoming_binary_buf_size, + }); + } + // We copy the data to avoid holding a ref to the engine.io // websocket buffer too long. binaries.push_back(Bytes::copy_from_slice(&data)); if state.incoming_binary_cnt.load(Ordering::Relaxed) > binaries.len() { Err(ParseError::NeedsMoreBinaryData) } else { - Ok(packet.take().unwrap()) + Ok(packet.take().unwrap().packet) } } _ => Err(ParseError::UnexpectedBinaryPacket), @@ -130,7 +163,7 @@ fn is_bin_packet_complete(packet: &PacketData, incoming_binary_cnt: usize) -> bo #[cfg(test)] mod test { - use std::collections::HashMap; + use std::{collections::HashMap, time::Duration}; use crate::is_bin_packet_complete; @@ -156,7 +189,11 @@ mod test { } fn decode(value: String) -> Packet { CommonParser - .decode_str(&Default::default(), value.into()) + .decode_str( + &ParseConfig::default(), + &ParserState::default(), + value.into(), + ) .unwrap() } @@ -410,68 +447,72 @@ mod test { } }; let state = ParserState::default(); + let config = ParseConfig::default(); let payload = format!("52-{json}"); assert!(matches!( - CommonParser.decode_str(&state, payload.into()), + CommonParser.decode_str(&config, &state, payload.into()), Err(ParseError::NeedsMoreBinaryData) )); assert!(matches!( - CommonParser.decode_bin(&state, Bytes::from_static(&[1])), + CommonParser.decode_bin(&config, &state, Bytes::from_static(&[1])), Err(ParseError::NeedsMoreBinaryData) )); let packet = CommonParser - .decode_bin(&state, Bytes::from_static(&[2])) + .decode_bin(&config, &state, Bytes::from_static(&[2])) .unwrap(); assert_eq!(packet, comparison_packet(None, "/")); // Check with ack ID let state = ParserState::default(); + let config = ParseConfig::default(); let payload = format!("52-254{json}"); assert!(matches!( - CommonParser.decode_str(&state, payload.into()), + CommonParser.decode_str(&config, &state, payload.into()), Err(ParseError::NeedsMoreBinaryData) )); assert!(matches!( - CommonParser.decode_bin(&state, Bytes::from_static(&[1])), + CommonParser.decode_bin(&config, &state, Bytes::from_static(&[1])), Err(ParseError::NeedsMoreBinaryData) )); let packet = CommonParser - .decode_bin(&state, Bytes::from_static(&[2])) + .decode_bin(&config, &state, Bytes::from_static(&[2])) .unwrap(); assert_eq!(packet, comparison_packet(Some(254), "/")); // Check with NS let state = ParserState::default(); + let config = ParseConfig::default(); let payload = format!("52-/admin™,{json}"); assert!(matches!( - CommonParser.decode_str(&state, payload.into()), + CommonParser.decode_str(&config, &state, payload.into()), Err(ParseError::NeedsMoreBinaryData) )); assert!(matches!( - CommonParser.decode_bin(&state, Bytes::from_static(&[1])), + CommonParser.decode_bin(&config, &state, Bytes::from_static(&[1])), Err(ParseError::NeedsMoreBinaryData) )); let packet = CommonParser - .decode_bin(&state, Bytes::from_static(&[2])) + .decode_bin(&config, &state, Bytes::from_static(&[2])) .unwrap(); assert_eq!(packet, comparison_packet(None, "/admin™")); // Check with ack ID and NS let state = ParserState::default(); + let config = ParseConfig::default(); let payload = format!("52-/admin™,254{json}"); assert!(matches!( - CommonParser.decode_str(&state, payload.into()), + CommonParser.decode_str(&config, &state, payload.into()), Err(ParseError::NeedsMoreBinaryData) )); assert!(matches!( - CommonParser.decode_bin(&state, Bytes::from_static(&[1])), + CommonParser.decode_bin(&config, &state, Bytes::from_static(&[1])), Err(ParseError::NeedsMoreBinaryData) )); let packet = CommonParser - .decode_bin(&state, Bytes::from_static(&[2])) + .decode_bin(&config, &state, Bytes::from_static(&[2])) .unwrap(); assert_eq!(packet, comparison_packet(Some(254), "/admin™")); @@ -515,12 +556,13 @@ mod test { let payload = format!("61-54{json}"); let state = ParserState::default(); + let config = ParseConfig::default(); assert!(matches!( - CommonParser.decode_str(&state, payload.into()), + CommonParser.decode_str(&config, &state, payload.into()), Err(ParseError::NeedsMoreBinaryData) )); let packet = CommonParser - .decode_bin(&state, Bytes::from_static(&[1])) + .decode_bin(&config, &state, Bytes::from_static(&[1])) .unwrap(); assert_eq!(packet, comparison_packet(54, "/")); @@ -529,11 +571,11 @@ mod test { let state = ParserState::default(); let payload = format!("61-/admin™,54{json}"); assert!(matches!( - CommonParser.decode_str(&state, payload.into()), + CommonParser.decode_str(&config, &state, payload.into()), Err(ParseError::NeedsMoreBinaryData) )); let packet = CommonParser - .decode_bin(&state, Bytes::from_static(&[1])) + .decode_bin(&config, &state, Bytes::from_static(&[1])) .unwrap(); assert_eq!(packet, comparison_packet(54, "/admin™")); } @@ -542,7 +584,11 @@ mod test { fn packet_reject_invalid_binary_event() { let payload = "5invalid".to_owned(); let err = CommonParser - .decode_str(&Default::default(), payload.into()) + .decode_str( + &ParseConfig::default(), + &ParserState::default(), + payload.into(), + ) .unwrap_err(); assert!(matches!(err, ParseError::InvalidAttachments)); @@ -580,7 +626,11 @@ mod test { #[test] fn unexpected_bin_packet() { - let err = CommonParser.decode_bin(&Default::default(), Bytes::new()); + let err = CommonParser.decode_bin( + &ParseConfig::default(), + &ParserState::default(), + Bytes::new(), + ); assert!(matches!(err, Err(ParseError::UnexpectedBinaryPacket))); } #[test] @@ -598,4 +648,71 @@ mod test { assert!(is_bin_packet_complete(&data, 0)); assert!(is_bin_packet_complete(&data, 1)); } + + #[test] + fn packet_decode_binary_timeout() { + let json = json!(["event", { "data": "value™" },{ "_placeholder": true, "num": 0},{ "_placeholder": true, "num": 1}]); + + let state = ParserState::default(); + let config = ParseConfig { + packet_completion_timeout: Duration::from_millis(10), + ..ParseConfig::default() + }; + + let payload = format!("52-{json}"); + assert!(matches!( + CommonParser.decode_str(&config, &state, payload.into()), + Err(ParseError::NeedsMoreBinaryData) + )); + std::thread::sleep(config.packet_completion_timeout + Duration::from_millis(5)); + + assert!(matches!( + CommonParser.decode_bin(&config, &state, Bytes::from_static(&[1])), + Err(ParseError::TimeoutWhileWaitingAttachments) + )); + } + + #[test] + fn packet_decode_binary_max_count() { + let json = json!(["event", { "data": "value™" },{ "_placeholder": true, "num": 0},{ "_placeholder": true, "num": 1}]); + + let state = ParserState::default(); + let config = ParseConfig { + max_incoming_binaries: 1, + ..ParseConfig::default() + }; + + let payload = format!("52-{json}"); + assert!(matches!( + CommonParser.decode_str(&config, &state, payload.into()), + Err(ParseError::TooManyAttachments { got: 2, max: 1 }) + )); + } + + #[test] + fn packet_decode_binary_max_buf_len() { + let json = json!(["event", { "data": "value™" },{ "_placeholder": true, "num": 0},{ "_placeholder": true, "num": 1}]); + + let state = ParserState::default(); + let config = ParseConfig { + max_incoming_binary_buf_size: 2, + ..ParseConfig::default() + }; + + let payload = format!("52-{json}"); + assert!(matches!( + CommonParser.decode_str(&config, &state, payload.into()), + Err(ParseError::NeedsMoreBinaryData) + )); + + assert!(matches!( + CommonParser.decode_bin(&config, &state, Bytes::from_static(&[1])), + Err(ParseError::NeedsMoreBinaryData) + )); + + assert!(matches!( + CommonParser.decode_bin(&config, &state, Bytes::from_static(&[1, 2])), + Err(ParseError::AttachmentsTooHeavy { current: 3, max: 2 }) + )); + } } diff --git a/crates/parser-msgpack/CHANGELOG.md b/crates/parser-msgpack/CHANGELOG.md index 8b7840c4..616871c8 100644 --- a/crates/parser-msgpack/CHANGELOG.md +++ b/crates/parser-msgpack/CHANGELOG.md @@ -1,3 +1,6 @@ +# socketioxide-parser-msgpack 0.17.3 +* deps: bump `socketioxide-core` to 0.19 + # socketioxide-parser-msgpack 0.17.2 * fix(security): bound the packet decoder recursion depth to prevent an unauthenticated remote DoS via deeply nested MsgPack packets (uncontrolled recursion → stack overflow, CWE-674). See [Github Advisory](https://github.com/Totodore/socketioxide/security/advisories/GHSA-c6g7-r2mf-pf5g) diff --git a/crates/parser-msgpack/Cargo.toml b/crates/parser-msgpack/Cargo.toml index efb0e8b7..f8e7ff8b 100644 --- a/crates/parser-msgpack/Cargo.toml +++ b/crates/parser-msgpack/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "socketioxide-parser-msgpack" description = "Msgpack parser for the socketioxide protocol" -version = "0.17.2" +version = "0.17.3" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -17,7 +17,7 @@ bytes.workspace = true serde.workspace = true rmp-serde.workspace = true rmp.workspace = true -socketioxide-core = { version = "0.18", path = "../socketioxide-core" } +socketioxide-core = { version = "0.19", path = "../socketioxide-core" } [dev-dependencies] serde_json.workspace = true diff --git a/crates/parser-msgpack/benches/packet_decode.rs b/crates/parser-msgpack/benches/packet_decode.rs index 6a809855..3b815474 100644 --- a/crates/parser-msgpack/benches/packet_decode.rs +++ b/crates/parser-msgpack/benches/packet_decode.rs @@ -15,7 +15,7 @@ fn encode(packet: Packet) -> Bytes { } fn decode(value: Bytes) -> Option { MsgPackParser - .decode_bin(&Default::default(), black_box(value)) + .decode_bin(&Default::default(), &Default::default(), black_box(value)) .ok() } diff --git a/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_bin.rs b/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_bin.rs index 312189f9..04da7d19 100644 --- a/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_bin.rs +++ b/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_bin.rs @@ -6,6 +6,6 @@ use socketioxide_core::parser::Parse; use socketioxide_parser_msgpack::MsgPackParser; fuzz_target!(|data: &[u8]| { MsgPackParser - .decode_bin(&Default::default(), Bytes::copy_from_slice(data)) + .decode_bin(&Default::default(), &Default::default(), Bytes::copy_from_slice(data)) .ok(); }); diff --git a/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_str.rs b/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_str.rs index 1f83030b..d104b5e0 100644 --- a/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_str.rs +++ b/crates/parser-msgpack/fuzz/fuzz_targets/decode_packet_str.rs @@ -7,5 +7,5 @@ use socketioxide_parser_msgpack::MsgPackParser; fuzz_target!(|data: &[u8]| { let data = unsafe { Str::from_bytes_unchecked(Bytes::copy_from_slice(data)) }; - MsgPackParser.decode_str(&Default::default(), data).ok(); + MsgPackParser.decode_str(&Default::default(), &Default::default(), data).ok(); }); diff --git a/crates/parser-msgpack/src/lib.rs b/crates/parser-msgpack/src/lib.rs index ea852f33..0d227761 100644 --- a/crates/parser-msgpack/src/lib.rs +++ b/crates/parser-msgpack/src/lib.rs @@ -24,7 +24,7 @@ use serde::Deserialize; use socketioxide_core::{ Str, Value, packet::Packet, - parser::{Parse, ParseError, ParserError, ParserState}, + parser::{Parse, ParseConfig, ParseError, ParserError, ParserState}, }; mod de; @@ -41,11 +41,21 @@ impl Parse for MsgPackParser { Value::Bytes(data.into()) } - fn decode_str(self, _: &ParserState, _data: Str) -> Result { + fn decode_str( + self, + _: &ParseConfig, + _: &ParserState, + _data: Str, + ) -> Result { Err(ParseError::UnexpectedStringPacket) } - fn decode_bin(self, _: &ParserState, bin: Bytes) -> Result { + fn decode_bin( + self, + _: &ParseConfig, + _: &ParserState, + bin: Bytes, + ) -> Result { deserialize_packet(bin) } @@ -102,7 +112,11 @@ mod tests { fn decode(value: &'static [u8]) -> Packet { MsgPackParser - .decode_bin(&Default::default(), Bytes::from_static(value)) + .decode_bin( + &Default::default(), + &Default::default(), + Bytes::from_static(value), + ) .unwrap() } fn encode(packet: Packet) -> Bytes { diff --git a/crates/parser-msgpack/tests/depth_limit.rs b/crates/parser-msgpack/tests/depth_limit.rs index 71ab05d9..8890b41d 100644 --- a/crates/parser-msgpack/tests/depth_limit.rs +++ b/crates/parser-msgpack/tests/depth_limit.rs @@ -37,7 +37,7 @@ fn deeply_nested_packet_is_rejected_not_crash() { // the stack overflow is deterministic whatever the test runner's stack size. let res = std::thread::Builder::new() .stack_size(512 * 1024) - .spawn(move || MsgPackParser.decode_bin(&Default::default(), packet)) + .spawn(move || MsgPackParser.decode_bin(&Default::default(), &Default::default(), packet)) .unwrap() .join() .unwrap(); @@ -52,7 +52,7 @@ fn deeply_nested_packet_is_rejected_not_crash() { fn nested_within_limit_is_accepted() { let packet = event_packet(&nested_arrays(100)); let packet = MsgPackParser - .decode_bin(&Default::default(), packet) + .decode_bin(&Default::default(), &Default::default(), packet) .unwrap(); assert!(matches!(packet.inner, PacketData::Event(_, None))); } @@ -66,7 +66,7 @@ fn large_flat_payload_is_accepted() { let packet = event_packet(&data); let packet = MsgPackParser - .decode_bin(&Default::default(), packet) + .decode_bin(&Default::default(), &Default::default(), packet) .unwrap(); assert!(matches!(packet.inner, PacketData::Event(_, None))); } diff --git a/crates/socketioxide-core/CHANGELOG.md b/crates/socketioxide-core/CHANGELOG.md index 909d6ab0..ae1cd7c9 100644 --- a/crates/socketioxide-core/CHANGELOG.md +++ b/crates/socketioxide-core/CHANGELOG.md @@ -1,3 +1,6 @@ +# socketioxide-core 0.19.0 +* feat(*breaking*): Add a new `ParseConfig` arg to parser `decode_{str/bin}` methods. + # socketioxide-core 0.18.2 * feat: add new `Volatile` broadcast flag diff --git a/crates/socketioxide-core/Cargo.toml b/crates/socketioxide-core/Cargo.toml index e85dd37f..e956ceb3 100644 --- a/crates/socketioxide-core/Cargo.toml +++ b/crates/socketioxide-core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "socketioxide-core" description = "Core of the socketioxide library. Contains basic types and interfaces for the socketioxide crate and all other related sub-crates." -version = "0.18.2" +version = "0.19.0" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/crates/socketioxide-core/src/parser.rs b/crates/socketioxide-core/src/parser.rs index ac7044d2..a0f0de96 100644 --- a/crates/socketioxide-core/src/parser.rs +++ b/crates/socketioxide-core/src/parser.rs @@ -23,6 +23,7 @@ use std::{ error::Error as StdError, fmt, sync::{Mutex, atomic::AtomicUsize}, + time::{Duration, Instant}, }; use bytes::Bytes; @@ -38,12 +39,69 @@ use crate::{Value, packet::Packet}; pub struct ParserState { /// Partial binary packet that is being received /// Stored here until all the binary payloads are received for common parser - pub partial_bin_packet: Mutex>, + pub partial_bin_packet: Mutex>, /// The number of expected binary attachments (used when receiving data for common parser) pub incoming_binary_cnt: AtomicUsize, } +/// A partial binary packet along when we started receiving it. +#[derive(Debug)] +pub struct PartialBinPacket { + /// The packet whose we are waiting for its attachments + pub packet: Packet, + + /// When the initial packet is received, used in conjunction + /// with [`ParseConfig::packet_completion_timeout`]. + pub initial_packet_recv: Instant, +} + +impl PartialBinPacket { + /// Create a new partial binary packet from a first `Str` Packet. + pub fn new(packet: Packet) -> Self { + Self { + packet, + initial_packet_recv: Instant::now(), + } + } +} + +/// A config passed to the parsers in order to securely decode packets +#[derive(Debug, Clone)] +pub struct ParseConfig { + /// Maximum buffer size of all the incoming binary attachments. + /// + /// If the sum of each attachment exceeds this limit, + /// the packet will be dropped and the connection closed. + /// + /// Default to 10 MB + pub max_incoming_binary_buf_size: usize, + + /// Maximum number of incoming binary attachments. + /// + /// If the [`incoming_binary_cnt`](ParserState::incoming_binary_cnt) is exceeded, + /// the packet will be dropped and the connection closed. + /// + /// Default to 10_000 + pub max_incoming_binaries: usize, + + /// Timeout after which it is no longer tolerable to wait for partial binary packets, + /// the entire packet will dropped and the connection closed. + /// + /// Default to 60 seconds + pub packet_completion_timeout: Duration, +} + +impl Default for ParseConfig { + fn default() -> Self { + Self { + max_incoming_binary_buf_size: 10e6 as usize, // 10 MB + max_incoming_binaries: 10_000, + packet_completion_timeout: Duration::from_secs(60), + } + } +} + /// All socket.io parser should implement this trait. /// Parsers should be stateless. pub trait Parse: Default + Copy { @@ -52,11 +110,21 @@ pub trait Parse: Default + Copy { /// Parse a given input string. If the payload needs more adjacent binary packet, /// the partial packet will be kept and a [`ParseError::NeedsMoreBinaryData`] will be returned. - fn decode_str(self, state: &ParserState, data: Str) -> Result; + fn decode_str( + self, + config: &ParseConfig, + state: &ParserState, + data: Str, + ) -> Result; /// Parse a given input binary. If the payload needs more adjacent binary packet, /// the partial packet is still kept and a [`ParseError::NeedsMoreBinaryData`] will be returned. - fn decode_bin(self, state: &ParserState, bin: Bytes) -> Result; + fn decode_bin( + self, + config: &ParseConfig, + state: &ParserState, + bin: Bytes, + ) -> Result; /// Convert any serializable data to a generic [`Value`] to be later included as a payload in the packet. /// @@ -163,6 +231,29 @@ pub enum ParseError { #[error("invalid attachments")] InvalidAttachments, + /// Too many attachments + #[error("too many attachments, got {got}, maximum: {max}")] + TooManyAttachments { + /// How many attachments we got + got: usize, + /// Maximum of attachment the client can send + max: usize, + }, + + /// Accumulated attachment bytes exceeded the configured maximum + #[error("attachment bytes cannot be more than {max} bytes, got: {current} bytes")] + AttachmentsTooHeavy { + /// The current attachment buffers size + current: usize, + + /// Maximum size the sum of each attachment buffers can be + max: usize, + }, + + /// Timeout while waiting attachments + #[error("The server exceeded its timeout while waiting for binary packet attachments")] + TimeoutWhileWaitingAttachments, + /// Received unexpected binary data #[error( "received unexpected binary data. Make sure you are using the same parser on both ends." @@ -534,11 +625,21 @@ pub mod test { Value::Bytes(Bytes::new()) } - fn decode_str(self, _: &ParserState, _: Str) -> Result { + fn decode_str( + self, + _: &ParseConfig, + _: &ParserState, + _: Str, + ) -> Result { Err(ParseError::ParserError(stub_err())) } - fn decode_bin(self, _: &ParserState, _: bytes::Bytes) -> Result { + fn decode_bin( + self, + _: &ParseConfig, + _: &ParserState, + _: bytes::Bytes, + ) -> Result { Err(ParseError::ParserError(stub_err())) } diff --git a/crates/socketioxide-mongodb/Cargo.toml b/crates/socketioxide-mongodb/Cargo.toml index 8d24da6a..afcc00d1 100644 --- a/crates/socketioxide-mongodb/Cargo.toml +++ b/crates/socketioxide-mongodb/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "socketioxide-mongodb" description = "MongoDB adapter for socketioxide" -version = "0.1.3" +version = "0.1.4" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -18,7 +18,7 @@ ttl-index = ["dep:bson"] default = ["mongodb", "ttl-index"] [dependencies] -socketioxide-core = { version = "0.18", path = "../socketioxide-core", features = [ +socketioxide-core = { version = "0.19", path = "../socketioxide-core", features = [ "remote-adapter", ] } futures-core.workspace = true diff --git a/crates/socketioxide-postgres/Cargo.toml b/crates/socketioxide-postgres/Cargo.toml index 0d9631db..6a10d943 100644 --- a/crates/socketioxide-postgres/Cargo.toml +++ b/crates/socketioxide-postgres/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "socketioxide-postgres" description = "PostgreSQL adapter for socketioxide" -version = "0.1.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -18,7 +18,7 @@ tokio-postgres = ["dep:tokio-postgres"] default = ["sqlx"] [dependencies] -socketioxide-core = { version = "0.18.1", path = "../socketioxide-core", features = [ +socketioxide-core = { version = "0.19", path = "../socketioxide-core", features = [ "remote-adapter", ] } futures-core.workspace = true diff --git a/crates/socketioxide-redis/Cargo.toml b/crates/socketioxide-redis/Cargo.toml index f044a8fb..60c5446f 100644 --- a/crates/socketioxide-redis/Cargo.toml +++ b/crates/socketioxide-redis/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "socketioxide-redis" description = "Redis adapter for the socket.io protocol" -version = "0.4.1" +version = "0.4.2" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -19,7 +19,7 @@ fred = ["dep:fred"] default = ["redis"] [dependencies] -socketioxide-core = { version = "0.18", path = "../socketioxide-core", features = [ +socketioxide-core = { version = "0.19", path = "../socketioxide-core", features = [ "remote-adapter", ] } futures-core.workspace = true diff --git a/crates/socketioxide/CHANGELOG.md b/crates/socketioxide/CHANGELOG.md index 7fcffc51..a7e01a08 100644 --- a/crates/socketioxide/CHANGELOG.md +++ b/crates/socketioxide/CHANGELOG.md @@ -1,3 +1,7 @@ +# socketioxide 0.18.7 +* fix(security): bump `socketioxide-parser-commong` to 0.17.2, fixing unlimited binary data attachments which could lead +to denial of service. See [Github Advisory](https://github.com/Totodore/socketioxide/security/advisories/GHSA-55mf-67qm-4wpg). + # socketioxide 0.18.6 * fix(security): bump `socketioxide-parser-msgpack` to 0.17.2, fixing an unauthenticated remote DoS in the opt-in MsgPack parser (deeply nested packets caused a stack overflow, CWE-674). See [Github Advisory](https://github.com/Totodore/socketioxide/security/advisories/GHSA-c6g7-r2mf-pf5g) diff --git a/crates/socketioxide/Cargo.toml b/crates/socketioxide/Cargo.toml index 4709ecad..94d0d2b0 100644 --- a/crates/socketioxide/Cargo.toml +++ b/crates/socketioxide/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "socketioxide" description = "Socket IO server implementation in rust as a Tower Service." -version = "0.18.6" +version = "0.18.7" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -13,8 +13,8 @@ license.workspace = true readme.workspace = true [dependencies] -engineioxide = { path = "../engineioxide", version = "0.17.6" } -socketioxide-core = { path = "../socketioxide-core", version = "0.18.2" } +engineioxide = { path = "../engineioxide", version = "0.17.7" } +socketioxide-core = { path = "../socketioxide-core", version = "0.19.0" } bytes.workspace = true futures-core.workspace = true @@ -31,8 +31,8 @@ matchit.workspace = true pin-project-lite.workspace = true # Parsers -socketioxide-parser-common = { path = "../parser-common", version = "0.17" } -socketioxide-parser-msgpack = { path = "../parser-msgpack", version = "0.17.2", optional = true } +socketioxide-parser-common = { path = "../parser-common", version = "0.17.2" } +socketioxide-parser-msgpack = { path = "../parser-msgpack", version = "0.17.3", optional = true } # Tracing tracing = { workspace = true, optional = true } diff --git a/crates/socketioxide/src/client.rs b/crates/socketioxide/src/client.rs index e4ac7bfd..91293cc6 100644 --- a/crates/socketioxide/src/client.rs +++ b/crates/socketioxide/src/client.rs @@ -286,7 +286,11 @@ impl EngineIoHandler for Client { fn on_message(self: &Arc, msg: Str, socket: Arc>>) { #[cfg(feature = "tracing")] tracing::debug!("received message: {:?}", msg); - let packet = match self.parser().decode_str(&socket.data.parser_state, msg) { + let packet = match self.parser().decode_str( + &self.config.parse_config, + &socket.data.parser_state, + msg, + ) { Ok(packet) => packet, Err(ParseError::NeedsMoreBinaryData) => return, Err(_e) => { @@ -325,7 +329,11 @@ impl EngineIoHandler for Client { fn on_binary(self: &Arc, data: Bytes, socket: Arc>>) { #[cfg(feature = "tracing")] tracing::debug!("received binary: {:?}", &data); - let packet = match self.parser().decode_bin(&socket.data.parser_state, data) { + let packet = match self.parser().decode_bin( + &self.config.parse_config, + &socket.data.parser_state, + data, + ) { Ok(packet) => packet, Err(ParseError::NeedsMoreBinaryData) => return, Err(_e) => { diff --git a/crates/socketioxide/src/io.rs b/crates/socketioxide/src/io.rs index 02c970b6..90865895 100644 --- a/crates/socketioxide/src/io.rs +++ b/crates/socketioxide/src/io.rs @@ -9,6 +9,7 @@ use serde::Serialize; use socketioxide_core::{ Sid, Uid, adapter::{DefinedAdapter, Room, RoomParam}, + parser::ParseConfig, }; use socketioxide_parser_common::CommonParser; #[cfg(feature = "msgpack")] @@ -32,18 +33,61 @@ use crate::{ /// /// Be sure that the selected parser matches the client parser. #[derive(Debug, Clone)] -pub struct ParserConfig(Parser); +pub struct ParserConfig { + parser: T, + config: ParseConfig, +} -impl ParserConfig { +impl ParserConfig<()> { /// Use a [`CommonParser`] to parse incoming and outgoing socket.io packets - pub fn common() -> Self { - ParserConfig(Parser::Common(CommonParser)) + pub fn common() -> ParserConfig { + ParserConfig { + parser: CommonParser, + config: ParseConfig::default(), + } } /// Use a [`MsgPackParser`] to parse incoming and outgoing socket.io packets #[cfg(feature = "msgpack")] - pub fn msgpack() -> Self { - ParserConfig(Parser::MsgPack(MsgPackParser)) + pub fn msgpack() -> ParserConfig { + ParserConfig { + parser: MsgPackParser, + config: ParseConfig::default(), + } + } +} +impl ParserConfig { + /// Maximum buffer size of all the incoming binary attachments. + /// + /// If the sum of each attachment exceeds this limit, + /// the packet will be dropped and the connection closed. + /// + /// It is important to set a limit for [`EngineIoConfig::max_payload`] that match. + /// + /// Default to 10 MB + pub fn max_incoming_binary_buf_size(mut self, value: usize) -> Self { + self.config.max_incoming_binary_buf_size = value; + self + } + + /// Maximum number of incoming binary attachments. + /// + /// If the incoming binary count is exceeded, + /// the packet will be dropped and the connection closed. + /// + /// Default to 10_000 + pub fn max_incoming_binaries(mut self, value: usize) -> Self { + self.config.max_incoming_binaries = value; + self + } + + /// Timeout after which it is no longer tolerable to wait for partial binary packets, + /// the entire packet will dropped and the connection closed. + /// + /// Default to 1min + pub fn packet_completion_timeout(mut self, timeout: Duration) -> Self { + self.config.packet_completion_timeout = timeout; + self } } @@ -66,6 +110,9 @@ pub struct SocketIoConfig { /// The parser to use to encode and decode socket.io packets pub(crate) parser: Parser, + /// Config for parser behaviors + pub(crate) parse_config: ParseConfig, + /// A global server identifier pub server_id: Uid, } @@ -80,6 +127,7 @@ impl Default for SocketIoConfig { ack_timeout: Duration::from_secs(5), connect_timeout: Duration::from_secs(45), parser: Parser::default(), + parse_config: ParseConfig::default(), server_id: Uid::new(), } } @@ -179,6 +227,29 @@ impl SocketIoBuilder { self } + /// The maximum size of an incoming message + /// + /// The default value is 64 MiB which should be reasonably big for all normal use-cases but small enough + /// to prevent memory eating by a malicious user. + pub fn ws_max_message_size(mut self, ws_max_message_size: usize) -> Self { + self.engine_config_builder = self + .engine_config_builder + .ws_max_message_size(ws_max_message_size); + self + } + + /// The maximum size of a single incoming message frame. + /// The limit is for frame payload NOT including the frame header. + /// + /// The default value is 16 MiB which should be reasonably big for all normal use-cases but small enough + /// to prevent memory eating by a malicious user. + pub fn ws_max_frame_size(mut self, ws_max_frame_size: usize) -> Self { + self.engine_config_builder = self + .engine_config_builder + .ws_max_frame_size(ws_max_frame_size); + self + } + /// Allowed transports on this server /// /// The `transports` array should have a size of 1 or 2 @@ -217,15 +288,34 @@ impl SocketIoBuilder { } /// Set a custom [`ParserConfig`] for this [`SocketIoBuilder`] + /// + /// # Using the msgpack parser /// ``` /// # use socketioxide::{SocketIo, ParserConfig}; /// let (io, layer) = SocketIo::builder() /// .with_parser(ParserConfig::msgpack()) /// .build_layer(); /// ``` + /// + /// # Using the common parser and customizing config + /// ``` + /// # use socketioxide::{SocketIo, ParserConfig}; + /// # use std::time::Duration; + /// let parser = ParserConfig::common() + /// .max_incoming_binary_buf_size(1e5 as usize) // 100 kb + /// .max_incoming_binaries(10) + /// .packet_completion_timeout(Duration::from_millis(200)); + /// + /// let (io, layer) = SocketIo::builder() + /// .with_parser(parser) + /// .build_layer(); + /// ``` #[inline] - pub fn with_parser(mut self, parser: ParserConfig) -> Self { - self.config.parser = parser.0; + #[expect(private_bounds)] // Parser should only be set from parser config anyway + pub fn with_parser>(mut self, parser_config: ParserConfig) -> Self { + let ParserConfig { parser, config } = parser_config; + self.config.parser = parser.into(); + self.config.parse_config = config; self } diff --git a/crates/socketioxide/src/parser.rs b/crates/socketioxide/src/parser.rs index 7b1135a4..295a47bb 100644 --- a/crates/socketioxide/src/parser.rs +++ b/crates/socketioxide/src/parser.rs @@ -7,7 +7,7 @@ use bytes::Bytes; use socketioxide_core::{ Str, Value, packet::Packet, - parser::{Parse, ParserState}, + parser::{Parse, ParseConfig, ParserState}, }; use serde::{Deserialize, Serialize}; @@ -54,28 +54,38 @@ impl Parse for Parser { value } - fn decode_bin(self, state: &ParserState, bin: Bytes) -> Result { + fn decode_bin( + self, + config: &ParseConfig, + state: &ParserState, + bin: Bytes, + ) -> Result { #[cfg(feature = "tracing")] tracing::trace!(?state, "decoding bin payload: {:X}", bin); let packet = match self { - Parser::Common(p) => p.decode_bin(state, bin), + Parser::Common(p) => p.decode_bin(config, state, bin), #[cfg(feature = "msgpack")] - Parser::MsgPack(p) => p.decode_bin(state, bin), + Parser::MsgPack(p) => p.decode_bin(config, state, bin), }?; #[cfg(feature = "tracing")] tracing::trace!(?packet, "bin payload decoded:"); Ok(packet) } - fn decode_str(self, state: &ParserState, data: Str) -> Result { + fn decode_str( + self, + config: &ParseConfig, + state: &ParserState, + data: Str, + ) -> Result { #[cfg(feature = "tracing")] tracing::trace!(?data, ?state, "decoding str payload:"); let packet = match self { - Parser::Common(p) => p.decode_str(state, data), + Parser::Common(p) => p.decode_str(config, state, data), #[cfg(feature = "msgpack")] - Parser::MsgPack(p) => p.decode_str(state, data), + Parser::MsgPack(p) => p.decode_str(config, state, data), }?; #[cfg(feature = "tracing")] @@ -139,3 +149,16 @@ impl Parse for Parser { } } } + +impl From for Parser { + fn from(value: CommonParser) -> Self { + Parser::Common(value) + } +} + +#[cfg(feature = "msgpack")] +impl From for Parser { + fn from(value: MsgPackParser) -> Self { + Parser::MsgPack(value) + } +} diff --git a/crates/socketioxide/tests/acknowledgement.rs b/crates/socketioxide/tests/acknowledgement.rs index 9bbba25a..6a1ba5e2 100644 --- a/crates/socketioxide/tests/acknowledgement.rs +++ b/crates/socketioxide/tests/acknowledgement.rs @@ -6,7 +6,7 @@ use futures_util::StreamExt; use socketioxide::SocketIo; use socketioxide::extract::SocketRef; use socketioxide_core::packet::PacketData; -use socketioxide_core::parser::Parse; +use socketioxide_core::parser::{Parse, ParseConfig, ParserState}; use socketioxide_parser_common::CommonParser; use tokio::sync::mpsc; use tokio::time::Duration; @@ -101,7 +101,13 @@ pub async fn broadcast_with_ack() { Message(msg) => msg, msg => panic!("Unexpected message: {msg:?}"), }; - let ack = match assert_ok!(parser.decode_str(&Default::default(), msg)).inner { + let ack = match assert_ok!(parser.decode_str( + &ParseConfig::default(), + &ParserState::default(), + msg + )) + .inner + { PacketData::Event(_, Some(ack)) => ack, _ => panic!("Unexpected packet"), }; diff --git a/examples/Cargo.lock b/examples/Cargo.lock index edf69dc1..6d0ae178 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -1082,7 +1082,7 @@ dependencies = [ [[package]] name = "engineioxide" -version = "0.17.6" +version = "0.17.7" dependencies = [ "bytes", "engineioxide-core", @@ -1099,7 +1099,7 @@ dependencies = [ "smallvec", "thiserror", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.30.0", "tokio-util", "tower-layer", "tower-service", @@ -1157,7 +1157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1796,7 +1796,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2749,7 +2749,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.4", + "socket2 0.5.10", "thiserror", "tokio", "tracing", @@ -2788,9 +2788,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3151,7 +3151,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3210,7 +3210,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3412,7 +3412,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tracing", "ulid", @@ -3764,7 +3764,7 @@ dependencies = [ [[package]] name = "socketioxide" -version = "0.18.5" +version = "0.18.7" dependencies = [ "bytes", "engineioxide", @@ -3789,20 +3789,24 @@ dependencies = [ [[package]] name = "socketioxide-core" -version = "0.18.2" +version = "0.19.0" dependencies = [ "arbitrary", "bytes", "engineioxide-core", "futures-core", + "futures-util", + "pin-project-lite", "serde", "smallvec", "thiserror", + "tokio", + "tracing", ] [[package]] name = "socketioxide-mongodb" -version = "0.1.3" +version = "0.1.4" dependencies = [ "bson 3.1.0", "futures-core", @@ -3820,7 +3824,7 @@ dependencies = [ [[package]] name = "socketioxide-parser-common" -version = "0.17.1" +version = "0.17.2" dependencies = [ "bytes", "itoa", @@ -3831,7 +3835,7 @@ dependencies = [ [[package]] name = "socketioxide-parser-msgpack" -version = "0.17.1" +version = "0.17.3" dependencies = [ "bytes", "rmp", @@ -3842,7 +3846,7 @@ dependencies = [ [[package]] name = "socketioxide-postgres" -version = "0.1.0" +version = "0.1.1" dependencies = [ "futures-core", "futures-util", @@ -3862,7 +3866,7 @@ dependencies = [ [[package]] name = "socketioxide-redis" -version = "0.4.1" +version = "0.4.2" dependencies = [ "bytes", "fred", @@ -4096,10 +4100,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4279,7 +4283,19 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.29.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.30.0", ] [[package]] @@ -4456,14 +4472,26 @@ name = "tungstenite" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "log", + "rand 0.9.4", + "thiserror", +] + +[[package]] +name = "tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" dependencies = [ "bytes", "data-encoding", "http", "httparse", "log", - "rand 0.9.4", - "sha1 0.10.6", + "rand 0.10.2", + "sha1 0.11.0", "thiserror", ] @@ -4941,7 +4969,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]]