diff --git a/.gitignore b/.gitignore index c8e9e486..fc1f302d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ target Cargo.lock .vscode +autobahn/client/ +autobahn/server/ diff --git a/Cargo.toml b/Cargo.toml index 9d3402e6..4923cca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,18 +19,20 @@ all-features = true [features] default = ["handshake"] -handshake = ["data-encoding", "http", "httparse", "sha1"] +handshake = ["data-encoding", "headers", "http", "httparse", "sha1"] url = ["dep:url"] native-tls = ["native-tls-crate"] native-tls-vendored = ["native-tls", "native-tls-crate/vendored"] rustls-tls-native-roots = ["__rustls-tls", "rustls-native-certs"] rustls-tls-webpki-roots = ["__rustls-tls", "webpki-roots"] __rustls-tls = ["rustls", "rustls-pki-types"] +deflate = ["flate2"] [dependencies] data-encoding = { version = "2", optional = true } byteorder = "1.3.2" bytes = "1.0" +headers = { version = "0.4.0", optional = true } http = { version = "1.0", optional = true } httparse = { version = "1.3.4", optional = true } log = "0.4.8" @@ -40,6 +42,10 @@ thiserror = "1.0.23" url = { version = "2.1.0", optional = true } utf-8 = "0.7.5" +[dependencies.flate2] +optional = true +version = "1.0" + [dependencies.native-tls-crate] optional = true package = "native-tls" @@ -88,11 +94,11 @@ required-features = ["handshake"] [[example]] name = "autobahn-client" -required-features = ["handshake"] +required-features = ["handshake", "deflate"] [[example]] name = "autobahn-server" -required-features = ["handshake"] +required-features = ["handshake", "deflate"] [[example]] name = "callback-error" diff --git a/README.md b/README.md index 8c1b2a2b..6b587370 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,6 @@ Choose the one that is appropriate for your needs. By default **no TLS feature is activated**, so make sure you use one of the TLS features, otherwise you won't be able to communicate with the TLS endpoints. -There is no support for permessage-deflate at the moment, but the PRs are welcome :wink: - Testing ------- diff --git a/autobahn/expected-results.json b/autobahn/expected-results.json index c3d82f4b..894f481e 100644 --- a/autobahn/expected-results.json +++ b/autobahn/expected-results.json @@ -120,884 +120,884 @@ "reportfile": "tungstenite_case_10_1_1.json" }, "12.1.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 411, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_1.json" }, "12.1.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3952, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_10.json" }, "12.1.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 817, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_11.json" }, "12.1.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 818, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_12.json" }, "12.1.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1421, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_13.json" }, "12.1.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 2126, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_14.json" }, "12.1.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3966, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_15.json" }, "12.1.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3751, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_16.json" }, "12.1.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3738, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_17.json" }, "12.1.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3726, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_18.json" }, "12.1.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 516, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_2.json" }, "12.1.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 307, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_3.json" }, "12.1.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 282, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_4.json" }, "12.1.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 450, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_5.json" }, "12.1.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 536, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_6.json" }, "12.1.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 741, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_7.json" }, "12.1.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1328, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_8.json" }, "12.1.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 2133, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_1_9.json" }, "12.2.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 150, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_1.json" }, "12.2.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 14629, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_10.json" }, "12.2.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1077, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_11.json" }, "12.2.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1962, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_12.json" }, "12.2.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3922, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_13.json" }, "12.2.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 8660, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_14.json" }, "12.2.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 16334, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_15.json" }, "12.2.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 15329, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_16.json" }, "12.2.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 15202, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_17.json" }, "12.2.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 15065, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_18.json" }, "12.2.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 106, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_2.json" }, "12.2.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 168, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_3.json" }, "12.2.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 271, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_4.json" }, "12.2.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 671, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_5.json" }, "12.2.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1044, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_6.json" }, "12.2.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 2139, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_7.json" }, "12.2.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3435, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_8.json" }, "12.2.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 7263, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_2_9.json" }, "12.3.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 101, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_1.json" }, "12.3.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 20119, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_10.json" }, "12.3.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1317, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_11.json" }, "12.3.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 2429, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_12.json" }, "12.3.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 4916, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_13.json" }, "12.3.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 10318, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_14.json" }, "12.3.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 20840, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_15.json" }, "12.3.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 20795, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_16.json" }, "12.3.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 20491, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_17.json" }, "12.3.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 20238, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_18.json" }, "12.3.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 122, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_2.json" }, "12.3.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 164, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_3.json" }, "12.3.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 315, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_4.json" }, "12.3.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 717, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_5.json" }, "12.3.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 2, + "duration": 1213, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_6.json" }, "12.3.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 2361, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_7.json" }, "12.3.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 4617, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_8.json" }, "12.3.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 9549, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_3_9.json" }, "12.4.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 407, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_1.json" }, "12.4.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 4848, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_10.json" }, "12.4.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 790, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_11.json" }, "12.4.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1050, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_12.json" }, "12.4.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1609, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_13.json" }, "12.4.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 2860, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_14.json" }, "12.4.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 5382, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_15.json" }, "12.4.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 5252, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_16.json" }, "12.4.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 5096, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_17.json" }, "12.4.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 5027, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_18.json" }, "12.4.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 469, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_2.json" }, "12.4.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 480, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_3.json" }, "12.4.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 610, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_4.json" }, "12.4.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 625, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_5.json" }, "12.4.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 922, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_6.json" }, "12.4.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1037, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_7.json" }, "12.4.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1558, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_8.json" }, "12.4.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 2636, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_4_9.json" }, "12.5.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 145, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_1.json" }, "12.5.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 12087, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_10.json" }, "12.5.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1830, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_11.json" }, "12.5.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1952, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_12.json" }, "12.5.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 4592, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_13.json" }, "12.5.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 8835, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_14.json" }, "12.5.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 13050, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_15.json" }, "12.5.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 11856, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_16.json" }, "12.5.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 11422, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_17.json" }, "12.5.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 11346, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_18.json" }, "12.5.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 171, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_2.json" }, "12.5.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 183, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_3.json" }, "12.5.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 302, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_4.json" }, "12.5.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 605, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_5.json" }, "12.5.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1020, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_6.json" }, "12.5.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1466, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_7.json" }, "12.5.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3207, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_8.json" }, "12.5.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 5814, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_12_5_9.json" }, "13.1.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 205, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_1.json" }, "13.1.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3938, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_10.json" }, "13.1.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 536, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_11.json" }, "13.1.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 751, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_12.json" }, "13.1.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1562, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_13.json" }, "13.1.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 2147, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_14.json" }, "13.1.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3936, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_15.json" }, "13.1.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3996, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_16.json" }, "13.1.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3912, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_17.json" }, "13.1.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 3924, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_18.json" }, "13.1.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 222, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_2.json" }, "13.1.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 247, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_3.json" }, "13.1.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 284, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_4.json" }, "13.1.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 368, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_5.json" }, "13.1.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 2, + "duration": 490, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_6.json" }, "13.1.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 2, + "duration": 722, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_7.json" }, "13.1.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1164, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_8.json" }, "13.1.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 2252, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_1_9.json" }, "13.2.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 260, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_1.json" }, "13.2.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 4048, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_10.json" }, "13.2.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 608, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_11.json" }, "13.2.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 734, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_12.json" }, "13.2.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1301, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_13.json" }, "13.2.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 2085, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_14.json" }, "13.2.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 2, + "duration": 3887, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_15.json" }, "13.2.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 4, + "duration": 3807, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_16.json" }, "13.2.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3766, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_17.json" }, "13.2.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3852, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_18.json" }, "13.2.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 245, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_2.json" }, "13.2.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 275, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_3.json" }, "13.2.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 295, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_4.json" }, "13.2.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 373, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_5.json" }, "13.2.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 498, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_6.json" }, "13.2.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 734, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_7.json" }, "13.2.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 1106, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_8.json" }, "13.2.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 2428, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_2_9.json" }, @@ -1506,128 +1506,128 @@ "reportfile": "tungstenite_case_13_6_9.json" }, "13.7.1": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 271, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_1.json" }, "13.7.10": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3822, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_10.json" }, "13.7.11": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 529, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_11.json" }, "13.7.12": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 716, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_12.json" }, "13.7.13": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 1263, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_13.json" }, "13.7.14": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 2147, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_14.json" }, "13.7.15": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3948, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_15.json" }, "13.7.16": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3864, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_16.json" }, "13.7.17": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 4061, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_17.json" }, "13.7.18": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 3912, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_18.json" }, "13.7.2": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 234, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_2.json" }, "13.7.3": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 271, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_3.json" }, "13.7.4": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 0, + "duration": 294, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_4.json" }, "13.7.5": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 372, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_5.json" }, "13.7.6": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 501, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_6.json" }, "13.7.7": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 2, + "duration": 709, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_7.json" }, "13.7.8": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 2, + "duration": 1147, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_8.json" }, "13.7.9": { - "behavior": "UNIMPLEMENTED", + "behavior": "OK", "behaviorClose": "OK", - "duration": 1, + "duration": 2040, "remoteCloseCode": 1000, "reportfile": "tungstenite_case_13_7_9.json" }, diff --git a/examples/autobahn-client.rs b/examples/autobahn-client.rs index dcc3e75f..6ea7d151 100644 --- a/examples/autobahn-client.rs +++ b/examples/autobahn-client.rs @@ -1,6 +1,9 @@ use log::*; -use tungstenite::{connect, Error, Message, Result}; +use tungstenite::{ + client::connect_with_config, connect, extensions::DeflateConfig, protocol::WebSocketConfig, + Error, Message, Result, +}; const AGENT: &str = "Tungstenite"; @@ -20,7 +23,11 @@ fn update_reports() -> Result<()> { fn run_test(case: u32) -> Result<()> { info!("Running test case {}", case); let case_url = format!("ws://localhost:9001/runCase?case={case}&agent={AGENT}"); - let (mut socket, _) = connect(case_url)?; + + let mut config = WebSocketConfig::default(); + config.compression = Some(DeflateConfig::default()); + + let (mut socket, _) = connect_with_config(case_url, Some(config), 3)?; loop { match socket.read()? { msg @ Message::Text(_) | msg @ Message::Binary(_) => { diff --git a/examples/autobahn-server.rs b/examples/autobahn-server.rs index dafe37bd..edb84d4c 100644 --- a/examples/autobahn-server.rs +++ b/examples/autobahn-server.rs @@ -4,7 +4,10 @@ use std::{ }; use log::*; -use tungstenite::{accept, handshake::HandshakeRole, Error, HandshakeError, Message, Result}; +use tungstenite::{ + accept_with_config, extensions::DeflateConfig, handshake::HandshakeRole, + protocol::WebSocketConfig, Error, HandshakeError, Message, Result, +}; fn must_not_block(err: HandshakeError) -> Error { match err { @@ -14,7 +17,10 @@ fn must_not_block(err: HandshakeError) -> Error { } fn handle_client(stream: TcpStream) -> Result<()> { - let mut socket = accept(stream).map_err(must_not_block)?; + let mut config = WebSocketConfig::default(); + config.compression = Some(DeflateConfig::default()); + + let mut socket = accept_with_config(stream, Some(config)).map_err(must_not_block)?; info!("Running test"); loop { match socket.read()? { diff --git a/examples/srv_accept_unmasked_frames.rs b/examples/srv_accept_unmasked_frames.rs index d27ddc39..eb42fab0 100644 --- a/examples/srv_accept_unmasked_frames.rs +++ b/examples/srv_accept_unmasked_frames.rs @@ -26,16 +26,15 @@ fn main() { Ok(response) }; - let config = Some(WebSocketConfig { - // This setting allows to accept client frames which are not masked - // This is not in compliance with RFC 6455 but might be handy in some - // rare cases where it is necessary to integrate with existing/legacy - // clients which are sending unmasked frames - accept_unmasked_frames: true, - ..<_>::default() - }); + let mut config = WebSocketConfig::default(); + // This setting allows to accept client frames which are not masked + // This is not in compliance with RFC 6455 but might be handy in some + // rare cases where it is necessary to integrate with existing/legacy + // clients which are sending unmasked frames + config.accept_unmasked_frames = true; - let mut websocket = accept_hdr_with_config(stream.unwrap(), callback, config).unwrap(); + let mut websocket = + accept_hdr_with_config(stream.unwrap(), callback, Some(config)).unwrap(); loop { let msg = websocket.read().unwrap(); diff --git a/scripts/autobahn-client.sh b/scripts/autobahn-client.sh index d0d8f1b6..139ebd03 100755 --- a/scripts/autobahn-client.sh +++ b/scripts/autobahn-client.sh @@ -32,5 +32,5 @@ docker run -d --rm \ wstest -m fuzzingserver -s 'autobahn/fuzzingserver.json' sleep 3 -cargo run --release --example autobahn-client +cargo run --release --example autobahn-client --features=deflate test_diff diff --git a/scripts/autobahn-server.sh b/scripts/autobahn-server.sh index 63e89e3f..ab737fed 100755 --- a/scripts/autobahn-server.sh +++ b/scripts/autobahn-server.sh @@ -22,7 +22,7 @@ function test_diff() { fi } -cargo run --release --example autobahn-server & WSSERVER_PID=$! +cargo run --release --example autobahn-server --features=deflate & WSSERVER_PID=$! sleep 3 docker run --rm \ diff --git a/src/error.rs b/src/error.rs index eaf7d24f..f3a53cf1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,6 +11,7 @@ use thiserror::Error; pub type Result = result::Result; /// Possible WebSocket errors. +#[non_exhaustive] #[derive(Error, Debug)] pub enum Error { /// WebSocket connection closed normally. This informs you of the close. @@ -73,6 +74,10 @@ pub enum Error { #[error("HTTP format error: {0}")] #[cfg(feature = "handshake")] HttpFormat(#[from] http::Error), + /// Error from `permessage-deflate` extension. + #[cfg(feature = "deflate")] + #[error("Deflate error: {0}")] + Deflate(#[from] crate::extensions::DeflateError), } impl From for Error { @@ -168,6 +173,7 @@ pub enum SubProtocolError { /// Indicates the specific type/cause of a protocol error. #[allow(missing_copy_implementations)] +#[non_exhaustive] #[derive(Error, Debug, PartialEq, Eq, Clone)] pub enum ProtocolError { /// Use of the wrong HTTP method (the WebSocket protocol requires the GET method be used). @@ -229,6 +235,9 @@ pub enum ProtocolError { /// Control frames must not be fragmented. #[error("Fragmented control frame")] FragmentedControlFrame, + /// Control frames must not be compressed. + #[error("Compressed control frame")] + CompressedControlFrame, /// Control frames must have a payload of 125 bytes or less. #[error("Control frame too big (payload must be 125 bytes or less)")] ControlFrameTooBig, @@ -241,6 +250,9 @@ pub enum ProtocolError { /// Received a continue frame despite there being nothing to continue. #[error("Continue frame but nothing to continue")] UnexpectedContinueFrame, + /// Received a compressed continue frame. + #[error("Continue frame must not have compress bit set")] + CompressedContinueFrame, /// Received data while waiting for more fragments. #[error("While waiting for more fragments received: {0}")] ExpectedFragment(Data), @@ -253,6 +265,15 @@ pub enum ProtocolError { /// The payload for the closing frame is invalid. #[error("Invalid close sequence")] InvalidCloseSequence, + /// The negotiation response included an extension not offered. + #[error("Extension negotiation response had invalid extension: {0}")] + InvalidExtension(String), + /// The negotiation response included an extension more than once. + #[error("Extension negotiation response had conflicting extension: {0}")] + ExtensionConflict(String), + /// The `Sec-WebSocket-Extensions` header is invalid. + #[error("Invalid \"Sec-WebSocket-Extensions\" header")] + InvalidExtensionsHeader, } /// Indicates the specific type/cause of URL error. diff --git a/src/extensions/compression/deflate.rs b/src/extensions/compression/deflate.rs new file mode 100644 index 00000000..38a166bc --- /dev/null +++ b/src/extensions/compression/deflate.rs @@ -0,0 +1,456 @@ +#[cfg(feature = "handshake")] +use std::convert::TryFrom; + +#[cfg(feature = "handshake")] +use bytes::BytesMut; +use flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress, Status}; +#[cfg(feature = "handshake")] +use http::HeaderValue; +use thiserror::Error; + +#[cfg(feature = "handshake")] +use crate::handshake::headers::{SecWebsocketExtensions, WebsocketExtension}; +use crate::protocol::Role; + +#[cfg(feature = "handshake")] +const PER_MESSAGE_DEFLATE: &str = "permessage-deflate"; +#[cfg(feature = "handshake")] +const CLIENT_NO_CONTEXT_TAKEOVER: &str = "client_no_context_takeover"; +#[cfg(feature = "handshake")] +const SERVER_NO_CONTEXT_TAKEOVER: &str = "server_no_context_takeover"; +#[cfg(feature = "handshake")] +const CLIENT_MAX_WINDOW_BITS: &str = "client_max_window_bits"; +#[cfg(feature = "handshake")] +const SERVER_MAX_WINDOW_BITS: &str = "server_max_window_bits"; + +const TRAILER: [u8; 4] = [0x00, 0x00, 0xff, 0xff]; + +/// Errors from `permessage-deflate` extension. +#[derive(Debug, Error)] +pub enum DeflateError { + /// Compress failed + #[error("Failed to compress")] + Compress(#[source] std::io::Error), + /// Decompress failed + #[error("Failed to decompress")] + Decompress(#[source] std::io::Error), + + /// Extension negotiation failed. + #[error("Extension negotiation failed")] + Negotiation(#[source] NegotiationError), +} + +/// Errors from `permessage-deflate` extension negotiation. +#[derive(Debug, Error)] +pub enum NegotiationError { + /// Unknown parameter in a negotiation response. + #[error("Unknown parameter in a negotiation response: {0}")] + UnknownParameter(String), + /// Duplicate parameter in a negotiation response. + #[error("Duplicate parameter in a negotiation response: {0}")] + DuplicateParameter(String), + /// Received `client_max_window_bits` in a negotiation response for an offer without it. + #[error("Received client_max_window_bits in a negotiation response for an offer without it")] + UnexpectedClientMaxWindowBits, + /// Received unsupported `server_max_window_bits` in a negotiation response. + #[error("Received unsupported server_max_window_bits in a negotiation response")] + ServerMaxWindowBitsNotSupported, + /// Invalid `client_max_window_bits` value in a negotiation response. + #[error("Invalid client_max_window_bits value in a negotiation response: {0}")] + InvalidClientMaxWindowBitsValue(String), + /// Invalid `server_max_window_bits` value in a negotiation response. + #[error("Invalid server_max_window_bits value in a negotiation response: {0}")] + InvalidServerMaxWindowBitsValue(String), + /// Missing `server_max_window_bits` value in a negotiation response. + #[error("Missing server_max_window_bits value in a negotiation response")] + MissingServerMaxWindowBitsValue, +} + +// Parameters `server_max_window_bits` and `client_max_window_bits` are not supported for now +// because custom window size requires `flate2/zlib` feature. +/// Configurations for `permessage-deflate` Per-Message Compression Extension. +#[derive(Clone, Copy, Debug, Default)] +pub struct DeflateConfig { + /// Compression level. + pub compression: Compression, + /// Request the peer server not to use context takeover. + pub server_no_context_takeover: bool, + /// Hint that context takeover is not used. + pub client_no_context_takeover: bool, +} + +#[cfg(feature = "handshake")] +impl DeflateConfig { + pub(crate) fn name(&self) -> &str { + PER_MESSAGE_DEFLATE + } + + /// Value for `Sec-WebSocket-Extensions` request header. + pub(crate) fn generate_offer(&self) -> WebsocketExtension { + let mut offers = Vec::new(); + if self.server_no_context_takeover { + offers.push(HeaderValue::from_static(SERVER_NO_CONTEXT_TAKEOVER)); + } + + // > a client informs the peer server of a hint that even if the server doesn't include the + // > "client_no_context_takeover" extension parameter in the corresponding + // > extension negotiation response to the offer, the client is not going + // > to use context takeover. + // > https://www.rfc-editor.org/rfc/rfc7692#section-7.1.1.2 + if self.client_no_context_takeover { + offers.push(HeaderValue::from_static(CLIENT_NO_CONTEXT_TAKEOVER)); + } + to_header_value(&offers) + } + + /// Returns negotiation response based on offers and `DeflateContext` to manage per message compression. + pub(crate) fn accept_offer( + &self, + offers: &SecWebsocketExtensions, + ) -> Option<(WebsocketExtension, DeflateContext)> { + // Accept the first valid offer for `permessage-deflate`. + // A server MUST decline an extension negotiation offer for this + // extension if any of the following conditions are met: + // 1. The negotiation offer contains an extension parameter not defined for use in an offer. + // 2. The negotiation offer contains an extension parameter with an invalid value. + // 3. The negotiation offer contains multiple extension parameters with the same name. + // 4. The server doesn't support the offered configuration. + offers.iter().find_map(|extension| { + if let Some(params) = (extension.name() == self.name()).then(|| extension.params()) { + let mut config = + DeflateConfig { compression: self.compression, ..DeflateConfig::default() }; + let mut agreed = Vec::new(); + let mut seen_server_no_context_takeover = false; + let mut seen_client_no_context_takeover = false; + let mut seen_client_max_window_bits = false; + for (key, val) in params { + match key { + SERVER_NO_CONTEXT_TAKEOVER => { + // Invalid offer with multiple params with same name is declined. + if seen_server_no_context_takeover { + return None; + } + seen_server_no_context_takeover = true; + config.server_no_context_takeover = true; + agreed.push(HeaderValue::from_static(SERVER_NO_CONTEXT_TAKEOVER)); + } + + CLIENT_NO_CONTEXT_TAKEOVER => { + // Invalid offer with multiple params with same name is declined. + if seen_client_no_context_takeover { + return None; + } + seen_client_no_context_takeover = true; + config.client_no_context_takeover = true; + agreed.push(HeaderValue::from_static(CLIENT_NO_CONTEXT_TAKEOVER)); + } + + // Max window bits are not supported at the moment. + SERVER_MAX_WINDOW_BITS => { + // Decline offer with invalid parameter value. + // `server_max_window_bits` requires a value in range [8, 15]. + if let Some(bits) = val { + if !is_valid_max_window_bits(bits) { + return None; + } + } else { + return None; + } + + // A server declines an extension negotiation offer with this parameter + // if the server doesn't support it. + return None; + } + + // Not supported, but server may ignore and accept the offer. + CLIENT_MAX_WINDOW_BITS => { + // Decline offer with invalid parameter value. + // `client_max_window_bits` requires a value in range [8, 15] or no value. + if let Some(bits) = val { + if !is_valid_max_window_bits(bits) { + return None; + } + } + + // Invalid offer with multiple params with same name is declined. + if seen_client_max_window_bits { + return None; + } + seen_client_max_window_bits = true; + } + + // Offer with unknown parameter MUST be declined. + _ => { + return None; + } + } + } + + Some((to_header_value(&agreed), DeflateContext::new(Role::Server, config))) + } else { + None + } + }) + } + + #[cfg(feature = "handshake")] + pub(crate) fn accept_response<'a>( + &'a self, + agreed: impl Iterator)>, + ) -> Result { + let mut config = DeflateConfig { + compression: self.compression, + // If this was hinted in the offer, the client won't use context takeover + // even if the response doesn't include it. + // See `generate_offer`. + client_no_context_takeover: self.client_no_context_takeover, + ..DeflateConfig::default() + }; + let mut seen_server_no_context_takeover = false; + let mut seen_client_no_context_takeover = false; + // A client MUST _Fail the WebSocket Connection_ if the peer server + // accepted an extension negotiation offer for this extension with an + // extension negotiation response meeting any of the following + // conditions: + // 1. The negotiation response contains an extension parameter not defined for use in a response. + // 2. The negotiation response contains an extension parameter with an invalid value. + // 3. The negotiation response contains multiple extension parameters with the same name. + // 4. The client does not support the configuration that the response represents. + for (key, val) in agreed { + match key { + SERVER_NO_CONTEXT_TAKEOVER => { + // Fail the connection when the response contains multiple parameters with the same name. + if seen_server_no_context_takeover { + return Err(DeflateError::Negotiation( + NegotiationError::DuplicateParameter(key.to_owned()), + )); + } + seen_server_no_context_takeover = true; + // A server MAY include the "server_no_context_takeover" extension + // parameter in an extension negotiation response even if the extension + // negotiation offer being accepted by the extension negotiation + // response didn't include the "server_no_context_takeover" extension + // parameter. + config.server_no_context_takeover = true; + } + + CLIENT_NO_CONTEXT_TAKEOVER => { + // Fail the connection when the response contains multiple parameters with the same name. + if seen_client_no_context_takeover { + return Err(DeflateError::Negotiation( + NegotiationError::DuplicateParameter(key.to_owned()), + )); + } + seen_client_no_context_takeover = true; + // The server may include this parameter in the response and the client MUST support it. + config.client_no_context_takeover = true; + } + + SERVER_MAX_WINDOW_BITS => { + // Fail the connection when the response contains a parameter with invalid value. + if let Some(bits) = val { + if !is_valid_max_window_bits(bits) { + return Err(DeflateError::Negotiation( + NegotiationError::InvalidServerMaxWindowBitsValue(bits.to_owned()), + )); + } + } else { + return Err(DeflateError::Negotiation( + NegotiationError::MissingServerMaxWindowBitsValue, + )); + } + + // A server may include the "server_max_window_bits" extension parameter + // in an extension negotiation response even if the extension + // negotiation offer being accepted by the response didn't include the + // "server_max_window_bits" extension parameter. + // + // However, but we need to fail the connection because we don't support it (condition 4). + return Err(DeflateError::Negotiation( + NegotiationError::ServerMaxWindowBitsNotSupported, + )); + } + + CLIENT_MAX_WINDOW_BITS => { + // Fail the connection when the response contains a parameter with invalid value. + if let Some(bits) = val { + if !is_valid_max_window_bits(bits) { + return Err(DeflateError::Negotiation( + NegotiationError::InvalidClientMaxWindowBitsValue(bits.to_owned()), + )); + } + } + + // Fail the connection because the parameter is invalid when the client didn't offer. + // + // If a received extension negotiation offer doesn't have the + // "client_max_window_bits" extension parameter, the corresponding + // extension negotiation response to the offer MUST NOT include the + // "client_max_window_bits" extension parameter. + return Err(DeflateError::Negotiation( + NegotiationError::UnexpectedClientMaxWindowBits, + )); + } + + // Response with unknown parameter MUST fail the WebSocket connection. + _ => { + return Err(DeflateError::Negotiation(NegotiationError::UnknownParameter( + key.to_owned(), + ))); + } + } + } + Ok(DeflateContext::new(Role::Client, config)) + } +} + +// A valid `client_max_window_bits` is no value or an integer in range `[8, 15]` without leading zeros. +// A valid `server_max_window_bits` is an integer in range `[8, 15]` without leading zeros. +#[cfg(feature = "handshake")] +fn is_valid_max_window_bits(bits: &str) -> bool { + // Note that values from `headers::SecWebSocketExtensions` is unquoted. + matches!(bits, "8" | "9" | "10" | "11" | "12" | "13" | "14" | "15") +} + +#[cfg(all(test, feature = "handshake"))] +mod tests { + use super::is_valid_max_window_bits; + + #[test] + fn valid_max_window_bits() { + for bits in 8..=15 { + assert!(is_valid_max_window_bits(&bits.to_string())); + } + } + + #[test] + fn invalid_max_window_bits() { + assert!(!is_valid_max_window_bits("")); + assert!(!is_valid_max_window_bits("0")); + assert!(!is_valid_max_window_bits("08")); + assert!(!is_valid_max_window_bits("+8")); + assert!(!is_valid_max_window_bits("-8")); + } +} + +#[derive(Debug)] +/// Manages per message compression using DEFLATE. +pub struct DeflateContext { + role: Role, + config: DeflateConfig, + compressor: Compress, + decompressor: Decompress, +} + +impl DeflateContext { + #[cfg(feature = "handshake")] + fn new(role: Role, config: DeflateConfig) -> Self { + DeflateContext { + role, + config, + compressor: Compress::new(config.compression, false), + decompressor: Decompress::new(false), + } + } + + fn own_context_takeover(&self) -> bool { + match self.role { + Role::Server => !self.config.server_no_context_takeover, + Role::Client => !self.config.client_no_context_takeover, + } + } + + fn peer_context_takeover(&self) -> bool { + match self.role { + Role::Server => !self.config.client_no_context_takeover, + Role::Client => !self.config.server_no_context_takeover, + } + } + + // Compress the data of message. + pub(crate) fn compress(&mut self, data: &[u8]) -> Result, DeflateError> { + // https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.1 + // 1. Compress all the octets of the payload of the message using DEFLATE. + let mut output = Vec::with_capacity(data.len()); + let before_in = self.compressor.total_in() as usize; + while (self.compressor.total_in() as usize) - before_in < data.len() { + let offset = (self.compressor.total_in() as usize) - before_in; + match self + .compressor + .compress_vec(&data[offset..], &mut output, FlushCompress::None) + .map_err(|e| DeflateError::Compress(e.into()))? + { + Status::Ok => continue, + Status::BufError => output.reserve(4096), + Status::StreamEnd => break, + } + } + // 2. If the resulting data does not end with an empty DEFLATE block + // with no compression (the "BTYPE" bits are set to 00), append an + // empty DEFLATE block with no compression to the tail end. + while !output.ends_with(&TRAILER) { + output.reserve(5); + match self + .compressor + .compress_vec(&[], &mut output, FlushCompress::Sync) + .map_err(|e| DeflateError::Compress(e.into()))? + { + Status::Ok | Status::BufError => continue, + Status::StreamEnd => break, + } + } + // 3. Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end. + // After this step, the last octet of the compressed data contains + // (possibly part of) the DEFLATE header bits with the "BTYPE" bits + // set to 00. + output.truncate(output.len() - 4); + + if !self.own_context_takeover() { + self.compressor.reset(); + } + + Ok(output) + } + + pub(crate) fn decompress( + &mut self, + mut data: Vec, + is_final: bool, + ) -> Result, DeflateError> { + if is_final { + data.extend_from_slice(&TRAILER); + } + + let before_in = self.decompressor.total_in() as usize; + let mut output = Vec::with_capacity(2 * data.len()); + loop { + let offset = (self.decompressor.total_in() as usize) - before_in; + match self + .decompressor + .decompress_vec(&data[offset..], &mut output, FlushDecompress::None) + .map_err(|e| DeflateError::Decompress(e.into()))? + { + Status::Ok => output.reserve(2 * output.len()), + Status::BufError | Status::StreamEnd => break, + } + } + + if is_final && !self.peer_context_takeover() { + self.decompressor.reset(false); + } + + Ok(output) + } +} + +#[cfg(feature = "handshake")] +fn to_header_value(params: &[HeaderValue]) -> WebsocketExtension { + let mut buf = BytesMut::from(PER_MESSAGE_DEFLATE.as_bytes()); + for param in params { + buf.extend_from_slice(b"; "); + buf.extend_from_slice(param.as_bytes()); + } + let header = HeaderValue::from_maybe_shared(buf.freeze()) + .expect("semicolon separated HeaderValue is valid"); + WebsocketExtension::try_from(header).expect("valid extension") +} diff --git a/src/extensions/compression/mod.rs b/src/extensions/compression/mod.rs new file mode 100644 index 00000000..467640c3 --- /dev/null +++ b/src/extensions/compression/mod.rs @@ -0,0 +1,4 @@ +//! [Per-Message Compression Extensions][rfc7692] +//! +//! [rfc7692]: https://tools.ietf.org/html/rfc7692 +pub mod deflate; diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs new file mode 100644 index 00000000..fe8eb938 --- /dev/null +++ b/src/extensions/mod.rs @@ -0,0 +1,18 @@ +//! WebSocket extensions. +// Only `permessage-deflate` is supported at the moment. + +#[cfg(feature = "deflate")] +mod compression; +#[cfg(feature = "deflate")] +use compression::deflate::DeflateContext; +#[cfg(feature = "deflate")] +pub use compression::deflate::{DeflateConfig, DeflateError}; + +/// Container for configured extensions. +#[derive(Debug, Default)] +#[allow(missing_copy_implementations)] +pub struct Extensions { + // Per-Message Compression. Only `permessage-deflate` is supported. + #[cfg(feature = "deflate")] + pub(crate) compression: Option, +} diff --git a/src/handshake/client.rs b/src/handshake/client.rs index ec719a52..bad8aaeb 100644 --- a/src/handshake/client.rs +++ b/src/handshake/client.rs @@ -5,6 +5,8 @@ use std::{ marker::PhantomData, }; +use crate::handshake::headers::SecWebsocketExtensions; +use headers::HeaderMapExt; use http::{ header::HeaderName, HeaderMap, Request as HttpRequest, Response as HttpResponse, StatusCode, }; @@ -19,6 +21,7 @@ use super::{ }; use crate::{ error::{Error, ProtocolError, Result, SubProtocolError, UrlError}, + extensions::Extensions, protocol::{Role, WebSocket, WebSocketConfig}, }; @@ -58,7 +61,7 @@ impl ClientHandshake { // Convert and verify the `http::Request` and turn it into the request as per RFC. // Also extract the key from it (it must be present in a correct request). - let (request, key) = generate_request(request)?; + let (request, key) = generate_request(request, &config)?; let machine = HandshakeMachine::start_write(stream, request); @@ -89,18 +92,24 @@ impl HandshakeRole for ClientHandshake { ProcessingResult::Continue(HandshakeMachine::start_read(stream)) } StageResult::DoneReading { stream, result, tail } => { - let result = match self.verify_data.verify_response(result) { - Ok(r) => r, - Err(Error::Http(mut e)) => { - *e.body_mut() = Some(tail); - return Err(Error::Http(e)); - } - Err(e) => return Err(e), - }; + let (result, extensions) = + match self.verify_data.verify_response(result, &self.config) { + Ok(r) => r, + Err(Error::Http(mut e)) => { + *e.body_mut() = Some(tail); + return Err(Error::Http(e)); + } + Err(e) => return Err(e), + }; debug!("Client handshake done."); - let websocket = - WebSocket::from_partially_read(stream, tail, Role::Client, self.config); + let websocket = WebSocket::from_partially_read_with_extensions( + stream, + tail, + Role::Client, + self.config, + extensions, + ); ProcessingResult::Done((websocket, result)) } }) @@ -108,7 +117,10 @@ impl HandshakeRole for ClientHandshake { } /// Verifies and generates a client WebSocket request from the original request and extracts a WebSocket key from it. -pub fn generate_request(mut request: Request) -> Result<(Vec, String)> { +pub fn generate_request( + mut request: Request, + config: &Option, +) -> Result<(Vec, String)> { let mut req = Vec::new(); write!( req, @@ -179,6 +191,9 @@ pub fn generate_request(mut request: Request) -> Result<(Vec, String)> { writeln!(req, "{}: {}\r", name, v.to_str()?).unwrap(); } + if let Some(offers) = config.and_then(|c| c.generate_offers()) { + writeln!(req, "Sec-WebSocket-Extensions: {}\r", offers.to_value().to_str()?).unwrap(); + } writeln!(req, "\r").unwrap(); trace!("Request: {:?}", String::from_utf8_lossy(&req)); Ok((req, key)) @@ -203,7 +218,11 @@ struct VerifyData { } impl VerifyData { - pub fn verify_response(&self, response: Response) -> Result { + pub fn verify_response( + &self, + response: Response, + _config: &Option, + ) -> Result<(Response, Option)> { // 1. If the status code received from the server is not 101, the // client handles the response per HTTP [RFC2616] procedures. (RFC 6455) if response.status() != StatusCode::SWITCHING_PROTOCOLS { @@ -248,7 +267,14 @@ impl VerifyData { // that was not present in the client's handshake (the server has // indicated an extension not requested by the client), the client // MUST _Fail the WebSocket Connection_. (RFC 6455) - // TODO + let extensions = if let Some(agreed) = headers + .typed_try_get::() + .map_err(|_| Error::Protocol(ProtocolError::InvalidExtensionsHeader))? + { + verify_extensions(&agreed, _config)? + } else { + None + }; // 6. If the response includes a |Sec-WebSocket-Protocol| header field // and this header field indicates the use of a subprotocol that was @@ -277,10 +303,49 @@ impl VerifyData { } } - Ok(response) + Ok((response, extensions)) } } +fn verify_extensions( + agreed_extensions: &SecWebsocketExtensions, + _config: &Option, +) -> Result> { + #[cfg(feature = "deflate")] + { + if let Some(compression) = _config.and_then(|c| c.compression) { + let mut extensions = None; + for extension in agreed_extensions.iter() { + // > If a server gives an invalid response, such as accepting a PMCE that the client did not offer, + // > the client MUST _Fail the WebSocket Connection_. + if extension.name() != compression.name() { + return Err(Error::Protocol(ProtocolError::InvalidExtension( + extension.name().to_string(), + ))); + } + + // Already had PMCE configured + if extensions.is_some() { + return Err(Error::Protocol(ProtocolError::ExtensionConflict( + extension.name().to_string(), + ))); + } + + extensions = Some(Extensions { + compression: Some(compression.accept_response(extension.params())?), + }); + } + return Ok(extensions); + } + } + + if let Some(extension) = agreed_extensions.iter().next() { + // The client didn't request anything, but got something + return Err(Error::Protocol(ProtocolError::InvalidExtension(extension.name().to_string()))); + } + Ok(None) +} + impl TryParse for Response { fn try_parse(buf: &[u8]) -> Result> { let mut hbuffer = [httparse::EMPTY_HEADER; MAX_HEADERS]; @@ -323,6 +388,8 @@ pub fn generate_key() -> String { mod tests { use super::{super::machine::TryParse, generate_key, generate_request, Response}; use crate::client::IntoClientRequest; + #[cfg(feature = "deflate")] + use crate::{extensions::DeflateConfig, protocol::WebSocketConfig}; #[test] fn random_keys() { @@ -357,7 +424,7 @@ mod tests { #[test] fn request_formatting() { let request = "ws://localhost/getCaseCount".into_client_request().unwrap(); - let (request, key) = generate_request(request).unwrap(); + let (request, key) = generate_request(request, &None).unwrap(); let correct = construct_expected("localhost", &key); assert_eq!(&request[..], &correct[..]); } @@ -365,7 +432,7 @@ mod tests { #[test] fn request_formatting_with_host() { let request = "wss://localhost:9001/getCaseCount".into_client_request().unwrap(); - let (request, key) = generate_request(request).unwrap(); + let (request, key) = generate_request(request, &None).unwrap(); let correct = construct_expected("localhost:9001", &key); assert_eq!(&request[..], &correct[..]); } @@ -373,11 +440,40 @@ mod tests { #[test] fn request_formatting_with_at() { let request = "wss://user:pass@localhost:9001/getCaseCount".into_client_request().unwrap(); - let (request, key) = generate_request(request).unwrap(); + let (request, key) = generate_request(request, &None).unwrap(); let correct = construct_expected("localhost:9001", &key); assert_eq!(&request[..], &correct[..]); } + #[cfg(feature = "deflate")] + #[test] + fn request_with_compression() { + let request = "ws://localhost/getCaseCount".into_client_request().unwrap(); + let (request, key) = generate_request( + request, + &Some(WebSocketConfig { + compression: Some(DeflateConfig::default()), + ..WebSocketConfig::default() + }), + ) + .unwrap(); + let correct = format!( + "\ + GET /getCaseCount HTTP/1.1\r\n\ + Host: {host}\r\n\ + Connection: Upgrade\r\n\ + Upgrade: websocket\r\n\ + Sec-WebSocket-Version: 13\r\n\ + Sec-WebSocket-Key: {key}\r\n\ + Sec-WebSocket-Extensions: permessage-deflate\r\n\ + \r\n", + host = "localhost", + key = key + ) + .into_bytes(); + assert_eq!(&request[..], &correct[..]); + } + #[test] fn response_parsing() { const DATA: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"; @@ -389,6 +485,6 @@ mod tests { #[test] fn invalid_custom_request() { let request = http::Request::builder().method("GET").body(()).unwrap(); - assert!(generate_request(request).is_err()); + assert!(generate_request(request, &None).is_err()); } } diff --git a/src/handshake/headers.rs b/src/handshake/headers/mod.rs similarity index 95% rename from src/handshake/headers.rs rename to src/handshake/headers/mod.rs index f336c65c..ae50c9f1 100644 --- a/src/handshake/headers.rs +++ b/src/handshake/headers/mod.rs @@ -1,5 +1,10 @@ //! HTTP Request and response header handling. +mod sec_websocket_extensions; +mod utils; + +pub use sec_websocket_extensions::{SecWebsocketExtensions, WebsocketExtension}; + use http::header::{HeaderMap, HeaderName, HeaderValue}; use httparse::Status; diff --git a/src/handshake/headers/sec_websocket_extensions.rs b/src/handshake/headers/sec_websocket_extensions.rs new file mode 100644 index 00000000..295eacb7 --- /dev/null +++ b/src/handshake/headers/sec_websocket_extensions.rs @@ -0,0 +1,432 @@ +use std::convert::TryFrom; + +use bytes::BytesMut; +use headers::{Error, Header, HeaderValue}; +use http::{header::SEC_WEBSOCKET_EXTENSIONS, HeaderName}; + +use super::utils::FlatCsv; + +/// `Sec-WebSocket-Extensions` header, defined in [RFC6455][RFC6455_11.3.2] +/// +/// The `Sec-WebSocket-Extensions` header field is used in the WebSocket +/// opening handshake. It is initially sent from the client to the +/// server, and then subsequently sent from the server to the client, to +/// agree on a set of protocol-level extensions to use for the duration +/// of the connection. +/// +/// ## ABNF +/// +/// ```text +/// Sec-WebSocket-Extensions = extension-list +/// extension-list = 1#extension +/// extension = extension-token *( ";" extension-param ) +/// extension-token = registered-token +/// registered-token = token +/// extension-param = token [ "=" (token | quoted-string) ] +/// ;When using the quoted-string syntax variant, the value +/// ;after quoted-string unescaping MUST conform to the +/// ;'token' ABNF. +/// ``` +/// +/// ## Example Values +/// +/// * `permessage-deflate` (defined in [RFC7692][RFC7692_7]) +/// * `permessage-deflate; server_max_window_bits=10` +/// * `permessage-deflate; server_max_window_bits=10, permessage-deflate` +/// +/// ## Example +/// +/// ```rust +/// # use tungstenite::handshake::headers::SecWebsocketExtensions; +/// let extensions = SecWebsocketExtensions::from_static("permessage-deflate"); +/// ``` +/// +/// ## Splitting and Combining +/// +/// Note that `Sec-WebSocket-Extensions` may be split or combined across multiple headers. +/// The following are equivalent: +/// ```text +/// Sec-WebSocket-Extensions: foo +/// Sec-WebSocket-Extensions: bar; baz=2 +/// ``` +/// ```text +/// Sec-WebSocket-Extensions: foo, bar; baz=2 +/// ``` +/// +/// `SecWebsocketExtensions` splits extensions when decoding and combines them into a single +/// value when encoding. +/// +/// [RFC6455_11.3.2]: https://tools.ietf.org/html/rfc6455#section-11.3.2 +/// [RFC7692_7]: https://tools.ietf.org/html/rfc7692#section-7 +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SecWebsocketExtensions(Vec); + +impl Header for SecWebsocketExtensions { + fn name() -> &'static HeaderName { + &SEC_WEBSOCKET_EXTENSIONS + } + + fn decode<'i, I: Iterator>(values: &mut I) -> Result { + let extensions = values + .cloned() + .flat_map(|v| { + FlatCsv::<','>::from(v).iter().map(WebsocketExtension::try_from).collect::>() + }) + .collect::, _>>()?; + if extensions.is_empty() { + Err(Error::invalid()) + } else { + Ok(SecWebsocketExtensions(extensions)) + } + } + + fn encode>(&self, values: &mut E) { + if !self.is_empty() { + values.extend(std::iter::once(self.to_value())); + } + } +} + +impl SecWebsocketExtensions { + /// Construct a `SecWebSocketExtensions` from `Vec`. + pub fn new(extensions: Vec) -> Self { + SecWebsocketExtensions(extensions) + } + + /// Construct a `SecWebSocketExtensions` from a static string. + /// + /// ## Panic + /// + /// Panics if the static string is not a valid extensions valie. + pub fn from_static(s: &'static str) -> Self { + let value = HeaderValue::from_static(s); + SecWebsocketExtensions::try_from(&value).expect("valid static string") + } + + /// Convert this `SecWebsocketExtensions` to a single `HeaderValue`. + pub fn to_value(&self) -> HeaderValue { + let values = self.0.iter().map(HeaderValue::from).collect::(); + HeaderValue::from(values) + } + + /// An iterator over the `WebsocketExtension`s in `SecWebsocketExtensions` header(s). + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Get the number of extensions. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if headers contain no extensions. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl TryFrom<&str> for SecWebsocketExtensions { + type Error = Error; + + fn try_from(value: &str) -> Result { + let value = HeaderValue::from_str(value).map_err(|_| Error::invalid())?; + SecWebsocketExtensions::try_from(&value) + } +} + +impl TryFrom<&HeaderValue> for SecWebsocketExtensions { + type Error = Error; + + fn try_from(value: &HeaderValue) -> Result { + let mut values = std::iter::once(value); + SecWebsocketExtensions::decode(&mut values) + } +} + +/// A WebSocket extension containing the name and parameters. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WebsocketExtension { + name: String, + params: Vec<(String, Option)>, +} + +impl WebsocketExtension { + /// Construct a `WebSocketExtension` from a static string. + /// + /// ## Panics + /// + /// This function panics if the argument is invalid. + pub fn from_static(src: &'static str) -> Self { + WebsocketExtension::try_from(HeaderValue::from_static(src)).expect("valid static value") + } + + /// Get the name of the extension. + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// An iterator over the parameters of this extension. + pub fn params(&self) -> impl Iterator)> { + self.params.iter().map(|(k, v)| (k.as_str(), v.as_ref().map(|v| v.as_str()))) + } +} + +impl TryFrom<&str> for WebsocketExtension { + type Error = Error; + + fn try_from(value: &str) -> Result { + if value.is_empty() { + Err(Error::invalid()) + } else { + let value = HeaderValue::from_str(value).map_err(|_| Error::invalid())?; + WebsocketExtension::try_from(value) + } + } +} + +impl TryFrom for WebsocketExtension { + type Error = Error; + + fn try_from(value: HeaderValue) -> Result { + let csv = FlatCsv::<','>::from(value); + // More than one extension was found + if csv.iter().count() > 1 { + return Err(Error::invalid()); + } + + let params = FlatCsv::<';'>::from(csv.value); + let mut params_iter = params.iter(); + let name = params_iter.next().ok_or_else(Error::invalid).and_then(parse_token)?; + let params = params_iter + .map(|p| { + let mut kv = p.splitn(2, '='); + let key = + kv.next().ok_or_else(Error::invalid).map(str::trim).and_then(parse_token)?; + let val = kv.next().map(str::trim).map(parse_value).transpose()?; + Ok((key, val)) + }) + .collect::, _>>()?; + Ok(WebsocketExtension { name, params }) + } +} + +impl From<&WebsocketExtension> for HeaderValue { + fn from(extension: &WebsocketExtension) -> Self { + let mut buf = BytesMut::from(extension.name.as_bytes()); + for (key, val) in &extension.params { + buf.extend_from_slice(b"; "); + buf.extend_from_slice(key.as_bytes()); + if let Some(val) = val { + buf.extend_from_slice(b"="); + buf.extend_from_slice(val.as_bytes()); + } + } + + HeaderValue::from_maybe_shared(buf.freeze()) + .expect("semicolon separated HeaderValueStrings are valid") + } +} + +fn parse_token(s: &str) -> Result { + if !s.is_empty() && s.chars().all(is_tchar) { + Ok(s.to_owned()) + } else { + Err(Error::invalid()) + } +} + +// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 +fn is_tchar(c: char) -> bool { + matches!( + c, + '!' | '#' | '$' | '%' | '&' | '\'' | '*' | + '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~' | + '0'..='9' | 'a'..='z' | 'A'..='Z' + ) +} + +fn parse_value(s: &str) -> Result { + if let Some(quoted) = s.strip_prefix('"') { + if let Some(val) = quoted.strip_suffix('"') { + parse_token(val) + } else { + // Only had starting double quote + Err(Error::invalid()) + } + } else { + // Not a quoted string. + parse_token(s) + } +} + +#[cfg(test)] +mod tests { + use headers::HeaderMapExt; + use http::HeaderMap; + + use super::*; + + fn test_encode(src: SecWebsocketExtensions) -> HeaderMap { + let mut map = HeaderMap::new(); + map.typed_insert(src); + map + } + + fn test_decode(src: &[&str]) -> Option { + let mut map = HeaderMap::new(); + + for val in src { + map.append(SecWebsocketExtensions::name(), val.parse().unwrap()); + } + + map.typed_get() + } + + #[test] + fn extensions_decode() { + let extensions = test_decode(&["key1; val1", "key2; val2"]).unwrap(); + assert_eq!(extensions.0.len(), 2); + assert_eq!(extensions.0[0], WebsocketExtension::try_from("key1; val1").unwrap()); + assert_eq!(extensions.0[1], WebsocketExtension::try_from("key2; val2").unwrap()); + + assert_eq!(test_decode(&[""]), None); + } + + #[test] + fn extensions_decode_split() { + // Split each extension into separate headers + let extensions = test_decode(&["key1; val1, key2; val2", "key3; val3"]).unwrap(); + assert_eq!(extensions.0.len(), 3); + assert_eq!(extensions.0[0], WebsocketExtension::try_from("key1; val1").unwrap()); + assert_eq!(extensions.0[1], WebsocketExtension::try_from("key2; val2").unwrap()); + assert_eq!(extensions.0[2], WebsocketExtension::try_from("key3; val3").unwrap()); + } + + #[test] + fn extensions_encode() { + let extensions = + SecWebsocketExtensions::new(vec![WebsocketExtension::from_static("foo; bar; baz=1")]); + let headers = test_encode(extensions); + let mut vals = headers.get_all(SEC_WEBSOCKET_EXTENSIONS).into_iter(); + assert_eq!(vals.next().unwrap(), "foo; bar; baz=1"); + assert_eq!(vals.next(), None); + + let extensions = SecWebsocketExtensions::new(vec![]); + let headers = test_encode(extensions); + let mut vals = headers.get_all(SEC_WEBSOCKET_EXTENSIONS).into_iter(); + assert_eq!(vals.next(), None); + } + + #[test] + fn extensions_encode_combine() { + // Multiple extensions are combined into a single header + let extensions = SecWebsocketExtensions::new(vec![ + WebsocketExtension::from_static("foo1; bar"), + WebsocketExtension::from_static("foo2; bar"), + WebsocketExtension::from_static("baz; quux"), + ]); + let headers = test_encode(extensions); + let mut vals = headers.get_all(SEC_WEBSOCKET_EXTENSIONS).into_iter(); + assert_eq!(vals.next().unwrap(), "foo1; bar, foo2; bar, baz; quux"); + assert_eq!(vals.next(), None); + } + + #[test] + fn extensions_iter() { + let extensions = SecWebsocketExtensions::new(vec![ + WebsocketExtension::from_static("foo; bar1; bar2=3"), + WebsocketExtension::from_static("baz; quux"), + ]); + assert_eq!(extensions.len(), 2); + + let mut iter = extensions.iter(); + let extension = iter.next().unwrap(); + assert_eq!(extension.name(), "foo"); + let mut params = extension.params(); + assert_eq!(params.next(), Some(("bar1", None))); + assert_eq!(params.next(), Some(("bar2", Some("3")))); + assert!(params.next().is_none()); + + let extension = iter.next().unwrap(); + assert_eq!(extension.name(), "baz"); + let mut params = extension.params(); + assert_eq!(params.next(), Some(("quux", None))); + assert!(params.next().is_none()); + + assert!(iter.next().is_none()); + } + + #[test] + fn extension_try_from_str_ok() { + let ext = WebsocketExtension::try_from("permessage-deflate").unwrap(); + assert_eq!(ext.name(), "permessage-deflate"); + let mut params = ext.params(); + assert_eq!(params.next(), None); + + let ext = + WebsocketExtension::try_from("permessage-deflate; client_max_window_bits").unwrap(); + assert_eq!(ext.name(), "permessage-deflate"); + let mut params = ext.params(); + assert_eq!(params.next(), Some(("client_max_window_bits", None))); + assert_eq!(params.next(), None); + + let ext = + WebsocketExtension::try_from("permessage-deflate; server_max_window_bits=10").unwrap(); + assert_eq!(ext.name(), "permessage-deflate"); + let mut params = ext.params(); + assert_eq!(params.next(), Some(("server_max_window_bits", Some("10")))); + assert_eq!(params.next(), None); + + let ext = WebsocketExtension::try_from("permessage-deflate; server_max_window_bits=\"10\"") + .unwrap(); + assert_eq!(ext.name(), "permessage-deflate"); + let mut params = ext.params(); + assert_eq!(params.next(), Some(("server_max_window_bits", Some("10")))); + assert_eq!(params.next(), None); + } + + #[test] + fn extension_try_from_str_err() { + assert!(WebsocketExtension::try_from("").is_err()); + // Only single extension is allowed + assert!(WebsocketExtension::try_from("permessage-deflate, permessage-snappy").is_err()); + } + + #[test] + fn parse_value_err() { + #[rustfmt::skip] + let cases = [ + // not token + "", + " ", + // Only starting quote + r#"""#, + r#""10"#, + // Multiple quotes + r#"""1"""#, + // Not a token after removing quotes + r#"" ""#, + r#"",""#, + ]; + for case in cases { + assert!(parse_value(case).is_err()); + } + } + + #[test] + fn parse_value_ok() { + #[rustfmt::skip] + let cases = [ + // Not quoted + r#"1"#, + r#"10"#, + r#"10.1"#, + // valid quoted-string + r#""9""#, + r#""val""#, + ]; + for case in cases { + assert!(parse_value(case).is_ok()); + } + } +} diff --git a/src/handshake/headers/utils.rs b/src/handshake/headers/utils.rs new file mode 100644 index 00000000..c25e880b --- /dev/null +++ b/src/handshake/headers/utils.rs @@ -0,0 +1,81 @@ +use std::iter::{once, FromIterator}; + +use bytes::{BufMut, BytesMut}; +use http::HeaderValue; + +#[derive(Debug, Clone)] +pub(crate) struct FlatCsv { + pub(crate) value: HeaderValue, +} + +impl FlatCsv { + const SEP_BYTES: [u8; 2] = [SEP as u8, b' ']; + + pub(crate) fn iter(&self) -> impl Iterator { + FlatCsvIterator::(self.value.to_str().ok()).map(str::trim) + } +} + +struct FlatCsvIterator<'a, const SEP: char>(Option<&'a str>); + +impl<'a, const SEP: char> Iterator for FlatCsvIterator<'a, SEP> { + type Item = &'a str; + + fn next(&mut self) -> Option { + let str = self.0?; + let mut in_quotes = false; + + for (idx, chr) in str.char_indices() { + if chr == '"' { + in_quotes = !in_quotes; + } + + if !in_quotes && chr == SEP { + self.0 = Some(&str[idx + SEP.len_utf8()..]); + return Some(&str[..idx]); + } + } + + self.0 = None; + Some(str) + } +} + +impl FromIterator for FlatCsv { + fn from_iter>(iter: T) -> Self { + let mut iter = iter.into_iter(); + + let first = match iter.next() { + None => return HeaderValue::from_static("").into(), + Some(first) => first, + }; + + let second = match iter.next() { + None => return first.into(), + Some(second) => second, + }; + + let mut buf = BytesMut::from(first.as_bytes()); + + for value in once(second).chain(iter) { + buf.put(Self::SEP_BYTES.as_ref()); + buf.put(value.as_bytes()); + } + + HeaderValue::from_maybe_shared(buf.freeze()) + .expect("delimited valid header values to be a valid header value") + .into() + } +} + +impl From for FlatCsv { + fn from(value: HeaderValue) -> Self { + Self { value } + } +} + +impl From> for HeaderValue { + fn from(value: FlatCsv) -> Self { + value.value + } +} diff --git a/src/handshake/server.rs b/src/handshake/server.rs index 1303c21f..08f22961 100644 --- a/src/handshake/server.rs +++ b/src/handshake/server.rs @@ -6,6 +6,8 @@ use std::{ result::Result as StdResult, }; +use crate::handshake::headers::SecWebsocketExtensions; +use headers::HeaderMapExt; use http::{ response::Builder, HeaderMap, Request as HttpRequest, Response as HttpResponse, StatusCode, }; @@ -20,6 +22,7 @@ use super::{ }; use crate::{ error::{Error, ProtocolError, Result}, + extensions::Extensions, protocol::{Role, WebSocket, WebSocketConfig}, }; @@ -202,6 +205,8 @@ pub struct ServerHandshake { config: Option, /// Error code/flag. If set, an error will be returned after sending response to the client. error_response: Option, + // Negotiated extension context for server. + extensions: Option, /// Internal stream type. _marker: PhantomData, } @@ -219,6 +224,7 @@ impl ServerHandshake { callback: Some(callback), config, error_response: None, + extensions: None, _marker: PhantomData, }, } @@ -240,7 +246,19 @@ impl HandshakeRole for ServerHandshake { return Err(Error::Protocol(ProtocolError::JunkAfterRequest)); } - let response = create_response(&result)?; + let mut response = create_response(&result)?; + if let Some(config) = &self.config { + if let Some((agreed, extensions)) = result + .headers() + .typed_try_get::() + .map_err(|_| Error::Protocol(ProtocolError::InvalidExtensionsHeader))? + .and_then(|values| config.accept_offers(&values)) + { + response.headers_mut().typed_insert(agreed); + self.extensions = Some(extensions); + } + } + let callback_result = if let Some(callback) = self.callback.take() { callback.on_request(&result, response) } else { @@ -283,7 +301,12 @@ impl HandshakeRole for ServerHandshake { return Err(Error::Http(http::Response::from_parts(parts, body))); } else { debug!("Server handshake done."); - let websocket = WebSocket::from_raw_socket(stream, Role::Server, self.config); + let websocket = WebSocket::from_raw_socket_with_extensions( + stream, + Role::Server, + self.config, + self.extensions.take(), + ); ProcessingResult::Done(websocket) } } diff --git a/src/lib.rs b/src/lib.rs index 6b79d5be..4d48a4ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ pub mod buffer; #[cfg(feature = "handshake")] pub mod client; pub mod error; +pub mod extensions; #[cfg(feature = "handshake")] pub mod handshake; pub mod protocol; diff --git a/src/protocol/frame/frame.rs b/src/protocol/frame/frame.rs index a0ca31bd..54e23ae8 100644 --- a/src/protocol/frame/frame.rs +++ b/src/protocol/frame/frame.rs @@ -315,6 +315,18 @@ impl Frame { Frame { header: FrameHeader { is_final, opcode, ..FrameHeader::default() }, payload: data } } + /// Create a new compressed data frame. + #[inline] + #[cfg(feature = "deflate")] + pub(crate) fn compressed_message(data: Vec, opcode: OpCode, is_final: bool) -> Frame { + debug_assert!(matches!(opcode, OpCode::Data(_)), "Invalid opcode for data frame."); + + Frame { + header: FrameHeader { is_final, opcode, rsv1: true, ..FrameHeader::default() }, + payload: data, + } + } + /// Create a new Pong control frame. #[inline] pub fn pong(data: Vec) -> Frame { diff --git a/src/protocol/message.rs b/src/protocol/message.rs index d71ac109..423e7505 100644 --- a/src/protocol/message.rs +++ b/src/protocol/message.rs @@ -79,6 +79,8 @@ use self::string_collect::StringCollector; #[derive(Debug)] pub struct IncompleteMessage { collector: IncompleteMessageCollector, + #[cfg(feature = "deflate")] + compressed: bool, } #[derive(Debug)] @@ -89,6 +91,7 @@ enum IncompleteMessageCollector { impl IncompleteMessage { /// Create new. + #[cfg(not(feature = "deflate"))] pub fn new(message_type: IncompleteMessageType) -> Self { IncompleteMessage { collector: match message_type { @@ -100,6 +103,25 @@ impl IncompleteMessage { } } + /// Create new. + #[cfg(feature = "deflate")] + pub fn new(message_type: IncompleteMessageType, compressed: bool) -> Self { + IncompleteMessage { + collector: match message_type { + IncompleteMessageType::Binary => IncompleteMessageCollector::Binary(Vec::new()), + IncompleteMessageType::Text => { + IncompleteMessageCollector::Text(StringCollector::new()) + } + }, + compressed, + } + } + + #[cfg(feature = "deflate")] + pub fn compressed(&self) -> bool { + self.compressed + } + /// Get the current filled size of the buffer. pub fn len(&self) -> usize { match self.collector { diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index fb1f7755..e30c3730 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -13,7 +13,12 @@ use self::{ }, message::{IncompleteMessage, IncompleteMessageType}, }; -use crate::error::{Error, ProtocolError, Result}; +#[cfg(feature = "handshake")] +use crate::handshake::headers::SecWebsocketExtensions; +use crate::{ + error::{Error, ProtocolError, Result}, + extensions::Extensions, +}; use log::*; use std::{ io::{self, Read, Write}, @@ -31,6 +36,7 @@ pub enum Role { /// The configuration for WebSocket connection. #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub struct WebSocketConfig { /// Does nothing, instead use `max_write_buffer_size`. #[deprecated] @@ -70,6 +76,9 @@ pub struct WebSocketConfig { /// some popular libraries that are sending unmasked frames, ignoring the RFC. /// By default this option is set to `false`, i.e. according to RFC 6455. pub accept_unmasked_frames: bool, + /// Optional configuration for Per-Message Compression Extension. + #[cfg(feature = "deflate")] + pub compression: Option, } impl Default for WebSocketConfig { @@ -82,6 +91,66 @@ impl Default for WebSocketConfig { max_message_size: Some(64 << 20), max_frame_size: Some(16 << 20), accept_unmasked_frames: false, + #[cfg(feature = "deflate")] + compression: None, + } + } +} + +#[cfg(feature = "handshake")] +impl WebSocketConfig { + // Generate extension negotiation offers for configured extensions. + // Only `permessage-deflate` is supported at the moment. + pub(crate) fn generate_offers(&self) -> Option { + #[cfg(feature = "deflate")] + { + let mut offers = Vec::new(); + if let Some(compression) = self.compression.map(|c| c.generate_offer()) { + offers.push(compression); + } + if offers.is_empty() { + None + } else { + Some(SecWebsocketExtensions::new(offers)) + } + } + #[cfg(not(feature = "deflate"))] + { + None + } + } + + /// Returns negotiation response based on offers and [Extensions] to manage extensions. + /// + /// This can be used with [WebSocket::from_raw_socket_with_extensions] for integration. + pub fn accept_offers( + &self, + #[allow(unused)] offers: &SecWebsocketExtensions, + ) -> Option<(SecWebsocketExtensions, Extensions)> { + #[cfg(feature = "deflate")] + { + // To support more extensions, store extension context in `Extensions` and + // concatenate negotiation responses from each extension. + let mut agreed_extensions = Vec::new(); + let mut extensions = Extensions::default(); + + if let Some(compression) = &self.compression { + if let Some((agreed, compression)) = compression.accept_offer(offers) { + agreed_extensions.push(agreed); + extensions.compression = Some(compression); + } + } + + if agreed_extensions.is_empty() { + None + } else { + Some((SecWebsocketExtensions::new(agreed_extensions), extensions)) + } + } + + #[cfg(not(feature = "deflate"))] + { + None } } } @@ -124,6 +193,18 @@ impl WebSocket { WebSocket { socket: stream, context: WebSocketContext::new(role, config) } } + /// Convert a raw socket into a WebSocket without performing a handshake. + pub fn from_raw_socket_with_extensions( + stream: Stream, + role: Role, + config: Option, + extensions: Option, + ) -> Self { + let mut context = WebSocketContext::new(role, config); + context.extensions = extensions; + WebSocket { socket: stream, context } + } + /// Convert a raw socket into a WebSocket without performing a handshake. /// /// Call this function if you're using Tungstenite as a part of a web framework @@ -144,6 +225,22 @@ impl WebSocket { } } + #[cfg(feature = "handshake")] + pub(crate) fn from_partially_read_with_extensions( + stream: Stream, + part: Vec, + role: Role, + config: Option, + extensions: Option, + ) -> Self { + WebSocket { + socket: stream, + context: WebSocketContext::from_partially_read_with_extensions( + part, role, config, extensions, + ), + } + } + /// Returns a shared reference to the inner stream. pub fn get_ref(&self) -> &Stream { &self.socket @@ -315,6 +412,8 @@ pub struct WebSocketContext { unflushed_additional: bool, /// The configuration for the websocket session. config: WebSocketConfig, + // Container for extensions. + pub(crate) extensions: Option, } impl WebSocketContext { @@ -346,6 +445,21 @@ impl WebSocketContext { additional_send: None, unflushed_additional: false, config, + extensions: None, + } + } + + #[cfg(feature = "handshake")] + pub(crate) fn from_partially_read_with_extensions( + part: Vec, + role: Role, + config: Option, + extensions: Option, + ) -> Self { + WebSocketContext { + frame: FrameCodec::from_partially_read(part), + extensions, + ..WebSocketContext::new(role, config) } } @@ -439,8 +553,8 @@ impl WebSocketContext { } let frame = match message { - Message::Text(data) => Frame::message(data.into(), OpCode::Data(OpData::Text), true), - Message::Binary(data) => Frame::message(data, OpCode::Data(OpData::Binary), true), + Message::Text(data) => self.prepare_data_frame(data.into(), OpData::Text)?, + Message::Binary(data) => self.prepare_data_frame(data, OpData::Binary)?, Message::Ping(data) => Frame::ping(data), Message::Pong(data) => { self.set_additional(Frame::pong(data)); @@ -458,6 +572,17 @@ impl WebSocketContext { Ok(()) } + fn prepare_data_frame(&mut self, data: Vec, opdata: OpData) -> Result { + debug_assert!(matches!(opdata, OpData::Text | OpData::Binary), "Invalid data frame kind"); + let opcode = OpCode::Data(opdata); + let is_final = true; + #[cfg(feature = "deflate")] + if let Some(pmce) = self.extensions.as_mut().and_then(|e| e.compression.as_mut()) { + return Ok(Frame::compressed_message(pmce.compress(&data)?, opcode, is_final)); + } + Ok(Frame::message(data, opcode, is_final)) + } + /// Flush writes. /// /// Ensures all messages previously passed to [`write`](Self::write) and automatically @@ -558,12 +683,14 @@ impl WebSocketContext { // the negotiated extensions defines the meaning of such a nonzero // value, the receiving endpoint MUST _Fail the WebSocket // Connection_. - { + let is_compressed = { let hdr = frame.header(); - if hdr.rsv1 || hdr.rsv2 || hdr.rsv3 { + if (hdr.rsv1 && !self.has_compression()) || hdr.rsv2 || hdr.rsv3 { return Err(Error::Protocol(ProtocolError::NonZeroReservedBits)); } - } + + hdr.rsv1 + }; match self.role { Role::Server => { @@ -598,6 +725,10 @@ impl WebSocketContext { _ if frame.payload().len() > 125 => { Err(Error::Protocol(ProtocolError::ControlFrameTooBig)) } + // Control frames must not have compress bit. + _ if is_compressed => { + Err(Error::Protocol(ProtocolError::CompressedControlFrame)) + } OpCtl::Close => Ok(self.do_close(frame.into_close()?).map(Message::Close)), OpCtl::Reserved(i) => { Err(Error::Protocol(ProtocolError::UnknownControlFrameType(i))) @@ -618,39 +749,34 @@ impl WebSocketContext { let fin = frame.header().is_final; match data { OpData::Continue => { - if let Some(ref mut msg) = self.incomplete { - msg.extend(frame.into_data(), self.config.max_message_size)?; - } else { + if self.incomplete.is_some() && is_compressed { return Err(Error::Protocol( - ProtocolError::UnexpectedContinueFrame, + ProtocolError::CompressedContinueFrame, )); } - if fin { - Ok(Some(self.incomplete.take().unwrap().complete()?)) - } else { - Ok(None) - } + + let msg = self + .incomplete + .take() + .ok_or(Error::Protocol(ProtocolError::UnexpectedContinueFrame))?; + self.extend_incomplete(msg, frame.into_data(), fin) } + c if self.incomplete.is_some() => { Err(Error::Protocol(ProtocolError::ExpectedFragment(c))) } + OpData::Text | OpData::Binary => { - let msg = { - let message_type = match data { - OpData::Text => IncompleteMessageType::Text, - OpData::Binary => IncompleteMessageType::Binary, - _ => panic!("Bug: message is not text nor binary"), - }; - let mut m = IncompleteMessage::new(message_type); - m.extend(frame.into_data(), self.config.max_message_size)?; - m + let message_type = match data { + OpData::Text => IncompleteMessageType::Text, + OpData::Binary => IncompleteMessageType::Binary, + _ => panic!("Bug: message is not text nor binary"), }; - if fin { - Ok(Some(msg.complete()?)) - } else { - self.incomplete = Some(msg); - Ok(None) - } + #[cfg(feature = "deflate")] + let msg = IncompleteMessage::new(message_type, is_compressed); + #[cfg(not(feature = "deflate"))] + let msg = IncompleteMessage::new(message_type); + self.extend_incomplete(msg, frame.into_data(), fin) } OpData::Reserved(i) => { Err(Error::Protocol(ProtocolError::UnknownDataFrameType(i))) @@ -669,6 +795,32 @@ impl WebSocketContext { } } + fn extend_incomplete( + &mut self, + mut msg: IncompleteMessage, + data: Vec, + is_final: bool, + ) -> Result> { + #[cfg(feature = "deflate")] + let data = if msg.compressed() { + // `msg.compressed()` is only true when compression is enabled so it's safe to unwrap + self.extensions + .as_mut() + .and_then(|x| x.compression.as_mut()) + .unwrap() + .decompress(data, is_final)? + } else { + data + }; + msg.extend(data, self.config.max_message_size)?; + if is_final { + Ok(Some(msg.complete()?)) + } else { + self.incomplete = Some(msg); + Ok(None) + } + } + /// Received a close frame. Tells if we need to return a close frame to the user. #[allow(clippy::option_option)] fn do_close<'t>(&mut self, close: Option>) -> Option>> { @@ -735,6 +887,17 @@ impl WebSocketContext { self.additional_send.replace(add); } } + + fn has_compression(&self) -> bool { + #[cfg(feature = "deflate")] + { + self.extensions.as_ref().and_then(|c| c.compression.as_ref()).is_some() + } + #[cfg(not(feature = "deflate"))] + { + false + } + } } /// The current connection state. diff --git a/tests/write.rs b/tests/write.rs index aa627cce..fb8d6e52 100644 --- a/tests/write.rs +++ b/tests/write.rs @@ -34,10 +34,13 @@ fn write_flush_behaviour() { const BATCH_ME_LEN: usize = 11; const WRITE_BUFFER_SIZE: usize = 600; + let mut config = WebSocketConfig::default(); + config.write_buffer_size = WRITE_BUFFER_SIZE; + let mut ws = WebSocket::from_raw_socket( MockWrite::default(), tungstenite::protocol::Role::Server, - Some(WebSocketConfig { write_buffer_size: WRITE_BUFFER_SIZE, ..<_>::default() }), + Some(config), ); assert_eq!(ws.get_ref().written_bytes, 0);