From 18ba5f3a36a1ee768c7b4e9f7fb9970705ff7aeb Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Wed, 5 Nov 2025 12:37:31 +0530 Subject: [PATCH 1/8] feat: add compio-ws with async WebSocket support for compio runtime --- Cargo.toml | 3 + compio-ws/Cargo.toml | 67 + compio-ws/README.md | 3 + compio-ws/autobahn/expected-results.json | 3623 ++++++++++++++++++++++ compio-ws/autobahn/fuzzingclient.json | 14 + compio-ws/autobahn/fuzzingserver.json | 9 + compio-ws/examples/autobahn-client.rs | 71 + compio-ws/examples/autobahn-server.rs | 69 + compio-ws/examples/client.rs | 42 + compio-ws/examples/client_tls.rs | 121 + compio-ws/examples/echo_server.rs | 63 + compio-ws/examples/echo_server_tls.rs | 121 + compio-ws/scripts/autobahn-client.sh | 37 + compio-ws/scripts/autobahn-server.sh | 43 + compio-ws/src/growable_sync_stream.rs | 347 +++ compio-ws/src/lib.rs | 300 ++ compio-ws/src/rustls.rs | 283 ++ compio-ws/src/stream.rs | 82 + 18 files changed, 5298 insertions(+) create mode 100644 compio-ws/Cargo.toml create mode 100644 compio-ws/README.md create mode 100644 compio-ws/autobahn/expected-results.json create mode 100644 compio-ws/autobahn/fuzzingclient.json create mode 100644 compio-ws/autobahn/fuzzingserver.json create mode 100644 compio-ws/examples/autobahn-client.rs create mode 100644 compio-ws/examples/autobahn-server.rs create mode 100644 compio-ws/examples/client.rs create mode 100644 compio-ws/examples/client_tls.rs create mode 100644 compio-ws/examples/echo_server.rs create mode 100644 compio-ws/examples/echo_server_tls.rs create mode 100755 compio-ws/scripts/autobahn-client.sh create mode 100755 compio-ws/scripts/autobahn-server.sh create mode 100644 compio-ws/src/growable_sync_stream.rs create mode 100644 compio-ws/src/lib.rs create mode 100644 compio-ws/src/rustls.rs create mode 100644 compio-ws/src/stream.rs diff --git a/Cargo.toml b/Cargo.toml index 55cafc20..964d90e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "compio-runtime", "compio-signal", "compio-tls", + "compio-ws", ] resolver = "2" @@ -25,6 +26,7 @@ license = "MIT" repository = "https://github.com/compio-rs/compio" [workspace.dependencies] +compio = { path = "./compio", version = "0.16.0" } compio-buf = { path = "./compio-buf", version = "0.7.0" } compio-driver = { path = "./compio-driver", version = "0.9.0", default-features = false } compio-runtime = { path = "./compio-runtime", version = "0.9.0" } @@ -38,6 +40,7 @@ compio-log = { path = "./compio-log", version = "0.1.0" } compio-tls = { path = "./compio-tls", version = "0.7.1", default-features = false } compio-process = { path = "./compio-process", version = "0.6.0" } compio-quic = { path = "./compio-quic", version = "0.5.0", default-features = false } +compio-ws = { path = "./compio-ws", version = "0.1.0", default-features = false } bytes = "1.7.1" cfg_aliases = "0.2.1" diff --git a/compio-ws/Cargo.toml b/compio-ws/Cargo.toml new file mode 100644 index 00000000..bb23f519 --- /dev/null +++ b/compio-ws/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "compio-ws" +version = "0.1.0" +description = "WebSocket library for the compio runtime" +edition = { workspace = true } +authors = { workspace = true } +readme = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + + +[dependencies] +rustls = { workspace = true, optional = true, features = [ + "logging", + "std", + "tls12", + "ring", +] } +rustls-native-certs = { version = "0.8", optional = true } +tungstenite = "0.27.0" +compio = { workspace = true, features = ["io", "io-compat", "macros"] } +compio-io = { workspace = true } +compio-net = { workspace = true, optional = true } +compio-tls = { workspace = true, optional = true, default-features = false, features = [ + "rustls", +] } +compio-runtime = { workspace = true } +compio-buf = { workspace = true } +webpki-roots = { version = "1.0.4", optional = true } +rustls-pemfile = "2.0" +log = "0.4" + +[features] +default = [] +connect = ["dep:compio-net"] +rustls = ["connect", "dep:compio-tls", "dep:rustls", "compio-tls/rustls"] +rustls-native-certs = ["rustls", "dep:rustls-native-certs"] +webpki-roots = ["rustls", "dep:webpki-roots"] + +[dev-dependencies] +log = "0.4" +env_logger = "0.11" + + +[[example]] +name = "echo_server" +required-features = ["connect"] + +[[example]] +name = "client" +required-features = ["connect"] + +[[example]] +name = "autobahn-client" +required-features = ["connect"] + +[[example]] +name = "autobahn-server" +required-features = ["connect"] + +[[example]] +name = "echo_server_tls" +required-features = ["connect", "rustls"] + +[[example]] +name = "client_tls" +required-features = ["connect", "rustls"] diff --git a/compio-ws/README.md b/compio-ws/README.md new file mode 100644 index 00000000..5af48527 --- /dev/null +++ b/compio-ws/README.md @@ -0,0 +1,3 @@ +# compio-ws + +WebSocket support for compio runtime. diff --git a/compio-ws/autobahn/expected-results.json b/compio-ws/autobahn/expected-results.json new file mode 100644 index 00000000..70a4efc2 --- /dev/null +++ b/compio-ws/autobahn/expected-results.json @@ -0,0 +1,3623 @@ +{ + "Tungstenite": { + "1.1.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_1.json" + }, + "1.1.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_2.json" + }, + "1.1.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_3.json" + }, + "1.1.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_4.json" + }, + "1.1.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_5.json" + }, + "1.1.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_6.json" + }, + "1.1.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_7.json" + }, + "1.1.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 6, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_1_8.json" + }, + "1.2.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_1.json" + }, + "1.2.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_2.json" + }, + "1.2.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_3.json" + }, + "1.2.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_4.json" + }, + "1.2.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_5.json" + }, + "1.2.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 31, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_6.json" + }, + "1.2.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 26, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_7.json" + }, + "1.2.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 36, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_1_2_8.json" + }, + "10.1.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_10_1_1.json" + }, + "12.1.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_1.json" + }, + "12.1.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_10.json" + }, + "12.1.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_11.json" + }, + "12.1.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_12.json" + }, + "12.1.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_13.json" + }, + "12.1.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_14.json" + }, + "12.1.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_15.json" + }, + "12.1.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_16.json" + }, + "12.1.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_17.json" + }, + "12.1.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_18.json" + }, + "12.1.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_2.json" + }, + "12.1.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_3.json" + }, + "12.1.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_4.json" + }, + "12.1.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_5.json" + }, + "12.1.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_6.json" + }, + "12.1.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_7.json" + }, + "12.1.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_8.json" + }, + "12.1.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_1_9.json" + }, + "12.2.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_1.json" + }, + "12.2.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_10.json" + }, + "12.2.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_11.json" + }, + "12.2.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_12.json" + }, + "12.2.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_13.json" + }, + "12.2.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_14.json" + }, + "12.2.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_15.json" + }, + "12.2.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_16.json" + }, + "12.2.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_17.json" + }, + "12.2.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_18.json" + }, + "12.2.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_2.json" + }, + "12.2.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_3.json" + }, + "12.2.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_4.json" + }, + "12.2.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_5.json" + }, + "12.2.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_6.json" + }, + "12.2.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_7.json" + }, + "12.2.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_8.json" + }, + "12.2.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_2_9.json" + }, + "12.3.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_1.json" + }, + "12.3.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_10.json" + }, + "12.3.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_11.json" + }, + "12.3.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_12.json" + }, + "12.3.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_13.json" + }, + "12.3.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_14.json" + }, + "12.3.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_15.json" + }, + "12.3.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_16.json" + }, + "12.3.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_17.json" + }, + "12.3.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_18.json" + }, + "12.3.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_2.json" + }, + "12.3.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_3.json" + }, + "12.3.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_4.json" + }, + "12.3.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_5.json" + }, + "12.3.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_6.json" + }, + "12.3.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_7.json" + }, + "12.3.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_8.json" + }, + "12.3.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_3_9.json" + }, + "12.4.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_1.json" + }, + "12.4.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_10.json" + }, + "12.4.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_11.json" + }, + "12.4.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_12.json" + }, + "12.4.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_13.json" + }, + "12.4.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_14.json" + }, + "12.4.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_15.json" + }, + "12.4.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_16.json" + }, + "12.4.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_17.json" + }, + "12.4.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_18.json" + }, + "12.4.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_2.json" + }, + "12.4.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_3.json" + }, + "12.4.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_4.json" + }, + "12.4.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_5.json" + }, + "12.4.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_6.json" + }, + "12.4.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_7.json" + }, + "12.4.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_8.json" + }, + "12.4.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_4_9.json" + }, + "12.5.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_1.json" + }, + "12.5.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_10.json" + }, + "12.5.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_11.json" + }, + "12.5.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_12.json" + }, + "12.5.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_13.json" + }, + "12.5.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_14.json" + }, + "12.5.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_15.json" + }, + "12.5.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_16.json" + }, + "12.5.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_17.json" + }, + "12.5.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_18.json" + }, + "12.5.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_2.json" + }, + "12.5.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_3.json" + }, + "12.5.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_4.json" + }, + "12.5.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_5.json" + }, + "12.5.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_6.json" + }, + "12.5.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_7.json" + }, + "12.5.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_8.json" + }, + "12.5.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_12_5_9.json" + }, + "13.1.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_1.json" + }, + "13.1.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_10.json" + }, + "13.1.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_11.json" + }, + "13.1.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_12.json" + }, + "13.1.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_13.json" + }, + "13.1.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_14.json" + }, + "13.1.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_15.json" + }, + "13.1.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_16.json" + }, + "13.1.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_17.json" + }, + "13.1.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_18.json" + }, + "13.1.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_2.json" + }, + "13.1.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_3.json" + }, + "13.1.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_4.json" + }, + "13.1.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_5.json" + }, + "13.1.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_6.json" + }, + "13.1.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_7.json" + }, + "13.1.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_8.json" + }, + "13.1.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_1_9.json" + }, + "13.2.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_1.json" + }, + "13.2.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_10.json" + }, + "13.2.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_11.json" + }, + "13.2.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_12.json" + }, + "13.2.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_13.json" + }, + "13.2.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_14.json" + }, + "13.2.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_15.json" + }, + "13.2.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 4, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_16.json" + }, + "13.2.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_17.json" + }, + "13.2.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_18.json" + }, + "13.2.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_2.json" + }, + "13.2.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_3.json" + }, + "13.2.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_4.json" + }, + "13.2.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_5.json" + }, + "13.2.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_6.json" + }, + "13.2.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_7.json" + }, + "13.2.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_8.json" + }, + "13.2.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_2_9.json" + }, + "13.3.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_1.json" + }, + "13.3.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_10.json" + }, + "13.3.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_11.json" + }, + "13.3.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_12.json" + }, + "13.3.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_13.json" + }, + "13.3.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_14.json" + }, + "13.3.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_15.json" + }, + "13.3.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_16.json" + }, + "13.3.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_17.json" + }, + "13.3.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_18.json" + }, + "13.3.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_2.json" + }, + "13.3.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_3.json" + }, + "13.3.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_4.json" + }, + "13.3.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_5.json" + }, + "13.3.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_6.json" + }, + "13.3.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_7.json" + }, + "13.3.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_8.json" + }, + "13.3.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_3_9.json" + }, + "13.4.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_1.json" + }, + "13.4.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_10.json" + }, + "13.4.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_11.json" + }, + "13.4.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_12.json" + }, + "13.4.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_13.json" + }, + "13.4.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_14.json" + }, + "13.4.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_15.json" + }, + "13.4.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_16.json" + }, + "13.4.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_17.json" + }, + "13.4.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_18.json" + }, + "13.4.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_2.json" + }, + "13.4.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_3.json" + }, + "13.4.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_4.json" + }, + "13.4.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_5.json" + }, + "13.4.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_6.json" + }, + "13.4.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_7.json" + }, + "13.4.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_8.json" + }, + "13.4.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_4_9.json" + }, + "13.5.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_1.json" + }, + "13.5.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_10.json" + }, + "13.5.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_11.json" + }, + "13.5.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_12.json" + }, + "13.5.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_13.json" + }, + "13.5.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_14.json" + }, + "13.5.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_15.json" + }, + "13.5.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_16.json" + }, + "13.5.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_17.json" + }, + "13.5.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_18.json" + }, + "13.5.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_2.json" + }, + "13.5.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_3.json" + }, + "13.5.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_4.json" + }, + "13.5.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_5.json" + }, + "13.5.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_6.json" + }, + "13.5.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_7.json" + }, + "13.5.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_8.json" + }, + "13.5.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_5_9.json" + }, + "13.6.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_1.json" + }, + "13.6.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_10.json" + }, + "13.6.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_11.json" + }, + "13.6.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_12.json" + }, + "13.6.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_13.json" + }, + "13.6.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_14.json" + }, + "13.6.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_15.json" + }, + "13.6.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_16.json" + }, + "13.6.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_17.json" + }, + "13.6.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_18.json" + }, + "13.6.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_2.json" + }, + "13.6.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_3.json" + }, + "13.6.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_4.json" + }, + "13.6.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_5.json" + }, + "13.6.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_6.json" + }, + "13.6.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_7.json" + }, + "13.6.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_8.json" + }, + "13.6.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_6_9.json" + }, + "13.7.1": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_1.json" + }, + "13.7.10": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_10.json" + }, + "13.7.11": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_11.json" + }, + "13.7.12": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_12.json" + }, + "13.7.13": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_13.json" + }, + "13.7.14": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_14.json" + }, + "13.7.15": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_15.json" + }, + "13.7.16": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_16.json" + }, + "13.7.17": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_17.json" + }, + "13.7.18": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_18.json" + }, + "13.7.2": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_2.json" + }, + "13.7.3": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_3.json" + }, + "13.7.4": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_4.json" + }, + "13.7.5": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_5.json" + }, + "13.7.6": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_6.json" + }, + "13.7.7": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_7.json" + }, + "13.7.8": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_8.json" + }, + "13.7.9": { + "behavior": "UNIMPLEMENTED", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_13_7_9.json" + }, + "2.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_1.json" + }, + "2.10": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_10.json" + }, + "2.11": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 10, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_11.json" + }, + "2.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_2.json" + }, + "2.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_3.json" + }, + "2.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_4.json" + }, + "2.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_2_5.json" + }, + "2.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 9, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_6.json" + }, + "2.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_7.json" + }, + "2.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_8.json" + }, + "2.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_2_9.json" + }, + "3.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_3_1.json" + }, + "3.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_3_2.json" + }, + "3.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_3_3.json" + }, + "3.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 5, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_3_4.json" + }, + "3.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_3_5.json" + }, + "3.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_3_6.json" + }, + "3.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_3_7.json" + }, + "4.1.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_1_1.json" + }, + "4.1.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_1_2.json" + }, + "4.1.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_1_3.json" + }, + "4.1.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_1_4.json" + }, + "4.1.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_1_5.json" + }, + "4.2.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_2_1.json" + }, + "4.2.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_2_2.json" + }, + "4.2.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_2_3.json" + }, + "4.2.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_2_4.json" + }, + "4.2.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_4_2_5.json" + }, + "5.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_1.json" + }, + "5.10": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_10.json" + }, + "5.11": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_11.json" + }, + "5.12": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_12.json" + }, + "5.13": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_13.json" + }, + "5.14": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_14.json" + }, + "5.15": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_15.json" + }, + "5.16": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_16.json" + }, + "5.17": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_17.json" + }, + "5.18": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_18.json" + }, + "5.19": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1004, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_19.json" + }, + "5.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_2.json" + }, + "5.20": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1005, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_20.json" + }, + "5.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_3.json" + }, + "5.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_4.json" + }, + "5.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_5.json" + }, + "5.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_6.json" + }, + "5.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_7.json" + }, + "5.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 17, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_5_8.json" + }, + "5.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_5_9.json" + }, + "6.1.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_1_1.json" + }, + "6.1.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_1_2.json" + }, + "6.1.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_1_3.json" + }, + "6.10.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_10_1.json" + }, + "6.10.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_10_2.json" + }, + "6.10.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_10_3.json" + }, + "6.11.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_11_1.json" + }, + "6.11.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_11_2.json" + }, + "6.11.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_11_3.json" + }, + "6.11.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_11_4.json" + }, + "6.11.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_11_5.json" + }, + "6.12.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_1.json" + }, + "6.12.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_2.json" + }, + "6.12.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_3.json" + }, + "6.12.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_4.json" + }, + "6.12.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_5.json" + }, + "6.12.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_6.json" + }, + "6.12.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_7.json" + }, + "6.12.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_12_8.json" + }, + "6.13.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_13_1.json" + }, + "6.13.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_13_2.json" + }, + "6.13.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_13_3.json" + }, + "6.13.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_13_4.json" + }, + "6.13.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_13_5.json" + }, + "6.14.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_1.json" + }, + "6.14.10": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_10.json" + }, + "6.14.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_2.json" + }, + "6.14.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_3.json" + }, + "6.14.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_4.json" + }, + "6.14.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_5.json" + }, + "6.14.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_6.json" + }, + "6.14.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_7.json" + }, + "6.14.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_8.json" + }, + "6.14.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_14_9.json" + }, + "6.15.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_15_1.json" + }, + "6.16.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_16_1.json" + }, + "6.16.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_16_2.json" + }, + "6.16.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_16_3.json" + }, + "6.17.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_17_1.json" + }, + "6.17.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_17_2.json" + }, + "6.17.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_17_3.json" + }, + "6.17.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_17_4.json" + }, + "6.17.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_17_5.json" + }, + "6.18.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_18_1.json" + }, + "6.18.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_18_2.json" + }, + "6.18.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_18_3.json" + }, + "6.18.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_18_4.json" + }, + "6.18.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_18_5.json" + }, + "6.19.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_19_1.json" + }, + "6.19.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_19_2.json" + }, + "6.19.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_19_3.json" + }, + "6.19.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_19_4.json" + }, + "6.19.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_19_5.json" + }, + "6.2.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_2_1.json" + }, + "6.2.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_2_2.json" + }, + "6.2.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 4, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_2_3.json" + }, + "6.2.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_2_4.json" + }, + "6.20.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_20_1.json" + }, + "6.20.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_20_2.json" + }, + "6.20.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_20_3.json" + }, + "6.20.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_20_4.json" + }, + "6.20.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_20_5.json" + }, + "6.20.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_20_6.json" + }, + "6.20.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_20_7.json" + }, + "6.21.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_1.json" + }, + "6.21.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_2.json" + }, + "6.21.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_3.json" + }, + "6.21.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_4.json" + }, + "6.21.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_5.json" + }, + "6.21.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_6.json" + }, + "6.21.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_7.json" + }, + "6.21.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_21_8.json" + }, + "6.22.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_1.json" + }, + "6.22.10": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_10.json" + }, + "6.22.11": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_11.json" + }, + "6.22.12": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_12.json" + }, + "6.22.13": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_13.json" + }, + "6.22.14": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_14.json" + }, + "6.22.15": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_15.json" + }, + "6.22.16": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_16.json" + }, + "6.22.17": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_17.json" + }, + "6.22.18": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_18.json" + }, + "6.22.19": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_19.json" + }, + "6.22.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_2.json" + }, + "6.22.20": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_20.json" + }, + "6.22.21": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_21.json" + }, + "6.22.22": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_22.json" + }, + "6.22.23": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_23.json" + }, + "6.22.24": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_24.json" + }, + "6.22.25": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_25.json" + }, + "6.22.26": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_26.json" + }, + "6.22.27": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_27.json" + }, + "6.22.28": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_28.json" + }, + "6.22.29": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_29.json" + }, + "6.22.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 3, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_3.json" + }, + "6.22.30": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_30.json" + }, + "6.22.31": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_31.json" + }, + "6.22.32": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_32.json" + }, + "6.22.33": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_33.json" + }, + "6.22.34": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_34.json" + }, + "6.22.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 4, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_4.json" + }, + "6.22.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_5.json" + }, + "6.22.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_6.json" + }, + "6.22.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_7.json" + }, + "6.22.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_8.json" + }, + "6.22.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_22_9.json" + }, + "6.23.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_23_1.json" + }, + "6.23.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_23_2.json" + }, + "6.23.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_23_3.json" + }, + "6.23.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_23_4.json" + }, + "6.23.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_23_5.json" + }, + "6.23.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_23_6.json" + }, + "6.23.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_23_7.json" + }, + "6.3.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_3_1.json" + }, + "6.3.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_3_2.json" + }, + "6.4.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1001, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_4_1.json" + }, + "6.4.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1002, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_4_2.json" + }, + "6.4.3": { + "behavior": "NON-STRICT", + "behaviorClose": "OK", + "duration": 2002, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_4_3.json" + }, + "6.4.4": { + "behavior": "NON-STRICT", + "behaviorClose": "OK", + "duration": 2002, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_4_4.json" + }, + "6.5.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_5_1.json" + }, + "6.5.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_5_2.json" + }, + "6.5.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_5_3.json" + }, + "6.5.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_5_4.json" + }, + "6.5.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_5_5.json" + }, + "6.6.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_6_1.json" + }, + "6.6.10": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_6_10.json" + }, + "6.6.11": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 5, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_6_11.json" + }, + "6.6.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_6_2.json" + }, + "6.6.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_6_3.json" + }, + "6.6.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_6_4.json" + }, + "6.6.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_6_5.json" + }, + "6.6.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_6_6.json" + }, + "6.6.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_6_7.json" + }, + "6.6.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_6_8.json" + }, + "6.6.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_6_9.json" + }, + "6.7.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_7_1.json" + }, + "6.7.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_7_2.json" + }, + "6.7.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_7_3.json" + }, + "6.7.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_7_4.json" + }, + "6.8.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_8_1.json" + }, + "6.8.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_6_8_2.json" + }, + "6.9.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_9_1.json" + }, + "6.9.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_9_2.json" + }, + "6.9.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_9_3.json" + }, + "6.9.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_6_9_4.json" + }, + "7.1.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_1_1.json" + }, + "7.1.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_1_2.json" + }, + "7.1.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_1_3.json" + }, + "7.1.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_1_4.json" + }, + "7.1.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_1_5.json" + }, + "7.1.6": { + "behavior": "INFORMATIONAL", + "behaviorClose": "INFORMATIONAL", + "duration": 16, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_1_6.json" + }, + "7.13.1": { + "behavior": "INFORMATIONAL", + "behaviorClose": "INFORMATIONAL", + "duration": 1, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_13_1.json" + }, + "7.13.2": { + "behavior": "INFORMATIONAL", + "behaviorClose": "INFORMATIONAL", + "duration": 2, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_13_2.json" + }, + "7.3.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_7_3_1.json" + }, + "7.3.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_7_3_2.json" + }, + "7.3.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_3_3.json" + }, + "7.3.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_3_4.json" + }, + "7.3.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_3_5.json" + }, + "7.3.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_7_3_6.json" + }, + "7.5.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": null, + "reportfile": "tungstenite_case_7_5_1.json" + }, + "7.7.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_7_7_1.json" + }, + "7.7.10": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 3000, + "reportfile": "tungstenite_case_7_7_10.json" + }, + "7.7.11": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 3999, + "reportfile": "tungstenite_case_7_7_11.json" + }, + "7.7.12": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 4000, + "reportfile": "tungstenite_case_7_7_12.json" + }, + "7.7.13": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 4999, + "reportfile": "tungstenite_case_7_7_13.json" + }, + "7.7.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1001, + "reportfile": "tungstenite_case_7_7_2.json" + }, + "7.7.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_7_3.json" + }, + "7.7.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1003, + "reportfile": "tungstenite_case_7_7_4.json" + }, + "7.7.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1007, + "reportfile": "tungstenite_case_7_7_5.json" + }, + "7.7.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1008, + "reportfile": "tungstenite_case_7_7_6.json" + }, + "7.7.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1009, + "reportfile": "tungstenite_case_7_7_7.json" + }, + "7.7.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1010, + "reportfile": "tungstenite_case_7_7_8.json" + }, + "7.7.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1011, + "reportfile": "tungstenite_case_7_7_9.json" + }, + "7.9.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_1.json" + }, + "7.9.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_2.json" + }, + "7.9.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_3.json" + }, + "7.9.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_4.json" + }, + "7.9.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_5.json" + }, + "7.9.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_6.json" + }, + "7.9.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 0, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_7.json" + }, + "7.9.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_8.json" + }, + "7.9.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 1, + "remoteCloseCode": 1002, + "reportfile": "tungstenite_case_7_9_9.json" + }, + "9.1.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 5, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_1_1.json" + }, + "9.1.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 6, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_1_2.json" + }, + "9.1.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 22, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_1_3.json" + }, + "9.1.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 110, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_1_4.json" + }, + "9.1.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 205, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_1_5.json" + }, + "9.1.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 320, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_1_6.json" + }, + "9.2.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 2, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_2_1.json" + }, + "9.2.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 6, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_2_2.json" + }, + "9.2.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 16, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_2_3.json" + }, + "9.2.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 123, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_2_4.json" + }, + "9.2.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 169, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_2_5.json" + }, + "9.2.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 360, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_2_6.json" + }, + "9.3.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 638, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_1.json" + }, + "9.3.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 209, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_2.json" + }, + "9.3.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 93, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_3.json" + }, + "9.3.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 64, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_4.json" + }, + "9.3.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 43, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_5.json" + }, + "9.3.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 50, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_6.json" + }, + "9.3.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 45, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_7.json" + }, + "9.3.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 51, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_8.json" + }, + "9.3.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 57, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_3_9.json" + }, + "9.4.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 608, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_1.json" + }, + "9.4.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 192, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_2.json" + }, + "9.4.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 75, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_3.json" + }, + "9.4.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 44, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_4.json" + }, + "9.4.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 29, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_5.json" + }, + "9.4.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 39, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_6.json" + }, + "9.4.7": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 35, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_7.json" + }, + "9.4.8": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 24, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_8.json" + }, + "9.4.9": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 29, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_4_9.json" + }, + "9.5.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 795, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_5_1.json" + }, + "9.5.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 425, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_5_2.json" + }, + "9.5.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 228, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_5_3.json" + }, + "9.5.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 114, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_5_4.json" + }, + "9.5.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 84, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_5_5.json" + }, + "9.5.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 51, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_5_6.json" + }, + "9.6.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 799, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_6_1.json" + }, + "9.6.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 429, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_6_2.json" + }, + "9.6.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 214, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_6_3.json" + }, + "9.6.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 127, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_6_4.json" + }, + "9.6.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 80, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_6_5.json" + }, + "9.6.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 57, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_6_6.json" + }, + "9.7.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 127, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_7_1.json" + }, + "9.7.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 121, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_7_2.json" + }, + "9.7.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 124, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_7_3.json" + }, + "9.7.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 146, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_7_4.json" + }, + "9.7.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 196, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_7_5.json" + }, + "9.7.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 351, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_7_6.json" + }, + "9.8.1": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 95, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_8_1.json" + }, + "9.8.2": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 119, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_8_2.json" + }, + "9.8.3": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 115, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_8_3.json" + }, + "9.8.4": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 134, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_8_4.json" + }, + "9.8.5": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 187, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_8_5.json" + }, + "9.8.6": { + "behavior": "OK", + "behaviorClose": "OK", + "duration": 328, + "remoteCloseCode": 1000, + "reportfile": "tungstenite_case_9_8_6.json" + } + } +} \ No newline at end of file diff --git a/compio-ws/autobahn/fuzzingclient.json b/compio-ws/autobahn/fuzzingclient.json new file mode 100644 index 00000000..26a442fb --- /dev/null +++ b/compio-ws/autobahn/fuzzingclient.json @@ -0,0 +1,14 @@ +{ + "outdir": "./autobahn/server", + "servers": [ + { + "agent": "Tungstenite", + "url": "ws://127.0.0.1:9002" + } + ], + "cases": [ + "*" + ], + "exclude-cases": [], + "exclude-agent-cases": {} +} \ No newline at end of file diff --git a/compio-ws/autobahn/fuzzingserver.json b/compio-ws/autobahn/fuzzingserver.json new file mode 100644 index 00000000..c6841427 --- /dev/null +++ b/compio-ws/autobahn/fuzzingserver.json @@ -0,0 +1,9 @@ +{ + "url": "ws://127.0.0.1:9001", + "outdir": "./autobahn/client", + "cases": [ + "*" + ], + "exclude-cases": [], + "exclude-agent-cases": {} +} \ No newline at end of file diff --git a/compio-ws/examples/autobahn-client.rs b/compio-ws/examples/autobahn-client.rs new file mode 100644 index 00000000..188011af --- /dev/null +++ b/compio-ws/examples/autobahn-client.rs @@ -0,0 +1,71 @@ +use compio_net::TcpStream; +use compio_ws::client_async; +use log::*; +use tungstenite::{Error, Result}; + +const AGENT: &str = "Tungstenite"; + +async fn get_case_count() -> Result { + let stream = TcpStream::connect("127.0.0.1:9001").await?; + let (mut socket, _) = client_async("ws://localhost:9001/getCaseCount", stream).await?; + let msg = socket.read().await?; + socket.close(None).await?; + Ok(msg + .to_text()? + .parse::() + .expect("Can't parse case count")) +} + +async fn update_reports() -> Result<()> { + let stream = TcpStream::connect("127.0.0.1:9001").await?; + let (mut socket, _) = client_async( + &format!("ws://localhost:9001/updateReports?agent={AGENT}"), + stream, + ) + .await?; + socket.close(None).await?; + Ok(()) +} + +async fn run_test(case: u32) -> Result<()> { + info!("Running test case {case}"); + let case_url = format!("ws://localhost:9001/runCase?case={case}&agent={AGENT}"); + let stream = TcpStream::connect("127.0.0.1:9001").await?; + let (mut ws_stream, _) = client_async(&case_url, stream).await?; + + loop { + match ws_stream.read().await { + Ok(msg) => { + if msg.is_text() || msg.is_binary() { + ws_stream.send(msg).await?; + } else if msg.is_close() { + break; + } + } + Err(e) => { + error!("Error reading message: {e}"); + return Err(e); + } + } + } + + Ok(()) +} + +#[compio::main] +async fn main() { + env_logger::init(); + + let total = get_case_count().await.expect("Error getting case count"); + + for case in 1..=total { + if let Err(e) = run_test(case).await { + match e { + Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8(_) => (), + err => error!("Testcase failed: {err}"), + } + } + } + + update_reports().await.expect("Error updating reports"); +} diff --git a/compio-ws/examples/autobahn-server.rs b/compio-ws/examples/autobahn-server.rs new file mode 100644 index 00000000..21d9f541 --- /dev/null +++ b/compio-ws/examples/autobahn-server.rs @@ -0,0 +1,69 @@ +use compio_net::{TcpListener, TcpStream}; +use compio_ws::{accept_async_with_config, WebSocketConfig}; +use log::*; +use std::net::SocketAddr; +use tungstenite::{Error, Result}; + +async fn accept_connection(peer: SocketAddr, stream: TcpStream) { + if let Err(e) = handle_connection(peer, stream).await { + match e { + Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8(_) => (), + err => error!("Error processing connection: {err}"), + } + } +} + +async fn handle_connection(peer: SocketAddr, stream: TcpStream) -> Result<()> { + let mut config = WebSocketConfig::default(); + config.max_message_size = Some(64 * 1024 * 1024); + config.max_frame_size = Some(16 * 1024 * 1024); + + let mut ws_stream = accept_async_with_config(stream, Some(config)) + .await + .expect("Failed to accept"); + + info!("New WebSocket connection: {peer}"); + + loop { + match ws_stream.read().await { + Ok(msg) => { + if msg.is_text() || msg.is_binary() { + ws_stream.send(msg).await?; + } + } + Err(e) => match e { + Error::ConnectionClosed => { + info!("Connection closed normally: {peer}"); + break; + } + _ => { + error!("Error: {e}"); + return Err(e); + } + }, + } + } + + Ok(()) +} + +#[compio::main] +async fn main() { + env_logger::init(); + + let addr = "127.0.0.1:9002"; + let listener = TcpListener::bind(addr).await.expect("Can't listen"); + info!("Listening on: {addr}"); + + loop { + match listener.accept().await { + Ok((stream, addr)) => { + info!("Peer address: {addr}"); + compio_runtime::spawn(accept_connection(addr, stream)).detach(); + } + Err(e) => { + error!("Error accepting connection: {e}"); + } + } + } +} diff --git a/compio-ws/examples/client.rs b/compio-ws/examples/client.rs new file mode 100644 index 00000000..e2ef3e97 --- /dev/null +++ b/compio-ws/examples/client.rs @@ -0,0 +1,42 @@ +use compio_net::TcpStream; +use compio_ws::client_async; +use tungstenite::Message; + +#[compio::main] +async fn main() -> Result<(), Box> { + println!("Connecting to WebSocket server"); + + let stream = TcpStream::connect("127.0.0.1:9002").await?; + let (mut websocket, _response) = client_async("ws://127.0.0.1:9002", stream).await?; + + println!("Connected to WebSocket server"); + + println!("Sending text message"); + websocket + .send(Message::Text("Hello, server!".into())) + .await?; + + println!("Sending binary message"); + websocket + .send(Message::Binary(vec![1, 2, 3, 4, 5].into())) + .await?; + + println!("Sending ping"); + websocket.send(Message::Ping(vec![42].into())).await?; + + println!("Reading responses"); + for i in 0..3 { + match websocket.read().await? { + Message::Text(text) => println!(" Response {}: Text: {}", i + 1, text), + Message::Binary(data) => println!(" Response {}: Binary: {} bytes", i + 1, data.len()), + Message::Pong(data) => println!(" Response {}: Pong: {:?}", i + 1, data), + other => println!(" Response {}: {:?}", i + 1, other), + } + } + + println!("Closing connection"); + websocket.close(None).await?; + println!("Connection closed successfully"); + + Ok(()) +} diff --git a/compio-ws/examples/client_tls.rs b/compio-ws/examples/client_tls.rs new file mode 100644 index 00000000..3d2a5cbb --- /dev/null +++ b/compio-ws/examples/client_tls.rs @@ -0,0 +1,121 @@ +use compio_ws::{connect_async_with_tls_connector, Connector}; +use rustls::ClientConfig; +use std::sync::Arc; +use tungstenite::Message; + +async fn create_insecure_tls_connector() -> Result> { + // Create a TLS connector that accepts self-signed certificates + // This is needed for testing with localhost self-signed certificates + // WARNING: This is insecure and should only be used for testing! + + #[derive(Debug)] + struct AcceptAllVerifier; + + impl rustls::client::danger::ServerCertVerifier for AcceptAllVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA1, + rustls::SignatureScheme::ECDSA_SHA1_Legacy, + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::ECDSA_NISTP521_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::ED25519, + rustls::SignatureScheme::ED448, + ] + } + } + + let config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(AcceptAllVerifier)) + .with_no_client_auth(); + + Ok(Connector::from(Arc::new(config))) +} + +#[compio::main] +async fn main() -> Result<(), Box> { + // Create an insecure TLS connector for testing with self-signed certificates + // This accepts the self-signed certificate generated by the server + let connector = create_insecure_tls_connector().await?; + + let (mut websocket, _response) = + connect_async_with_tls_connector("wss://127.0.0.1:9002", Some(connector)).await?; + + println!("Successfully connected to WebSocket TLS server!"); + println!(); + + println!("Test 1: Sending text message"); + websocket + .send(Message::Text("Hello, secure server!".into())) + .await?; + + println!("Test 2: Sending binary message"); + websocket + .send(Message::Binary(vec![1, 2, 3, 4, 5].into())) + .await?; + + println!("Test 3: Sending ping"); + websocket.send(Message::Ping(vec![42].into())).await?; + + println!("Reading responses from server:"); + + for i in 0..3 { + match websocket.read().await? { + Message::Text(text) => { + println!(" Response {}: Text: {}", i + 1, text); + } + Message::Binary(data) => { + let data_str = String::from_utf8_lossy(&data); + println!(" Response {}: Binary: {}", i + 1, data_str); + } + Message::Pong(data) => { + println!(" Response {}: Pong: {:?}", i + 1, data); + } + other => { + println!(" Response {}: {:?}", i + 1, other); + } + } + } + + println!("Closing TLS connection..."); + websocket.close(None).await?; + println!("TLS connection closed successfully!"); + + Ok(()) +} diff --git a/compio-ws/examples/echo_server.rs b/compio-ws/examples/echo_server.rs new file mode 100644 index 00000000..d4fa5f62 --- /dev/null +++ b/compio-ws/examples/echo_server.rs @@ -0,0 +1,63 @@ +use compio_net::{TcpListener, TcpStream}; +use compio_ws::accept_async; +use tungstenite::Message; + +#[compio::main] +async fn main() -> Result<(), Box> { + let listener = TcpListener::bind("127.0.0.1:9002").await?; + println!("WebSocket echo server listening on ws://127.0.0.1:9002"); + + loop { + let (stream, addr) = listener.accept().await?; + println!("New client connected: {addr}"); + + compio_runtime::spawn(async move { + if let Err(e) = handle_client(stream).await { + eprintln!("Error handling client {addr}: {e}"); + } + }) + .detach(); + } +} + +async fn handle_client(stream: TcpStream) -> Result<(), Box> { + let mut websocket = accept_async(stream).await?; + println!("Handshake successful"); + + loop { + match websocket.read().await? { + Message::Text(text) => { + println!("Received text: {}", text.len()); + let echo_msg = format!("Echo: {text}"); + println!("Sending echo: {echo_msg}"); + + websocket.send(Message::Text(text)).await?; + println!("Echo sent successfully"); + } + Message::Binary(data) => { + println!("Received {} bytes of binary data", data.len()); + println!("Sending binary echo..."); + websocket.send(Message::Binary(data)).await?; + println!("Binary echo sent successfully"); + } + Message::Ping(data) => { + println!("Received ping, sending pong"); + websocket.send(Message::Pong(data)).await?; + println!("Pong sent successfully"); + } + Message::Pong(_) => { + println!("Received pong"); + } + Message::Close(frame) => { + println!("Received close frame: {frame:?}"); + break; + } + Message::Frame(_) => { + println!("Received raw frame"); + } + } + } + + println!("Client disconnected"); + Ok(()) +} diff --git a/compio-ws/examples/echo_server_tls.rs b/compio-ws/examples/echo_server_tls.rs new file mode 100644 index 00000000..21b326bf --- /dev/null +++ b/compio-ws/examples/echo_server_tls.rs @@ -0,0 +1,121 @@ +use compio_net::{TcpListener, TcpStream}; +use compio_tls::TlsAcceptor; +use compio_ws::accept_async; +use rustls::ServerConfig; +use std::fs; +use std::sync::Arc; +use tungstenite::Message; + +async fn create_tls_acceptor() -> Result> { + // Load certificate and key from files + // Generate these files with: + // openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -days 365 -nodes -subj "/CN=localhost" + + let cert_file = fs::read_to_string("localhost.crt")?; + let key_file = fs::read_to_string("localhost.key")?; + + // Parse certificate + let cert_der = rustls_pemfile::certs(&mut cert_file.as_bytes()) + .collect::, _>>()? + .into_iter() + .next() + .ok_or("No certificate found in localhost.crt")?; + + // Parse private key + let key_der = rustls_pemfile::private_key(&mut key_file.as_bytes())? + .ok_or("No private key found in localhost.key")?; + + let cert_chain = vec![cert_der]; + + let config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, key_der)?; + + Ok(TlsAcceptor::from(Arc::new(config))) +} + +#[compio::main] +async fn main() -> Result<(), Box> { + // Check if certificate files exist + if !std::path::Path::new("localhost.crt").exists() + || !std::path::Path::new("localhost.key").exists() + { + eprintln!("Error: Certificate files not found!"); + eprintln!("Please generate them with:"); + eprintln!("openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -days 365 -nodes -subj \"/CN=localhost\""); + return Err("Missing certificate files".into()); + } + + // Create TLS acceptor + let tls_acceptor = create_tls_acceptor().await?; + + let listener = TcpListener::bind("127.0.0.1:9002").await?; + println!("WebSocket TLS echo server listening on wss://127.0.0.1:9002"); + + loop { + let (stream, addr) = listener.accept().await?; + println!("New client connected: {addr}"); + + let acceptor = tls_acceptor.clone(); + compio_runtime::spawn(async move { + if let Err(e) = handle_client_tls(stream, acceptor).await { + eprintln!("Error handling client {addr}: {e}"); + } + }) + .detach(); + } +} + +async fn handle_client_tls( + stream: TcpStream, + acceptor: TlsAcceptor, +) -> Result<(), Box> { + // Perform TLS handshake + println!("Performing TLS handshake..."); + let tls_stream = acceptor.accept(stream).await?; + println!("TLS handshake completed"); + + // Perform WebSocket handshake + println!("Performing WebSocket handshake..."); + let mut websocket = accept_async(tls_stream).await?; + println!("WebSocket handshake successful"); + + loop { + match websocket.read().await? { + Message::Text(text) => { + println!("Received text: {text}"); + let echo_msg = format!("Echo: {text}"); + println!("Sending echo: {echo_msg}"); + + websocket.send(Message::Text(echo_msg.into())).await?; + println!("Echo sent successfully"); + } + Message::Binary(data) => { + println!("Received {} bytes of binary data", data.len()); + println!("Sending binary echo..."); + let mut echo_data = b"TLS Binary Echo: ".to_vec(); + echo_data.extend_from_slice(&data); + websocket.send(Message::Binary(echo_data.into())).await?; + println!("Binary echo sent successfully"); + } + Message::Ping(data) => { + println!("Received ping, sending pong"); + websocket.send(Message::Pong(data)).await?; + println!("Pong sent successfully"); + } + Message::Pong(_) => { + println!("Received pong"); + } + Message::Close(frame) => { + println!("Received close frame: {frame:?}"); + break; + } + Message::Frame(_) => { + println!("Received raw frame"); + } + } + } + + println!("TLS client disconnected"); + Ok(()) +} diff --git a/compio-ws/scripts/autobahn-client.sh b/compio-ws/scripts/autobahn-client.sh new file mode 100755 index 00000000..730c0976 --- /dev/null +++ b/compio-ws/scripts/autobahn-client.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Autobahn client test script for compio-ws +set -euo pipefail +set -x +SOURCE_DIR=$(readlink -f "${BASH_SOURCE[0]}") +SOURCE_DIR=$(dirname "$SOURCE_DIR") +cd "${SOURCE_DIR}/../.." + +CONTAINER_NAME=fuzzingserver +function cleanup() { + docker container stop "${CONTAINER_NAME}" +} +trap cleanup TERM EXIT + +function test_diff() { + if ! diff -q \ + <(jq -S 'del(."Tungstenite" | .. | .duration?)' 'compio-ws/autobahn/expected-results.json') \ + <(jq -S 'del(."Tungstenite" | .. | .duration?)' 'compio-ws/autobahn/client/index.json') + then + echo 'Difference in results, either this is a regression or' \ + 'one should update compio-ws/autobahn/expected-results.json with the new results.' + exit 64 + fi +} + +docker run -d --rm \ + -v "${PWD}/compio-ws/autobahn:/autobahn" \ + -p 9001:9001 \ + --init \ + --name "${CONTAINER_NAME}" \ + crossbario/autobahn-testsuite \ + wstest -m fuzzingserver -s 'autobahn/fuzzingserver.json' + +sleep 5 +cargo build --release --features connect -p compio-ws --example autobahn-client +cargo run --release --features connect -p compio-ws --example autobahn-client +test_diff \ No newline at end of file diff --git a/compio-ws/scripts/autobahn-server.sh b/compio-ws/scripts/autobahn-server.sh new file mode 100755 index 00000000..4d2a4a4e --- /dev/null +++ b/compio-ws/scripts/autobahn-server.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +# Get to workspace root (compio/) +SOURCE_DIR=$(readlink -f "${BASH_SOURCE[0]}") +SOURCE_DIR=$(dirname "$SOURCE_DIR") +cd "${SOURCE_DIR}/../.." # Go up to workspace root + +WSSERVER_PID="" + +function cleanup() { + if [ -n "${WSSERVER_PID}" ]; then + kill -9 ${WSSERVER_PID} 2>/dev/null || true + fi +} +trap cleanup TERM EXIT INT + +function test_diff() { + if ! diff -q \ + <(jq -S 'del(."Tungstenite" | .. | .duration?)' 'compio-ws/autobahn/expected-results.json') \ + <(jq -S 'del(."Tungstenite" | .. | .duration?)' 'compio-ws/autobahn/server/index.json') + then + echo 'Difference in results, either this is a regression or' \ + 'one should update compio-ws/autobahn/expected-results.json with the new results.' + exit 64 + fi +} + +cargo build --release --features connect -p compio-ws --example autobahn-server + +cargo run --release --features connect -p compio-ws --example autobahn-server & +WSSERVER_PID=$! + +sleep 5 + +docker run --rm \ + -v "${PWD}/compio-ws/autobahn:/autobahn" \ + --network host \ + crossbario/autobahn-testsuite \ + wstest -m fuzzingclient -s 'autobahn/fuzzingclient.json' + +test_diff \ No newline at end of file diff --git a/compio-ws/src/growable_sync_stream.rs b/compio-ws/src/growable_sync_stream.rs new file mode 100644 index 00000000..5e8c968c --- /dev/null +++ b/compio-ws/src/growable_sync_stream.rs @@ -0,0 +1,347 @@ +use std::io::{self, Read, Write}; + +use compio_buf::{BufResult, IntoInner, IoBuf}; +use compio_io::{AsyncRead, AsyncWrite}; + +/// A growable buffered stream adapter that bridges async I/O with sync traits. +/// +/// This is similar to `compio_io::compat::SyncStream` but with dynamically growing +/// buffers that can expand beyond the initial capacity up to a configurable maximum. +/// +/// # Buffer Growth Strategy +/// +/// - **Read buffer**: Grows as needed to accommodate incoming data, up to `max_buffer_size` +/// - **Write buffer**: Grows as needed for outgoing data, up to `max_buffer_size` +/// - Both buffers shrink back to `base_capacity` when fully consumed and capacity exceeds 4x base +/// +/// # Usage Pattern +/// +/// The sync `Read` and `Write` implementations will return `WouldBlock` errors when +/// buffers need servicing via the async methods: +/// +/// - Call `fill_read_buf()` when `Read::read()` returns `WouldBlock` +/// - Call `flush_write_buf()` when `Write::write()` returns `WouldBlock` +/// +/// # Note on flush() +/// +/// The `Write::flush()` method intentionally returns `Ok(())` without checking if there's +/// buffered data. This is for compatibility with libraries like tungstenite that call +/// `flush()` after every write. Actual flushing happens via the async `flush_write_buf()` method. +#[derive(Debug)] +pub struct GrowableSyncStream { + inner: S, + read_buf: Vec, + read_pos: usize, + write_buf: Vec, + eof: bool, + base_capacity: usize, + max_buffer_size: usize, +} + +impl GrowableSyncStream { + const DEFAULT_BASE_CAPACITY: usize = 8 * 1024; // 8KB base + const DEFAULT_MAX_BUFFER: usize = 64 * 1024 * 1024; // 64MB max + + /// Creates a new `GrowableSyncStream` with default buffer sizes. + /// + /// - Base capacity: 8KB + /// - Max buffer size: 64MB + pub fn new(stream: S) -> Self { + Self::with_capacity(Self::DEFAULT_BASE_CAPACITY, stream) + } + + /// Creates a new `GrowableSyncStream` with a custom base capacity. + /// + /// The maximum buffer size defaults to 64MB. + pub fn with_capacity(base_capacity: usize, stream: S) -> Self { + Self { + inner: stream, + read_buf: Vec::with_capacity(base_capacity), + read_pos: 0, + write_buf: Vec::with_capacity(base_capacity), + eof: false, + base_capacity, + max_buffer_size: Self::DEFAULT_MAX_BUFFER, + } + } + + /// Creates a new `GrowableSyncStream` with custom base capacity and maximum buffer size. + pub fn with_limits(base_capacity: usize, max_buffer_size: usize, stream: S) -> Self { + Self { + inner: stream, + read_buf: Vec::with_capacity(base_capacity), + read_pos: 0, + write_buf: Vec::with_capacity(base_capacity), + eof: false, + base_capacity, + max_buffer_size, + } + } + + /// Returns a reference to the underlying stream. + pub fn get_ref(&self) -> &S { + &self.inner + } + + /// Returns a mutable reference to the underlying stream. + pub fn get_mut(&mut self) -> &mut S { + &mut self.inner + } + + /// Consumes the `GrowableSyncStream`, returning the underlying stream. + pub fn into_inner(self) -> S { + self.inner + } + + /// Returns `true` if the stream has reached EOF. + pub fn is_eof(&self) -> bool { + self.eof + } + + /// Returns the available bytes in the read buffer. + fn available_read(&self) -> &[u8] { + &self.read_buf[self.read_pos..] + } + + /// Marks `amt` bytes as consumed from the read buffer. + /// + /// Resets the buffer when all data is consumed and shrinks capacity + /// if it has grown significantly beyond the base capacity. + fn consume_read(&mut self, amt: usize) { + self.read_pos += amt; + + // Shrink oversized buffers back to base capacity + if self.read_pos >= self.read_buf.len() { + self.read_pos = 0; + + if self.read_buf.capacity() > self.base_capacity * 4 { + self.read_buf = Vec::with_capacity(self.base_capacity); + } else { + self.read_buf.clear(); + } + } + } +} + +impl Read for GrowableSyncStream { + /// Reads data from the internal buffer. + /// + /// Returns `WouldBlock` if the buffer is empty and not at EOF, + /// indicating that `fill_read_buf()` should be called. + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let available = self.available_read(); + + if available.is_empty() && !self.eof { + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "need to fill read buffer", + )); + } + + let to_read = available.len().min(buf.len()); + buf[..to_read].copy_from_slice(&available[..to_read]); + self.consume_read(to_read); + + Ok(to_read) + } +} + +impl Write for GrowableSyncStream { + /// Writes data to the internal buffer. + /// + /// Returns `WouldBlock` if the buffer needs flushing or has reached max capacity. + /// In the latter case, it may write partial data before returning `WouldBlock`. + fn write(&mut self, buf: &[u8]) -> io::Result { + // Check if we should flush first + if self.write_buf.len() > self.base_capacity * 2 / 3 && !self.write_buf.is_empty() { + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "need to flush write buffer", + )); + } + + // Check if write would exceed max buffer size + if self.write_buf.len() + buf.len() > self.max_buffer_size { + let space = self.max_buffer_size - self.write_buf.len(); + if space == 0 { + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "write buffer full, need to flush", + )); + } + self.write_buf.extend_from_slice(&buf[..space]); + return Ok(space); + } + + self.write_buf.extend_from_slice(buf); + Ok(buf.len()) + } + + /// Returns `Ok(())` without checking for buffered data. + /// + /// **Important**: This does NOT actually flush data to the underlying stream. + /// This behavior is intentional for compatibility with libraries like tungstenite + /// that call `flush()` after every write operation. The actual async flush + /// happens when `flush_write_buf()` is called. + /// + /// This prevents spurious errors in sync code that expects `flush()` to succeed + /// after successfully buffering data. + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl GrowableSyncStream { + /// Fills the read buffer by reading from the underlying async stream. + /// + /// This method: + /// 1. Compacts the buffer if there's unconsumed data + /// 2. Ensures there's space for at least `base_capacity` more bytes + /// 3. Reads data from the underlying stream + /// 4. Returns the number of bytes read (0 indicates EOF) + /// + /// # Errors + /// + /// Returns an error if: + /// - The read buffer has reached `max_buffer_size` + /// - The underlying stream returns an error + pub async fn fill_read_buf(&mut self) -> io::Result { + if self.eof { + return Ok(0); + } + + // Compact buffer, move unconsumed data to the front + if self.read_pos > 0 && self.read_pos < self.read_buf.len() { + let buf_len = self.read_buf.len(); + let remaining = buf_len - self.read_pos; + self.read_buf.copy_within(self.read_pos..buf_len, 0); + + // SAFETY: We're setting the length to the amount of data we just moved. + // The data from 0..remaining is initialized (just moved from read_pos..buf_len) + unsafe { + self.read_buf.set_len(remaining); + } + self.read_pos = 0; + } else if self.read_pos >= self.read_buf.len() { + // All data consumed, reset buffer + self.read_pos = 0; + if self.read_buf.capacity() > self.base_capacity * 4 { + self.read_buf = Vec::with_capacity(self.base_capacity); + } else { + self.read_buf.clear(); + } + } + + let current_len = self.read_buf.len(); + + if current_len >= self.max_buffer_size { + return Err(io::Error::new( + io::ErrorKind::OutOfMemory, + format!("read buffer size limit ({}) exceeded", self.max_buffer_size), + )); + } + + let capacity = self.read_buf.capacity(); + let available_space = capacity - current_len; + + let target_space = self.base_capacity; + if available_space < target_space { + let new_capacity = current_len + target_space; + self.read_buf.reserve_exact(new_capacity - capacity); + } + + let capacity = self.read_buf.capacity(); + let len = self.read_buf.len(); + + // SAFETY: We're extending the buffer to its capacity to allow reading into + // uninitialized memory. This is safe because: + // 1. We save the original length and restore it on error + // 2. The async read operation initializes the bytes it writes to + // 3. We update the length based on how many bytes were actually read + unsafe { + self.read_buf.set_len(capacity); + } + + let buf = std::mem::take(&mut self.read_buf); + + let read_slice = IoBuf::slice(buf, len..); + + let BufResult(result, mut buf) = self.inner.read(read_slice).await.into_inner(); + + match result { + Ok(n) => { + if n == 0 { + self.eof = true; + unsafe { + buf.set_len(len); + } + } else { + unsafe { + buf.set_len(len + n); + } + } + self.read_buf = buf; + Ok(n) + } + Err(e) => { + unsafe { + buf.set_len(len); + } + self.read_buf = buf; + Err(e) + } + } + } +} + +impl GrowableSyncStream { + /// Flushes the write buffer to the underlying async stream. + /// + /// This method: + /// 1. Writes all buffered data to the underlying stream + /// 2. Calls `flush()` on the underlying stream + /// 3. Returns the total number of bytes flushed + /// + /// On error, any unwritten data remains in the buffer and can be retried. + /// + /// # Errors + /// + /// Returns an error if the underlying stream returns an error. + /// In this case, the buffer retains any data that wasn't successfully written. + pub async fn flush_write_buf(&mut self) -> io::Result { + if self.write_buf.is_empty() { + return Ok(0); + } + + let total = self.write_buf.len(); + let mut buf = std::mem::take(&mut self.write_buf); + let mut flushed = 0; + + while flushed < total { + let write_slice = IoBuf::slice(buf, flushed..); + + let BufResult(result, returned_buf) = self.inner.write(write_slice).await.into_inner(); + buf = returned_buf; + + match result { + Ok(0) => { + self.write_buf = buf[flushed..].to_vec(); + return Err(io::Error::new(io::ErrorKind::WriteZero, "write returned 0")); + } + Ok(n) => { + flushed += n; + } + Err(e) => { + self.write_buf = buf[flushed..].to_vec(); + return Err(e); + } + } + } + + self.write_buf = Vec::with_capacity(self.base_capacity); + + self.inner.flush().await?; + + Ok(flushed) + } +} diff --git a/compio-ws/src/lib.rs b/compio-ws/src/lib.rs new file mode 100644 index 00000000..a076de68 --- /dev/null +++ b/compio-ws/src/lib.rs @@ -0,0 +1,300 @@ +//! Async WebSocket support for compio. +//! +//! This library is an implementation of WebSocket handshakes and streams for compio. +//! It is based on the tungstenite crate which implements all required WebSocket protocol +//! logic. This crate brings compio support / compio integration to it. +//! +//! Each WebSocket stream implements message reading and writing. + +pub mod growable_sync_stream; +pub mod stream; + +#[cfg(feature = "rustls")] +pub mod rustls; + +use std::io::ErrorKind; + +use compio_io::{AsyncRead, AsyncWrite}; +use growable_sync_stream::GrowableSyncStream; + +use tungstenite::{ + Error as WsError, HandshakeError, Message, WebSocket, + client::IntoClientRequest, + handshake::server::{Callback, NoCallback}, + protocol::CloseFrame, +}; + +pub use crate::stream::MaybeTlsStream; + +pub use tungstenite::{Message as WebSocketMessage, handshake::client::Response}; + +pub use tungstenite::protocol::WebSocketConfig; + +pub use tungstenite::error::Error as TungsteniteError; + +#[cfg(feature = "rustls")] +pub use crate::rustls::{ + AutoStream, ConnectStream, Connector, client_async_tls, client_async_tls_with_config, + client_async_tls_with_connector, client_async_tls_with_connector_and_config, connect_async, + connect_async_with_config, connect_async_with_tls_connector, + connect_async_with_tls_connector_and_config, +}; + +pub struct WebSocketStream { + inner: WebSocket>, +} + +impl WebSocketStream +where + S: AsyncRead + AsyncWrite + Unpin + std::fmt::Debug, +{ + pub async fn send(&mut self, message: Message) -> Result<(), WsError> { + // Send the message - this buffers it + // Since CompioStream::flush() now returns Ok, this should succeed on first try + self.inner.send(message)?; + + // flush the buffer to the network + self.inner + .get_mut() + .flush_write_buf() + .await + .map_err(WsError::Io)?; + + Ok(()) + } + + pub async fn read(&mut self) -> Result { + loop { + match self.inner.read() { + Ok(msg) => { + // Always try to flush after read (close frames need this) + let _ = self.inner.get_mut().flush_write_buf().await; + return Ok(msg); + } + Err(WsError::Io(ref e)) if e.kind() == ErrorKind::WouldBlock => { + // Need more data - fill the read buffer + self.inner + .get_mut() + .fill_read_buf() + .await + .map_err(WsError::Io)?; + // Retry the read + continue; + } + Err(e) => { + // Always try to flush on error (close frames) + let _ = self.inner.get_mut().flush_write_buf().await; + return Err(e); + } + } + } + } + + pub async fn close(&mut self, close_frame: Option) -> Result<(), WsError> { + loop { + match self.inner.close(close_frame.clone()) { + Ok(()) => return Ok(()), + Err(WsError::Io(ref e)) if e.kind() == ErrorKind::WouldBlock => { + let sync_stream = self.inner.get_mut(); + + let flushed = sync_stream.flush_write_buf().await.map_err(WsError::Io)?; + + if flushed == 0 { + sync_stream.fill_read_buf().await.map_err(WsError::Io)?; + } + continue; + } + Err(e) => return Err(e), + } + } + } + + pub fn get_ref(&self) -> &S { + self.inner.get_ref().get_ref() + } + + pub fn get_mut(&mut self) -> &mut S { + self.inner.get_mut().get_mut() + } + + pub fn get_inner(self) -> WebSocket> { + self.inner + } +} + +/// Accepts a new WebSocket connection with the provided stream. +/// +/// This function will internally call `server::accept` to create a +/// handshake representation and returns a future representing the +/// resolution of the WebSocket handshake. The returned future will resolve +/// to either `WebSocketStream` or `Error` depending if it's successful +/// or not. +/// +/// This is typically used after a socket has been accepted from a +/// `TcpListener`. That socket is then passed to this function to perform +/// the server half of accepting a client's websocket connection. +pub async fn accept_async(stream: S) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin + std::fmt::Debug, +{ + accept_hdr_async(stream, NoCallback).await +} + +/// The same as `accept_async()` but the one can specify a websocket configuration. +/// Please refer to `accept_async()` for more details. +pub async fn accept_async_with_config( + stream: S, + config: Option, +) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin + std::fmt::Debug, +{ + accept_hdr_with_config_async(stream, NoCallback, config).await +} +/// Accepts a new WebSocket connection with the provided stream. +/// +/// This function does the same as `accept_async()` but accepts an extra callback +/// for header processing. The callback receives headers of the incoming +/// requests and is able to add extra headers to the reply. +pub async fn accept_hdr_async(stream: S, callback: C) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin + std::fmt::Debug, + C: Callback, +{ + accept_hdr_with_config_async(stream, callback, None).await +} + +/// The same as `accept_hdr_async()` but the one can specify a websocket configuration. +/// Please refer to `accept_hdr_async()` for more details. +pub async fn accept_hdr_with_config_async( + stream: S, + callback: C, + config: Option, +) -> Result, WsError> +where + S: AsyncRead + AsyncWrite + Unpin + std::fmt::Debug, + C: Callback, +{ + let sync_stream = GrowableSyncStream::new(stream); + let mut handshake_result = tungstenite::accept_hdr_with_config(sync_stream, callback, config); + + loop { + match handshake_result { + Ok(mut websocket) => { + websocket + .get_mut() + .flush_write_buf() + .await + .map_err(WsError::Io)?; + return Ok(WebSocketStream { inner: websocket }); + } + Err(HandshakeError::Interrupted(mut mid_handshake)) => { + let sync_stream = mid_handshake.get_mut().get_mut(); + + sync_stream.flush_write_buf().await.map_err(WsError::Io)?; + + sync_stream.fill_read_buf().await.map_err(WsError::Io)?; + + handshake_result = mid_handshake.handshake(); + } + Err(HandshakeError::Failure(error)) => { + return Err(error); + } + } + } +} + +/// Creates a WebSocket handshake from a request and a stream. +/// +/// For convenience, the user may call this with a url string, a URL, +/// or a `Request`. Calling with `Request` allows the user to add +/// a WebSocket protocol or other custom headers. +/// +/// Internally, this creates a handshake representation and returns +/// a future representing the resolution of the WebSocket handshake. The +/// returned future will resolve to either `WebSocketStream` or `Error` +/// depending on whether the handshake is successful. +/// +/// This is typically used for clients who have already established, for +/// example, a TCP connection to the remote server. +pub async fn client_async( + request: R, + stream: S, +) -> Result<(WebSocketStream, tungstenite::handshake::client::Response), WsError> +where + R: IntoClientRequest, + S: AsyncRead + AsyncWrite + Unpin + std::fmt::Debug, +{ + client_async_with_config(request, stream, None).await +} + +/// The same as `client_async()` but the one can specify a websocket configuration. +/// Please refer to `client_async()` for more details. +pub async fn client_async_with_config( + request: R, + stream: S, + config: Option, +) -> Result<(WebSocketStream, tungstenite::handshake::client::Response), WsError> +where + R: IntoClientRequest, + S: AsyncRead + AsyncWrite + Unpin, +{ + let sync_stream = GrowableSyncStream::new(stream); + let mut handshake_result = + tungstenite::client::client_with_config(request, sync_stream, config); + + loop { + match handshake_result { + Ok((mut websocket, response)) => { + // Ensure any remaining data is flushed + websocket + .get_mut() + .flush_write_buf() + .await + .map_err(WsError::Io)?; + return Ok((WebSocketStream { inner: websocket }, response)); + } + Err(HandshakeError::Interrupted(mut mid_handshake)) => { + let sync_stream = mid_handshake.get_mut().get_mut(); + + // For handshake: always try both operations + sync_stream.flush_write_buf().await.map_err(WsError::Io)?; + + sync_stream.fill_read_buf().await.map_err(WsError::Io)?; + + handshake_result = mid_handshake.handshake(); + } + Err(HandshakeError::Failure(error)) => { + return Err(error); + } + } + } +} + +#[inline] +#[allow(clippy::result_large_err)] +#[cfg(feature = "rustls")] +pub(crate) fn domain( + request: &tungstenite::handshake::client::Request, +) -> Result { + request + .uri() + .host() + .map(|host| { + // If host is an IPv6 address, it might be surrounded by brackets. These brackets are + // *not* part of a valid IP, so they must be stripped out. + // + // The URI from the request is guaranteed to be valid, so we don't need a separate + // check for the closing bracket. + let host = if host.starts_with('[') { + &host[1..host.len() - 1] + } else { + host + }; + + host.to_owned() + }) + .ok_or(tungstenite::Error::Url( + tungstenite::error::UrlError::NoHostName, + )) +} diff --git a/compio-ws/src/rustls.rs b/compio-ws/src/rustls.rs new file mode 100644 index 00000000..35f107c1 --- /dev/null +++ b/compio-ws/src/rustls.rs @@ -0,0 +1,283 @@ +use compio_io::{AsyncRead, AsyncWrite}; +use compio_net::TcpStream; +use compio_tls::TlsConnector; +use rustls::{ClientConfig, RootCertStore}; + +use tungstenite::Error; +use tungstenite::client::{IntoClientRequest, uri_mode}; +use tungstenite::handshake::client::{Request, Response}; +use tungstenite::stream::Mode; + +use std::sync::Arc; + +use crate::stream::MaybeTlsStream; +use crate::{WebSocketConfig, WebSocketStream, client_async_with_config, domain}; + +pub type AutoStream = MaybeTlsStream; + +pub type Connector = TlsConnector; + +async fn wrap_stream( + socket: S, + domain: String, + connector: Option, + mode: Mode, +) -> Result, Error> +where + S: AsyncRead + AsyncWrite + Unpin + 'static, +{ + match mode { + Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), + Mode::Tls => { + let stream = { + let connector = if let Some(connector) = connector { + connector + } else { + // Only create root_store when we actually have certificate features enabled + #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] + let mut root_store = { + let mut store = RootCertStore::empty(); + + #[cfg(feature = "rustls-native-certs")] + { + let cert_result = rustls_native_certs::load_native_certs(); + + // Log any errors that occurred + for err in &cert_result.errors { + log::warn!("Error loading native certificate: {err}"); + } + + if !cert_result.certs.is_empty() { + let (added, ignored) = + store.add_parsable_certificates(cert_result.certs); + log::debug!( + "Added {added} native root certificates (ignored {ignored})" + ); + + // Only fail if webpki-roots is NOT enabled as fallback + #[cfg(not(feature = "webpki-roots"))] + if added == 0 { + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No valid native root certificates found", + ))); + } + } else { + log::warn!("No native root certificates found"); + + // Only fail if webpki-roots is NOT enabled as fallback + #[cfg(not(feature = "webpki-roots"))] + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No native root certificates found", + ))); + } + } + + // Load webpki-roots whenever the feature is enabled + // This serves as a fallback when native-certs is also enabled + #[cfg(feature = "webpki-roots")] + { + use log::debug; + + let webpki_certs: Vec<_> = + webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(); + store.extend(webpki_certs); + debug!( + "Added {} webpki root certificates", + webpki_roots::TLS_SERVER_ROOTS.len() + ); + } + + store + }; + + // Check if we have neither feature enabled + #[cfg(not(any(feature = "rustls-native-certs", feature = "webpki-roots")))] + { + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No root certificate features enabled. Enable either 'rustls-native-certs' or 'webpki-roots'", + ))); + } + + // Check if root_store is empty (only when features are enabled) + #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] + if root_store.is_empty() { + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No root certificates available", + ))); + } + + // Create the TLS connector (only when features are enabled) + #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] + { + TlsConnector::from(Arc::new( + ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(), + )) + } + }; + + connector + .connect(&domain, socket) + .await + .map_err(Error::Io)? + }; + Ok(MaybeTlsStream::Tls(stream)) + } + } +} + +/// Creates a WebSocket handshake from a request and a stream, +/// upgrading the stream to TLS if required. +pub async fn client_async_tls( + request: R, + stream: S, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, + S: AsyncRead + AsyncWrite + Unpin + 'static, + AutoStream: Unpin, +{ + client_async_tls_with_connector_and_config(request, stream, None, None).await +} + +/// The same as `client_async_tls()` but the one can specify a websocket configuration. +pub async fn client_async_tls_with_config( + request: R, + stream: S, + config: Option, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, + S: AsyncRead + AsyncWrite + Unpin + 'static, + AutoStream: Unpin, +{ + client_async_tls_with_connector_and_config(request, stream, None, config).await +} + +/// The same as `client_async_tls()` but the one can specify a connector. +pub async fn client_async_tls_with_connector( + request: R, + stream: S, + connector: Option, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, + S: AsyncRead + AsyncWrite + Unpin + 'static, + AutoStream: Unpin, +{ + client_async_tls_with_connector_and_config(request, stream, connector, None).await +} + +/// The same as `client_async_tls()` but the one can specify a websocket configuration, +/// and an optional connector. +pub async fn client_async_tls_with_connector_and_config( + request: R, + stream: S, + connector: Option, + config: Option, +) -> Result<(WebSocketStream>, Response), Error> +where + R: IntoClientRequest + Unpin, + S: AsyncRead + AsyncWrite + Unpin + 'static, + AutoStream: Unpin, +{ + let request: Request = request.into_client_request()?; + + let domain = domain(&request)?; + + let mode = uri_mode(request.uri())?; + + let stream = wrap_stream(stream, domain, connector, mode).await?; + client_async_with_config(request, stream, config).await +} + +pub type ConnectStream = AutoStream; + +/// Connect to a given URL. +pub async fn connect_async( + request: R, +) -> Result<(WebSocketStream, Response), Error> +where + R: IntoClientRequest + Unpin, +{ + connect_async_with_config(request, None, false).await +} + +/// The same as `connect_async()` but the one can specify a websocket configuration. +/// `disable_nagle` specifies if the Nagle's algorithm must be disabled, i.e. `set_nodelay(true)`. +/// If you don't know what the Nagle's algorithm is, better leave it to `false`. +pub async fn connect_async_with_config( + request: R, + config: Option, + disable_nagle: bool, +) -> Result<(WebSocketStream, Response), Error> +where + R: IntoClientRequest + Unpin, +{ + let request: Request = request.into_client_request()?; + + let domain = domain(&request)?; + let port = port(&request)?; + + let socket = TcpStream::connect((domain.as_str(), port)) + .await + .map_err(Error::Io)?; + + if disable_nagle { + socket.set_nodelay(true).map_err(Error::Io)?; + } + + client_async_tls_with_connector_and_config(request, socket, None, config).await +} + +/// The same as `connect_async()` but the one can specify a TLS connector. +pub async fn connect_async_with_tls_connector( + request: R, + connector: Option, +) -> Result<(WebSocketStream, Response), Error> +where + R: IntoClientRequest + Unpin, +{ + connect_async_with_tls_connector_and_config(request, connector, None).await +} + +/// The same as `connect_async()` but the one can specify a websocket configuration, +/// a TLS connector, and whether to disable Nagle's algorithm. +pub async fn connect_async_with_tls_connector_and_config( + request: R, + connector: Option, + config: Option, +) -> Result<(WebSocketStream, Response), Error> +where + R: IntoClientRequest + Unpin, +{ + let request: Request = request.into_client_request()?; + + let domain = domain(&request)?; + let port = port(&request)?; + + let socket = TcpStream::connect((domain.as_str(), port)) + .await + .map_err(Error::Io)?; + client_async_tls_with_connector_and_config(request, socket, connector, config).await +} + +#[inline] +#[allow(clippy::result_large_err)] +fn port(request: &Request) -> Result { + request + .uri() + .port_u16() + .or_else(|| match uri_mode(request.uri()).ok()? { + Mode::Plain => Some(80), + Mode::Tls => Some(443), + }) + .ok_or(Error::Url( + tungstenite::error::UrlError::UnsupportedUrlScheme, + )) +} diff --git a/compio-ws/src/stream.rs b/compio-ws/src/stream.rs new file mode 100644 index 00000000..b045ba14 --- /dev/null +++ b/compio-ws/src/stream.rs @@ -0,0 +1,82 @@ +use compio_buf::{BufResult, IoBuf, IoBufMut}; +use compio_io::{AsyncRead, AsyncWrite}; +#[cfg(feature = "rustls")] +use compio_tls::TlsStream; +use std::io::Result as IoResult; + +/// Stream that can be either plain TCP or TLS-encrypted +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum MaybeTlsStream { + /// Plain, unencrypted stream + Plain(S), + /// TLS-encrypted stream + #[cfg(feature = "rustls")] + Tls(TlsStream), +} + +impl MaybeTlsStream { + pub fn plain(stream: S) -> Self { + MaybeTlsStream::Plain(stream) + } + + #[cfg(feature = "rustls")] + pub fn tls(stream: TlsStream) -> Self { + MaybeTlsStream::Tls(stream) + } + + pub fn is_tls(&self) -> bool { + #[cfg(feature = "rustls")] + { + matches!(self, MaybeTlsStream::Tls(_)) + } + #[cfg(not(feature = "rustls"))] + { + false + } + } +} + +impl AsyncRead for MaybeTlsStream +where + S: AsyncRead + AsyncWrite + Unpin + 'static, +{ + async fn read(&mut self, buf: B) -> BufResult { + match self { + MaybeTlsStream::Plain(stream) => stream.read(buf).await, + #[cfg(feature = "rustls")] + MaybeTlsStream::Tls(stream) => stream.read(buf).await, + } + } +} + +impl AsyncWrite for MaybeTlsStream +where + S: AsyncRead + AsyncWrite + Unpin + 'static, +{ + async fn write(&mut self, buf: B) -> BufResult { + match self { + MaybeTlsStream::Plain(stream) => stream.write(buf).await, + #[cfg(feature = "rustls")] + MaybeTlsStream::Tls(stream) => stream.write(buf).await, + } + } + + async fn flush(&mut self) -> IoResult<()> { + match self { + MaybeTlsStream::Plain(stream) => stream.flush().await, + #[cfg(feature = "rustls")] + MaybeTlsStream::Tls(stream) => stream.flush().await, + } + } + + async fn shutdown(&mut self) -> IoResult<()> { + match self { + MaybeTlsStream::Plain(stream) => stream.shutdown().await, + #[cfg(feature = "rustls")] + MaybeTlsStream::Tls(stream) => stream.shutdown().await, + } + } +} + +impl Unpin for MaybeTlsStream where S: Unpin {} From 56bba8f876539f93309433ca18a143a88150cd00 Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Wed, 5 Nov 2025 23:08:19 +0530 Subject: [PATCH 2/8] chore: fix clippy and fmt issues --- compio-ws/examples/autobahn-server.rs | 5 ++- compio-ws/examples/client_tls.rs | 5 ++- compio-ws/examples/echo_server_tls.rs | 12 ++++-- compio-ws/src/growable_sync_stream.rs | 56 ++++++++++++++++----------- compio-ws/src/lib.rs | 48 +++++++++++------------ compio-ws/src/rustls.rs | 46 ++++++++++++---------- compio-ws/src/stream.rs | 3 +- 7 files changed, 98 insertions(+), 77 deletions(-) diff --git a/compio-ws/examples/autobahn-server.rs b/compio-ws/examples/autobahn-server.rs index 21d9f541..b1efb634 100644 --- a/compio-ws/examples/autobahn-server.rs +++ b/compio-ws/examples/autobahn-server.rs @@ -1,7 +1,8 @@ +use std::net::SocketAddr; + use compio_net::{TcpListener, TcpStream}; -use compio_ws::{accept_async_with_config, WebSocketConfig}; +use compio_ws::{WebSocketConfig, accept_async_with_config}; use log::*; -use std::net::SocketAddr; use tungstenite::{Error, Result}; async fn accept_connection(peer: SocketAddr, stream: TcpStream) { diff --git a/compio-ws/examples/client_tls.rs b/compio-ws/examples/client_tls.rs index 3d2a5cbb..87bba4b3 100644 --- a/compio-ws/examples/client_tls.rs +++ b/compio-ws/examples/client_tls.rs @@ -1,6 +1,7 @@ -use compio_ws::{connect_async_with_tls_connector, Connector}; -use rustls::ClientConfig; use std::sync::Arc; + +use compio_ws::{Connector, connect_async_with_tls_connector}; +use rustls::ClientConfig; use tungstenite::Message; async fn create_insecure_tls_connector() -> Result> { diff --git a/compio-ws/examples/echo_server_tls.rs b/compio-ws/examples/echo_server_tls.rs index 21b326bf..aea6234f 100644 --- a/compio-ws/examples/echo_server_tls.rs +++ b/compio-ws/examples/echo_server_tls.rs @@ -1,15 +1,16 @@ +use std::{fs, sync::Arc}; + use compio_net::{TcpListener, TcpStream}; use compio_tls::TlsAcceptor; use compio_ws::accept_async; use rustls::ServerConfig; -use std::fs; -use std::sync::Arc; use tungstenite::Message; async fn create_tls_acceptor() -> Result> { // Load certificate and key from files // Generate these files with: - // openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -days 365 -nodes -subj "/CN=localhost" + // openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt + // -days 365 -nodes -subj "/CN=localhost" let cert_file = fs::read_to_string("localhost.crt")?; let key_file = fs::read_to_string("localhost.key")?; @@ -42,7 +43,10 @@ async fn main() -> Result<(), Box> { { eprintln!("Error: Certificate files not found!"); eprintln!("Please generate them with:"); - eprintln!("openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -days 365 -nodes -subj \"/CN=localhost\""); + eprintln!( + "openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -days \ + 365 -nodes -subj \"/CN=localhost\"" + ); return Err("Missing certificate files".into()); } diff --git a/compio-ws/src/growable_sync_stream.rs b/compio-ws/src/growable_sync_stream.rs index 5e8c968c..ec7dcfa8 100644 --- a/compio-ws/src/growable_sync_stream.rs +++ b/compio-ws/src/growable_sync_stream.rs @@ -5,28 +5,33 @@ use compio_io::{AsyncRead, AsyncWrite}; /// A growable buffered stream adapter that bridges async I/O with sync traits. /// -/// This is similar to `compio_io::compat::SyncStream` but with dynamically growing -/// buffers that can expand beyond the initial capacity up to a configurable maximum. +/// This is similar to `compio_io::compat::SyncStream` but with dynamically +/// growing buffers that can expand beyond the initial capacity up to a +/// configurable maximum. /// /// # Buffer Growth Strategy /// -/// - **Read buffer**: Grows as needed to accommodate incoming data, up to `max_buffer_size` -/// - **Write buffer**: Grows as needed for outgoing data, up to `max_buffer_size` -/// - Both buffers shrink back to `base_capacity` when fully consumed and capacity exceeds 4x base +/// - **Read buffer**: Grows as needed to accommodate incoming data, up to +/// `max_buffer_size` +/// - **Write buffer**: Grows as needed for outgoing data, up to +/// `max_buffer_size` +/// - Both buffers shrink back to `base_capacity` when fully consumed and +/// capacity exceeds 4x base /// /// # Usage Pattern /// -/// The sync `Read` and `Write` implementations will return `WouldBlock` errors when -/// buffers need servicing via the async methods: +/// The sync `Read` and `Write` implementations will return `WouldBlock` errors +/// when buffers need servicing via the async methods: /// /// - Call `fill_read_buf()` when `Read::read()` returns `WouldBlock` /// - Call `flush_write_buf()` when `Write::write()` returns `WouldBlock` /// /// # Note on flush() /// -/// The `Write::flush()` method intentionally returns `Ok(())` without checking if there's -/// buffered data. This is for compatibility with libraries like tungstenite that call -/// `flush()` after every write. Actual flushing happens via the async `flush_write_buf()` method. +/// The `Write::flush()` method intentionally returns `Ok(())` without checking +/// if there's buffered data. This is for compatibility with libraries like +/// tungstenite that call `flush()` after every write. Actual flushing happens +/// via the async `flush_write_buf()` method. #[derive(Debug)] pub struct GrowableSyncStream { inner: S, @@ -39,8 +44,11 @@ pub struct GrowableSyncStream { } impl GrowableSyncStream { - const DEFAULT_BASE_CAPACITY: usize = 8 * 1024; // 8KB base - const DEFAULT_MAX_BUFFER: usize = 64 * 1024 * 1024; // 64MB max + const DEFAULT_BASE_CAPACITY: usize = 8 * 1024; + // 8KB base + const DEFAULT_MAX_BUFFER: usize = 64 * 1024 * 1024; + + // 64MB max /// Creates a new `GrowableSyncStream` with default buffer sizes. /// @@ -65,7 +73,8 @@ impl GrowableSyncStream { } } - /// Creates a new `GrowableSyncStream` with custom base capacity and maximum buffer size. + /// Creates a new `GrowableSyncStream` with custom base capacity and maximum + /// buffer size. pub fn with_limits(base_capacity: usize, max_buffer_size: usize, stream: S) -> Self { Self { inner: stream, @@ -149,8 +158,9 @@ impl Read for GrowableSyncStream { impl Write for GrowableSyncStream { /// Writes data to the internal buffer. /// - /// Returns `WouldBlock` if the buffer needs flushing or has reached max capacity. - /// In the latter case, it may write partial data before returning `WouldBlock`. + /// Returns `WouldBlock` if the buffer needs flushing or has reached max + /// capacity. In the latter case, it may write partial data before + /// returning `WouldBlock`. fn write(&mut self, buf: &[u8]) -> io::Result { // Check if we should flush first if self.write_buf.len() > self.base_capacity * 2 / 3 && !self.write_buf.is_empty() { @@ -179,13 +189,14 @@ impl Write for GrowableSyncStream { /// Returns `Ok(())` without checking for buffered data. /// - /// **Important**: This does NOT actually flush data to the underlying stream. - /// This behavior is intentional for compatibility with libraries like tungstenite - /// that call `flush()` after every write operation. The actual async flush - /// happens when `flush_write_buf()` is called. + /// **Important**: This does NOT actually flush data to the underlying + /// stream. This behavior is intentional for compatibility with + /// libraries like tungstenite that call `flush()` after every write + /// operation. The actual async flush happens when `flush_write_buf()` + /// is called. /// - /// This prevents spurious errors in sync code that expects `flush()` to succeed - /// after successfully buffering data. + /// This prevents spurious errors in sync code that expects `flush()` to + /// succeed after successfully buffering data. fn flush(&mut self) -> io::Result<()> { Ok(()) } @@ -307,7 +318,8 @@ impl GrowableSyncStream { /// # Errors /// /// Returns an error if the underlying stream returns an error. - /// In this case, the buffer retains any data that wasn't successfully written. + /// In this case, the buffer retains any data that wasn't successfully + /// written. pub async fn flush_write_buf(&mut self) -> io::Result { if self.write_buf.is_empty() { return Ok(0); diff --git a/compio-ws/src/lib.rs b/compio-ws/src/lib.rs index a076de68..06b2a72b 100644 --- a/compio-ws/src/lib.rs +++ b/compio-ws/src/lib.rs @@ -1,8 +1,9 @@ //! Async WebSocket support for compio. //! -//! This library is an implementation of WebSocket handshakes and streams for compio. -//! It is based on the tungstenite crate which implements all required WebSocket protocol -//! logic. This crate brings compio support / compio integration to it. +//! This library is an implementation of WebSocket handshakes and streams for +//! compio. It is based on the tungstenite crate which implements all required +//! WebSocket protocol logic. This crate brings compio support / compio +//! integration to it. //! //! Each WebSocket stream implements message reading and writing. @@ -16,21 +17,16 @@ use std::io::ErrorKind; use compio_io::{AsyncRead, AsyncWrite}; use growable_sync_stream::GrowableSyncStream; - use tungstenite::{ Error as WsError, HandshakeError, Message, WebSocket, client::IntoClientRequest, handshake::server::{Callback, NoCallback}, protocol::CloseFrame, }; - -pub use crate::stream::MaybeTlsStream; - -pub use tungstenite::{Message as WebSocketMessage, handshake::client::Response}; - -pub use tungstenite::protocol::WebSocketConfig; - -pub use tungstenite::error::Error as TungsteniteError; +pub use tungstenite::{ + Message as WebSocketMessage, error::Error as TungsteniteError, handshake::client::Response, + protocol::WebSocketConfig, +}; #[cfg(feature = "rustls")] pub use crate::rustls::{ @@ -39,6 +35,7 @@ pub use crate::rustls::{ connect_async_with_config, connect_async_with_tls_connector, connect_async_with_tls_connector_and_config, }; +pub use crate::stream::MaybeTlsStream; pub struct WebSocketStream { inner: WebSocket>, @@ -140,8 +137,8 @@ where accept_hdr_async(stream, NoCallback).await } -/// The same as `accept_async()` but the one can specify a websocket configuration. -/// Please refer to `accept_async()` for more details. +/// The same as `accept_async()` but the one can specify a websocket +/// configuration. Please refer to `accept_async()` for more details. pub async fn accept_async_with_config( stream: S, config: Option, @@ -153,9 +150,9 @@ where } /// Accepts a new WebSocket connection with the provided stream. /// -/// This function does the same as `accept_async()` but accepts an extra callback -/// for header processing. The callback receives headers of the incoming -/// requests and is able to add extra headers to the reply. +/// This function does the same as `accept_async()` but accepts an extra +/// callback for header processing. The callback receives headers of the +/// incoming requests and is able to add extra headers to the reply. pub async fn accept_hdr_async(stream: S, callback: C) -> Result, WsError> where S: AsyncRead + AsyncWrite + Unpin + std::fmt::Debug, @@ -164,8 +161,8 @@ where accept_hdr_with_config_async(stream, callback, None).await } -/// The same as `accept_hdr_async()` but the one can specify a websocket configuration. -/// Please refer to `accept_hdr_async()` for more details. +/// The same as `accept_hdr_async()` but the one can specify a websocket +/// configuration. Please refer to `accept_hdr_async()` for more details. pub async fn accept_hdr_with_config_async( stream: S, callback: C, @@ -228,8 +225,8 @@ where client_async_with_config(request, stream, None).await } -/// The same as `client_async()` but the one can specify a websocket configuration. -/// Please refer to `client_async()` for more details. +/// The same as `client_async()` but the one can specify a websocket +/// configuration. Please refer to `client_async()` for more details. pub async fn client_async_with_config( request: R, stream: S, @@ -281,11 +278,12 @@ pub(crate) fn domain( .uri() .host() .map(|host| { - // If host is an IPv6 address, it might be surrounded by brackets. These brackets are - // *not* part of a valid IP, so they must be stripped out. + // If host is an IPv6 address, it might be surrounded by brackets. These + // brackets are *not* part of a valid IP, so they must be stripped + // out. // - // The URI from the request is guaranteed to be valid, so we don't need a separate - // check for the closing bracket. + // The URI from the request is guaranteed to be valid, so we don't need a + // separate check for the closing bracket. let host = if host.starts_with('[') { &host[1..host.len() - 1] } else { diff --git a/compio-ws/src/rustls.rs b/compio-ws/src/rustls.rs index 35f107c1..1bce8ebc 100644 --- a/compio-ws/src/rustls.rs +++ b/compio-ws/src/rustls.rs @@ -1,17 +1,19 @@ +use std::sync::Arc; + use compio_io::{AsyncRead, AsyncWrite}; use compio_net::TcpStream; use compio_tls::TlsConnector; use rustls::{ClientConfig, RootCertStore}; +use tungstenite::{ + Error, + client::{IntoClientRequest, uri_mode}, + handshake::client::{Request, Response}, + stream::Mode, +}; -use tungstenite::Error; -use tungstenite::client::{IntoClientRequest, uri_mode}; -use tungstenite::handshake::client::{Request, Response}; -use tungstenite::stream::Mode; - -use std::sync::Arc; - -use crate::stream::MaybeTlsStream; -use crate::{WebSocketConfig, WebSocketStream, client_async_with_config, domain}; +use crate::{ + WebSocketConfig, WebSocketStream, client_async_with_config, domain, stream::MaybeTlsStream, +}; pub type AutoStream = MaybeTlsStream; @@ -35,7 +37,7 @@ where } else { // Only create root_store when we actually have certificate features enabled #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] - let mut root_store = { + let root_store = { let mut store = RootCertStore::empty(); #[cfg(feature = "rustls-native-certs")] @@ -80,8 +82,7 @@ where { use log::debug; - let webpki_certs: Vec<_> = - webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(); + let webpki_certs = webpki_roots::TLS_SERVER_ROOTS.to_vec(); store.extend(webpki_certs); debug!( "Added {} webpki root certificates", @@ -97,7 +98,8 @@ where { return Err(Error::Io(std::io::Error::new( std::io::ErrorKind::NotFound, - "No root certificate features enabled. Enable either 'rustls-native-certs' or 'webpki-roots'", + "No root certificate features enabled. Enable either \ + 'rustls-native-certs' or 'webpki-roots'", ))); } @@ -145,7 +147,8 @@ where client_async_tls_with_connector_and_config(request, stream, None, None).await } -/// The same as `client_async_tls()` but the one can specify a websocket configuration. +/// The same as `client_async_tls()` but the one can specify a websocket +/// configuration. pub async fn client_async_tls_with_config( request: R, stream: S, @@ -173,8 +176,8 @@ where client_async_tls_with_connector_and_config(request, stream, connector, None).await } -/// The same as `client_async_tls()` but the one can specify a websocket configuration, -/// and an optional connector. +/// The same as `client_async_tls()` but the one can specify a websocket +/// configuration, and an optional connector. pub async fn client_async_tls_with_connector_and_config( request: R, stream: S, @@ -208,9 +211,10 @@ where connect_async_with_config(request, None, false).await } -/// The same as `connect_async()` but the one can specify a websocket configuration. -/// `disable_nagle` specifies if the Nagle's algorithm must be disabled, i.e. `set_nodelay(true)`. -/// If you don't know what the Nagle's algorithm is, better leave it to `false`. +/// The same as `connect_async()` but the one can specify a websocket +/// configuration. `disable_nagle` specifies if the Nagle's algorithm must be +/// disabled, i.e. `set_nodelay(true)`. If you don't know what the Nagle's +/// algorithm is, better leave it to `false`. pub async fn connect_async_with_config( request: R, config: Option, @@ -246,8 +250,8 @@ where connect_async_with_tls_connector_and_config(request, connector, None).await } -/// The same as `connect_async()` but the one can specify a websocket configuration, -/// a TLS connector, and whether to disable Nagle's algorithm. +/// The same as `connect_async()` but the one can specify a websocket +/// configuration, a TLS connector, and whether to disable Nagle's algorithm. pub async fn connect_async_with_tls_connector_and_config( request: R, connector: Option, diff --git a/compio-ws/src/stream.rs b/compio-ws/src/stream.rs index b045ba14..facdc9a3 100644 --- a/compio-ws/src/stream.rs +++ b/compio-ws/src/stream.rs @@ -1,8 +1,9 @@ +use std::io::Result as IoResult; + use compio_buf::{BufResult, IoBuf, IoBufMut}; use compio_io::{AsyncRead, AsyncWrite}; #[cfg(feature = "rustls")] use compio_tls::TlsStream; -use std::io::Result as IoResult; /// Stream that can be either plain TCP or TLS-encrypted #[derive(Debug)] From 82cd69a587d3c0dee1355e69f15fd8472bf4a9b5 Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Thu, 6 Nov 2025 14:33:07 +0530 Subject: [PATCH 3/8] fix: remove readme --- compio-ws/README.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 compio-ws/README.md diff --git a/compio-ws/README.md b/compio-ws/README.md deleted file mode 100644 index 5af48527..00000000 --- a/compio-ws/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# compio-ws - -WebSocket support for compio runtime. From eea2f0587b1cd4161308aa07637c4710b3d6e4d4 Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Thu, 6 Nov 2025 14:33:31 +0530 Subject: [PATCH 4/8] fix: remove `compio` as dependency --- compio-ws/Cargo.toml | 22 +++++++++++----------- compio-ws/examples/autobahn-client.rs | 2 +- compio-ws/examples/autobahn-server.rs | 2 +- compio-ws/examples/client.rs | 2 +- compio-ws/examples/client_tls.rs | 2 +- compio-ws/examples/echo_server.rs | 2 +- compio-ws/examples/echo_server_tls.rs | 2 +- compio-ws/src/rustls.rs | 2 ++ 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/compio-ws/Cargo.toml b/compio-ws/Cargo.toml index bb23f519..ffd237cb 100644 --- a/compio-ws/Cargo.toml +++ b/compio-ws/Cargo.toml @@ -10,36 +10,37 @@ repository = { workspace = true } [dependencies] -rustls = { workspace = true, optional = true, features = [ - "logging", - "std", - "tls12", - "ring", -] } +rustls = { workspace = true, optional = true, default-features = false } rustls-native-certs = { version = "0.8", optional = true } tungstenite = "0.27.0" -compio = { workspace = true, features = ["io", "io-compat", "macros"] } compio-io = { workspace = true } compio-net = { workspace = true, optional = true } compio-tls = { workspace = true, optional = true, default-features = false, features = [ "rustls", ] } compio-runtime = { workspace = true } +compio-driver = { workspace = true } compio-buf = { workspace = true } webpki-roots = { version = "1.0.4", optional = true } rustls-pemfile = "2.0" log = "0.4" [features] -default = [] +default = ["io-uring"] +io-uring = ["compio-driver/io-uring"] +polling = ["compio-driver/polling"] connect = ["dep:compio-net"] rustls = ["connect", "dep:compio-tls", "dep:rustls", "compio-tls/rustls"] rustls-native-certs = ["rustls", "dep:rustls-native-certs"] webpki-roots = ["rustls", "dep:webpki-roots"] +ring = ["rustls?/ring", "compio-tls?/ring"] + [dev-dependencies] log = "0.4" env_logger = "0.11" +compio-net = { workspace = true } +compio-macros = { workspace = true } [[example]] @@ -60,8 +61,7 @@ required-features = ["connect"] [[example]] name = "echo_server_tls" -required-features = ["connect", "rustls"] - +required-features = ["connect", "rustls", "ring"] [[example]] name = "client_tls" -required-features = ["connect", "rustls"] +required-features = ["connect", "rustls", "ring"] diff --git a/compio-ws/examples/autobahn-client.rs b/compio-ws/examples/autobahn-client.rs index 188011af..4e9b3a0d 100644 --- a/compio-ws/examples/autobahn-client.rs +++ b/compio-ws/examples/autobahn-client.rs @@ -52,7 +52,7 @@ async fn run_test(case: u32) -> Result<()> { Ok(()) } -#[compio::main] +#[compio_macros::main] async fn main() { env_logger::init(); diff --git a/compio-ws/examples/autobahn-server.rs b/compio-ws/examples/autobahn-server.rs index b1efb634..e68fde12 100644 --- a/compio-ws/examples/autobahn-server.rs +++ b/compio-ws/examples/autobahn-server.rs @@ -48,7 +48,7 @@ async fn handle_connection(peer: SocketAddr, stream: TcpStream) -> Result<()> { Ok(()) } -#[compio::main] +#[compio_macros::main] async fn main() { env_logger::init(); diff --git a/compio-ws/examples/client.rs b/compio-ws/examples/client.rs index e2ef3e97..1bd0049c 100644 --- a/compio-ws/examples/client.rs +++ b/compio-ws/examples/client.rs @@ -2,7 +2,7 @@ use compio_net::TcpStream; use compio_ws::client_async; use tungstenite::Message; -#[compio::main] +#[compio_macros::main] async fn main() -> Result<(), Box> { println!("Connecting to WebSocket server"); diff --git a/compio-ws/examples/client_tls.rs b/compio-ws/examples/client_tls.rs index 87bba4b3..9fe27bd1 100644 --- a/compio-ws/examples/client_tls.rs +++ b/compio-ws/examples/client_tls.rs @@ -69,7 +69,7 @@ async fn create_insecure_tls_connector() -> Result Result<(), Box> { // Create an insecure TLS connector for testing with self-signed certificates // This accepts the self-signed certificate generated by the server diff --git a/compio-ws/examples/echo_server.rs b/compio-ws/examples/echo_server.rs index d4fa5f62..41478e76 100644 --- a/compio-ws/examples/echo_server.rs +++ b/compio-ws/examples/echo_server.rs @@ -2,7 +2,7 @@ use compio_net::{TcpListener, TcpStream}; use compio_ws::accept_async; use tungstenite::Message; -#[compio::main] +#[compio_macros::main] async fn main() -> Result<(), Box> { let listener = TcpListener::bind("127.0.0.1:9002").await?; println!("WebSocket echo server listening on ws://127.0.0.1:9002"); diff --git a/compio-ws/examples/echo_server_tls.rs b/compio-ws/examples/echo_server_tls.rs index aea6234f..973cc19c 100644 --- a/compio-ws/examples/echo_server_tls.rs +++ b/compio-ws/examples/echo_server_tls.rs @@ -35,7 +35,7 @@ async fn create_tls_acceptor() -> Result Ok(TlsAcceptor::from(Arc::new(config))) } -#[compio::main] +#[compio_macros::main] async fn main() -> Result<(), Box> { // Check if certificate files exist if !std::path::Path::new("localhost.crt").exists() diff --git a/compio-ws/src/rustls.rs b/compio-ws/src/rustls.rs index 1bce8ebc..e6a1c073 100644 --- a/compio-ws/src/rustls.rs +++ b/compio-ws/src/rustls.rs @@ -1,8 +1,10 @@ +#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] use std::sync::Arc; use compio_io::{AsyncRead, AsyncWrite}; use compio_net::TcpStream; use compio_tls::TlsConnector; +#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] use rustls::{ClientConfig, RootCertStore}; use tungstenite::{ Error, From 8adec071d6e0fab01970b917ab8fe892e09bca3f Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Thu, 6 Nov 2025 14:59:21 +0530 Subject: [PATCH 5/8] fix: add tests --- compio-ws/Cargo.toml | 2 +- compio-ws/tests/websocket.rs | 174 +++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 compio-ws/tests/websocket.rs diff --git a/compio-ws/Cargo.toml b/compio-ws/Cargo.toml index ffd237cb..2918d83f 100644 --- a/compio-ws/Cargo.toml +++ b/compio-ws/Cargo.toml @@ -41,7 +41,7 @@ log = "0.4" env_logger = "0.11" compio-net = { workspace = true } compio-macros = { workspace = true } - +futures-channel = { workspace = true } [[example]] name = "echo_server" diff --git a/compio-ws/tests/websocket.rs b/compio-ws/tests/websocket.rs new file mode 100644 index 00000000..72b019fc --- /dev/null +++ b/compio-ws/tests/websocket.rs @@ -0,0 +1,174 @@ +#![cfg(feature = "connect")] + +use compio_net::{TcpListener, TcpStream}; +use compio_ws::{accept_async, client_async}; +use futures_channel::oneshot; +use tungstenite::Message; + +#[compio_macros::test] +async fn test_handshake() { + let (tx, rx) = oneshot::channel(); + + compio_runtime::spawn(async move { + let listener = TcpListener::bind("127.0.0.1:12345").await.unwrap(); + tx.send(()).unwrap(); + + let (stream, _) = listener.accept().await.unwrap(); + let _ws = accept_async(stream).await.expect("Server handshake failed"); + }) + .detach(); + + rx.await.expect("Failed to wait for server"); + + let tcp = TcpStream::connect("127.0.0.1:12345") + .await + .expect("Failed to connect"); + let (_ws, _response) = client_async("ws://127.0.0.1:12345", tcp) + .await + .expect("Client handshake failed"); +} + +#[compio_macros::test] +async fn test_echo_message() { + let (tx, rx) = oneshot::channel(); + + compio_runtime::spawn(async move { + let listener = TcpListener::bind("127.0.0.1:12346").await.unwrap(); + tx.send(()).unwrap(); + + let (stream, _) = listener.accept().await.unwrap(); + let mut ws = accept_async(stream).await.unwrap(); + + let msg = ws.read().await.unwrap(); + ws.send(msg).await.unwrap(); + }) + .detach(); + + rx.await.unwrap(); + + let tcp = TcpStream::connect("127.0.0.1:12346").await.unwrap(); + let (mut ws, _) = client_async("ws://127.0.0.1:12346", tcp).await.unwrap(); + + let test_msg = "Hello, WebSocket!"; + ws.send(Message::Text(test_msg.into())).await.unwrap(); + + let response = ws.read().await.unwrap(); + assert_eq!(response, Message::Text(test_msg.into())); +} + +#[compio_macros::test] +async fn test_binary_message() { + let (tx, rx) = oneshot::channel(); + + compio_runtime::spawn(async move { + let listener = TcpListener::bind("127.0.0.1:12347").await.unwrap(); + tx.send(()).unwrap(); + + let (stream, _) = listener.accept().await.unwrap(); + let mut ws = accept_async(stream).await.unwrap(); + + let msg = ws.read().await.unwrap(); + ws.send(msg).await.unwrap(); + }) + .detach(); + + rx.await.unwrap(); + + let tcp = TcpStream::connect("127.0.0.1:12347").await.unwrap(); + let (mut ws, _) = client_async("ws://127.0.0.1:12347", tcp).await.unwrap(); + + let test_data = vec![1, 2, 3, 4, 5]; + ws.send(Message::Binary(test_data.clone().into())) + .await + .unwrap(); + + let response = ws.read().await.unwrap(); + assert_eq!(response, Message::Binary(test_data.into())); +} + +#[compio_macros::test] +async fn test_ping_pong() { + let (tx, rx) = oneshot::channel(); + + compio_runtime::spawn(async move { + let listener = TcpListener::bind("127.0.0.1:12348").await.unwrap(); + tx.send(()).unwrap(); + + let (stream, _) = listener.accept().await.unwrap(); + let mut ws = accept_async(stream).await.unwrap(); + + let msg = ws.read().await.unwrap(); + if let Message::Ping(data) = msg { + ws.send(Message::Pong(data)).await.unwrap(); + } + }) + .detach(); + + rx.await.unwrap(); + + let tcp = TcpStream::connect("127.0.0.1:12348").await.unwrap(); + let (mut ws, _) = client_async("ws://127.0.0.1:12348", tcp).await.unwrap(); + + let ping_data = vec![42]; + ws.send(Message::Ping(ping_data.clone().into())) + .await + .unwrap(); + + let response = ws.read().await.unwrap(); + assert_eq!(response, Message::Pong(ping_data.into())); +} + +#[compio_macros::test] +async fn test_close_handshake() { + let (tx, rx) = oneshot::channel(); + + compio_runtime::spawn(async move { + let listener = TcpListener::bind("127.0.0.1:12349").await.unwrap(); + tx.send(()).unwrap(); + + let (stream, _) = listener.accept().await.unwrap(); + let mut ws = accept_async(stream).await.unwrap(); + + let msg = ws.read().await.unwrap(); + assert!(msg.is_close()); + }) + .detach(); + + rx.await.unwrap(); + + let tcp = TcpStream::connect("127.0.0.1:12349").await.unwrap(); + let (mut ws, _) = client_async("ws://127.0.0.1:12349", tcp).await.unwrap(); + + ws.close(None).await.unwrap(); +} + +#[compio_macros::test] +async fn test_multiple_messages() { + let (tx, rx) = oneshot::channel(); + + compio_runtime::spawn(async move { + let listener = TcpListener::bind("127.0.0.1:12350").await.unwrap(); + tx.send(()).unwrap(); + + let (stream, _) = listener.accept().await.unwrap(); + let mut ws = accept_async(stream).await.unwrap(); + + for _ in 0..3 { + let msg = ws.read().await.unwrap(); + ws.send(msg).await.unwrap(); + } + }) + .detach(); + + rx.await.unwrap(); + + let tcp = TcpStream::connect("127.0.0.1:12350").await.unwrap(); + let (mut ws, _) = client_async("ws://127.0.0.1:12350", tcp).await.unwrap(); + + for i in 0..3 { + let msg = format!("Message {}", i); + ws.send(Message::Text(msg.clone().into())).await.unwrap(); + let response = ws.read().await.unwrap(); + assert_eq!(response, Message::Text(msg.into())); + } +} From 2be895c400bb30957e291755b733c9a1fdf0dee8 Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Thu, 6 Nov 2025 15:03:02 +0530 Subject: [PATCH 6/8] fix: echo_server --- compio-ws/examples/echo_server.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compio-ws/examples/echo_server.rs b/compio-ws/examples/echo_server.rs index 41478e76..b74c2113 100644 --- a/compio-ws/examples/echo_server.rs +++ b/compio-ws/examples/echo_server.rs @@ -27,10 +27,8 @@ async fn handle_client(stream: TcpStream) -> Result<(), Box { - println!("Received text: {}", text.len()); - let echo_msg = format!("Echo: {text}"); - println!("Sending echo: {echo_msg}"); - + let text_str = text.to_string(); + println!("Received text: {}", text_str); websocket.send(Message::Text(text)).await?; println!("Echo sent successfully"); } From 6a536175ce7929656762194235b817e71d99da6f Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Fri, 7 Nov 2025 00:55:30 +0530 Subject: [PATCH 7/8] fix: move driver and runtime to dev-dependencies --- compio-ws/Cargo.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/compio-ws/Cargo.toml b/compio-ws/Cargo.toml index 2918d83f..26c276fb 100644 --- a/compio-ws/Cargo.toml +++ b/compio-ws/Cargo.toml @@ -18,17 +18,13 @@ compio-net = { workspace = true, optional = true } compio-tls = { workspace = true, optional = true, default-features = false, features = [ "rustls", ] } -compio-runtime = { workspace = true } -compio-driver = { workspace = true } compio-buf = { workspace = true } webpki-roots = { version = "1.0.4", optional = true } rustls-pemfile = "2.0" log = "0.4" [features] -default = ["io-uring"] -io-uring = ["compio-driver/io-uring"] -polling = ["compio-driver/polling"] +default = [] connect = ["dep:compio-net"] rustls = ["connect", "dep:compio-tls", "dep:rustls", "compio-tls/rustls"] rustls-native-certs = ["rustls", "dep:rustls-native-certs"] @@ -42,6 +38,8 @@ env_logger = "0.11" compio-net = { workspace = true } compio-macros = { workspace = true } futures-channel = { workspace = true } +compio-runtime = { workspace = true } +compio-driver = { workspace = true } [[example]] name = "echo_server" From cfa0c6033bba4ddddb39931b6902031a6a8cb6c3 Mon Sep 17 00:00:00 2001 From: Krishna Vishal Date: Tue, 11 Nov 2025 02:04:01 +0530 Subject: [PATCH 8/8] Address review comments --- Cargo.toml | 1 - compio-ws/Cargo.toml | 10 ++- compio-ws/src/lib.rs | 1 - compio-ws/src/rustls.rs | 133 ++++++++++++++++++----------------- compio-ws/src/stream.rs | 9 ++- compio-ws/tests/websocket.rs | 70 +++++++++--------- compio/Cargo.toml | 8 ++- compio/src/lib.rs | 3 + 8 files changed, 127 insertions(+), 108 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 964d90e2..8f34e816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ license = "MIT" repository = "https://github.com/compio-rs/compio" [workspace.dependencies] -compio = { path = "./compio", version = "0.16.0" } compio-buf = { path = "./compio-buf", version = "0.7.0" } compio-driver = { path = "./compio-driver", version = "0.9.0", default-features = false } compio-runtime = { path = "./compio-runtime", version = "0.9.0" } diff --git a/compio-ws/Cargo.toml b/compio-ws/Cargo.toml index 26c276fb..a268b49f 100644 --- a/compio-ws/Cargo.toml +++ b/compio-ws/Cargo.toml @@ -11,8 +11,8 @@ repository = { workspace = true } [dependencies] rustls = { workspace = true, optional = true, default-features = false } -rustls-native-certs = { version = "0.8", optional = true } -tungstenite = "0.27.0" +rustls-platform-verifier = { version = "0.6.0", optional = true } +tungstenite = "0.28.0" compio-io = { workspace = true } compio-net = { workspace = true, optional = true } compio-tls = { workspace = true, optional = true, default-features = false, features = [ @@ -27,7 +27,7 @@ log = "0.4" default = [] connect = ["dep:compio-net"] rustls = ["connect", "dep:compio-tls", "dep:rustls", "compio-tls/rustls"] -rustls-native-certs = ["rustls", "dep:rustls-native-certs"] +rustls-platform-verifier = ["rustls", "dep:rustls-platform-verifier"] webpki-roots = ["rustls", "dep:webpki-roots"] ring = ["rustls?/ring", "compio-tls?/ring"] @@ -63,3 +63,7 @@ required-features = ["connect", "rustls", "ring"] [[example]] name = "client_tls" required-features = ["connect", "rustls", "ring"] + +[[test]] +name = "websocket" +required-features = ["connect", "compio-driver/io-uring"] diff --git a/compio-ws/src/lib.rs b/compio-ws/src/lib.rs index 06b2a72b..03d31602 100644 --- a/compio-ws/src/lib.rs +++ b/compio-ws/src/lib.rs @@ -35,7 +35,6 @@ pub use crate::rustls::{ connect_async_with_config, connect_async_with_tls_connector, connect_async_with_tls_connector_and_config, }; -pub use crate::stream::MaybeTlsStream; pub struct WebSocketStream { inner: WebSocket>, diff --git a/compio-ws/src/rustls.rs b/compio-ws/src/rustls.rs index e6a1c073..29ccae6d 100644 --- a/compio-ws/src/rustls.rs +++ b/compio-ws/src/rustls.rs @@ -1,10 +1,10 @@ -#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] +#[cfg(any(feature = "rustls-platform-verifier", feature = "webpki-roots"))] use std::sync::Arc; use compio_io::{AsyncRead, AsyncWrite}; use compio_net::TcpStream; use compio_tls::TlsConnector; -#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] +#[cfg(any(feature = "rustls-platform-verifier", feature = "webpki-roots"))] use rustls::{ClientConfig, RootCertStore}; use tungstenite::{ Error, @@ -37,92 +37,93 @@ where let connector = if let Some(connector) = connector { connector } else { - // Only create root_store when we actually have certificate features enabled - #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] - let root_store = { - let mut store = RootCertStore::empty(); - - #[cfg(feature = "rustls-native-certs")] - { - let cert_result = rustls_native_certs::load_native_certs(); - - // Log any errors that occurred - for err in &cert_result.errors { - log::warn!("Error loading native certificate: {err}"); - } + // Create TLS connector with platform verifier when feature is enabled + #[cfg(feature = "rustls-platform-verifier")] + { + use rustls_platform_verifier::BuilderVerifierExt; - if !cert_result.certs.is_empty() { - let (added, ignored) = - store.add_parsable_certificates(cert_result.certs); + // Use platform's native certificate verification + // This provides better security and enterprise integration + let config_result = ClientConfig::builder().with_platform_verifier(); + + match config_result { + Ok(config_builder) => { log::debug!( - "Added {added} native root certificates (ignored {ignored})" + "Using rustls-platform-verifier for certificate validation" ); + TlsConnector::from(Arc::new(config_builder.with_no_client_auth())) + } + Err(e) => { + log::warn!("Error creating platform verifier: {e}"); // Only fail if webpki-roots is NOT enabled as fallback #[cfg(not(feature = "webpki-roots"))] - if added == 0 { + { return Err(Error::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - "No valid native root certificates found", + std::io::ErrorKind::Other, + format!("Failed to create platform verifier: {}", e), ))); } - } else { - log::warn!("No native root certificates found"); - // Only fail if webpki-roots is NOT enabled as fallback - #[cfg(not(feature = "webpki-roots"))] - return Err(Error::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - "No native root certificates found", - ))); + // Fall through to webpki-roots if available + #[cfg(feature = "webpki-roots")] + { + use log::debug; + + let mut root_store = RootCertStore::empty(); + let webpki_certs = webpki_roots::TLS_SERVER_ROOTS.to_vec(); + root_store.extend(webpki_certs); + debug!( + "Falling back to {} webpki root certificates", + webpki_roots::TLS_SERVER_ROOTS.len() + ); + + TlsConnector::from(Arc::new( + ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(), + )) + } } } + } - // Load webpki-roots whenever the feature is enabled - // This serves as a fallback when native-certs is also enabled - #[cfg(feature = "webpki-roots")] - { - use log::debug; - - let webpki_certs = webpki_roots::TLS_SERVER_ROOTS.to_vec(); - store.extend(webpki_certs); - debug!( - "Added {} webpki root certificates", - webpki_roots::TLS_SERVER_ROOTS.len() - ); - } - - store - }; - - // Check if we have neither feature enabled - #[cfg(not(any(feature = "rustls-native-certs", feature = "webpki-roots")))] + // Use webpki-roots when platform-verifier is not available + // This serves as a fallback or standalone certificate source + #[cfg(all( + feature = "webpki-roots", + not(feature = "rustls-platform-verifier") + ))] { - return Err(Error::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - "No root certificate features enabled. Enable either \ - 'rustls-native-certs' or 'webpki-roots'", - ))); - } + use log::debug; - // Check if root_store is empty (only when features are enabled) - #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] - if root_store.is_empty() { - return Err(Error::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - "No root certificates available", - ))); - } + let mut root_store = RootCertStore::empty(); + let webpki_certs = webpki_roots::TLS_SERVER_ROOTS.to_vec(); + root_store.extend(webpki_certs); + debug!( + "Using {} webpki root certificates", + webpki_roots::TLS_SERVER_ROOTS.len() + ); - // Create the TLS connector (only when features are enabled) - #[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))] - { TlsConnector::from(Arc::new( ClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth(), )) } + + // Check if we have neither feature enabled + #[cfg(not(any( + feature = "rustls-platform-verifier", + feature = "webpki-roots" + )))] + { + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No root certificate features enabled. Enable either \ + 'rustls-platform-verifier' or 'webpki-roots'", + ))); + } }; connector diff --git a/compio-ws/src/stream.rs b/compio-ws/src/stream.rs index facdc9a3..47d9acc0 100644 --- a/compio-ws/src/stream.rs +++ b/compio-ws/src/stream.rs @@ -1,11 +1,15 @@ +#[cfg(feature = "rustls")] use std::io::Result as IoResult; +#[cfg(feature = "rustls")] use compio_buf::{BufResult, IoBuf, IoBufMut}; +#[cfg(feature = "rustls")] use compio_io::{AsyncRead, AsyncWrite}; #[cfg(feature = "rustls")] use compio_tls::TlsStream; /// Stream that can be either plain TCP or TLS-encrypted +#[cfg(feature = "rustls")] #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum MaybeTlsStream { @@ -16,6 +20,7 @@ pub enum MaybeTlsStream { Tls(TlsStream), } +#[cfg(feature = "rustls")] impl MaybeTlsStream { pub fn plain(stream: S) -> Self { MaybeTlsStream::Plain(stream) @@ -38,6 +43,7 @@ impl MaybeTlsStream { } } +#[cfg(feature = "rustls")] impl AsyncRead for MaybeTlsStream where S: AsyncRead + AsyncWrite + Unpin + 'static, @@ -51,6 +57,7 @@ where } } +#[cfg(feature = "rustls")] impl AsyncWrite for MaybeTlsStream where S: AsyncRead + AsyncWrite + Unpin + 'static, @@ -79,5 +86,3 @@ where } } } - -impl Unpin for MaybeTlsStream where S: Unpin {} diff --git a/compio-ws/tests/websocket.rs b/compio-ws/tests/websocket.rs index 72b019fc..fcdd58b9 100644 --- a/compio-ws/tests/websocket.rs +++ b/compio-ws/tests/websocket.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "connect")] - use compio_net::{TcpListener, TcpStream}; use compio_ws::{accept_async, client_async}; use futures_channel::oneshot; @@ -10,20 +8,19 @@ async fn test_handshake() { let (tx, rx) = oneshot::channel(); compio_runtime::spawn(async move { - let listener = TcpListener::bind("127.0.0.1:12345").await.unwrap(); - tx.send(()).unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tx.send(addr).unwrap(); let (stream, _) = listener.accept().await.unwrap(); let _ws = accept_async(stream).await.expect("Server handshake failed"); }) .detach(); - rx.await.expect("Failed to wait for server"); + let addr = rx.await.expect("Failed to wait for server"); - let tcp = TcpStream::connect("127.0.0.1:12345") - .await - .expect("Failed to connect"); - let (_ws, _response) = client_async("ws://127.0.0.1:12345", tcp) + let tcp = TcpStream::connect(&addr).await.expect("Failed to connect"); + let (_ws, _response) = client_async(&format!("ws://{}", addr), tcp) .await .expect("Client handshake failed"); } @@ -33,8 +30,9 @@ async fn test_echo_message() { let (tx, rx) = oneshot::channel(); compio_runtime::spawn(async move { - let listener = TcpListener::bind("127.0.0.1:12346").await.unwrap(); - tx.send(()).unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tx.send(addr).unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut ws = accept_async(stream).await.unwrap(); @@ -44,10 +42,10 @@ async fn test_echo_message() { }) .detach(); - rx.await.unwrap(); + let addr = rx.await.unwrap(); - let tcp = TcpStream::connect("127.0.0.1:12346").await.unwrap(); - let (mut ws, _) = client_async("ws://127.0.0.1:12346", tcp).await.unwrap(); + let tcp = TcpStream::connect(&addr).await.unwrap(); + let (mut ws, _) = client_async(&format!("ws://{}", addr), tcp).await.unwrap(); let test_msg = "Hello, WebSocket!"; ws.send(Message::Text(test_msg.into())).await.unwrap(); @@ -61,8 +59,9 @@ async fn test_binary_message() { let (tx, rx) = oneshot::channel(); compio_runtime::spawn(async move { - let listener = TcpListener::bind("127.0.0.1:12347").await.unwrap(); - tx.send(()).unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tx.send(addr).unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut ws = accept_async(stream).await.unwrap(); @@ -72,10 +71,10 @@ async fn test_binary_message() { }) .detach(); - rx.await.unwrap(); + let addr = rx.await.unwrap(); - let tcp = TcpStream::connect("127.0.0.1:12347").await.unwrap(); - let (mut ws, _) = client_async("ws://127.0.0.1:12347", tcp).await.unwrap(); + let tcp = TcpStream::connect(&addr).await.unwrap(); + let (mut ws, _) = client_async(&format!("ws://{}", addr), tcp).await.unwrap(); let test_data = vec![1, 2, 3, 4, 5]; ws.send(Message::Binary(test_data.clone().into())) @@ -91,8 +90,9 @@ async fn test_ping_pong() { let (tx, rx) = oneshot::channel(); compio_runtime::spawn(async move { - let listener = TcpListener::bind("127.0.0.1:12348").await.unwrap(); - tx.send(()).unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tx.send(addr).unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut ws = accept_async(stream).await.unwrap(); @@ -104,10 +104,10 @@ async fn test_ping_pong() { }) .detach(); - rx.await.unwrap(); + let addr = rx.await.unwrap(); - let tcp = TcpStream::connect("127.0.0.1:12348").await.unwrap(); - let (mut ws, _) = client_async("ws://127.0.0.1:12348", tcp).await.unwrap(); + let tcp = TcpStream::connect(&addr).await.unwrap(); + let (mut ws, _) = client_async(&format!("ws://{}", addr), tcp).await.unwrap(); let ping_data = vec![42]; ws.send(Message::Ping(ping_data.clone().into())) @@ -123,8 +123,9 @@ async fn test_close_handshake() { let (tx, rx) = oneshot::channel(); compio_runtime::spawn(async move { - let listener = TcpListener::bind("127.0.0.1:12349").await.unwrap(); - tx.send(()).unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tx.send(addr).unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut ws = accept_async(stream).await.unwrap(); @@ -134,10 +135,10 @@ async fn test_close_handshake() { }) .detach(); - rx.await.unwrap(); + let addr = rx.await.unwrap(); - let tcp = TcpStream::connect("127.0.0.1:12349").await.unwrap(); - let (mut ws, _) = client_async("ws://127.0.0.1:12349", tcp).await.unwrap(); + let tcp = TcpStream::connect(&addr).await.unwrap(); + let (mut ws, _) = client_async(&format!("ws://{}", addr), tcp).await.unwrap(); ws.close(None).await.unwrap(); } @@ -147,8 +148,9 @@ async fn test_multiple_messages() { let (tx, rx) = oneshot::channel(); compio_runtime::spawn(async move { - let listener = TcpListener::bind("127.0.0.1:12350").await.unwrap(); - tx.send(()).unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tx.send(addr).unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut ws = accept_async(stream).await.unwrap(); @@ -160,10 +162,10 @@ async fn test_multiple_messages() { }) .detach(); - rx.await.unwrap(); + let addr = rx.await.unwrap(); - let tcp = TcpStream::connect("127.0.0.1:12350").await.unwrap(); - let (mut ws, _) = client_async("ws://127.0.0.1:12350", tcp).await.unwrap(); + let tcp = TcpStream::connect(&addr).await.unwrap(); + let (mut ws, _) = client_async(&format!("ws://{}", addr), tcp).await.unwrap(); for i in 0..3 { let msg = format!("Message {}", i); diff --git a/compio/Cargo.toml b/compio/Cargo.toml index ddacfeda..d6cd04fb 100644 --- a/compio/Cargo.toml +++ b/compio/Cargo.toml @@ -43,6 +43,7 @@ compio-log = { workspace = true } compio-tls = { workspace = true, optional = true } compio-process = { workspace = true, optional = true } compio-quic = { workspace = true, optional = true } +compio-ws = { workspace = true, optional = true } # Shared dev dependencies for all platforms [dev-dependencies] @@ -93,7 +94,10 @@ rustls = ["tls", "compio-tls/rustls"] process = ["dep:compio-process"] quic = ["dep:compio-quic"] h3 = ["quic", "compio-quic/h3"] +ws = ["dep:compio-ws", "compio-ws/connect"] +ws-rustls = ["ws", "compio-ws/rustls"] all = [ + "io-uring", "time", "macros", "signal", @@ -103,6 +107,8 @@ all = [ "process", "quic", "h3", + "ws", + "ws-rustls", ] arrayvec = ["compio-buf/arrayvec"] @@ -112,7 +118,7 @@ criterion = ["compio-runtime?/criterion"] enable_log = ["compio-log/enable_log"] -ring = ["compio-tls?/ring", "compio-quic?/ring"] +ring = ["compio-tls?/ring", "compio-quic?/ring", "compio-ws?/ring"] aws-lc-rs = ["compio-tls?/aws-lc-rs", "compio-quic?/aws-lc-rs"] aws-lc-rs-fips = ["compio-tls?/aws-lc-rs-fips", "compio-quic?/aws-lc-rs-fips"] diff --git a/compio/src/lib.rs b/compio/src/lib.rs index 272cf895..1945b319 100644 --- a/compio/src/lib.rs +++ b/compio/src/lib.rs @@ -50,6 +50,9 @@ pub use compio_signal as signal; #[cfg(feature = "tls")] #[doc(inline)] pub use compio_tls as tls; +#[cfg(feature = "ws")] +#[doc(inline)] +pub use compio_ws as ws; #[cfg(feature = "event")] #[doc(no_inline)] pub use runtime::event;