aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHyun Jae Moon <hyunjaemoon@google.com>2023-06-23 23:20:15 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-06-23 23:20:15 +0000
commitd315d70d3ae4bfce47a24a405f0347a2b2d28e14 (patch)
treef2329b8b25bad3fa6cf6503811e26184777a755c
parentacc49cfa7d01df83c20a9b4dc77882a14b044916 (diff)
parentc942d7365f3e74235f8224a5545fe0ff47bb0f0d (diff)
downloadtungstenite-d315d70d3ae4bfce47a24a405f0347a2b2d28e14.tar.gz
Import tungstenite crate am: 5a343be86d am: cb1946636c am: 7e176b2a1d am: 23a62b3adf am: c942d7365f
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/tungstenite/+/2635499 Change-Id: I66a0696367d3740d19250508a916ce1424d64eef Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--Android.bp28
-rw-r--r--CHANGELOG.md68
-rw-r--r--Cargo.lock1501
-rw-r--r--Cargo.toml151
-rw-r--r--Cargo.toml.orig73
l---------LICENSE1
-rw-r--r--LICENSE-APACHE201
-rw-r--r--LICENSE-MIT20
-rw-r--r--METADATA20
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS1
-rw-r--r--README.md86
-rw-r--r--benches/buffer.rs127
-rw-r--r--src/buffer.rs125
-rw-r--r--src/client.rs266
-rw-r--r--src/error.rs282
-rw-r--r--src/handshake/client.rs359
-rw-r--r--src/handshake/headers.rs81
-rw-r--r--src/handshake/machine.rs125
-rw-r--r--src/handshake/mod.rs135
-rw-r--r--src/handshake/server.rs324
-rw-r--r--src/lib.rs48
-rw-r--r--src/protocol/frame/coding.rs291
-rw-r--r--src/protocol/frame/frame.rs477
-rw-r--r--src/protocol/frame/mask.rs73
-rw-r--r--src/protocol/frame/mod.rs279
-rw-r--r--src/protocol/message.rs370
-rw-r--r--src/protocol/mod.rs789
-rw-r--r--src/server.rs68
-rw-r--r--src/stream.rs145
-rw-r--r--src/tls.rs235
-rw-r--r--src/util.rs58
32 files changed, 6807 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..f26a21d
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,28 @@
+// This file is generated by cargo2android.py --run --device --features .
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library {
+ name: "libtungstenite",
+ host_supported: true,
+ crate_name: "tungstenite",
+ cargo_env_compat: true,
+ cargo_pkg_version: "0.19.0",
+ srcs: ["src/lib.rs"],
+ edition: "2018",
+ rustlibs: [
+ "libbyteorder",
+ "libbytes",
+ "liblog_rust",
+ "librand",
+ "libthiserror",
+ "libutf8",
+ ],
+ apex_available: [
+ "//apex_available:platform",
+ "//apex_available:anyapex",
+ ],
+ product_available: true,
+ vendor_available: true,
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0aa0a41
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,68 @@
+# 0.19.0
+
+- Update TLS dependencies.
+- Exchanging `base64` for `data-encoding`.
+
+# 0.18.0
+
+- Make handshake dependencies optional with a new `handshake` feature (now a default one!).
+- Return HTTP error responses (their HTTP body) upon non 101 status codes.
+
+# 0.17.3
+
+- Respect the case-sentitivity of the "Origin" header to keep compatibility with the older servers that use case-sensitive comparison.
+
+# 0.17.2
+
+- Fix panic when invalid manually constructed `http::Request` is passed to `tungstenite`.
+- Downgrade the MSRV to `1.56` due to some other crates that rely on us not being quite ready for `1.58`.
+
+# 0.17.1
+
+- Specify the minimum required Rust version.
+
+# 0.17.0
+
+- Update of dependencies (primarily `sha1`).
+- Add support of the fragmented messages (allow the user to send the frames without composing the full message).
+- Overhaul of the client's request generation process. Now the users are able to pass the constructed `http::Request` "as is" to `tungstenite-rs`, letting the library to check the correctness of the request and specifying their own headers (including its own key if necessary). No changes for those ones who used the client in a normal way by connecting using a URL/URI (most common use-case).
+
+# 0.16.0
+
+- Update of dependencies (primarily `rustls`, `webpki-roots`, `rustls-native-certs`).
+- When the close frame is received, the reply that is automatically sent to the initiator has the same code (so we just echo the frame back). Previously a new close frame was created (i.e. the close code / reason was always the same regardless of what code / reason specified by the initiator). Now it’s more symmetrical and arguably more intuitive behavior (see [#246](https://github.com/snapview/tungstenite-rs/pull/246) for more context).
+- The internal `ReadBuffer` implementation uses heap instead of stack to store the buffer. This should solve issues with possible stack overflows in some scenarios (see [#241](https://github.com/snapview/tungstenite-rs/pull/241) for more context).
+
+# 0.15.0
+
+- Allow selecting the method of loading root certificates if `rustls` is used as TLS implementation.
+ - Two new feature flags `rustls-tls-native-roots` and `rustls-tls-webpki-roots` have been added
+ that activate the respective method to load certificates.
+ - The `rustls-tls` flag was removed to raise awareness of this change. Otherwise, compilation
+ would have continue to work and potential errors (due to different or missing certificates)
+ only occurred at runtime.
+ - The new feature flags are additive. If both are enabled, both methods will be used to add
+ certificates to the TLS configuration.
+- Allow specifying a connector (for more fine-grained configuration of the TLS).
+
+# 0.14.0
+
+- Use `rustls-native-certs` instead of `webpki-root` when `rustls-tls` feature is enabled.
+- Don't use `native-tls` as a default feature (see #202 for more details).
+- New fast and safe implementation of the reading buffer (replacement for the `input_buffer`).
+- Remove some errors from the `Error` enum that can't be triggered anymore with the new buffer implementation.
+
+# 0.13.0
+
+- Add `CapacityError`, `UrlError`, and `ProtocolError` types to represent the different types of capacity, URL, and protocol errors respectively.
+- Modify variants `Error::Capacity`, `Error::Url`, and `Error::Protocol` to hold the above errors types instead of string error messages.
+- Add `handshake::derive_accept_key` to facilitate external handshakes.
+- Add support for `rustls` as TLS backend. The previous `tls` feature flag is now removed in favor
+ of `native-tls` and `rustls-tls`, which allows to pick the TLS backend. The error API surface had
+ to be changed to support the new error types coming from rustls related crates.
+
+# 0.12.0
+
+- Add facilities to allow clients to follow HTTP 3XX redirects.
+- Allow accepting unmasked clients on the server side to be compatible with some legacy / invalid clients.
+- Update of dependencies and documentation fixes.
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..f4f2159
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1501 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "ciborium"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "3.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7"
+dependencies = [
+ "bitflags",
+ "clap_lex",
+ "indexmap",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "criterion"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
+dependencies = [
+ "anes",
+ "atty",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "itertools",
+ "lazy_static",
+ "num-traits",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d"
+dependencies = [
+ "autocfg",
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+ "memoffset",
+ "once_cell",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
+dependencies = [
+ "cfg-if 1.0.0",
+ "once_cell",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ccfd8c0ee4cce11e45b3fd6f9d5e69e0cc62912aa6a0cb1bf4617b0eba5a12f"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
+
+[[package]]
+name = "digest"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "either"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
+
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "half"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "input_buffer"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acee673b88a760f5d1f7b2677a90ab797878282ca36ebd0ed8d560361bee9810"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "js-sys"
+version = "0.3.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.141"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
+
+[[package]]
+name = "oorandom"
+version = "11.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
+
+[[package]]
+name = "openssl"
+version = "0.10.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-src"
+version = "111.22.0+1.1.1q"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "openssl-src",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "plotters"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9428003b84df1496fb9d6eeee9c5f8145cb41ca375eb0dad204328888832811f"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0918736323d1baff32ee0eade54984f6f201ad7e97d5cfb5d6ab4a358529615"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rayon"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d"
+dependencies = [
+ "autocfg",
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3eb76a3b09109e78c52d45979fea3cd8ddaadb223531d0846bedb60e72c3e99"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.100.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "syn"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if 1.0.0",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+
+[[package]]
+name = "thiserror"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tungstenite"
+version = "0.19.0"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "criterion",
+ "data-encoding",
+ "env_logger",
+ "http",
+ "httparse",
+ "input_buffer",
+ "log",
+ "native-tls",
+ "net2",
+ "rand",
+ "rustls",
+ "rustls-native-certs",
+ "sha1",
+ "thiserror",
+ "url",
+ "utf-8",
+ "webpki",
+ "webpki-roots",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
+
+[[package]]
+name = "web-sys"
+version = "0.3.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125"
+dependencies = [
+ "rustls-webpki",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..896a836
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,151 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+rust-version = "1.51"
+name = "tungstenite"
+version = "0.19.0"
+authors = [
+ "Alexey Galakhov",
+ "Daniel Abramov",
+]
+include = [
+ "benches/**/*",
+ "src/**/*",
+ "LICENSE-*",
+ "README.md",
+ "CHANGELOG.md",
+]
+description = "Lightweight stream-based WebSocket implementation"
+homepage = "https://github.com/snapview/tungstenite-rs"
+documentation = "https://docs.rs/tungstenite/0.19.0"
+readme = "README.md"
+keywords = [
+ "websocket",
+ "io",
+ "web",
+]
+categories = [
+ "web-programming::websocket",
+ "network-programming",
+]
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/snapview/tungstenite-rs"
+
+[package.metadata.docs.rs]
+all-features = true
+
+[[bench]]
+name = "buffer"
+harness = false
+
+[dependencies.byteorder]
+version = "1.3.2"
+
+[dependencies.bytes]
+version = "1.0"
+
+[dependencies.data-encoding]
+version = "2"
+optional = true
+
+[dependencies.http]
+version = "0.2"
+optional = true
+
+[dependencies.httparse]
+version = "1.3.4"
+optional = true
+
+[dependencies.log]
+version = "0.4.8"
+
+[dependencies.native-tls-crate]
+version = "0.2.3"
+optional = true
+package = "native-tls"
+
+[dependencies.rand]
+version = "0.8.0"
+
+[dependencies.rustls]
+version = "0.21.0"
+optional = true
+
+[dependencies.rustls-native-certs]
+version = "0.6.0"
+optional = true
+
+[dependencies.sha1]
+version = "0.10"
+optional = true
+
+[dependencies.thiserror]
+version = "1.0.23"
+
+[dependencies.url]
+version = "2.1.0"
+optional = true
+
+[dependencies.utf-8]
+version = "0.7.5"
+
+[dependencies.webpki]
+version = "0.22"
+features = ["std"]
+optional = true
+
+[dependencies.webpki-roots]
+version = "0.23"
+optional = true
+
+[dev-dependencies.criterion]
+version = "0.4.0"
+
+[dev-dependencies.env_logger]
+version = "0.10.0"
+
+[dev-dependencies.input_buffer]
+version = "0.5.0"
+
+[dev-dependencies.net2]
+version = "0.2.37"
+
+[dev-dependencies.rand]
+version = "0.8.4"
+
+[features]
+__rustls-tls = [
+ "rustls",
+ "webpki",
+]
+default = ["handshake"]
+handshake = [
+ "data-encoding",
+ "http",
+ "httparse",
+ "sha1",
+ "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",
+]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..6640559
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,73 @@
+[package]
+name = "tungstenite"
+description = "Lightweight stream-based WebSocket implementation"
+categories = ["web-programming::websocket", "network-programming"]
+keywords = ["websocket", "io", "web"]
+authors = ["Alexey Galakhov", "Daniel Abramov"]
+license = "MIT OR Apache-2.0"
+readme = "README.md"
+homepage = "https://github.com/snapview/tungstenite-rs"
+documentation = "https://docs.rs/tungstenite/0.19.0"
+repository = "https://github.com/snapview/tungstenite-rs"
+version = "0.19.0"
+edition = "2018"
+rust-version = "1.51"
+include = ["benches/**/*", "src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"]
+
+[package.metadata.docs.rs]
+all-features = true
+
+[features]
+default = ["handshake"]
+handshake = ["data-encoding", "http", "httparse", "sha1", "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", "webpki"]
+
+[dependencies]
+data-encoding = { version = "2", optional = true }
+byteorder = "1.3.2"
+bytes = "1.0"
+http = { version = "0.2", optional = true }
+httparse = { version = "1.3.4", optional = true }
+log = "0.4.8"
+rand = "0.8.0"
+sha1 = { version = "0.10", optional = true }
+thiserror = "1.0.23"
+url = { version = "2.1.0", optional = true }
+utf-8 = "0.7.5"
+
+[dependencies.native-tls-crate]
+optional = true
+package = "native-tls"
+version = "0.2.3"
+
+[dependencies.rustls]
+optional = true
+version = "0.21.0"
+
+[dependencies.rustls-native-certs]
+optional = true
+version = "0.6.0"
+
+[dependencies.webpki]
+optional = true
+version = "0.22"
+features = ["std"]
+
+[dependencies.webpki-roots]
+optional = true
+version = "0.23"
+
+[dev-dependencies]
+criterion = "0.4.0"
+env_logger = "0.10.0"
+input_buffer = "0.5.0"
+net2 = "0.2.37"
+rand = "0.8.4"
+
+[[bench]]
+name = "buffer"
+harness = false
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 0000000..6b579aa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+LICENSE-APACHE \ No newline at end of file
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644
index 0000000..16fe87b
--- /dev/null
+++ b/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..dfb98c6
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Alexey Galakhov
+Copyright (c) 2016 Jason Housley
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..5d7d855
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "tungstenite"
+description: "Lightweight stream-based WebSocket implementation"
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://crates.io/crates/tungstenite"
+ }
+ url {
+ type: ARCHIVE
+ value: "https://static.crates.io/crates/tungstenite/tungstenite-0.19.0.crate"
+ }
+ version: "0.19.0"
+ # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same.
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2023
+ month: 6
+ day: 2
+ }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..45dc4dd
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:master:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7e662e4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+# Tungstenite
+
+Lightweight stream-based WebSocket implementation for [Rust](https://www.rust-lang.org/).
+
+```rust
+use std::net::TcpListener;
+use std::thread::spawn;
+use tungstenite::accept;
+
+/// A WebSocket echo server
+fn main () {
+ let server = TcpListener::bind("127.0.0.1:9001").unwrap();
+ for stream in server.incoming() {
+ spawn (move || {
+ let mut websocket = accept(stream.unwrap()).unwrap();
+ loop {
+ let msg = websocket.read_message().unwrap();
+
+ // We do not want to send back ping/pong messages.
+ if msg.is_binary() || msg.is_text() {
+ websocket.write_message(msg).unwrap();
+ }
+ }
+ });
+ }
+}
+```
+
+Take a look at the examples section to see how to write a simple client/server.
+
+**NOTE:** `tungstenite-rs` is more like a barebone to build reliable modern networking applications
+using WebSockets. If you're looking for a modern production-ready "batteries included" WebSocket
+library that allows you to efficiently use non-blocking sockets and do "full-duplex" communication,
+take a look at [`tokio-tungstenite`](https://github.com/snapview/tokio-tungstenite).
+
+[![MIT licensed](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE-MIT)
+[![Apache-2.0 licensed](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE-APACHE)
+[![Crates.io](https://img.shields.io/crates/v/tungstenite.svg?maxAge=2592000)](https://crates.io/crates/tungstenite)
+[![Build Status](https://travis-ci.org/snapview/tungstenite-rs.svg?branch=master)](https://travis-ci.org/snapview/tungstenite-rs)
+
+[Documentation](https://docs.rs/tungstenite)
+
+Introduction
+------------
+This library provides an implementation of WebSockets,
+[RFC6455](https://tools.ietf.org/html/rfc6455). It allows for both synchronous (like TcpStream)
+and asynchronous usage and is easy to integrate into any third-party event loops including
+[MIO](https://github.com/tokio-rs/mio). The API design abstracts away all the internals of the
+WebSocket protocol but still makes them accessible for those who wants full control over the
+network.
+
+Why Tungstenite?
+----------------
+
+It's formerly WS2, the 2nd implementation of WS. WS2 is the chemical formula of
+tungsten disulfide, the tungstenite mineral.
+
+Features
+--------
+
+Tungstenite provides a complete implementation of the WebSocket specification.
+TLS is supported on all platforms using `native-tls` or `rustls`. The following
+features are available:
+
+* `native-tls`
+* `native-tls-vendored`
+* `rustls-tls-native-roots`
+* `rustls-tls-webpki-roots`
+
+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
+-------
+
+Tungstenite is thoroughly tested and passes the [Autobahn Test Suite](https://crossbar.io/autobahn/) for
+WebSockets. It is also covered by internal unit tests as well as possible.
+
+Contributing
+------------
+
+Please report bugs and make feature requests [here](https://github.com/snapview/tungstenite-rs/issues).
diff --git a/benches/buffer.rs b/benches/buffer.rs
new file mode 100644
index 0000000..4f50649
--- /dev/null
+++ b/benches/buffer.rs
@@ -0,0 +1,127 @@
+use std::io::Result as IoResult;
+use std::io::{Cursor, Read};
+
+use bytes::Buf;
+use criterion::*;
+use input_buffer::InputBuffer;
+
+use tungstenite::buffer::ReadBuffer;
+
+const CHUNK_SIZE: usize = 4096;
+
+/// A FIFO buffer for reading packets from the network.
+#[derive(Debug)]
+pub struct StackReadBuffer<const CHUNK_SIZE: usize> {
+ storage: Cursor<Vec<u8>>,
+ chunk: [u8; CHUNK_SIZE],
+}
+
+impl<const CHUNK_SIZE: usize> StackReadBuffer<CHUNK_SIZE> {
+ /// Create a new empty input buffer.
+ pub fn new() -> Self {
+ Self::with_capacity(CHUNK_SIZE)
+ }
+
+ /// Create a new empty input buffer with a given `capacity`.
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self::from_partially_read(Vec::with_capacity(capacity))
+ }
+
+ /// Create a input buffer filled with previously read data.
+ pub fn from_partially_read(part: Vec<u8>) -> Self {
+ Self { storage: Cursor::new(part), chunk: [0; CHUNK_SIZE] }
+ }
+
+ /// Get a cursor to the data storage.
+ pub fn as_cursor(&self) -> &Cursor<Vec<u8>> {
+ &self.storage
+ }
+
+ /// Get a cursor to the mutable data storage.
+ pub fn as_cursor_mut(&mut self) -> &mut Cursor<Vec<u8>> {
+ &mut self.storage
+ }
+
+ /// Consume the `ReadBuffer` and get the internal storage.
+ pub fn into_vec(mut self) -> Vec<u8> {
+ // Current implementation of `tungstenite-rs` expects that the `into_vec()` drains
+ // the data from the container that has already been read by the cursor.
+ self.clean_up();
+
+ // Now we can safely return the internal container.
+ self.storage.into_inner()
+ }
+
+ /// Read next portion of data from the given input stream.
+ pub fn read_from<S: Read>(&mut self, stream: &mut S) -> IoResult<usize> {
+ self.clean_up();
+ let size = stream.read(&mut self.chunk)?;
+ self.storage.get_mut().extend_from_slice(&self.chunk[..size]);
+ Ok(size)
+ }
+
+ /// Cleans ups the part of the vector that has been already read by the cursor.
+ fn clean_up(&mut self) {
+ let pos = self.storage.position() as usize;
+ self.storage.get_mut().drain(0..pos).count();
+ self.storage.set_position(0);
+ }
+}
+
+impl<const CHUNK_SIZE: usize> Buf for StackReadBuffer<CHUNK_SIZE> {
+ fn remaining(&self) -> usize {
+ Buf::remaining(self.as_cursor())
+ }
+
+ fn chunk(&self) -> &[u8] {
+ Buf::chunk(self.as_cursor())
+ }
+
+ fn advance(&mut self, cnt: usize) {
+ Buf::advance(self.as_cursor_mut(), cnt)
+ }
+}
+
+impl<const CHUNK_SIZE: usize> Default for StackReadBuffer<CHUNK_SIZE> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[inline]
+fn input_buffer(mut stream: impl Read) {
+ let mut buffer = InputBuffer::with_capacity(CHUNK_SIZE);
+ while buffer.read_from(&mut stream).unwrap() != 0 {}
+}
+
+#[inline]
+fn stack_read_buffer(mut stream: impl Read) {
+ let mut buffer = StackReadBuffer::<CHUNK_SIZE>::new();
+ while buffer.read_from(&mut stream).unwrap() != 0 {}
+}
+
+#[inline]
+fn heap_read_buffer(mut stream: impl Read) {
+ let mut buffer = ReadBuffer::<CHUNK_SIZE>::new();
+ while buffer.read_from(&mut stream).unwrap() != 0 {}
+}
+
+fn benchmark(c: &mut Criterion) {
+ const STREAM_SIZE: usize = 1024 * 1024 * 4;
+ let data: Vec<u8> = (0..STREAM_SIZE).map(|_| rand::random()).collect();
+ let stream = Cursor::new(data);
+
+ let mut group = c.benchmark_group("buffers");
+ group.throughput(Throughput::Bytes(STREAM_SIZE as u64));
+ group.bench_function("InputBuffer", |b| b.iter(|| input_buffer(black_box(stream.clone()))));
+ group.bench_function("ReadBuffer (stack)", |b| {
+ b.iter(|| stack_read_buffer(black_box(stream.clone())))
+ });
+ group.bench_function("ReadBuffer (heap)", |b| {
+ b.iter(|| heap_read_buffer(black_box(stream.clone())))
+ });
+ group.finish();
+}
+
+criterion_group!(benches, benchmark);
+criterion_main!(benches);
diff --git a/src/buffer.rs b/src/buffer.rs
new file mode 100644
index 0000000..a5e7490
--- /dev/null
+++ b/src/buffer.rs
@@ -0,0 +1,125 @@
+//! A buffer for reading data from the network.
+//!
+//! The `ReadBuffer` is a buffer of bytes similar to a first-in, first-out queue.
+//! It is filled by reading from a stream supporting `Read` and is then
+//! accessible as a cursor for reading bytes.
+
+use std::io::{Cursor, Read, Result as IoResult};
+
+use bytes::Buf;
+
+/// A FIFO buffer for reading packets from the network.
+#[derive(Debug)]
+pub struct ReadBuffer<const CHUNK_SIZE: usize> {
+ storage: Cursor<Vec<u8>>,
+ chunk: Box<[u8; CHUNK_SIZE]>,
+}
+
+impl<const CHUNK_SIZE: usize> ReadBuffer<CHUNK_SIZE> {
+ /// Create a new empty input buffer.
+ pub fn new() -> Self {
+ Self::with_capacity(CHUNK_SIZE)
+ }
+
+ /// Create a new empty input buffer with a given `capacity`.
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self::from_partially_read(Vec::with_capacity(capacity))
+ }
+
+ /// Create a input buffer filled with previously read data.
+ pub fn from_partially_read(part: Vec<u8>) -> Self {
+ Self { storage: Cursor::new(part), chunk: Box::new([0; CHUNK_SIZE]) }
+ }
+
+ /// Get a cursor to the data storage.
+ pub fn as_cursor(&self) -> &Cursor<Vec<u8>> {
+ &self.storage
+ }
+
+ /// Get a cursor to the mutable data storage.
+ pub fn as_cursor_mut(&mut self) -> &mut Cursor<Vec<u8>> {
+ &mut self.storage
+ }
+
+ /// Consume the `ReadBuffer` and get the internal storage.
+ pub fn into_vec(mut self) -> Vec<u8> {
+ // Current implementation of `tungstenite-rs` expects that the `into_vec()` drains
+ // the data from the container that has already been read by the cursor.
+ self.clean_up();
+
+ // Now we can safely return the internal container.
+ self.storage.into_inner()
+ }
+
+ /// Read next portion of data from the given input stream.
+ pub fn read_from<S: Read>(&mut self, stream: &mut S) -> IoResult<usize> {
+ self.clean_up();
+ let size = stream.read(&mut *self.chunk)?;
+ self.storage.get_mut().extend_from_slice(&self.chunk[..size]);
+ Ok(size)
+ }
+
+ /// Cleans ups the part of the vector that has been already read by the cursor.
+ fn clean_up(&mut self) {
+ let pos = self.storage.position() as usize;
+ self.storage.get_mut().drain(0..pos).count();
+ self.storage.set_position(0);
+ }
+}
+
+impl<const CHUNK_SIZE: usize> Buf for ReadBuffer<CHUNK_SIZE> {
+ fn remaining(&self) -> usize {
+ Buf::remaining(self.as_cursor())
+ }
+
+ fn chunk(&self) -> &[u8] {
+ Buf::chunk(self.as_cursor())
+ }
+
+ fn advance(&mut self, cnt: usize) {
+ Buf::advance(self.as_cursor_mut(), cnt)
+ }
+}
+
+impl<const CHUNK_SIZE: usize> Default for ReadBuffer<CHUNK_SIZE> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn simple_reading() {
+ let mut input = Cursor::new(b"Hello World!".to_vec());
+ let mut buffer = ReadBuffer::<4096>::new();
+ let size = buffer.read_from(&mut input).unwrap();
+ assert_eq!(size, 12);
+ assert_eq!(buffer.chunk(), b"Hello World!");
+ }
+
+ #[test]
+ fn reading_in_chunks() {
+ let mut inp = Cursor::new(b"Hello World!".to_vec());
+ let mut buf = ReadBuffer::<4>::new();
+
+ let size = buf.read_from(&mut inp).unwrap();
+ assert_eq!(size, 4);
+ assert_eq!(buf.chunk(), b"Hell");
+
+ buf.advance(2);
+ assert_eq!(buf.chunk(), b"ll");
+ assert_eq!(buf.storage.get_mut(), b"Hell");
+
+ let size = buf.read_from(&mut inp).unwrap();
+ assert_eq!(size, 4);
+ assert_eq!(buf.chunk(), b"llo Wo");
+ assert_eq!(buf.storage.get_mut(), b"llo Wo");
+
+ let size = buf.read_from(&mut inp).unwrap();
+ assert_eq!(size, 4);
+ assert_eq!(buf.chunk(), b"llo World!");
+ }
+}
diff --git a/src/client.rs b/src/client.rs
new file mode 100644
index 0000000..9301939
--- /dev/null
+++ b/src/client.rs
@@ -0,0 +1,266 @@
+//! Methods to connect to a WebSocket as a client.
+
+use std::{
+ io::{Read, Write},
+ net::{SocketAddr, TcpStream, ToSocketAddrs},
+ result::Result as StdResult,
+};
+
+use http::{request::Parts, Uri};
+use log::*;
+
+use url::Url;
+
+use crate::{
+ handshake::client::{generate_key, Request, Response},
+ protocol::WebSocketConfig,
+ stream::MaybeTlsStream,
+};
+
+use crate::{
+ error::{Error, Result, UrlError},
+ handshake::{client::ClientHandshake, HandshakeError},
+ protocol::WebSocket,
+ stream::{Mode, NoDelay},
+};
+
+/// Connect to the given WebSocket in blocking mode.
+///
+/// Uses a websocket configuration passed as an argument to the function. Calling it with `None` is
+/// equal to calling `connect()` function.
+///
+/// The URL may be either ws:// or wss://.
+/// To support wss:// URLs, you must activate the TLS feature on the crate level. Please refer to the
+/// project's [README][readme] for more information on available features.
+///
+/// This function "just works" for those who wants a simple blocking solution
+/// similar to `std::net::TcpStream`. If you want a non-blocking or other
+/// custom stream, call `client` instead.
+///
+/// This function uses `native_tls` or `rustls` to do TLS depending on the feature flags enabled. If
+/// you want to use other TLS libraries, use `client` instead. There is no need to enable any of
+/// the `*-tls` features if you don't call `connect` since it's the only function that uses them.
+///
+/// [readme]: https://github.com/snapview/tungstenite-rs/#features
+pub fn connect_with_config<Req: IntoClientRequest>(
+ request: Req,
+ config: Option<WebSocketConfig>,
+ max_redirects: u8,
+) -> Result<(WebSocket<MaybeTlsStream<TcpStream>>, Response)> {
+ fn try_client_handshake(
+ request: Request,
+ config: Option<WebSocketConfig>,
+ ) -> Result<(WebSocket<MaybeTlsStream<TcpStream>>, Response)> {
+ let uri = request.uri();
+ let mode = uri_mode(uri)?;
+ let host = request.uri().host().ok_or(Error::Url(UrlError::NoHostName))?;
+ let port = uri.port_u16().unwrap_or(match mode {
+ Mode::Plain => 80,
+ Mode::Tls => 443,
+ });
+ let addrs = (host, port).to_socket_addrs()?;
+ let mut stream = connect_to_some(addrs.as_slice(), request.uri())?;
+ NoDelay::set_nodelay(&mut stream, true)?;
+
+ #[cfg(not(any(feature = "native-tls", feature = "__rustls-tls")))]
+ let client = client_with_config(request, MaybeTlsStream::Plain(stream), config);
+ #[cfg(any(feature = "native-tls", feature = "__rustls-tls"))]
+ let client = crate::tls::client_tls_with_config(request, stream, config, None);
+
+ client.map_err(|e| match e {
+ HandshakeError::Failure(f) => f,
+ HandshakeError::Interrupted(_) => panic!("Bug: blocking handshake not blocked"),
+ })
+ }
+
+ fn create_request(parts: &Parts, uri: &Uri) -> Request {
+ let mut builder =
+ Request::builder().uri(uri.clone()).method(parts.method.clone()).version(parts.version);
+ *builder.headers_mut().expect("Failed to create `Request`") = parts.headers.clone();
+ builder.body(()).expect("Failed to create `Request`")
+ }
+
+ let (parts, _) = request.into_client_request()?.into_parts();
+ let mut uri = parts.uri.clone();
+
+ for attempt in 0..(max_redirects + 1) {
+ let request = create_request(&parts, &uri);
+
+ match try_client_handshake(request, config) {
+ Err(Error::Http(res)) if res.status().is_redirection() && attempt < max_redirects => {
+ if let Some(location) = res.headers().get("Location") {
+ uri = location.to_str()?.parse::<Uri>()?;
+ debug!("Redirecting to {:?}", uri);
+ continue;
+ } else {
+ warn!("No `Location` found in redirect");
+ return Err(Error::Http(res));
+ }
+ }
+ other => return other,
+ }
+ }
+
+ unreachable!("Bug in a redirect handling logic")
+}
+
+/// Connect to the given WebSocket in blocking mode.
+///
+/// The URL may be either ws:// or wss://.
+/// To support wss:// URLs, feature `native-tls` or `rustls-tls` must be turned on.
+///
+/// This function "just works" for those who wants a simple blocking solution
+/// similar to `std::net::TcpStream`. If you want a non-blocking or other
+/// custom stream, call `client` instead.
+///
+/// This function uses `native_tls` or `rustls` to do TLS depending on the feature flags enabled. If
+/// you want to use other TLS libraries, use `client` instead. There is no need to enable any of
+/// the `*-tls` features if you don't call `connect` since it's the only function that uses them.
+pub fn connect<Req: IntoClientRequest>(
+ request: Req,
+) -> Result<(WebSocket<MaybeTlsStream<TcpStream>>, Response)> {
+ connect_with_config(request, None, 3)
+}
+
+fn connect_to_some(addrs: &[SocketAddr], uri: &Uri) -> Result<TcpStream> {
+ for addr in addrs {
+ debug!("Trying to contact {} at {}...", uri, addr);
+ if let Ok(stream) = TcpStream::connect(addr) {
+ return Ok(stream);
+ }
+ }
+ Err(Error::Url(UrlError::UnableToConnect(uri.to_string())))
+}
+
+/// Get the mode of the given URL.
+///
+/// This function may be used to ease the creation of custom TLS streams
+/// in non-blocking algorithms or for use with TLS libraries other than `native_tls` or `rustls`.
+pub fn uri_mode(uri: &Uri) -> Result<Mode> {
+ match uri.scheme_str() {
+ Some("ws") => Ok(Mode::Plain),
+ Some("wss") => Ok(Mode::Tls),
+ _ => Err(Error::Url(UrlError::UnsupportedUrlScheme)),
+ }
+}
+
+/// Do the client handshake over the given stream given a web socket configuration. Passing `None`
+/// as configuration is equal to calling `client()` function.
+///
+/// Use this function if you need a nonblocking handshake support or if you
+/// want to use a custom stream like `mio::net::TcpStream` or `openssl::ssl::SslStream`.
+/// Any stream supporting `Read + Write` will do.
+pub fn client_with_config<Stream, Req>(
+ request: Req,
+ stream: Stream,
+ config: Option<WebSocketConfig>,
+) -> StdResult<(WebSocket<Stream>, Response), HandshakeError<ClientHandshake<Stream>>>
+where
+ Stream: Read + Write,
+ Req: IntoClientRequest,
+{
+ ClientHandshake::start(stream, request.into_client_request()?, config)?.handshake()
+}
+
+/// Do the client handshake over the given stream.
+///
+/// Use this function if you need a nonblocking handshake support or if you
+/// want to use a custom stream like `mio::net::TcpStream` or `openssl::ssl::SslStream`.
+/// Any stream supporting `Read + Write` will do.
+pub fn client<Stream, Req>(
+ request: Req,
+ stream: Stream,
+) -> StdResult<(WebSocket<Stream>, Response), HandshakeError<ClientHandshake<Stream>>>
+where
+ Stream: Read + Write,
+ Req: IntoClientRequest,
+{
+ client_with_config(request, stream, None)
+}
+
+/// Trait for converting various types into HTTP requests used for a client connection.
+///
+/// This trait is implemented by default for string slices, strings, `url::Url`, `http::Uri` and
+/// `http::Request<()>`. Note that the implementation for `http::Request<()>` is trivial and will
+/// simply take your request and pass it as is further without altering any headers or URLs, so
+/// be aware of this. If you just want to connect to the endpoint with a certain URL, better pass
+/// a regular string containing the URL in which case `tungstenite-rs` will take care for generating
+/// the proper `http::Request<()>` for you.
+pub trait IntoClientRequest {
+ /// Convert into a `Request` that can be used for a client connection.
+ fn into_client_request(self) -> Result<Request>;
+}
+
+impl<'a> IntoClientRequest for &'a str {
+ fn into_client_request(self) -> Result<Request> {
+ self.parse::<Uri>()?.into_client_request()
+ }
+}
+
+impl<'a> IntoClientRequest for &'a String {
+ fn into_client_request(self) -> Result<Request> {
+ <&str as IntoClientRequest>::into_client_request(self)
+ }
+}
+
+impl IntoClientRequest for String {
+ fn into_client_request(self) -> Result<Request> {
+ <&str as IntoClientRequest>::into_client_request(&self)
+ }
+}
+
+impl<'a> IntoClientRequest for &'a Uri {
+ fn into_client_request(self) -> Result<Request> {
+ self.clone().into_client_request()
+ }
+}
+
+impl IntoClientRequest for Uri {
+ fn into_client_request(self) -> Result<Request> {
+ let authority = self.authority().ok_or(Error::Url(UrlError::NoHostName))?.as_str();
+ let host = authority
+ .find('@')
+ .map(|idx| authority.split_at(idx + 1).1)
+ .unwrap_or_else(|| authority);
+
+ if host.is_empty() {
+ return Err(Error::Url(UrlError::EmptyHostName));
+ }
+
+ let req = Request::builder()
+ .method("GET")
+ .header("Host", host)
+ .header("Connection", "Upgrade")
+ .header("Upgrade", "websocket")
+ .header("Sec-WebSocket-Version", "13")
+ .header("Sec-WebSocket-Key", generate_key())
+ .uri(self)
+ .body(())?;
+ Ok(req)
+ }
+}
+
+impl<'a> IntoClientRequest for &'a Url {
+ fn into_client_request(self) -> Result<Request> {
+ self.as_str().into_client_request()
+ }
+}
+
+impl IntoClientRequest for Url {
+ fn into_client_request(self) -> Result<Request> {
+ self.as_str().into_client_request()
+ }
+}
+
+impl IntoClientRequest for Request {
+ fn into_client_request(self) -> Result<Request> {
+ Ok(self)
+ }
+}
+
+impl<'h, 'b> IntoClientRequest for httparse::Request<'h, 'b> {
+ fn into_client_request(self) -> Result<Request> {
+ use crate::handshake::headers::FromHttparse;
+ Request::from_httparse(self)
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..c830024
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,282 @@
+//! Error handling.
+
+use std::{io, result, str, string};
+
+use crate::protocol::{frame::coding::Data, Message};
+#[cfg(feature = "handshake")]
+use http::{header::HeaderName, Response};
+use thiserror::Error;
+
+/// Result type of all Tungstenite library calls.
+pub type Result<T, E = Error> = result::Result<T, E>;
+
+/// Possible WebSocket errors.
+#[derive(Error, Debug)]
+pub enum Error {
+ /// WebSocket connection closed normally. This informs you of the close.
+ /// It's not an error as such and nothing wrong happened.
+ ///
+ /// This is returned as soon as the close handshake is finished (we have both sent and
+ /// received a close frame) on the server end and as soon as the server has closed the
+ /// underlying connection if this endpoint is a client.
+ ///
+ /// Thus when you receive this, it is safe to drop the underlying connection.
+ ///
+ /// Receiving this error means that the WebSocket object is not usable anymore and the
+ /// only meaningful action with it is dropping it.
+ #[error("Connection closed normally")]
+ ConnectionClosed,
+ /// Trying to work with already closed connection.
+ ///
+ /// Trying to read or write after receiving `ConnectionClosed` causes this.
+ ///
+ /// As opposed to `ConnectionClosed`, this indicates your code tries to operate on the
+ /// connection when it really shouldn't anymore, so this really indicates a programmer
+ /// error on your part.
+ #[error("Trying to work with closed connection")]
+ AlreadyClosed,
+ /// Input-output error. Apart from WouldBlock, these are generally errors with the
+ /// underlying connection and you should probably consider them fatal.
+ #[error("IO error: {0}")]
+ Io(#[from] io::Error),
+ /// TLS error.
+ ///
+ /// Note that this error variant is enabled unconditionally even if no TLS feature is enabled,
+ /// to provide a feature-agnostic API surface.
+ #[error("TLS error: {0}")]
+ Tls(#[from] TlsError),
+ /// - When reading: buffer capacity exhausted.
+ /// - When writing: your message is bigger than the configured max message size
+ /// (64MB by default).
+ #[error("Space limit exceeded: {0}")]
+ Capacity(#[from] CapacityError),
+ /// Protocol violation.
+ #[error("WebSocket protocol error: {0}")]
+ Protocol(#[from] ProtocolError),
+ /// Message send queue full.
+ #[error("Send queue is full")]
+ SendQueueFull(Message),
+ /// UTF coding error.
+ #[error("UTF-8 encoding error")]
+ Utf8,
+ /// Invalid URL.
+ #[error("URL error: {0}")]
+ Url(#[from] UrlError),
+ /// HTTP error.
+ #[error("HTTP error: {}", .0.status())]
+ #[cfg(feature = "handshake")]
+ Http(Response<Option<Vec<u8>>>),
+ /// HTTP format error.
+ #[error("HTTP format error: {0}")]
+ #[cfg(feature = "handshake")]
+ HttpFormat(#[from] http::Error),
+}
+
+impl From<str::Utf8Error> for Error {
+ fn from(_: str::Utf8Error) -> Self {
+ Error::Utf8
+ }
+}
+
+impl From<string::FromUtf8Error> for Error {
+ fn from(_: string::FromUtf8Error) -> Self {
+ Error::Utf8
+ }
+}
+
+#[cfg(feature = "handshake")]
+impl From<http::header::InvalidHeaderValue> for Error {
+ fn from(err: http::header::InvalidHeaderValue) -> Self {
+ Error::HttpFormat(err.into())
+ }
+}
+
+#[cfg(feature = "handshake")]
+impl From<http::header::InvalidHeaderName> for Error {
+ fn from(err: http::header::InvalidHeaderName) -> Self {
+ Error::HttpFormat(err.into())
+ }
+}
+
+#[cfg(feature = "handshake")]
+impl From<http::header::ToStrError> for Error {
+ fn from(_: http::header::ToStrError) -> Self {
+ Error::Utf8
+ }
+}
+
+#[cfg(feature = "handshake")]
+impl From<http::uri::InvalidUri> for Error {
+ fn from(err: http::uri::InvalidUri) -> Self {
+ Error::HttpFormat(err.into())
+ }
+}
+
+#[cfg(feature = "handshake")]
+impl From<http::status::InvalidStatusCode> for Error {
+ fn from(err: http::status::InvalidStatusCode) -> Self {
+ Error::HttpFormat(err.into())
+ }
+}
+
+#[cfg(feature = "handshake")]
+impl From<httparse::Error> for Error {
+ fn from(err: httparse::Error) -> Self {
+ match err {
+ httparse::Error::TooManyHeaders => Error::Capacity(CapacityError::TooManyHeaders),
+ e => Error::Protocol(ProtocolError::HttparseError(e)),
+ }
+ }
+}
+
+/// Indicates the specific type/cause of a capacity error.
+#[derive(Error, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum CapacityError {
+ /// Too many headers provided (see [`httparse::Error::TooManyHeaders`]).
+ #[error("Too many headers")]
+ TooManyHeaders,
+ /// Received header is too long.
+ /// Message is bigger than the maximum allowed size.
+ #[error("Message too long: {size} > {max_size}")]
+ MessageTooLong {
+ /// The size of the message.
+ size: usize,
+ /// The maximum allowed message size.
+ max_size: usize,
+ },
+}
+
+/// Indicates the specific type/cause of a protocol error.
+#[allow(missing_copy_implementations)]
+#[derive(Error, Debug, PartialEq, Eq, Clone)]
+pub enum ProtocolError {
+ /// Use of the wrong HTTP method (the WebSocket protocol requires the GET method be used).
+ #[error("Unsupported HTTP method used - only GET is allowed")]
+ WrongHttpMethod,
+ /// Wrong HTTP version used (the WebSocket protocol requires version 1.1 or higher).
+ #[error("HTTP version must be 1.1 or higher")]
+ WrongHttpVersion,
+ /// Missing `Connection: upgrade` HTTP header.
+ #[error("No \"Connection: upgrade\" header")]
+ MissingConnectionUpgradeHeader,
+ /// Missing `Upgrade: websocket` HTTP header.
+ #[error("No \"Upgrade: websocket\" header")]
+ MissingUpgradeWebSocketHeader,
+ /// Missing `Sec-WebSocket-Version: 13` HTTP header.
+ #[error("No \"Sec-WebSocket-Version: 13\" header")]
+ MissingSecWebSocketVersionHeader,
+ /// Missing `Sec-WebSocket-Key` HTTP header.
+ #[error("No \"Sec-WebSocket-Key\" header")]
+ MissingSecWebSocketKey,
+ /// The `Sec-WebSocket-Accept` header is either not present or does not specify the correct key value.
+ #[error("Key mismatch in \"Sec-WebSocket-Accept\" header")]
+ SecWebSocketAcceptKeyMismatch,
+ /// Garbage data encountered after client request.
+ #[error("Junk after client request")]
+ JunkAfterRequest,
+ /// Custom responses must be unsuccessful.
+ #[error("Custom response must not be successful")]
+ CustomResponseSuccessful,
+ /// Invalid header is passed. Or the header is missing in the request. Or not present at all. Check the request that you pass.
+ #[error("Missing, duplicated or incorrect header {0}")]
+ #[cfg(feature = "handshake")]
+ InvalidHeader(HeaderName),
+ /// No more data while still performing handshake.
+ #[error("Handshake not finished")]
+ HandshakeIncomplete,
+ /// Wrapper around a [`httparse::Error`] value.
+ #[error("httparse error: {0}")]
+ #[cfg(feature = "handshake")]
+ HttparseError(#[from] httparse::Error),
+ /// Not allowed to send after having sent a closing frame.
+ #[error("Sending after closing is not allowed")]
+ SendAfterClosing,
+ /// Remote sent data after sending a closing frame.
+ #[error("Remote sent after having closed")]
+ ReceivedAfterClosing,
+ /// Reserved bits in frame header are non-zero.
+ #[error("Reserved bits are non-zero")]
+ NonZeroReservedBits,
+ /// The server must close the connection when an unmasked frame is received.
+ #[error("Received an unmasked frame from client")]
+ UnmaskedFrameFromClient,
+ /// The client must close the connection when a masked frame is received.
+ #[error("Received a masked frame from server")]
+ MaskedFrameFromServer,
+ /// Control frames must not be fragmented.
+ #[error("Fragmented control frame")]
+ FragmentedControlFrame,
+ /// Control frames must have a payload of 125 bytes or less.
+ #[error("Control frame too big (payload must be 125 bytes or less)")]
+ ControlFrameTooBig,
+ /// Type of control frame not recognised.
+ #[error("Unknown control frame type: {0}")]
+ UnknownControlFrameType(u8),
+ /// Type of data frame not recognised.
+ #[error("Unknown data frame type: {0}")]
+ UnknownDataFrameType(u8),
+ /// Received a continue frame despite there being nothing to continue.
+ #[error("Continue frame but nothing to continue")]
+ UnexpectedContinueFrame,
+ /// Received data while waiting for more fragments.
+ #[error("While waiting for more fragments received: {0}")]
+ ExpectedFragment(Data),
+ /// Connection closed without performing the closing handshake.
+ #[error("Connection reset without closing handshake")]
+ ResetWithoutClosingHandshake,
+ /// Encountered an invalid opcode.
+ #[error("Encountered invalid opcode: {0}")]
+ InvalidOpcode(u8),
+ /// The payload for the closing frame is invalid.
+ #[error("Invalid close sequence")]
+ InvalidCloseSequence,
+}
+
+/// Indicates the specific type/cause of URL error.
+#[derive(Error, Debug, PartialEq, Eq)]
+pub enum UrlError {
+ /// TLS is used despite not being compiled with the TLS feature enabled.
+ #[error("TLS support not compiled in")]
+ TlsFeatureNotEnabled,
+ /// The URL does not include a host name.
+ #[error("No host name in the URL")]
+ NoHostName,
+ /// Failed to connect with this URL.
+ #[error("Unable to connect to {0}")]
+ UnableToConnect(String),
+ /// Unsupported URL scheme used (only `ws://` or `wss://` may be used).
+ #[error("URL scheme not supported")]
+ UnsupportedUrlScheme,
+ /// The URL host name, though included, is empty.
+ #[error("URL contains empty host name")]
+ EmptyHostName,
+ /// The URL does not include a path/query.
+ #[error("No path/query in URL")]
+ NoPathOrQuery,
+}
+
+/// TLS errors.
+///
+/// Note that even if you enable only the rustls-based TLS support, the error at runtime could still
+/// be `Native`, as another crate in the dependency graph may enable native TLS support.
+#[allow(missing_copy_implementations)]
+#[derive(Error, Debug)]
+#[non_exhaustive]
+pub enum TlsError {
+ /// Native TLS error.
+ #[cfg(feature = "native-tls")]
+ #[error("native-tls error: {0}")]
+ Native(#[from] native_tls_crate::Error),
+ /// Rustls error.
+ #[cfg(feature = "__rustls-tls")]
+ #[error("rustls error: {0}")]
+ Rustls(#[from] rustls::Error),
+ /// Webpki error.
+ #[cfg(feature = "__rustls-tls")]
+ #[error("webpki error: {0}")]
+ Webpki(#[from] webpki::Error),
+ /// DNS name resolution error.
+ #[cfg(feature = "__rustls-tls")]
+ #[error("Invalid DNS name")]
+ InvalidDnsName,
+}
diff --git a/src/handshake/client.rs b/src/handshake/client.rs
new file mode 100644
index 0000000..a6d9f1c
--- /dev/null
+++ b/src/handshake/client.rs
@@ -0,0 +1,359 @@
+//! Client handshake machine.
+
+use std::{
+ io::{Read, Write},
+ marker::PhantomData,
+};
+
+use http::{
+ header::HeaderName, HeaderMap, Request as HttpRequest, Response as HttpResponse, StatusCode,
+};
+use httparse::Status;
+use log::*;
+
+use super::{
+ derive_accept_key,
+ headers::{FromHttparse, MAX_HEADERS},
+ machine::{HandshakeMachine, StageResult, TryParse},
+ HandshakeRole, MidHandshake, ProcessingResult,
+};
+use crate::{
+ error::{Error, ProtocolError, Result, UrlError},
+ protocol::{Role, WebSocket, WebSocketConfig},
+};
+
+/// Client request type.
+pub type Request = HttpRequest<()>;
+
+/// Client response type.
+pub type Response = HttpResponse<Option<Vec<u8>>>;
+
+/// Client handshake role.
+#[derive(Debug)]
+pub struct ClientHandshake<S> {
+ verify_data: VerifyData,
+ config: Option<WebSocketConfig>,
+ _marker: PhantomData<S>,
+}
+
+impl<S: Read + Write> ClientHandshake<S> {
+ /// Initiate a client handshake.
+ pub fn start(
+ stream: S,
+ request: Request,
+ config: Option<WebSocketConfig>,
+ ) -> Result<MidHandshake<Self>> {
+ if request.method() != http::Method::GET {
+ return Err(Error::Protocol(ProtocolError::WrongHttpMethod));
+ }
+
+ if request.version() < http::Version::HTTP_11 {
+ return Err(Error::Protocol(ProtocolError::WrongHttpVersion));
+ }
+
+ // Check the URI scheme: only ws or wss are supported
+ let _ = crate::client::uri_mode(request.uri())?;
+
+ // 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 machine = HandshakeMachine::start_write(stream, request);
+
+ let client = {
+ let accept_key = derive_accept_key(key.as_ref());
+ ClientHandshake { verify_data: VerifyData { accept_key }, config, _marker: PhantomData }
+ };
+
+ trace!("Client handshake initiated.");
+ Ok(MidHandshake { role: client, machine })
+ }
+}
+
+impl<S: Read + Write> HandshakeRole for ClientHandshake<S> {
+ type IncomingData = Response;
+ type InternalStream = S;
+ type FinalResult = (WebSocket<S>, Response);
+ fn stage_finished(
+ &mut self,
+ finish: StageResult<Self::IncomingData, Self::InternalStream>,
+ ) -> Result<ProcessingResult<Self::InternalStream, Self::FinalResult>> {
+ Ok(match finish {
+ StageResult::DoneWriting(stream) => {
+ 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),
+ };
+
+ debug!("Client handshake done.");
+ let websocket =
+ WebSocket::from_partially_read(stream, tail, Role::Client, self.config);
+ ProcessingResult::Done((websocket, result))
+ }
+ })
+ }
+}
+
+/// 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<u8>, String)> {
+ let mut req = Vec::new();
+ write!(
+ req,
+ "GET {path} {version:?}\r\n",
+ path = request.uri().path_and_query().ok_or(Error::Url(UrlError::NoPathOrQuery))?.as_str(),
+ version = request.version()
+ )
+ .unwrap();
+
+ // Headers that must be present in a correct request.
+ const KEY_HEADERNAME: &str = "Sec-WebSocket-Key";
+ const WEBSOCKET_HEADERS: [&str; 5] =
+ ["Host", "Connection", "Upgrade", "Sec-WebSocket-Version", KEY_HEADERNAME];
+
+ // We must extract a WebSocket key from a properly formed request or fail if it's not present.
+ let key = request
+ .headers()
+ .get(KEY_HEADERNAME)
+ .ok_or_else(|| {
+ Error::Protocol(ProtocolError::InvalidHeader(
+ HeaderName::from_bytes(KEY_HEADERNAME.as_bytes()).unwrap(),
+ ))
+ })?
+ .to_str()?
+ .to_owned();
+
+ // We must check that all necessary headers for a valid request are present. Note that we have to
+ // deal with the fact that some apps seem to have a case-sensitive check for headers which is not
+ // correct and should not considered the correct behavior, but it seems like some apps ignore it.
+ // `http` by default writes all headers in lower-case which is fine (and does not violate the RFC)
+ // but some servers seem to be poorely written and ignore RFC.
+ //
+ // See similar problem in `hyper`: https://github.com/hyperium/hyper/issues/1492
+ let headers = request.headers_mut();
+ for &header in &WEBSOCKET_HEADERS {
+ let value = headers.remove(header).ok_or_else(|| {
+ Error::Protocol(ProtocolError::InvalidHeader(
+ HeaderName::from_bytes(header.as_bytes()).unwrap(),
+ ))
+ })?;
+ write!(req, "{header}: {value}\r\n", header = header, value = value.to_str()?).unwrap();
+ }
+
+ // Now we must ensure that the headers that we've written once are not anymore present in the map.
+ // If they do, then the request is invalid (some headers are duplicated there for some reason).
+ let insensitive: Vec<String> =
+ WEBSOCKET_HEADERS.iter().map(|h| h.to_ascii_lowercase()).collect();
+ for (k, v) in headers {
+ let mut name = k.as_str();
+
+ // We have already written the necessary headers once (above) and removed them from the map.
+ // If we encounter them again, then the request is considered invalid and error is returned.
+ // Note that we can't use `.contains()`, since `&str` does not coerce to `&String` in Rust.
+ if insensitive.iter().any(|x| x == name) {
+ return Err(Error::Protocol(ProtocolError::InvalidHeader(k.clone())));
+ }
+
+ // Relates to the issue of some servers treating headers in a case-sensitive way, please see:
+ // https://github.com/snapview/tungstenite-rs/pull/119 (original fix of the problem)
+ if name == "sec-websocket-protocol" {
+ name = "Sec-WebSocket-Protocol";
+ }
+
+ if name == "origin" {
+ name = "Origin";
+ }
+
+ writeln!(req, "{}: {}\r", name, v.to_str()?).unwrap();
+ }
+
+ writeln!(req, "\r").unwrap();
+ trace!("Request: {:?}", String::from_utf8_lossy(&req));
+ Ok((req, key))
+}
+
+/// Information for handshake verification.
+#[derive(Debug)]
+struct VerifyData {
+ /// Accepted server key.
+ accept_key: String,
+}
+
+impl VerifyData {
+ pub fn verify_response(&self, response: Response) -> Result<Response> {
+ // 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 {
+ return Err(Error::Http(response));
+ }
+
+ let headers = response.headers();
+
+ // 2. If the response lacks an |Upgrade| header field or the |Upgrade|
+ // header field contains a value that is not an ASCII case-
+ // insensitive match for the value "websocket", the client MUST
+ // _Fail the WebSocket Connection_. (RFC 6455)
+ if !headers
+ .get("Upgrade")
+ .and_then(|h| h.to_str().ok())
+ .map(|h| h.eq_ignore_ascii_case("websocket"))
+ .unwrap_or(false)
+ {
+ return Err(Error::Protocol(ProtocolError::MissingUpgradeWebSocketHeader));
+ }
+ // 3. If the response lacks a |Connection| header field or the
+ // |Connection| header field doesn't contain a token that is an
+ // ASCII case-insensitive match for the value "Upgrade", the client
+ // MUST _Fail the WebSocket Connection_. (RFC 6455)
+ if !headers
+ .get("Connection")
+ .and_then(|h| h.to_str().ok())
+ .map(|h| h.eq_ignore_ascii_case("Upgrade"))
+ .unwrap_or(false)
+ {
+ return Err(Error::Protocol(ProtocolError::MissingConnectionUpgradeHeader));
+ }
+ // 4. If the response lacks a |Sec-WebSocket-Accept| header field or
+ // the |Sec-WebSocket-Accept| contains a value other than the
+ // base64-encoded SHA-1 of ... the client MUST _Fail the WebSocket
+ // Connection_. (RFC 6455)
+ if !headers.get("Sec-WebSocket-Accept").map(|h| h == &self.accept_key).unwrap_or(false) {
+ return Err(Error::Protocol(ProtocolError::SecWebSocketAcceptKeyMismatch));
+ }
+ // 5. If the response includes a |Sec-WebSocket-Extensions| header
+ // field and this header field indicates the use of an extension
+ // 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
+
+ // 6. If the response includes a |Sec-WebSocket-Protocol| header field
+ // and this header field indicates the use of a subprotocol that was
+ // not present in the client's handshake (the server has indicated a
+ // subprotocol not requested by the client), the client MUST _Fail
+ // the WebSocket Connection_. (RFC 6455)
+ // TODO
+
+ Ok(response)
+ }
+}
+
+impl TryParse for Response {
+ fn try_parse(buf: &[u8]) -> Result<Option<(usize, Self)>> {
+ let mut hbuffer = [httparse::EMPTY_HEADER; MAX_HEADERS];
+ let mut req = httparse::Response::new(&mut hbuffer);
+ Ok(match req.parse(buf)? {
+ Status::Partial => None,
+ Status::Complete(size) => Some((size, Response::from_httparse(req)?)),
+ })
+ }
+}
+
+impl<'h, 'b: 'h> FromHttparse<httparse::Response<'h, 'b>> for Response {
+ fn from_httparse(raw: httparse::Response<'h, 'b>) -> Result<Self> {
+ if raw.version.expect("Bug: no HTTP version") < /*1.*/1 {
+ return Err(Error::Protocol(ProtocolError::WrongHttpMethod));
+ }
+
+ let headers = HeaderMap::from_httparse(raw.headers)?;
+
+ let mut response = Response::new(None);
+ *response.status_mut() = StatusCode::from_u16(raw.code.expect("Bug: no HTTP status code"))?;
+ *response.headers_mut() = headers;
+ // TODO: httparse only supports HTTP 0.9/1.0/1.1 but not HTTP 2.0
+ // so the only valid value we could get in the response would be 1.1.
+ *response.version_mut() = http::Version::HTTP_11;
+
+ Ok(response)
+ }
+}
+
+/// Generate a random key for the `Sec-WebSocket-Key` header.
+pub fn generate_key() -> String {
+ // a base64-encoded (see Section 4 of [RFC4648]) value that,
+ // when decoded, is 16 bytes in length (RFC 6455)
+ let r: [u8; 16] = rand::random();
+ data_encoding::BASE64.encode(&r)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{super::machine::TryParse, generate_key, generate_request, Response};
+ use crate::client::IntoClientRequest;
+
+ #[test]
+ fn random_keys() {
+ let k1 = generate_key();
+ println!("Generated random key 1: {}", k1);
+ let k2 = generate_key();
+ println!("Generated random key 2: {}", k2);
+ assert_ne!(k1, k2);
+ assert_eq!(k1.len(), k2.len());
+ assert_eq!(k1.len(), 24);
+ assert_eq!(k2.len(), 24);
+ assert!(k1.ends_with("=="));
+ assert!(k2.ends_with("=="));
+ assert!(k1[..22].find('=').is_none());
+ assert!(k2[..22].find('=').is_none());
+ }
+
+ fn construct_expected(host: &str, key: &str) -> Vec<u8> {
+ 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\
+ \r\n",
+ host = host,
+ key = key
+ )
+ .into_bytes()
+ }
+
+ #[test]
+ fn request_formatting() {
+ let request = "ws://localhost/getCaseCount".into_client_request().unwrap();
+ let (request, key) = generate_request(request).unwrap();
+ let correct = construct_expected("localhost", &key);
+ assert_eq!(&request[..], &correct[..]);
+ }
+
+ #[test]
+ fn request_formatting_with_host() {
+ let request = "wss://localhost:9001/getCaseCount".into_client_request().unwrap();
+ let (request, key) = generate_request(request).unwrap();
+ let correct = construct_expected("localhost:9001", &key);
+ assert_eq!(&request[..], &correct[..]);
+ }
+
+ #[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 correct = construct_expected("localhost:9001", &key);
+ 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";
+ let (_, resp) = Response::try_parse(DATA).unwrap().unwrap();
+ assert_eq!(resp.status(), http::StatusCode::OK);
+ assert_eq!(resp.headers().get("Content-Type").unwrap(), &b"text/html"[..],);
+ }
+
+ #[test]
+ fn invalid_custom_request() {
+ let request = http::Request::builder().method("GET").body(()).unwrap();
+ assert!(generate_request(request).is_err());
+ }
+}
diff --git a/src/handshake/headers.rs b/src/handshake/headers.rs
new file mode 100644
index 0000000..f336c65
--- /dev/null
+++ b/src/handshake/headers.rs
@@ -0,0 +1,81 @@
+//! HTTP Request and response header handling.
+
+use http::header::{HeaderMap, HeaderName, HeaderValue};
+use httparse::Status;
+
+use super::machine::TryParse;
+use crate::error::Result;
+
+/// Limit for the number of header lines.
+pub const MAX_HEADERS: usize = 124;
+
+/// Trait to convert raw objects into HTTP parseables.
+pub(crate) trait FromHttparse<T>: Sized {
+ /// Convert raw object into parsed HTTP headers.
+ fn from_httparse(raw: T) -> Result<Self>;
+}
+
+impl<'b: 'h, 'h> FromHttparse<&'b [httparse::Header<'h>]> for HeaderMap {
+ fn from_httparse(raw: &'b [httparse::Header<'h>]) -> Result<Self> {
+ let mut headers = HeaderMap::new();
+ for h in raw {
+ headers.append(
+ HeaderName::from_bytes(h.name.as_bytes())?,
+ HeaderValue::from_bytes(h.value)?,
+ );
+ }
+
+ Ok(headers)
+ }
+}
+impl TryParse for HeaderMap {
+ fn try_parse(buf: &[u8]) -> Result<Option<(usize, Self)>> {
+ let mut hbuffer = [httparse::EMPTY_HEADER; MAX_HEADERS];
+ Ok(match httparse::parse_headers(buf, &mut hbuffer)? {
+ Status::Partial => None,
+ Status::Complete((size, hdr)) => Some((size, HeaderMap::from_httparse(hdr)?)),
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::{super::machine::TryParse, HeaderMap};
+
+ #[test]
+ fn headers() {
+ const DATA: &[u8] = b"Host: foo.com\r\n\
+ Connection: Upgrade\r\n\
+ Upgrade: websocket\r\n\
+ \r\n";
+ let (_, hdr) = HeaderMap::try_parse(DATA).unwrap().unwrap();
+ assert_eq!(hdr.get("Host").unwrap(), &b"foo.com"[..]);
+ assert_eq!(hdr.get("Upgrade").unwrap(), &b"websocket"[..]);
+ assert_eq!(hdr.get("Connection").unwrap(), &b"Upgrade"[..]);
+ }
+
+ #[test]
+ fn headers_iter() {
+ const DATA: &[u8] = b"Host: foo.com\r\n\
+ Sec-WebSocket-Extensions: permessage-deflate\r\n\
+ Connection: Upgrade\r\n\
+ Sec-WebSocket-ExtenSIONS: permessage-unknown\r\n\
+ Upgrade: websocket\r\n\
+ \r\n";
+ let (_, hdr) = HeaderMap::try_parse(DATA).unwrap().unwrap();
+ let mut iter = hdr.get_all("Sec-WebSocket-Extensions").iter();
+ assert_eq!(iter.next().unwrap(), &b"permessage-deflate"[..]);
+ assert_eq!(iter.next().unwrap(), &b"permessage-unknown"[..]);
+ assert_eq!(iter.next(), None);
+ }
+
+ #[test]
+ fn headers_incomplete() {
+ const DATA: &[u8] = b"Host: foo.com\r\n\
+ Connection: Upgrade\r\n\
+ Upgrade: websocket\r\n";
+ let hdr = HeaderMap::try_parse(DATA).unwrap();
+ assert!(hdr.is_none());
+ }
+}
diff --git a/src/handshake/machine.rs b/src/handshake/machine.rs
new file mode 100644
index 0000000..eacb4bf
--- /dev/null
+++ b/src/handshake/machine.rs
@@ -0,0 +1,125 @@
+//! WebSocket handshake machine.
+
+use bytes::Buf;
+use log::*;
+use std::io::{Cursor, Read, Write};
+
+use crate::{
+ error::{Error, ProtocolError, Result},
+ util::NonBlockingResult,
+ ReadBuffer,
+};
+
+/// A generic handshake state machine.
+#[derive(Debug)]
+pub struct HandshakeMachine<Stream> {
+ stream: Stream,
+ state: HandshakeState,
+}
+
+impl<Stream> HandshakeMachine<Stream> {
+ /// Start reading data from the peer.
+ pub fn start_read(stream: Stream) -> Self {
+ HandshakeMachine { stream, state: HandshakeState::Reading(ReadBuffer::new()) }
+ }
+ /// Start writing data to the peer.
+ pub fn start_write<D: Into<Vec<u8>>>(stream: Stream, data: D) -> Self {
+ HandshakeMachine { stream, state: HandshakeState::Writing(Cursor::new(data.into())) }
+ }
+ /// Returns a shared reference to the inner stream.
+ pub fn get_ref(&self) -> &Stream {
+ &self.stream
+ }
+ /// Returns a mutable reference to the inner stream.
+ pub fn get_mut(&mut self) -> &mut Stream {
+ &mut self.stream
+ }
+}
+
+impl<Stream: Read + Write> HandshakeMachine<Stream> {
+ /// Perform a single handshake round.
+ pub fn single_round<Obj: TryParse>(mut self) -> Result<RoundResult<Obj, Stream>> {
+ trace!("Doing handshake round.");
+ match self.state {
+ HandshakeState::Reading(mut buf) => {
+ let read = buf.read_from(&mut self.stream).no_block()?;
+ match read {
+ Some(0) => Err(Error::Protocol(ProtocolError::HandshakeIncomplete)),
+ Some(_) => Ok(if let Some((size, obj)) = Obj::try_parse(Buf::chunk(&buf))? {
+ buf.advance(size);
+ RoundResult::StageFinished(StageResult::DoneReading {
+ result: obj,
+ stream: self.stream,
+ tail: buf.into_vec(),
+ })
+ } else {
+ RoundResult::Incomplete(HandshakeMachine {
+ state: HandshakeState::Reading(buf),
+ ..self
+ })
+ }),
+ None => Ok(RoundResult::WouldBlock(HandshakeMachine {
+ state: HandshakeState::Reading(buf),
+ ..self
+ })),
+ }
+ }
+ HandshakeState::Writing(mut buf) => {
+ assert!(buf.has_remaining());
+ if let Some(size) = self.stream.write(Buf::chunk(&buf)).no_block()? {
+ assert!(size > 0);
+ buf.advance(size);
+ Ok(if buf.has_remaining() {
+ RoundResult::Incomplete(HandshakeMachine {
+ state: HandshakeState::Writing(buf),
+ ..self
+ })
+ } else {
+ RoundResult::StageFinished(StageResult::DoneWriting(self.stream))
+ })
+ } else {
+ Ok(RoundResult::WouldBlock(HandshakeMachine {
+ state: HandshakeState::Writing(buf),
+ ..self
+ }))
+ }
+ }
+ }
+ }
+}
+
+/// The result of the round.
+#[derive(Debug)]
+pub enum RoundResult<Obj, Stream> {
+ /// Round not done, I/O would block.
+ WouldBlock(HandshakeMachine<Stream>),
+ /// Round done, state unchanged.
+ Incomplete(HandshakeMachine<Stream>),
+ /// Stage complete.
+ StageFinished(StageResult<Obj, Stream>),
+}
+
+/// The result of the stage.
+#[derive(Debug)]
+pub enum StageResult<Obj, Stream> {
+ /// Reading round finished.
+ #[allow(missing_docs)]
+ DoneReading { result: Obj, stream: Stream, tail: Vec<u8> },
+ /// Writing round finished.
+ DoneWriting(Stream),
+}
+
+/// The parseable object.
+pub trait TryParse: Sized {
+ /// Return Ok(None) if incomplete, Err on syntax error.
+ fn try_parse(data: &[u8]) -> Result<Option<(usize, Self)>>;
+}
+
+/// The handshake state.
+#[derive(Debug)]
+enum HandshakeState {
+ /// Reading data from the peer.
+ Reading(ReadBuffer),
+ /// Sending data to the peer.
+ Writing(Cursor<Vec<u8>>),
+}
diff --git a/src/handshake/mod.rs b/src/handshake/mod.rs
new file mode 100644
index 0000000..a8db9a9
--- /dev/null
+++ b/src/handshake/mod.rs
@@ -0,0 +1,135 @@
+//! WebSocket handshake control.
+
+pub mod client;
+pub mod headers;
+pub mod machine;
+pub mod server;
+
+use std::{
+ error::Error as ErrorTrait,
+ fmt,
+ io::{Read, Write},
+};
+
+use sha1::{Digest, Sha1};
+
+use self::machine::{HandshakeMachine, RoundResult, StageResult, TryParse};
+use crate::error::Error;
+
+/// A WebSocket handshake.
+#[derive(Debug)]
+pub struct MidHandshake<Role: HandshakeRole> {
+ role: Role,
+ machine: HandshakeMachine<Role::InternalStream>,
+}
+
+impl<Role: HandshakeRole> MidHandshake<Role> {
+ /// Allow access to machine
+ pub fn get_ref(&self) -> &HandshakeMachine<Role::InternalStream> {
+ &self.machine
+ }
+
+ /// Allow mutable access to machine
+ pub fn get_mut(&mut self) -> &mut HandshakeMachine<Role::InternalStream> {
+ &mut self.machine
+ }
+
+ /// Restarts the handshake process.
+ pub fn handshake(mut self) -> Result<Role::FinalResult, HandshakeError<Role>> {
+ let mut mach = self.machine;
+ loop {
+ mach = match mach.single_round()? {
+ RoundResult::WouldBlock(m) => {
+ return Err(HandshakeError::Interrupted(MidHandshake { machine: m, ..self }))
+ }
+ RoundResult::Incomplete(m) => m,
+ RoundResult::StageFinished(s) => match self.role.stage_finished(s)? {
+ ProcessingResult::Continue(m) => m,
+ ProcessingResult::Done(result) => return Ok(result),
+ },
+ }
+ }
+ }
+}
+
+/// A handshake result.
+pub enum HandshakeError<Role: HandshakeRole> {
+ /// Handshake was interrupted (would block).
+ Interrupted(MidHandshake<Role>),
+ /// Handshake failed.
+ Failure(Error),
+}
+
+impl<Role: HandshakeRole> fmt::Debug for HandshakeError<Role> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ HandshakeError::Interrupted(_) => write!(f, "HandshakeError::Interrupted(...)"),
+ HandshakeError::Failure(ref e) => write!(f, "HandshakeError::Failure({:?})", e),
+ }
+ }
+}
+
+impl<Role: HandshakeRole> fmt::Display for HandshakeError<Role> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ HandshakeError::Interrupted(_) => write!(f, "Interrupted handshake (WouldBlock)"),
+ HandshakeError::Failure(ref e) => write!(f, "{}", e),
+ }
+ }
+}
+
+impl<Role: HandshakeRole> ErrorTrait for HandshakeError<Role> {}
+
+impl<Role: HandshakeRole> From<Error> for HandshakeError<Role> {
+ fn from(err: Error) -> Self {
+ HandshakeError::Failure(err)
+ }
+}
+
+/// Handshake role.
+pub trait HandshakeRole {
+ #[doc(hidden)]
+ type IncomingData: TryParse;
+ #[doc(hidden)]
+ type InternalStream: Read + Write;
+ #[doc(hidden)]
+ type FinalResult;
+ #[doc(hidden)]
+ fn stage_finished(
+ &mut self,
+ finish: StageResult<Self::IncomingData, Self::InternalStream>,
+ ) -> Result<ProcessingResult<Self::InternalStream, Self::FinalResult>, Error>;
+}
+
+/// Stage processing result.
+#[doc(hidden)]
+#[derive(Debug)]
+pub enum ProcessingResult<Stream, FinalResult> {
+ Continue(HandshakeMachine<Stream>),
+ Done(FinalResult),
+}
+
+/// Derive the `Sec-WebSocket-Accept` response header from a `Sec-WebSocket-Key` request header.
+///
+/// This function can be used to perform a handshake before passing a raw TCP stream to
+/// [`WebSocket::from_raw_socket`][crate::protocol::WebSocket::from_raw_socket].
+pub fn derive_accept_key(request_key: &[u8]) -> String {
+ // ... field is constructed by concatenating /key/ ...
+ // ... with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455)
+ const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ let mut sha1 = Sha1::default();
+ sha1.update(request_key);
+ sha1.update(WS_GUID);
+ data_encoding::BASE64.encode(&sha1.finalize())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::derive_accept_key;
+
+ #[test]
+ fn key_conversion() {
+ // example from RFC 6455
+ assert_eq!(derive_accept_key(b"dGhlIHNhbXBsZSBub25jZQ=="), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
+ }
+}
diff --git a/src/handshake/server.rs b/src/handshake/server.rs
new file mode 100644
index 0000000..bc072ce
--- /dev/null
+++ b/src/handshake/server.rs
@@ -0,0 +1,324 @@
+//! Server handshake machine.
+
+use std::{
+ io::{self, Read, Write},
+ marker::PhantomData,
+ result::Result as StdResult,
+};
+
+use http::{
+ response::Builder, HeaderMap, Request as HttpRequest, Response as HttpResponse, StatusCode,
+};
+use httparse::Status;
+use log::*;
+
+use super::{
+ derive_accept_key,
+ headers::{FromHttparse, MAX_HEADERS},
+ machine::{HandshakeMachine, StageResult, TryParse},
+ HandshakeRole, MidHandshake, ProcessingResult,
+};
+use crate::{
+ error::{Error, ProtocolError, Result},
+ protocol::{Role, WebSocket, WebSocketConfig},
+};
+
+/// Server request type.
+pub type Request = HttpRequest<()>;
+
+/// Server response type.
+pub type Response = HttpResponse<()>;
+
+/// Server error response type.
+pub type ErrorResponse = HttpResponse<Option<String>>;
+
+fn create_parts<T>(request: &HttpRequest<T>) -> Result<Builder> {
+ if request.method() != http::Method::GET {
+ return Err(Error::Protocol(ProtocolError::WrongHttpMethod));
+ }
+
+ if request.version() < http::Version::HTTP_11 {
+ return Err(Error::Protocol(ProtocolError::WrongHttpVersion));
+ }
+
+ if !request
+ .headers()
+ .get("Connection")
+ .and_then(|h| h.to_str().ok())
+ .map(|h| h.split(|c| c == ' ' || c == ',').any(|p| p.eq_ignore_ascii_case("Upgrade")))
+ .unwrap_or(false)
+ {
+ return Err(Error::Protocol(ProtocolError::MissingConnectionUpgradeHeader));
+ }
+
+ if !request
+ .headers()
+ .get("Upgrade")
+ .and_then(|h| h.to_str().ok())
+ .map(|h| h.eq_ignore_ascii_case("websocket"))
+ .unwrap_or(false)
+ {
+ return Err(Error::Protocol(ProtocolError::MissingUpgradeWebSocketHeader));
+ }
+
+ if !request.headers().get("Sec-WebSocket-Version").map(|h| h == "13").unwrap_or(false) {
+ return Err(Error::Protocol(ProtocolError::MissingSecWebSocketVersionHeader));
+ }
+
+ let key = request
+ .headers()
+ .get("Sec-WebSocket-Key")
+ .ok_or(Error::Protocol(ProtocolError::MissingSecWebSocketKey))?;
+
+ let builder = Response::builder()
+ .status(StatusCode::SWITCHING_PROTOCOLS)
+ .version(request.version())
+ .header("Connection", "Upgrade")
+ .header("Upgrade", "websocket")
+ .header("Sec-WebSocket-Accept", derive_accept_key(key.as_bytes()));
+
+ Ok(builder)
+}
+
+/// Create a response for the request.
+pub fn create_response(request: &Request) -> Result<Response> {
+ Ok(create_parts(request)?.body(())?)
+}
+
+/// Create a response for the request with a custom body.
+pub fn create_response_with_body<T>(
+ request: &HttpRequest<T>,
+ generate_body: impl FnOnce() -> T,
+) -> Result<HttpResponse<T>> {
+ Ok(create_parts(request)?.body(generate_body())?)
+}
+
+/// Write `response` to the stream `w`.
+pub fn write_response<T>(mut w: impl io::Write, response: &HttpResponse<T>) -> Result<()> {
+ writeln!(
+ w,
+ "{version:?} {status}\r",
+ version = response.version(),
+ status = response.status()
+ )?;
+
+ for (k, v) in response.headers() {
+ writeln!(w, "{}: {}\r", k, v.to_str()?)?;
+ }
+
+ writeln!(w, "\r")?;
+
+ Ok(())
+}
+
+impl TryParse for Request {
+ fn try_parse(buf: &[u8]) -> Result<Option<(usize, Self)>> {
+ let mut hbuffer = [httparse::EMPTY_HEADER; MAX_HEADERS];
+ let mut req = httparse::Request::new(&mut hbuffer);
+ Ok(match req.parse(buf)? {
+ Status::Partial => None,
+ Status::Complete(size) => Some((size, Request::from_httparse(req)?)),
+ })
+ }
+}
+
+impl<'h, 'b: 'h> FromHttparse<httparse::Request<'h, 'b>> for Request {
+ fn from_httparse(raw: httparse::Request<'h, 'b>) -> Result<Self> {
+ if raw.method.expect("Bug: no method in header") != "GET" {
+ return Err(Error::Protocol(ProtocolError::WrongHttpMethod));
+ }
+
+ if raw.version.expect("Bug: no HTTP version") < /*1.*/1 {
+ return Err(Error::Protocol(ProtocolError::WrongHttpVersion));
+ }
+
+ let headers = HeaderMap::from_httparse(raw.headers)?;
+
+ let mut request = Request::new(());
+ *request.method_mut() = http::Method::GET;
+ *request.headers_mut() = headers;
+ *request.uri_mut() = raw.path.expect("Bug: no path in header").parse()?;
+ // TODO: httparse only supports HTTP 0.9/1.0/1.1 but not HTTP 2.0
+ // so the only valid value we could get in the response would be 1.1.
+ *request.version_mut() = http::Version::HTTP_11;
+
+ Ok(request)
+ }
+}
+
+/// The callback trait.
+///
+/// The callback is called when the server receives an incoming WebSocket
+/// handshake request from the client. Specifying a callback allows you to analyze incoming headers
+/// and add additional headers to the response that server sends to the client and/or reject the
+/// connection based on the incoming headers.
+pub trait Callback: Sized {
+ /// Called whenever the server read the request from the client and is ready to reply to it.
+ /// May return additional reply headers.
+ /// Returning an error resulting in rejecting the incoming connection.
+ fn on_request(
+ self,
+ request: &Request,
+ response: Response,
+ ) -> StdResult<Response, ErrorResponse>;
+}
+
+impl<F> Callback for F
+where
+ F: FnOnce(&Request, Response) -> StdResult<Response, ErrorResponse>,
+{
+ fn on_request(
+ self,
+ request: &Request,
+ response: Response,
+ ) -> StdResult<Response, ErrorResponse> {
+ self(request, response)
+ }
+}
+
+/// Stub for callback that does nothing.
+#[derive(Clone, Copy, Debug)]
+pub struct NoCallback;
+
+impl Callback for NoCallback {
+ fn on_request(
+ self,
+ _request: &Request,
+ response: Response,
+ ) -> StdResult<Response, ErrorResponse> {
+ Ok(response)
+ }
+}
+
+/// Server handshake role.
+#[allow(missing_copy_implementations)]
+#[derive(Debug)]
+pub struct ServerHandshake<S, C> {
+ /// Callback which is called whenever the server read the request from the client and is ready
+ /// to reply to it. The callback returns an optional headers which will be added to the reply
+ /// which the server sends to the user.
+ callback: Option<C>,
+ /// WebSocket configuration.
+ config: Option<WebSocketConfig>,
+ /// Error code/flag. If set, an error will be returned after sending response to the client.
+ error_response: Option<ErrorResponse>,
+ /// Internal stream type.
+ _marker: PhantomData<S>,
+}
+
+impl<S: Read + Write, C: Callback> ServerHandshake<S, C> {
+ /// Start server handshake. `callback` specifies a custom callback which the user can pass to
+ /// the handshake, this callback will be called when the a websocket client connects to the
+ /// server, you can specify the callback if you want to add additional header to the client
+ /// upon join based on the incoming headers.
+ pub fn start(stream: S, callback: C, config: Option<WebSocketConfig>) -> MidHandshake<Self> {
+ trace!("Server handshake initiated.");
+ MidHandshake {
+ machine: HandshakeMachine::start_read(stream),
+ role: ServerHandshake {
+ callback: Some(callback),
+ config,
+ error_response: None,
+ _marker: PhantomData,
+ },
+ }
+ }
+}
+
+impl<S: Read + Write, C: Callback> HandshakeRole for ServerHandshake<S, C> {
+ type IncomingData = Request;
+ type InternalStream = S;
+ type FinalResult = WebSocket<S>;
+
+ fn stage_finished(
+ &mut self,
+ finish: StageResult<Self::IncomingData, Self::InternalStream>,
+ ) -> Result<ProcessingResult<Self::InternalStream, Self::FinalResult>> {
+ Ok(match finish {
+ StageResult::DoneReading { stream, result, tail } => {
+ if !tail.is_empty() {
+ return Err(Error::Protocol(ProtocolError::JunkAfterRequest));
+ }
+
+ let response = create_response(&result)?;
+ let callback_result = if let Some(callback) = self.callback.take() {
+ callback.on_request(&result, response)
+ } else {
+ Ok(response)
+ };
+
+ match callback_result {
+ Ok(response) => {
+ let mut output = vec![];
+ write_response(&mut output, &response)?;
+ ProcessingResult::Continue(HandshakeMachine::start_write(stream, output))
+ }
+
+ Err(resp) => {
+ if resp.status().is_success() {
+ return Err(Error::Protocol(ProtocolError::CustomResponseSuccessful));
+ }
+
+ self.error_response = Some(resp);
+ let resp = self.error_response.as_ref().unwrap();
+
+ let mut output = vec![];
+ write_response(&mut output, resp)?;
+
+ if let Some(body) = resp.body() {
+ output.extend_from_slice(body.as_bytes());
+ }
+
+ ProcessingResult::Continue(HandshakeMachine::start_write(stream, output))
+ }
+ }
+ }
+
+ StageResult::DoneWriting(stream) => {
+ if let Some(err) = self.error_response.take() {
+ debug!("Server handshake failed.");
+
+ let (parts, body) = err.into_parts();
+ let body = body.map(|b| b.as_bytes().to_vec());
+ 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);
+ ProcessingResult::Done(websocket)
+ }
+ }
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{super::machine::TryParse, create_response, Request};
+
+ #[test]
+ fn request_parsing() {
+ const DATA: &[u8] = b"GET /script.ws HTTP/1.1\r\nHost: foo.com\r\n\r\n";
+ let (_, req) = Request::try_parse(DATA).unwrap().unwrap();
+ assert_eq!(req.uri().path(), "/script.ws");
+ assert_eq!(req.headers().get("Host").unwrap(), &b"foo.com"[..]);
+ }
+
+ #[test]
+ fn request_replying() {
+ const DATA: &[u8] = b"\
+ GET /script.ws HTTP/1.1\r\n\
+ Host: foo.com\r\n\
+ Connection: upgrade\r\n\
+ Upgrade: websocket\r\n\
+ Sec-WebSocket-Version: 13\r\n\
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
+ \r\n";
+ let (_, req) = Request::try_parse(DATA).unwrap().unwrap();
+ let response = create_response(&req).unwrap();
+
+ assert_eq!(
+ response.headers().get("Sec-WebSocket-Accept").unwrap(),
+ b"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=".as_ref()
+ );
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..ec2f828
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,48 @@
+//! Lightweight, flexible WebSockets for Rust.
+#![deny(
+ missing_docs,
+ missing_copy_implementations,
+ missing_debug_implementations,
+ trivial_casts,
+ trivial_numeric_casts,
+ unstable_features,
+ unused_must_use,
+ unused_mut,
+ unused_imports,
+ unused_import_braces
+)]
+
+#[cfg(feature = "handshake")]
+pub use http;
+
+pub mod buffer;
+#[cfg(feature = "handshake")]
+pub mod client;
+pub mod error;
+#[cfg(feature = "handshake")]
+pub mod handshake;
+pub mod protocol;
+#[cfg(feature = "handshake")]
+mod server;
+pub mod stream;
+#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))]
+mod tls;
+pub mod util;
+
+const READ_BUFFER_CHUNK_SIZE: usize = 4096;
+type ReadBuffer = buffer::ReadBuffer<READ_BUFFER_CHUNK_SIZE>;
+
+pub use crate::{
+ error::{Error, Result},
+ protocol::{Message, WebSocket},
+};
+
+#[cfg(feature = "handshake")]
+pub use crate::{
+ client::{client, connect},
+ handshake::{client::ClientHandshake, server::ServerHandshake, HandshakeError},
+ server::{accept, accept_hdr, accept_hdr_with_config, accept_with_config},
+};
+
+#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))]
+pub use tls::{client_tls, client_tls_with_config, Connector};
diff --git a/src/protocol/frame/coding.rs b/src/protocol/frame/coding.rs
new file mode 100644
index 0000000..827b7ca
--- /dev/null
+++ b/src/protocol/frame/coding.rs
@@ -0,0 +1,291 @@
+//! Various codes defined in RFC 6455.
+
+use std::{
+ convert::{From, Into},
+ fmt,
+};
+
+/// WebSocket message opcode as in RFC 6455.
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum OpCode {
+ /// Data (text or binary).
+ Data(Data),
+ /// Control message (close, ping, pong).
+ Control(Control),
+}
+
+/// Data opcodes as in RFC 6455
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum Data {
+ /// 0x0 denotes a continuation frame
+ Continue,
+ /// 0x1 denotes a text frame
+ Text,
+ /// 0x2 denotes a binary frame
+ Binary,
+ /// 0x3-7 are reserved for further non-control frames
+ Reserved(u8),
+}
+
+/// Control opcodes as in RFC 6455
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum Control {
+ /// 0x8 denotes a connection close
+ Close,
+ /// 0x9 denotes a ping
+ Ping,
+ /// 0xa denotes a pong
+ Pong,
+ /// 0xb-f are reserved for further control frames
+ Reserved(u8),
+}
+
+impl fmt::Display for Data {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ Data::Continue => write!(f, "CONTINUE"),
+ Data::Text => write!(f, "TEXT"),
+ Data::Binary => write!(f, "BINARY"),
+ Data::Reserved(x) => write!(f, "RESERVED_DATA_{}", x),
+ }
+ }
+}
+
+impl fmt::Display for Control {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ Control::Close => write!(f, "CLOSE"),
+ Control::Ping => write!(f, "PING"),
+ Control::Pong => write!(f, "PONG"),
+ Control::Reserved(x) => write!(f, "RESERVED_CONTROL_{}", x),
+ }
+ }
+}
+
+impl fmt::Display for OpCode {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ OpCode::Data(d) => d.fmt(f),
+ OpCode::Control(c) => c.fmt(f),
+ }
+ }
+}
+
+impl From<OpCode> for u8 {
+ fn from(code: OpCode) -> Self {
+ use self::{
+ Control::{Close, Ping, Pong},
+ Data::{Binary, Continue, Text},
+ OpCode::*,
+ };
+ match code {
+ Data(Continue) => 0,
+ Data(Text) => 1,
+ Data(Binary) => 2,
+ Data(self::Data::Reserved(i)) => i,
+
+ Control(Close) => 8,
+ Control(Ping) => 9,
+ Control(Pong) => 10,
+ Control(self::Control::Reserved(i)) => i,
+ }
+ }
+}
+
+impl From<u8> for OpCode {
+ fn from(byte: u8) -> OpCode {
+ use self::{
+ Control::{Close, Ping, Pong},
+ Data::{Binary, Continue, Text},
+ OpCode::*,
+ };
+ match byte {
+ 0 => Data(Continue),
+ 1 => Data(Text),
+ 2 => Data(Binary),
+ i @ 3..=7 => Data(self::Data::Reserved(i)),
+ 8 => Control(Close),
+ 9 => Control(Ping),
+ 10 => Control(Pong),
+ i @ 11..=15 => Control(self::Control::Reserved(i)),
+ _ => panic!("Bug: OpCode out of range"),
+ }
+ }
+}
+
+use self::CloseCode::*;
+/// Status code used to indicate why an endpoint is closing the WebSocket connection.
+#[derive(Debug, Eq, PartialEq, Clone, Copy)]
+pub enum CloseCode {
+ /// Indicates a normal closure, meaning that the purpose for
+ /// which the connection was established has been fulfilled.
+ Normal,
+ /// Indicates that an endpoint is "going away", such as a server
+ /// going down or a browser having navigated away from a page.
+ Away,
+ /// Indicates that an endpoint is terminating the connection due
+ /// to a protocol error.
+ Protocol,
+ /// Indicates that an endpoint is terminating the connection
+ /// because it has received a type of data it cannot accept (e.g., an
+ /// endpoint that understands only text data MAY send this if it
+ /// receives a binary message).
+ Unsupported,
+ /// Indicates that no status code was included in a closing frame. This
+ /// close code makes it possible to use a single method, `on_close` to
+ /// handle even cases where no close code was provided.
+ Status,
+ /// Indicates an abnormal closure. If the abnormal closure was due to an
+ /// error, this close code will not be used. Instead, the `on_error` method
+ /// of the handler will be called with the error. However, if the connection
+ /// is simply dropped, without an error, this close code will be sent to the
+ /// handler.
+ Abnormal,
+ /// Indicates that an endpoint is terminating the connection
+ /// because it has received data within a message that was not
+ /// consistent with the type of the message (e.g., non-UTF-8 \[RFC3629\]
+ /// data within a text message).
+ Invalid,
+ /// Indicates that an endpoint is terminating the connection
+ /// because it has received a message that violates its policy. This
+ /// is a generic status code that can be returned when there is no
+ /// other more suitable status code (e.g., Unsupported or Size) or if there
+ /// is a need to hide specific details about the policy.
+ Policy,
+ /// Indicates that an endpoint is terminating the connection
+ /// because it has received a message that is too big for it to
+ /// process.
+ Size,
+ /// Indicates that an endpoint (client) is terminating the
+ /// connection because it has expected the server to negotiate one or
+ /// more extension, but the server didn't return them in the response
+ /// message of the WebSocket handshake. The list of extensions that
+ /// are needed should be given as the reason for closing.
+ /// Note that this status code is not used by the server, because it
+ /// can fail the WebSocket handshake instead.
+ Extension,
+ /// Indicates that a server is terminating the connection because
+ /// it encountered an unexpected condition that prevented it from
+ /// fulfilling the request.
+ Error,
+ /// Indicates that the server is restarting. A client may choose to reconnect,
+ /// and if it does, it should use a randomized delay of 5-30 seconds between attempts.
+ Restart,
+ /// Indicates that the server is overloaded and the client should either connect
+ /// to a different IP (when multiple targets exist), or reconnect to the same IP
+ /// when a user has performed an action.
+ Again,
+ #[doc(hidden)]
+ Tls,
+ #[doc(hidden)]
+ Reserved(u16),
+ #[doc(hidden)]
+ Iana(u16),
+ #[doc(hidden)]
+ Library(u16),
+ #[doc(hidden)]
+ Bad(u16),
+}
+
+impl CloseCode {
+ /// Check if this CloseCode is allowed.
+ pub fn is_allowed(self) -> bool {
+ !matches!(self, Bad(_) | Reserved(_) | Status | Abnormal | Tls)
+ }
+}
+
+impl fmt::Display for CloseCode {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let code: u16 = self.into();
+ write!(f, "{}", code)
+ }
+}
+
+impl From<CloseCode> for u16 {
+ fn from(code: CloseCode) -> u16 {
+ match code {
+ Normal => 1000,
+ Away => 1001,
+ Protocol => 1002,
+ Unsupported => 1003,
+ Status => 1005,
+ Abnormal => 1006,
+ Invalid => 1007,
+ Policy => 1008,
+ Size => 1009,
+ Extension => 1010,
+ Error => 1011,
+ Restart => 1012,
+ Again => 1013,
+ Tls => 1015,
+ Reserved(code) => code,
+ Iana(code) => code,
+ Library(code) => code,
+ Bad(code) => code,
+ }
+ }
+}
+
+impl<'t> From<&'t CloseCode> for u16 {
+ fn from(code: &'t CloseCode) -> u16 {
+ (*code).into()
+ }
+}
+
+impl From<u16> for CloseCode {
+ fn from(code: u16) -> CloseCode {
+ match code {
+ 1000 => Normal,
+ 1001 => Away,
+ 1002 => Protocol,
+ 1003 => Unsupported,
+ 1005 => Status,
+ 1006 => Abnormal,
+ 1007 => Invalid,
+ 1008 => Policy,
+ 1009 => Size,
+ 1010 => Extension,
+ 1011 => Error,
+ 1012 => Restart,
+ 1013 => Again,
+ 1015 => Tls,
+ 1..=999 => Bad(code),
+ 1016..=2999 => Reserved(code),
+ 3000..=3999 => Iana(code),
+ 4000..=4999 => Library(code),
+ _ => Bad(code),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn opcode_from_u8() {
+ let byte = 2u8;
+ assert_eq!(OpCode::from(byte), OpCode::Data(Data::Binary));
+ }
+
+ #[test]
+ fn opcode_into_u8() {
+ let text = OpCode::Data(Data::Text);
+ let byte: u8 = text.into();
+ assert_eq!(byte, 1u8);
+ }
+
+ #[test]
+ fn closecode_from_u16() {
+ let byte = 1008u16;
+ assert_eq!(CloseCode::from(byte), CloseCode::Policy);
+ }
+
+ #[test]
+ fn closecode_into_u16() {
+ let text = CloseCode::Away;
+ let byte: u16 = text.into();
+ assert_eq!(byte, 1001u16);
+ assert_eq!(u16::from(text), 1001u16);
+ }
+}
diff --git a/src/protocol/frame/frame.rs b/src/protocol/frame/frame.rs
new file mode 100644
index 0000000..08def34
--- /dev/null
+++ b/src/protocol/frame/frame.rs
@@ -0,0 +1,477 @@
+use byteorder::{ByteOrder, NetworkEndian, ReadBytesExt, WriteBytesExt};
+use log::*;
+use std::{
+ borrow::Cow,
+ default::Default,
+ fmt,
+ io::{Cursor, ErrorKind, Read, Write},
+ result::Result as StdResult,
+ str::Utf8Error,
+ string::{FromUtf8Error, String},
+};
+
+use super::{
+ coding::{CloseCode, Control, Data, OpCode},
+ mask::{apply_mask, generate_mask},
+};
+use crate::error::{Error, ProtocolError, Result};
+
+/// A struct representing the close command.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct CloseFrame<'t> {
+ /// The reason as a code.
+ pub code: CloseCode,
+ /// The reason as text string.
+ pub reason: Cow<'t, str>,
+}
+
+impl<'t> CloseFrame<'t> {
+ /// Convert into a owned string.
+ pub fn into_owned(self) -> CloseFrame<'static> {
+ CloseFrame { code: self.code, reason: self.reason.into_owned().into() }
+ }
+}
+
+impl<'t> fmt::Display for CloseFrame<'t> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{} ({})", self.reason, self.code)
+ }
+}
+
+/// A struct representing a WebSocket frame header.
+#[allow(missing_copy_implementations)]
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct FrameHeader {
+ /// Indicates that the frame is the last one of a possibly fragmented message.
+ pub is_final: bool,
+ /// Reserved for protocol extensions.
+ pub rsv1: bool,
+ /// Reserved for protocol extensions.
+ pub rsv2: bool,
+ /// Reserved for protocol extensions.
+ pub rsv3: bool,
+ /// WebSocket protocol opcode.
+ pub opcode: OpCode,
+ /// A frame mask, if any.
+ pub mask: Option<[u8; 4]>,
+}
+
+impl Default for FrameHeader {
+ fn default() -> Self {
+ FrameHeader {
+ is_final: true,
+ rsv1: false,
+ rsv2: false,
+ rsv3: false,
+ opcode: OpCode::Control(Control::Close),
+ mask: None,
+ }
+ }
+}
+
+impl FrameHeader {
+ /// Parse a header from an input stream.
+ /// Returns `None` if insufficient data and does not consume anything in this case.
+ /// Payload size is returned along with the header.
+ pub fn parse(cursor: &mut Cursor<impl AsRef<[u8]>>) -> Result<Option<(Self, u64)>> {
+ let initial = cursor.position();
+ match Self::parse_internal(cursor) {
+ ret @ Ok(None) => {
+ cursor.set_position(initial);
+ ret
+ }
+ ret => ret,
+ }
+ }
+
+ /// Get the size of the header formatted with given payload length.
+ #[allow(clippy::len_without_is_empty)]
+ pub fn len(&self, length: u64) -> usize {
+ 2 + LengthFormat::for_length(length).extra_bytes() + if self.mask.is_some() { 4 } else { 0 }
+ }
+
+ /// Format a header for given payload size.
+ pub fn format(&self, length: u64, output: &mut impl Write) -> Result<()> {
+ let code: u8 = self.opcode.into();
+
+ let one = {
+ code | if self.is_final { 0x80 } else { 0 }
+ | if self.rsv1 { 0x40 } else { 0 }
+ | if self.rsv2 { 0x20 } else { 0 }
+ | if self.rsv3 { 0x10 } else { 0 }
+ };
+
+ let lenfmt = LengthFormat::for_length(length);
+
+ let two = { lenfmt.length_byte() | if self.mask.is_some() { 0x80 } else { 0 } };
+
+ output.write_all(&[one, two])?;
+ match lenfmt {
+ LengthFormat::U8(_) => (),
+ LengthFormat::U16 => output.write_u16::<NetworkEndian>(length as u16)?,
+ LengthFormat::U64 => output.write_u64::<NetworkEndian>(length)?,
+ }
+
+ if let Some(ref mask) = self.mask {
+ output.write_all(mask)?
+ }
+
+ Ok(())
+ }
+
+ /// Generate a random frame mask and store this in the header.
+ ///
+ /// Of course this does not change frame contents. It just generates a mask.
+ pub(crate) fn set_random_mask(&mut self) {
+ self.mask = Some(generate_mask())
+ }
+}
+
+impl FrameHeader {
+ /// Internal parse engine.
+ /// Returns `None` if insufficient data.
+ /// Payload size is returned along with the header.
+ fn parse_internal(cursor: &mut impl Read) -> Result<Option<(Self, u64)>> {
+ let (first, second) = {
+ let mut head = [0u8; 2];
+ if cursor.read(&mut head)? != 2 {
+ return Ok(None);
+ }
+ trace!("Parsed headers {:?}", head);
+ (head[0], head[1])
+ };
+
+ trace!("First: {:b}", first);
+ trace!("Second: {:b}", second);
+
+ let is_final = first & 0x80 != 0;
+
+ let rsv1 = first & 0x40 != 0;
+ let rsv2 = first & 0x20 != 0;
+ let rsv3 = first & 0x10 != 0;
+
+ let opcode = OpCode::from(first & 0x0F);
+ trace!("Opcode: {:?}", opcode);
+
+ let masked = second & 0x80 != 0;
+ trace!("Masked: {:?}", masked);
+
+ let length = {
+ let length_byte = second & 0x7F;
+ let length_length = LengthFormat::for_byte(length_byte).extra_bytes();
+ if length_length > 0 {
+ match cursor.read_uint::<NetworkEndian>(length_length) {
+ Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => {
+ return Ok(None);
+ }
+ Err(err) => {
+ return Err(err.into());
+ }
+ Ok(read) => read,
+ }
+ } else {
+ u64::from(length_byte)
+ }
+ };
+
+ let mask = if masked {
+ let mut mask_bytes = [0u8; 4];
+ if cursor.read(&mut mask_bytes)? != 4 {
+ return Ok(None);
+ } else {
+ Some(mask_bytes)
+ }
+ } else {
+ None
+ };
+
+ // Disallow bad opcode
+ match opcode {
+ OpCode::Control(Control::Reserved(_)) | OpCode::Data(Data::Reserved(_)) => {
+ return Err(Error::Protocol(ProtocolError::InvalidOpcode(first & 0x0F)))
+ }
+ _ => (),
+ }
+
+ let hdr = FrameHeader { is_final, rsv1, rsv2, rsv3, opcode, mask };
+
+ Ok(Some((hdr, length)))
+ }
+}
+
+/// A struct representing a WebSocket frame.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Frame {
+ header: FrameHeader,
+ payload: Vec<u8>,
+}
+
+impl Frame {
+ /// Get the length of the frame.
+ /// This is the length of the header + the length of the payload.
+ #[inline]
+ pub fn len(&self) -> usize {
+ let length = self.payload.len();
+ self.header.len(length as u64) + length
+ }
+
+ /// Check if the frame is empty.
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Get a reference to the frame's header.
+ #[inline]
+ pub fn header(&self) -> &FrameHeader {
+ &self.header
+ }
+
+ /// Get a mutable reference to the frame's header.
+ #[inline]
+ pub fn header_mut(&mut self) -> &mut FrameHeader {
+ &mut self.header
+ }
+
+ /// Get a reference to the frame's payload.
+ #[inline]
+ pub fn payload(&self) -> &Vec<u8> {
+ &self.payload
+ }
+
+ /// Get a mutable reference to the frame's payload.
+ #[inline]
+ pub fn payload_mut(&mut self) -> &mut Vec<u8> {
+ &mut self.payload
+ }
+
+ /// Test whether the frame is masked.
+ #[inline]
+ pub(crate) fn is_masked(&self) -> bool {
+ self.header.mask.is_some()
+ }
+
+ /// Generate a random mask for the frame.
+ ///
+ /// This just generates a mask, payload is not changed. The actual masking is performed
+ /// either on `format()` or on `apply_mask()` call.
+ #[inline]
+ pub(crate) fn set_random_mask(&mut self) {
+ self.header.set_random_mask()
+ }
+
+ /// This method unmasks the payload and should only be called on frames that are actually
+ /// masked. In other words, those frames that have just been received from a client endpoint.
+ #[inline]
+ pub(crate) fn apply_mask(&mut self) {
+ if let Some(mask) = self.header.mask.take() {
+ apply_mask(&mut self.payload, mask)
+ }
+ }
+
+ /// Consume the frame into its payload as binary.
+ #[inline]
+ pub fn into_data(self) -> Vec<u8> {
+ self.payload
+ }
+
+ /// Consume the frame into its payload as string.
+ #[inline]
+ pub fn into_string(self) -> StdResult<String, FromUtf8Error> {
+ String::from_utf8(self.payload)
+ }
+
+ /// Get frame payload as `&str`.
+ #[inline]
+ pub fn to_text(&self) -> Result<&str, Utf8Error> {
+ std::str::from_utf8(&self.payload)
+ }
+
+ /// Consume the frame into a closing frame.
+ #[inline]
+ pub(crate) fn into_close(self) -> Result<Option<CloseFrame<'static>>> {
+ match self.payload.len() {
+ 0 => Ok(None),
+ 1 => Err(Error::Protocol(ProtocolError::InvalidCloseSequence)),
+ _ => {
+ let mut data = self.payload;
+ let code = NetworkEndian::read_u16(&data[0..2]).into();
+ data.drain(0..2);
+ let text = String::from_utf8(data)?;
+ Ok(Some(CloseFrame { code, reason: text.into() }))
+ }
+ }
+ }
+
+ /// Create a new data frame.
+ #[inline]
+ pub fn message(data: Vec<u8>, opcode: OpCode, is_final: bool) -> Frame {
+ debug_assert!(matches!(opcode, OpCode::Data(_)), "Invalid opcode for data frame.");
+
+ Frame { header: FrameHeader { is_final, opcode, ..FrameHeader::default() }, payload: data }
+ }
+
+ /// Create a new Pong control frame.
+ #[inline]
+ pub fn pong(data: Vec<u8>) -> Frame {
+ Frame {
+ header: FrameHeader {
+ opcode: OpCode::Control(Control::Pong),
+ ..FrameHeader::default()
+ },
+ payload: data,
+ }
+ }
+
+ /// Create a new Ping control frame.
+ #[inline]
+ pub fn ping(data: Vec<u8>) -> Frame {
+ Frame {
+ header: FrameHeader {
+ opcode: OpCode::Control(Control::Ping),
+ ..FrameHeader::default()
+ },
+ payload: data,
+ }
+ }
+
+ /// Create a new Close control frame.
+ #[inline]
+ pub fn close(msg: Option<CloseFrame>) -> Frame {
+ let payload = if let Some(CloseFrame { code, reason }) = msg {
+ let mut p = Vec::with_capacity(reason.as_bytes().len() + 2);
+ p.write_u16::<NetworkEndian>(code.into()).unwrap(); // can't fail
+ p.extend_from_slice(reason.as_bytes());
+ p
+ } else {
+ Vec::new()
+ };
+
+ Frame { header: FrameHeader::default(), payload }
+ }
+
+ /// Create a frame from given header and data.
+ pub fn from_payload(header: FrameHeader, payload: Vec<u8>) -> Self {
+ Frame { header, payload }
+ }
+
+ /// Write a frame out to a buffer
+ pub fn format(mut self, output: &mut impl Write) -> Result<()> {
+ self.header.format(self.payload.len() as u64, output)?;
+ self.apply_mask();
+ output.write_all(self.payload())?;
+ Ok(())
+ }
+}
+
+impl fmt::Display for Frame {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "
+<FRAME>
+final: {}
+reserved: {} {} {}
+opcode: {}
+length: {}
+payload length: {}
+payload: 0x{}
+ ",
+ self.header.is_final,
+ self.header.rsv1,
+ self.header.rsv2,
+ self.header.rsv3,
+ self.header.opcode,
+ // self.mask.map(|mask| format!("{:?}", mask)).unwrap_or("NONE".into()),
+ self.len(),
+ self.payload.len(),
+ self.payload.iter().map(|byte| format!("{:02x}", byte)).collect::<String>()
+ )
+ }
+}
+
+/// Handling of the length format.
+enum LengthFormat {
+ U8(u8),
+ U16,
+ U64,
+}
+
+impl LengthFormat {
+ /// Get the length format for a given data size.
+ #[inline]
+ fn for_length(length: u64) -> Self {
+ if length < 126 {
+ LengthFormat::U8(length as u8)
+ } else if length < 65536 {
+ LengthFormat::U16
+ } else {
+ LengthFormat::U64
+ }
+ }
+
+ /// Get the size of the length encoding.
+ #[inline]
+ fn extra_bytes(&self) -> usize {
+ match *self {
+ LengthFormat::U8(_) => 0,
+ LengthFormat::U16 => 2,
+ LengthFormat::U64 => 8,
+ }
+ }
+
+ /// Encode the given length.
+ #[inline]
+ fn length_byte(&self) -> u8 {
+ match *self {
+ LengthFormat::U8(b) => b,
+ LengthFormat::U16 => 126,
+ LengthFormat::U64 => 127,
+ }
+ }
+
+ /// Get the length format for a given length byte.
+ #[inline]
+ fn for_byte(byte: u8) -> Self {
+ match byte & 0x7F {
+ 126 => LengthFormat::U16,
+ 127 => LengthFormat::U64,
+ b => LengthFormat::U8(b),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use super::super::coding::{Data, OpCode};
+ use std::io::Cursor;
+
+ #[test]
+ fn parse() {
+ let mut raw: Cursor<Vec<u8>> =
+ Cursor::new(vec![0x82, 0x07, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
+ let (header, length) = FrameHeader::parse(&mut raw).unwrap().unwrap();
+ assert_eq!(length, 7);
+ let mut payload = Vec::new();
+ raw.read_to_end(&mut payload).unwrap();
+ let frame = Frame::from_payload(header, payload);
+ assert_eq!(frame.into_data(), vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
+ }
+
+ #[test]
+ fn format() {
+ let frame = Frame::ping(vec![0x01, 0x02]);
+ let mut buf = Vec::with_capacity(frame.len());
+ frame.format(&mut buf).unwrap();
+ assert_eq!(buf, vec![0x89, 0x02, 0x01, 0x02]);
+ }
+
+ #[test]
+ fn display() {
+ let f = Frame::message("hi there".into(), OpCode::Data(Data::Text), true);
+ let view = format!("{}", f);
+ assert!(view.contains("payload:"));
+ }
+}
diff --git a/src/protocol/frame/mask.rs b/src/protocol/frame/mask.rs
new file mode 100644
index 0000000..31ff264
--- /dev/null
+++ b/src/protocol/frame/mask.rs
@@ -0,0 +1,73 @@
+/// Generate a random frame mask.
+#[inline]
+pub fn generate_mask() -> [u8; 4] {
+ rand::random()
+}
+
+/// Mask/unmask a frame.
+#[inline]
+pub fn apply_mask(buf: &mut [u8], mask: [u8; 4]) {
+ apply_mask_fast32(buf, mask)
+}
+
+/// A safe unoptimized mask application.
+#[inline]
+fn apply_mask_fallback(buf: &mut [u8], mask: [u8; 4]) {
+ for (i, byte) in buf.iter_mut().enumerate() {
+ *byte ^= mask[i & 3];
+ }
+}
+
+/// Faster version of `apply_mask()` which operates on 4-byte blocks.
+#[inline]
+pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) {
+ let mask_u32 = u32::from_ne_bytes(mask);
+
+ let (prefix, words, suffix) = unsafe { buf.align_to_mut::<u32>() };
+ apply_mask_fallback(prefix, mask);
+ let head = prefix.len() & 3;
+ let mask_u32 = if head > 0 {
+ if cfg!(target_endian = "big") {
+ mask_u32.rotate_left(8 * head as u32)
+ } else {
+ mask_u32.rotate_right(8 * head as u32)
+ }
+ } else {
+ mask_u32
+ };
+ for word in words.iter_mut() {
+ *word ^= mask_u32;
+ }
+ apply_mask_fallback(suffix, mask_u32.to_ne_bytes());
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_apply_mask() {
+ let mask = [0x6d, 0xb6, 0xb2, 0x80];
+ let unmasked = vec![
+ 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9,
+ 0x12, 0x03,
+ ];
+
+ for data_len in 0..=unmasked.len() {
+ let unmasked = &unmasked[0..data_len];
+ // Check masking with different alignment.
+ for off in 0..=3 {
+ if unmasked.len() < off {
+ continue;
+ }
+ let mut masked = unmasked.to_vec();
+ apply_mask_fallback(&mut masked[off..], mask);
+
+ let mut masked_fast = unmasked.to_vec();
+ apply_mask_fast32(&mut masked_fast[off..], mask);
+
+ assert_eq!(masked, masked_fast);
+ }
+ }
+ }
+}
diff --git a/src/protocol/frame/mod.rs b/src/protocol/frame/mod.rs
new file mode 100644
index 0000000..39066be
--- /dev/null
+++ b/src/protocol/frame/mod.rs
@@ -0,0 +1,279 @@
+//! Utilities to work with raw WebSocket frames.
+
+pub mod coding;
+
+#[allow(clippy::module_inception)]
+mod frame;
+mod mask;
+
+use std::io::{Error as IoError, ErrorKind as IoErrorKind, Read, Write};
+
+use log::*;
+
+pub use self::frame::{CloseFrame, Frame, FrameHeader};
+use crate::{
+ error::{CapacityError, Error, Result},
+ ReadBuffer,
+};
+
+/// A reader and writer for WebSocket frames.
+#[derive(Debug)]
+pub struct FrameSocket<Stream> {
+ /// The underlying network stream.
+ stream: Stream,
+ /// Codec for reading/writing frames.
+ codec: FrameCodec,
+}
+
+impl<Stream> FrameSocket<Stream> {
+ /// Create a new frame socket.
+ pub fn new(stream: Stream) -> Self {
+ FrameSocket { stream, codec: FrameCodec::new() }
+ }
+
+ /// Create a new frame socket from partially read data.
+ pub fn from_partially_read(stream: Stream, part: Vec<u8>) -> Self {
+ FrameSocket { stream, codec: FrameCodec::from_partially_read(part) }
+ }
+
+ /// Extract a stream from the socket.
+ pub fn into_inner(self) -> (Stream, Vec<u8>) {
+ (self.stream, self.codec.in_buffer.into_vec())
+ }
+
+ /// Returns a shared reference to the inner stream.
+ pub fn get_ref(&self) -> &Stream {
+ &self.stream
+ }
+
+ /// Returns a mutable reference to the inner stream.
+ pub fn get_mut(&mut self) -> &mut Stream {
+ &mut self.stream
+ }
+}
+
+impl<Stream> FrameSocket<Stream>
+where
+ Stream: Read,
+{
+ /// Read a frame from stream.
+ pub fn read_frame(&mut self, max_size: Option<usize>) -> Result<Option<Frame>> {
+ self.codec.read_frame(&mut self.stream, max_size)
+ }
+}
+
+impl<Stream> FrameSocket<Stream>
+where
+ Stream: Write,
+{
+ /// Write a frame to stream.
+ ///
+ /// This function guarantees that the frame is queued regardless of any errors.
+ /// There is no need to resend the frame. In order to handle WouldBlock or Incomplete,
+ /// call write_pending() afterwards.
+ pub fn write_frame(&mut self, frame: Frame) -> Result<()> {
+ self.codec.write_frame(&mut self.stream, frame)
+ }
+
+ /// Complete pending write, if any.
+ pub fn write_pending(&mut self) -> Result<()> {
+ self.codec.write_pending(&mut self.stream)
+ }
+}
+
+/// A codec for WebSocket frames.
+#[derive(Debug)]
+pub(super) struct FrameCodec {
+ /// Buffer to read data from the stream.
+ in_buffer: ReadBuffer,
+ /// Buffer to send packets to the network.
+ out_buffer: Vec<u8>,
+ /// Header and remaining size of the incoming packet being processed.
+ header: Option<(FrameHeader, u64)>,
+}
+
+impl FrameCodec {
+ /// Create a new frame codec.
+ pub(super) fn new() -> Self {
+ Self { in_buffer: ReadBuffer::new(), out_buffer: Vec::new(), header: None }
+ }
+
+ /// Create a new frame codec from partially read data.
+ pub(super) fn from_partially_read(part: Vec<u8>) -> Self {
+ Self {
+ in_buffer: ReadBuffer::from_partially_read(part),
+ out_buffer: Vec::new(),
+ header: None,
+ }
+ }
+
+ /// Read a frame from the provided stream.
+ pub(super) fn read_frame<Stream>(
+ &mut self,
+ stream: &mut Stream,
+ max_size: Option<usize>,
+ ) -> Result<Option<Frame>>
+ where
+ Stream: Read,
+ {
+ let max_size = max_size.unwrap_or_else(usize::max_value);
+
+ let payload = loop {
+ {
+ let cursor = self.in_buffer.as_cursor_mut();
+
+ if self.header.is_none() {
+ self.header = FrameHeader::parse(cursor)?;
+ }
+
+ if let Some((_, ref length)) = self.header {
+ let length = *length;
+
+ // Enforce frame size limit early and make sure `length`
+ // is not too big (fits into `usize`).
+ if length > max_size as u64 {
+ return Err(Error::Capacity(CapacityError::MessageTooLong {
+ size: length as usize,
+ max_size,
+ }));
+ }
+
+ let input_size = cursor.get_ref().len() as u64 - cursor.position();
+ if length <= input_size {
+ // No truncation here since `length` is checked above
+ let mut payload = Vec::with_capacity(length as usize);
+ if length > 0 {
+ cursor.take(length).read_to_end(&mut payload)?;
+ }
+ break payload;
+ }
+ }
+ }
+
+ // Not enough data in buffer.
+ let size = self.in_buffer.read_from(stream)?;
+ if size == 0 {
+ trace!("no frame received");
+ return Ok(None);
+ }
+ };
+
+ let (header, length) = self.header.take().expect("Bug: no frame header");
+ debug_assert_eq!(payload.len() as u64, length);
+ let frame = Frame::from_payload(header, payload);
+ trace!("received frame {}", frame);
+ Ok(Some(frame))
+ }
+
+ /// Write a frame to the provided stream.
+ pub(super) fn write_frame<Stream>(&mut self, stream: &mut Stream, frame: Frame) -> Result<()>
+ where
+ Stream: Write,
+ {
+ trace!("writing frame {}", frame);
+ self.out_buffer.reserve(frame.len());
+ frame.format(&mut self.out_buffer).expect("Bug: can't write to vector");
+ self.write_pending(stream)
+ }
+
+ /// Complete pending write, if any.
+ pub(super) fn write_pending<Stream>(&mut self, stream: &mut Stream) -> Result<()>
+ where
+ Stream: Write,
+ {
+ while !self.out_buffer.is_empty() {
+ let len = stream.write(&self.out_buffer)?;
+ if len == 0 {
+ // This is the same as "Connection reset by peer"
+ return Err(IoError::new(
+ IoErrorKind::ConnectionReset,
+ "Connection reset while sending",
+ )
+ .into());
+ }
+ self.out_buffer.drain(0..len);
+ }
+ stream.flush()?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+impl FrameCodec {
+ /// Returns the size of the output buffer.
+ pub(super) fn output_buffer_len(&self) -> usize {
+ self.out_buffer.len()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use crate::error::{CapacityError, Error};
+
+ use super::{Frame, FrameSocket};
+
+ use std::io::Cursor;
+
+ #[test]
+ fn read_frames() {
+ let raw = Cursor::new(vec![
+ 0x82, 0x07, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x82, 0x03, 0x03, 0x02, 0x01,
+ 0x99,
+ ]);
+ let mut sock = FrameSocket::new(raw);
+
+ assert_eq!(
+ sock.read_frame(None).unwrap().unwrap().into_data(),
+ vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]
+ );
+ assert_eq!(sock.read_frame(None).unwrap().unwrap().into_data(), vec![0x03, 0x02, 0x01]);
+ assert!(sock.read_frame(None).unwrap().is_none());
+
+ let (_, rest) = sock.into_inner();
+ assert_eq!(rest, vec![0x99]);
+ }
+
+ #[test]
+ fn from_partially_read() {
+ let raw = Cursor::new(vec![0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
+ let mut sock = FrameSocket::from_partially_read(raw, vec![0x82, 0x07, 0x01]);
+ assert_eq!(
+ sock.read_frame(None).unwrap().unwrap().into_data(),
+ vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]
+ );
+ }
+
+ #[test]
+ fn write_frames() {
+ let mut sock = FrameSocket::new(Vec::new());
+
+ let frame = Frame::ping(vec![0x04, 0x05]);
+ sock.write_frame(frame).unwrap();
+
+ let frame = Frame::pong(vec![0x01]);
+ sock.write_frame(frame).unwrap();
+
+ let (buf, _) = sock.into_inner();
+ assert_eq!(buf, vec![0x89, 0x02, 0x04, 0x05, 0x8a, 0x01, 0x01]);
+ }
+
+ #[test]
+ fn parse_overflow() {
+ let raw = Cursor::new(vec![
+ 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+ ]);
+ let mut sock = FrameSocket::new(raw);
+ let _ = sock.read_frame(None); // should not crash
+ }
+
+ #[test]
+ fn size_limit_hit() {
+ let raw = Cursor::new(vec![0x82, 0x07, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
+ let mut sock = FrameSocket::new(raw);
+ assert!(matches!(
+ sock.read_frame(Some(5)),
+ Err(Error::Capacity(CapacityError::MessageTooLong { size: 7, max_size: 5 }))
+ ));
+ }
+}
diff --git a/src/protocol/message.rs b/src/protocol/message.rs
new file mode 100644
index 0000000..cdebabc
--- /dev/null
+++ b/src/protocol/message.rs
@@ -0,0 +1,370 @@
+use std::{
+ convert::{AsRef, From, Into, TryFrom},
+ fmt,
+ result::Result as StdResult,
+ str,
+};
+
+use super::frame::{CloseFrame, Frame};
+use crate::error::{CapacityError, Error, Result};
+
+mod string_collect {
+ use utf8::DecodeError;
+
+ use crate::error::{Error, Result};
+
+ #[derive(Debug)]
+ pub struct StringCollector {
+ data: String,
+ incomplete: Option<utf8::Incomplete>,
+ }
+
+ impl StringCollector {
+ pub fn new() -> Self {
+ StringCollector { data: String::new(), incomplete: None }
+ }
+
+ pub fn len(&self) -> usize {
+ self.data
+ .len()
+ .saturating_add(self.incomplete.map(|i| i.buffer_len as usize).unwrap_or(0))
+ }
+
+ pub fn extend<T: AsRef<[u8]>>(&mut self, tail: T) -> Result<()> {
+ let mut input: &[u8] = tail.as_ref();
+
+ if let Some(mut incomplete) = self.incomplete.take() {
+ if let Some((result, rest)) = incomplete.try_complete(input) {
+ input = rest;
+ if let Ok(text) = result {
+ self.data.push_str(text);
+ } else {
+ return Err(Error::Utf8);
+ }
+ } else {
+ input = &[];
+ self.incomplete = Some(incomplete);
+ }
+ }
+
+ if !input.is_empty() {
+ match utf8::decode(input) {
+ Ok(text) => {
+ self.data.push_str(text);
+ Ok(())
+ }
+ Err(DecodeError::Incomplete { valid_prefix, incomplete_suffix }) => {
+ self.data.push_str(valid_prefix);
+ self.incomplete = Some(incomplete_suffix);
+ Ok(())
+ }
+ Err(DecodeError::Invalid { valid_prefix, .. }) => {
+ self.data.push_str(valid_prefix);
+ Err(Error::Utf8)
+ }
+ }
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn into_string(self) -> Result<String> {
+ if self.incomplete.is_some() {
+ Err(Error::Utf8)
+ } else {
+ Ok(self.data)
+ }
+ }
+ }
+}
+
+use self::string_collect::StringCollector;
+
+/// A struct representing the incomplete message.
+#[derive(Debug)]
+pub struct IncompleteMessage {
+ collector: IncompleteMessageCollector,
+}
+
+#[derive(Debug)]
+enum IncompleteMessageCollector {
+ Text(StringCollector),
+ Binary(Vec<u8>),
+}
+
+impl IncompleteMessage {
+ /// Create new.
+ pub fn new(message_type: IncompleteMessageType) -> Self {
+ IncompleteMessage {
+ collector: match message_type {
+ IncompleteMessageType::Binary => IncompleteMessageCollector::Binary(Vec::new()),
+ IncompleteMessageType::Text => {
+ IncompleteMessageCollector::Text(StringCollector::new())
+ }
+ },
+ }
+ }
+
+ /// Get the current filled size of the buffer.
+ pub fn len(&self) -> usize {
+ match self.collector {
+ IncompleteMessageCollector::Text(ref t) => t.len(),
+ IncompleteMessageCollector::Binary(ref b) => b.len(),
+ }
+ }
+
+ /// Add more data to an existing message.
+ pub fn extend<T: AsRef<[u8]>>(&mut self, tail: T, size_limit: Option<usize>) -> Result<()> {
+ // Always have a max size. This ensures an error in case of concatenating two buffers
+ // of more than `usize::max_value()` bytes in total.
+ let max_size = size_limit.unwrap_or_else(usize::max_value);
+ let my_size = self.len();
+ let portion_size = tail.as_ref().len();
+ // Be careful about integer overflows here.
+ if my_size > max_size || portion_size > max_size - my_size {
+ return Err(Error::Capacity(CapacityError::MessageTooLong {
+ size: my_size + portion_size,
+ max_size,
+ }));
+ }
+
+ match self.collector {
+ IncompleteMessageCollector::Binary(ref mut v) => {
+ v.extend(tail.as_ref());
+ Ok(())
+ }
+ IncompleteMessageCollector::Text(ref mut t) => t.extend(tail),
+ }
+ }
+
+ /// Convert an incomplete message into a complete one.
+ pub fn complete(self) -> Result<Message> {
+ match self.collector {
+ IncompleteMessageCollector::Binary(v) => Ok(Message::Binary(v)),
+ IncompleteMessageCollector::Text(t) => {
+ let text = t.into_string()?;
+ Ok(Message::Text(text))
+ }
+ }
+ }
+}
+
+/// The type of incomplete message.
+pub enum IncompleteMessageType {
+ Text,
+ Binary,
+}
+
+/// An enum representing the various forms of a WebSocket message.
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum Message {
+ /// A text WebSocket message
+ Text(String),
+ /// A binary WebSocket message
+ Binary(Vec<u8>),
+ /// A ping message with the specified payload
+ ///
+ /// The payload here must have a length less than 125 bytes
+ Ping(Vec<u8>),
+ /// A pong message with the specified payload
+ ///
+ /// The payload here must have a length less than 125 bytes
+ Pong(Vec<u8>),
+ /// A close message with the optional close frame.
+ Close(Option<CloseFrame<'static>>),
+ /// Raw frame. Note, that you're not going to get this value while reading the message.
+ Frame(Frame),
+}
+
+impl Message {
+ /// Create a new text WebSocket message from a stringable.
+ pub fn text<S>(string: S) -> Message
+ where
+ S: Into<String>,
+ {
+ Message::Text(string.into())
+ }
+
+ /// Create a new binary WebSocket message by converting to Vec<u8>.
+ pub fn binary<B>(bin: B) -> Message
+ where
+ B: Into<Vec<u8>>,
+ {
+ Message::Binary(bin.into())
+ }
+
+ /// Indicates whether a message is a text message.
+ pub fn is_text(&self) -> bool {
+ matches!(*self, Message::Text(_))
+ }
+
+ /// Indicates whether a message is a binary message.
+ pub fn is_binary(&self) -> bool {
+ matches!(*self, Message::Binary(_))
+ }
+
+ /// Indicates whether a message is a ping message.
+ pub fn is_ping(&self) -> bool {
+ matches!(*self, Message::Ping(_))
+ }
+
+ /// Indicates whether a message is a pong message.
+ pub fn is_pong(&self) -> bool {
+ matches!(*self, Message::Pong(_))
+ }
+
+ /// Indicates whether a message is a close message.
+ pub fn is_close(&self) -> bool {
+ matches!(*self, Message::Close(_))
+ }
+
+ /// Get the length of the WebSocket message.
+ pub fn len(&self) -> usize {
+ match *self {
+ Message::Text(ref string) => string.len(),
+ Message::Binary(ref data) | Message::Ping(ref data) | Message::Pong(ref data) => {
+ data.len()
+ }
+ Message::Close(ref data) => data.as_ref().map(|d| d.reason.len()).unwrap_or(0),
+ Message::Frame(ref frame) => frame.len(),
+ }
+ }
+
+ /// Returns true if the WebSocket message has no content.
+ /// For example, if the other side of the connection sent an empty string.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Consume the WebSocket and return it as binary data.
+ pub fn into_data(self) -> Vec<u8> {
+ match self {
+ Message::Text(string) => string.into_bytes(),
+ Message::Binary(data) | Message::Ping(data) | Message::Pong(data) => data,
+ Message::Close(None) => Vec::new(),
+ Message::Close(Some(frame)) => frame.reason.into_owned().into_bytes(),
+ Message::Frame(frame) => frame.into_data(),
+ }
+ }
+
+ /// Attempt to consume the WebSocket message and convert it to a String.
+ pub fn into_text(self) -> Result<String> {
+ match self {
+ Message::Text(string) => Ok(string),
+ Message::Binary(data) | Message::Ping(data) | Message::Pong(data) => {
+ Ok(String::from_utf8(data)?)
+ }
+ Message::Close(None) => Ok(String::new()),
+ Message::Close(Some(frame)) => Ok(frame.reason.into_owned()),
+ Message::Frame(frame) => Ok(frame.into_string()?),
+ }
+ }
+
+ /// Attempt to get a &str from the WebSocket message,
+ /// this will try to convert binary data to utf8.
+ pub fn to_text(&self) -> Result<&str> {
+ match *self {
+ Message::Text(ref string) => Ok(string),
+ Message::Binary(ref data) | Message::Ping(ref data) | Message::Pong(ref data) => {
+ Ok(str::from_utf8(data)?)
+ }
+ Message::Close(None) => Ok(""),
+ Message::Close(Some(ref frame)) => Ok(&frame.reason),
+ Message::Frame(ref frame) => Ok(frame.to_text()?),
+ }
+ }
+}
+
+impl From<String> for Message {
+ fn from(string: String) -> Self {
+ Message::text(string)
+ }
+}
+
+impl<'s> From<&'s str> for Message {
+ fn from(string: &'s str) -> Self {
+ Message::text(string)
+ }
+}
+
+impl<'b> From<&'b [u8]> for Message {
+ fn from(data: &'b [u8]) -> Self {
+ Message::binary(data)
+ }
+}
+
+impl From<Vec<u8>> for Message {
+ fn from(data: Vec<u8>) -> Self {
+ Message::binary(data)
+ }
+}
+
+impl From<Message> for Vec<u8> {
+ fn from(message: Message) -> Self {
+ message.into_data()
+ }
+}
+
+impl TryFrom<Message> for String {
+ type Error = Error;
+
+ fn try_from(value: Message) -> StdResult<Self, Self::Error> {
+ value.into_text()
+ }
+}
+
+impl fmt::Display for Message {
+ fn fmt(&self, f: &mut fmt::Formatter) -> StdResult<(), fmt::Error> {
+ if let Ok(string) = self.to_text() {
+ write!(f, "{}", string)
+ } else {
+ write!(f, "Binary Data<length={}>", self.len())
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn display() {
+ let t = Message::text("test".to_owned());
+ assert_eq!(t.to_string(), "test".to_owned());
+
+ let bin = Message::binary(vec![0, 1, 3, 4, 241]);
+ assert_eq!(bin.to_string(), "Binary Data<length=5>".to_owned());
+ }
+
+ #[test]
+ fn binary_convert() {
+ let bin = [6u8, 7, 8, 9, 10, 241];
+ let msg = Message::from(&bin[..]);
+ assert!(msg.is_binary());
+ assert!(msg.into_text().is_err());
+ }
+
+ #[test]
+ fn binary_convert_vec() {
+ let bin = vec![6u8, 7, 8, 9, 10, 241];
+ let msg = Message::from(bin);
+ assert!(msg.is_binary());
+ assert!(msg.into_text().is_err());
+ }
+
+ #[test]
+ fn binary_convert_into_vec() {
+ let bin = vec![6u8, 7, 8, 9, 10, 241];
+ let bin_copy = bin.clone();
+ let msg = Message::from(bin);
+ let serialized: Vec<u8> = msg.into();
+ assert_eq!(bin_copy, serialized);
+ }
+
+ #[test]
+ fn text_convert() {
+ let s = "kiwotsukete";
+ let msg = Message::from(s);
+ assert!(msg.is_text());
+ }
+}
diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs
new file mode 100644
index 0000000..94397e9
--- /dev/null
+++ b/src/protocol/mod.rs
@@ -0,0 +1,789 @@
+//! Generic WebSocket message stream.
+
+pub mod frame;
+
+mod message;
+
+pub use self::{frame::CloseFrame, message::Message};
+
+use log::*;
+use std::{
+ collections::VecDeque,
+ io::{ErrorKind as IoErrorKind, Read, Write},
+ mem::replace,
+};
+
+use self::{
+ frame::{
+ coding::{CloseCode, Control as OpCtl, Data as OpData, OpCode},
+ Frame, FrameCodec,
+ },
+ message::{IncompleteMessage, IncompleteMessageType},
+};
+use crate::{
+ error::{Error, ProtocolError, Result},
+ util::NonBlockingResult,
+};
+
+/// Indicates a Client or Server role of the websocket
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Role {
+ /// This socket is a server
+ Server,
+ /// This socket is a client
+ Client,
+}
+
+/// The configuration for WebSocket connection.
+#[derive(Debug, Clone, Copy)]
+pub struct WebSocketConfig {
+ /// The size of the send queue. You can use it to turn on/off the backpressure features. `None`
+ /// means here that the size of the queue is unlimited. The default value is the unlimited
+ /// queue.
+ pub max_send_queue: Option<usize>,
+ /// The maximum size of a message. `None` means no size limit. The default value is 64 MiB
+ /// which should be reasonably big for all normal use-cases but small enough to prevent
+ /// memory eating by a malicious user.
+ pub max_message_size: Option<usize>,
+ /// The maximum size of a single message frame. `None` means no size limit. The limit is for
+ /// frame payload NOT including the frame header. The default value is 16 MiB which should
+ /// be reasonably big for all normal use-cases but small enough to prevent memory eating
+ /// by a malicious user.
+ pub max_frame_size: Option<usize>,
+ /// When set to `true`, the server will accept and handle unmasked frames
+ /// from the client. According to the RFC 6455, the server must close the
+ /// connection to the client in such cases, however it seems like there are
+ /// 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,
+}
+
+impl Default for WebSocketConfig {
+ fn default() -> Self {
+ WebSocketConfig {
+ max_send_queue: None,
+ max_message_size: Some(64 << 20),
+ max_frame_size: Some(16 << 20),
+ accept_unmasked_frames: false,
+ }
+ }
+}
+
+/// WebSocket input-output stream.
+///
+/// This is THE structure you want to create to be able to speak the WebSocket protocol.
+/// It may be created by calling `connect`, `accept` or `client` functions.
+#[derive(Debug)]
+pub struct WebSocket<Stream> {
+ /// The underlying socket.
+ socket: Stream,
+ /// The context for managing a WebSocket.
+ context: WebSocketContext,
+}
+
+impl<Stream> WebSocket<Stream> {
+ /// 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
+ /// or together with an existing one. If you need an initial handshake, use
+ /// `connect()` or `accept()` functions of the crate to construct a websocket.
+ pub fn from_raw_socket(stream: Stream, role: Role, config: Option<WebSocketConfig>) -> Self {
+ WebSocket { socket: stream, context: WebSocketContext::new(role, config) }
+ }
+
+ /// 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
+ /// or together with an existing one. If you need an initial handshake, use
+ /// `connect()` or `accept()` functions of the crate to construct a websocket.
+ pub fn from_partially_read(
+ stream: Stream,
+ part: Vec<u8>,
+ role: Role,
+ config: Option<WebSocketConfig>,
+ ) -> Self {
+ WebSocket {
+ socket: stream,
+ context: WebSocketContext::from_partially_read(part, role, config),
+ }
+ }
+
+ /// Returns a shared reference to the inner stream.
+ pub fn get_ref(&self) -> &Stream {
+ &self.socket
+ }
+ /// Returns a mutable reference to the inner stream.
+ pub fn get_mut(&mut self) -> &mut Stream {
+ &mut self.socket
+ }
+
+ /// Change the configuration.
+ pub fn set_config(&mut self, set_func: impl FnOnce(&mut WebSocketConfig)) {
+ self.context.set_config(set_func)
+ }
+
+ /// Read the configuration.
+ pub fn get_config(&self) -> &WebSocketConfig {
+ self.context.get_config()
+ }
+
+ /// Check if it is possible to read messages.
+ ///
+ /// Reading is impossible after receiving `Message::Close`. It is still possible after
+ /// sending close frame since the peer still may send some data before confirming close.
+ pub fn can_read(&self) -> bool {
+ self.context.can_read()
+ }
+
+ /// Check if it is possible to write messages.
+ ///
+ /// Writing gets impossible immediately after sending or receiving `Message::Close`.
+ pub fn can_write(&self) -> bool {
+ self.context.can_write()
+ }
+}
+
+impl<Stream: Read + Write> WebSocket<Stream> {
+ /// Read a message from stream, if possible.
+ ///
+ /// This will queue responses to ping and close messages to be sent. It will call
+ /// `write_pending` before trying to read in order to make sure that those responses
+ /// make progress even if you never call `write_pending`. That does mean that they
+ /// get sent out earliest on the next call to `read_message`, `write_message` or `write_pending`.
+ ///
+ /// ## Closing the connection
+ /// When the remote endpoint decides to close the connection this will return
+ /// the close message with an optional close frame.
+ ///
+ /// You should continue calling `read_message`, `write_message` or `write_pending` to drive
+ /// the reply to the close frame until [Error::ConnectionClosed] is returned. Once that happens
+ /// it is safe to drop the underlying connection.
+ pub fn read_message(&mut self) -> Result<Message> {
+ self.context.read_message(&mut self.socket)
+ }
+
+ /// Send a message to stream, if possible.
+ ///
+ /// WebSocket will buffer a configurable number of messages at a time, except to reply to Ping
+ /// requests. A Pong reply will jump the queue because the
+ /// [websocket RFC](https://tools.ietf.org/html/rfc6455#section-5.5.2) specifies it should be sent
+ /// as soon as is practical.
+ ///
+ /// Note that upon receiving a ping message, tungstenite cues a pong reply automatically.
+ /// When you call either `read_message`, `write_message` or `write_pending` next it will try to send
+ /// that pong out if the underlying connection can take more data. This means you should not
+ /// respond to ping frames manually.
+ ///
+ /// You can however send pong frames manually in order to indicate a unidirectional heartbeat
+ /// as described in [RFC 6455](https://tools.ietf.org/html/rfc6455#section-5.5.3). Note that
+ /// if `read_message` returns a ping, you should call `write_pending` until it doesn't return
+ /// WouldBlock before passing a pong to `write_message`, otherwise the response to the
+ /// ping will not be sent, but rather replaced by your custom pong message.
+ ///
+ /// ## Errors
+ /// - If the WebSocket's send queue is full, `SendQueueFull` will be returned
+ /// along with the passed message. Otherwise, the message is queued and Ok(()) is returned.
+ /// - If the connection is closed and should be dropped, this will return [Error::ConnectionClosed].
+ /// - If you try again after [Error::ConnectionClosed] was returned either from here or from `read_message`,
+ /// [Error::AlreadyClosed] will be returned. This indicates a program error on your part.
+ /// - [Error::Io] is returned if the underlying connection returns an error
+ /// (consider these fatal except for WouldBlock).
+ /// - [Error::Capacity] if your message size is bigger than the configured max message size.
+ pub fn write_message(&mut self, message: Message) -> Result<()> {
+ self.context.write_message(&mut self.socket, message)
+ }
+
+ /// Flush the pending send queue.
+ pub fn write_pending(&mut self) -> Result<()> {
+ self.context.write_pending(&mut self.socket)
+ }
+
+ /// Close the connection.
+ ///
+ /// This function guarantees that the close frame will be queued.
+ /// There is no need to call it again. Calling this function is
+ /// the same as calling `write_message(Message::Close(..))`.
+ ///
+ /// After queuing the close frame you should continue calling `read_message` or
+ /// `write_pending` to drive the close handshake to completion.
+ ///
+ /// The websocket RFC defines that the underlying connection should be closed
+ /// by the server. Tungstenite takes care of this asymmetry for you.
+ ///
+ /// When the close handshake is finished (we have both sent and received
+ /// a close message), `read_message` or `write_pending` will return
+ /// [Error::ConnectionClosed] if this endpoint is the server.
+ ///
+ /// If this endpoint is a client, [Error::ConnectionClosed] will only be
+ /// returned after the server has closed the underlying connection.
+ ///
+ /// It is thus safe to drop the underlying connection as soon as [Error::ConnectionClosed]
+ /// is returned from `read_message` or `write_pending`.
+ pub fn close(&mut self, code: Option<CloseFrame>) -> Result<()> {
+ self.context.close(&mut self.socket, code)
+ }
+}
+
+/// A context for managing WebSocket stream.
+#[derive(Debug)]
+pub struct WebSocketContext {
+ /// Server or client?
+ role: Role,
+ /// encoder/decoder of frame.
+ frame: FrameCodec,
+ /// The state of processing, either "active" or "closing".
+ state: WebSocketState,
+ /// Receive: an incomplete message being processed.
+ incomplete: Option<IncompleteMessage>,
+ /// Send: a data send queue.
+ send_queue: VecDeque<Frame>,
+ /// Send: an OOB pong message.
+ pong: Option<Frame>,
+ /// The configuration for the websocket session.
+ config: WebSocketConfig,
+}
+
+impl WebSocketContext {
+ /// Create a WebSocket context that manages a post-handshake stream.
+ pub fn new(role: Role, config: Option<WebSocketConfig>) -> Self {
+ WebSocketContext {
+ role,
+ frame: FrameCodec::new(),
+ state: WebSocketState::Active,
+ incomplete: None,
+ send_queue: VecDeque::new(),
+ pong: None,
+ config: config.unwrap_or_default(),
+ }
+ }
+
+ /// Create a WebSocket context that manages an post-handshake stream.
+ pub fn from_partially_read(part: Vec<u8>, role: Role, config: Option<WebSocketConfig>) -> Self {
+ WebSocketContext {
+ frame: FrameCodec::from_partially_read(part),
+ ..WebSocketContext::new(role, config)
+ }
+ }
+
+ /// Change the configuration.
+ pub fn set_config(&mut self, set_func: impl FnOnce(&mut WebSocketConfig)) {
+ set_func(&mut self.config)
+ }
+
+ /// Read the configuration.
+ pub fn get_config(&self) -> &WebSocketConfig {
+ &self.config
+ }
+
+ /// Check if it is possible to read messages.
+ ///
+ /// Reading is impossible after receiving `Message::Close`. It is still possible after
+ /// sending close frame since the peer still may send some data before confirming close.
+ pub fn can_read(&self) -> bool {
+ self.state.can_read()
+ }
+
+ /// Check if it is possible to write messages.
+ ///
+ /// Writing gets impossible immediately after sending or receiving `Message::Close`.
+ pub fn can_write(&self) -> bool {
+ self.state.is_active()
+ }
+
+ /// Read a message from the provided stream, if possible.
+ ///
+ /// This function sends pong and close responses automatically.
+ /// However, it never blocks on write.
+ pub fn read_message<Stream>(&mut self, stream: &mut Stream) -> Result<Message>
+ where
+ Stream: Read + Write,
+ {
+ // Do not read from already closed connections.
+ self.state.check_active()?;
+
+ loop {
+ // Since we may get ping or close, we need to reply to the messages even during read.
+ // Thus we call write_pending() but ignore its blocking.
+ self.write_pending(stream).no_block()?;
+ // If we get here, either write blocks or we have nothing to write.
+ // Thus if read blocks, just let it return WouldBlock.
+ if let Some(message) = self.read_message_frame(stream)? {
+ trace!("Received message {}", message);
+ return Ok(message);
+ }
+ }
+ }
+
+ /// Send a message to the provided stream, if possible.
+ ///
+ /// WebSocket will buffer a configurable number of messages at a time, except to reply to Ping
+ /// and Close requests. If the WebSocket's send queue is full, `SendQueueFull` will be returned
+ /// along with the passed message. Otherwise, the message is queued and Ok(()) is returned.
+ ///
+ /// Note that only the last pong frame is stored to be sent, and only the
+ /// most recent pong frame is sent if multiple pong frames are queued.
+ pub fn write_message<Stream>(&mut self, stream: &mut Stream, message: Message) -> Result<()>
+ where
+ Stream: Read + Write,
+ {
+ // When terminated, return AlreadyClosed.
+ self.state.check_active()?;
+
+ // Do not write after sending a close frame.
+ if !self.state.is_active() {
+ return Err(Error::Protocol(ProtocolError::SendAfterClosing));
+ }
+
+ if let Some(max_send_queue) = self.config.max_send_queue {
+ if self.send_queue.len() >= max_send_queue {
+ // Try to make some room for the new message.
+ // Do not return here if write would block, ignore WouldBlock silently
+ // since we must queue the message anyway.
+ self.write_pending(stream).no_block()?;
+ }
+
+ if self.send_queue.len() >= max_send_queue {
+ return Err(Error::SendQueueFull(message));
+ }
+ }
+
+ 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::Ping(data) => Frame::ping(data),
+ Message::Pong(data) => {
+ self.pong = Some(Frame::pong(data));
+ return self.write_pending(stream);
+ }
+ Message::Close(code) => return self.close(stream, code),
+ Message::Frame(f) => f,
+ };
+
+ self.send_queue.push_back(frame);
+ self.write_pending(stream)
+ }
+
+ /// Flush the pending send queue.
+ pub fn write_pending<Stream>(&mut self, stream: &mut Stream) -> Result<()>
+ where
+ Stream: Read + Write,
+ {
+ // First, make sure we have no pending frame sending.
+ self.frame.write_pending(stream)?;
+
+ // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
+ // response, unless it already received a Close frame. It SHOULD
+ // respond with Pong frame as soon as is practical. (RFC 6455)
+ if let Some(pong) = self.pong.take() {
+ trace!("Sending pong reply");
+ self.send_one_frame(stream, pong)?;
+ }
+ // If we have any unsent frames, send them.
+ trace!("Frames still in queue: {}", self.send_queue.len());
+ while let Some(data) = self.send_queue.pop_front() {
+ self.send_one_frame(stream, data)?;
+ }
+
+ // If we get to this point, the send queue is empty and the underlying socket is still
+ // willing to take more data.
+
+ // If we're closing and there is nothing to send anymore, we should close the connection.
+ if self.role == Role::Server && !self.state.can_read() {
+ // The underlying TCP connection, in most normal cases, SHOULD be closed
+ // first by the server, so that it holds the TIME_WAIT state and not the
+ // client (as this would prevent it from re-opening the connection for 2
+ // maximum segment lifetimes (2MSL), while there is no corresponding
+ // server impact as a TIME_WAIT connection is immediately reopened upon
+ // a new SYN with a higher seq number). (RFC 6455)
+ self.state = WebSocketState::Terminated;
+ Err(Error::ConnectionClosed)
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Close the connection.
+ ///
+ /// This function guarantees that the close frame will be queued.
+ /// There is no need to call it again. Calling this function is
+ /// the same as calling `write(Message::Close(..))`.
+ pub fn close<Stream>(&mut self, stream: &mut Stream, code: Option<CloseFrame>) -> Result<()>
+ where
+ Stream: Read + Write,
+ {
+ if let WebSocketState::Active = self.state {
+ self.state = WebSocketState::ClosedByUs;
+ let frame = Frame::close(code);
+ self.send_queue.push_back(frame);
+ } else {
+ // Already closed, nothing to do.
+ }
+ self.write_pending(stream)
+ }
+
+ /// Try to decode one message frame. May return None.
+ fn read_message_frame<Stream>(&mut self, stream: &mut Stream) -> Result<Option<Message>>
+ where
+ Stream: Read + Write,
+ {
+ if let Some(mut frame) = self
+ .frame
+ .read_frame(stream, self.config.max_frame_size)
+ .check_connection_reset(self.state)?
+ {
+ if !self.state.can_read() {
+ return Err(Error::Protocol(ProtocolError::ReceivedAfterClosing));
+ }
+ // MUST be 0 unless an extension is negotiated that defines meanings
+ // for non-zero values. If a nonzero value is received and none of
+ // the negotiated extensions defines the meaning of such a nonzero
+ // value, the receiving endpoint MUST _Fail the WebSocket
+ // Connection_.
+ {
+ let hdr = frame.header();
+ if hdr.rsv1 || hdr.rsv2 || hdr.rsv3 {
+ return Err(Error::Protocol(ProtocolError::NonZeroReservedBits));
+ }
+ }
+
+ match self.role {
+ Role::Server => {
+ if frame.is_masked() {
+ // A server MUST remove masking for data frames received from a client
+ // as described in Section 5.3. (RFC 6455)
+ frame.apply_mask()
+ } else if !self.config.accept_unmasked_frames {
+ // The server MUST close the connection upon receiving a
+ // frame that is not masked. (RFC 6455)
+ // The only exception here is if the user explicitly accepts given
+ // stream by setting WebSocketConfig.accept_unmasked_frames to true
+ return Err(Error::Protocol(ProtocolError::UnmaskedFrameFromClient));
+ }
+ }
+ Role::Client => {
+ if frame.is_masked() {
+ // A client MUST close a connection if it detects a masked frame. (RFC 6455)
+ return Err(Error::Protocol(ProtocolError::MaskedFrameFromServer));
+ }
+ }
+ }
+
+ match frame.header().opcode {
+ OpCode::Control(ctl) => {
+ match ctl {
+ // All control frames MUST have a payload length of 125 bytes or less
+ // and MUST NOT be fragmented. (RFC 6455)
+ _ if !frame.header().is_final => {
+ Err(Error::Protocol(ProtocolError::FragmentedControlFrame))
+ }
+ _ if frame.payload().len() > 125 => {
+ Err(Error::Protocol(ProtocolError::ControlFrameTooBig))
+ }
+ OpCtl::Close => Ok(self.do_close(frame.into_close()?).map(Message::Close)),
+ OpCtl::Reserved(i) => {
+ Err(Error::Protocol(ProtocolError::UnknownControlFrameType(i)))
+ }
+ OpCtl::Ping => {
+ let data = frame.into_data();
+ // No ping processing after we sent a close frame.
+ if self.state.is_active() {
+ self.pong = Some(Frame::pong(data.clone()));
+ }
+ Ok(Some(Message::Ping(data)))
+ }
+ OpCtl::Pong => Ok(Some(Message::Pong(frame.into_data()))),
+ }
+ }
+
+ OpCode::Data(data) => {
+ 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 {
+ return Err(Error::Protocol(
+ ProtocolError::UnexpectedContinueFrame,
+ ));
+ }
+ if fin {
+ Ok(Some(self.incomplete.take().unwrap().complete()?))
+ } else {
+ Ok(None)
+ }
+ }
+ 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
+ };
+ if fin {
+ Ok(Some(msg.complete()?))
+ } else {
+ self.incomplete = Some(msg);
+ Ok(None)
+ }
+ }
+ OpData::Reserved(i) => {
+ Err(Error::Protocol(ProtocolError::UnknownDataFrameType(i)))
+ }
+ }
+ }
+ } // match opcode
+ } else {
+ // Connection closed by peer
+ match replace(&mut self.state, WebSocketState::Terminated) {
+ WebSocketState::ClosedByPeer | WebSocketState::CloseAcknowledged => {
+ Err(Error::ConnectionClosed)
+ }
+ _ => Err(Error::Protocol(ProtocolError::ResetWithoutClosingHandshake)),
+ }
+ }
+ }
+
+ /// 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<CloseFrame<'t>>) -> Option<Option<CloseFrame<'t>>> {
+ debug!("Received close frame: {:?}", close);
+ match self.state {
+ WebSocketState::Active => {
+ self.state = WebSocketState::ClosedByPeer;
+
+ let close = close.map(|frame| {
+ if !frame.code.is_allowed() {
+ CloseFrame {
+ code: CloseCode::Protocol,
+ reason: "Protocol violation".into(),
+ }
+ } else {
+ frame
+ }
+ });
+
+ let reply = Frame::close(close.clone());
+ debug!("Replying to close with {:?}", reply);
+ self.send_queue.push_back(reply);
+
+ Some(close)
+ }
+ WebSocketState::ClosedByPeer | WebSocketState::CloseAcknowledged => {
+ // It is already closed, just ignore.
+ None
+ }
+ WebSocketState::ClosedByUs => {
+ // We received a reply.
+ self.state = WebSocketState::CloseAcknowledged;
+ Some(close)
+ }
+ WebSocketState::Terminated => unreachable!(),
+ }
+ }
+
+ /// Send a single pending frame.
+ fn send_one_frame<Stream>(&mut self, stream: &mut Stream, mut frame: Frame) -> Result<()>
+ where
+ Stream: Read + Write,
+ {
+ match self.role {
+ Role::Server => {}
+ Role::Client => {
+ // 5. If the data is being sent by the client, the frame(s) MUST be
+ // masked as defined in Section 5.3. (RFC 6455)
+ frame.set_random_mask();
+ }
+ }
+
+ trace!("Sending frame: {:?}", frame);
+ self.frame.write_frame(stream, frame).check_connection_reset(self.state)
+ }
+}
+
+/// The current connection state.
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum WebSocketState {
+ /// The connection is active.
+ Active,
+ /// We initiated a close handshake.
+ ClosedByUs,
+ /// The peer initiated a close handshake.
+ ClosedByPeer,
+ /// The peer replied to our close handshake.
+ CloseAcknowledged,
+ /// The connection does not exist anymore.
+ Terminated,
+}
+
+impl WebSocketState {
+ /// Tell if we're allowed to process normal messages.
+ fn is_active(self) -> bool {
+ matches!(self, WebSocketState::Active)
+ }
+
+ /// Tell if we should process incoming data. Note that if we send a close frame
+ /// but the remote hasn't confirmed, they might have sent data before they receive our
+ /// close frame, so we should still pass those to client code, hence ClosedByUs is valid.
+ fn can_read(self) -> bool {
+ matches!(self, WebSocketState::Active | WebSocketState::ClosedByUs)
+ }
+
+ /// Check if the state is active, return error if not.
+ fn check_active(self) -> Result<()> {
+ match self {
+ WebSocketState::Terminated => Err(Error::AlreadyClosed),
+ _ => Ok(()),
+ }
+ }
+}
+
+/// Translate "Connection reset by peer" into `ConnectionClosed` if appropriate.
+trait CheckConnectionReset {
+ fn check_connection_reset(self, state: WebSocketState) -> Self;
+}
+
+impl<T> CheckConnectionReset for Result<T> {
+ fn check_connection_reset(self, state: WebSocketState) -> Self {
+ match self {
+ Err(Error::Io(io_error)) => Err({
+ if !state.can_read() && io_error.kind() == IoErrorKind::ConnectionReset {
+ Error::ConnectionClosed
+ } else {
+ Error::Io(io_error)
+ }
+ }),
+ x => x,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{Message, Role, WebSocket, WebSocketConfig};
+ use crate::error::{CapacityError, Error};
+
+ use std::{io, io::Cursor};
+
+ struct WriteMoc<Stream>(Stream);
+
+ impl<Stream> io::Write for WriteMoc<Stream> {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ Ok(buf.len())
+ }
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+ }
+
+ impl<Stream: io::Read> io::Read for WriteMoc<Stream> {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ self.0.read(buf)
+ }
+ }
+
+ struct WouldBlockStreamMoc;
+
+ impl io::Write for WouldBlockStreamMoc {
+ fn write(&mut self, _: &[u8]) -> io::Result<usize> {
+ Err(io::Error::new(io::ErrorKind::WouldBlock, "would block"))
+ }
+ fn flush(&mut self) -> io::Result<()> {
+ Err(io::Error::new(io::ErrorKind::WouldBlock, "would block"))
+ }
+ }
+
+ impl io::Read for WouldBlockStreamMoc {
+ fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
+ Err(io::Error::new(io::ErrorKind::WouldBlock, "would block"))
+ }
+ }
+
+ #[test]
+ fn queue_logic() {
+ // Create a socket with the queue size of 1.
+ let mut socket = WebSocket::from_raw_socket(
+ WouldBlockStreamMoc,
+ Role::Client,
+ Some(WebSocketConfig { max_send_queue: Some(1), ..Default::default() }),
+ );
+
+ // Test message that we're going to send.
+ let message = Message::Binary(vec![0xFF; 1024]);
+
+ // Helper to check the error.
+ let assert_would_block = |error| {
+ if let Error::Io(io_error) = error {
+ assert_eq!(io_error.kind(), io::ErrorKind::WouldBlock);
+ } else {
+ panic!("Expected WouldBlock error");
+ }
+ };
+
+ // The first attempt of writing must not fail, since the queue is empty at start.
+ // But since the underlying mock object always returns `WouldBlock`, so is the result.
+ assert_would_block(dbg!(socket.write_message(message.clone()).unwrap_err()));
+
+ // Any subsequent attempts must return an error telling that the queue is full.
+ for _i in 0..100 {
+ assert!(matches!(
+ socket.write_message(message.clone()).unwrap_err(),
+ Error::SendQueueFull(..)
+ ));
+ }
+
+ // The size of the output buffer must not be bigger than the size of that message
+ // that we managed to write to the output buffer at first. Since we could not make
+ // any progress (because of the logic of the moc buffer), the size remains unchanged.
+ if socket.context.frame.output_buffer_len() > message.len() {
+ panic!("Too many frames in the queue");
+ }
+ }
+
+ #[test]
+ fn receive_messages() {
+ let incoming = Cursor::new(vec![
+ 0x89, 0x02, 0x01, 0x02, 0x8a, 0x01, 0x03, 0x01, 0x07, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
+ 0x2c, 0x20, 0x80, 0x06, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x82, 0x03, 0x01, 0x02,
+ 0x03,
+ ]);
+ let mut socket = WebSocket::from_raw_socket(WriteMoc(incoming), Role::Client, None);
+ assert_eq!(socket.read_message().unwrap(), Message::Ping(vec![1, 2]));
+ assert_eq!(socket.read_message().unwrap(), Message::Pong(vec![3]));
+ assert_eq!(socket.read_message().unwrap(), Message::Text("Hello, World!".into()));
+ assert_eq!(socket.read_message().unwrap(), Message::Binary(vec![0x01, 0x02, 0x03]));
+ }
+
+ #[test]
+ fn size_limiting_text_fragmented() {
+ let incoming = Cursor::new(vec![
+ 0x01, 0x07, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x80, 0x06, 0x57, 0x6f, 0x72,
+ 0x6c, 0x64, 0x21,
+ ]);
+ let limit = WebSocketConfig { max_message_size: Some(10), ..WebSocketConfig::default() };
+ let mut socket = WebSocket::from_raw_socket(WriteMoc(incoming), Role::Client, Some(limit));
+
+ assert!(matches!(
+ socket.read_message(),
+ Err(Error::Capacity(CapacityError::MessageTooLong { size: 13, max_size: 10 }))
+ ));
+ }
+
+ #[test]
+ fn size_limiting_binary() {
+ let incoming = Cursor::new(vec![0x82, 0x03, 0x01, 0x02, 0x03]);
+ let limit = WebSocketConfig { max_message_size: Some(2), ..WebSocketConfig::default() };
+ let mut socket = WebSocket::from_raw_socket(WriteMoc(incoming), Role::Client, Some(limit));
+
+ assert!(matches!(
+ socket.read_message(),
+ Err(Error::Capacity(CapacityError::MessageTooLong { size: 3, max_size: 2 }))
+ ));
+ }
+}
diff --git a/src/server.rs b/src/server.rs
new file mode 100644
index 0000000..e79bccb
--- /dev/null
+++ b/src/server.rs
@@ -0,0 +1,68 @@
+//! Methods to accept an incoming WebSocket connection on a server.
+
+pub use crate::handshake::server::ServerHandshake;
+
+use crate::handshake::{
+ server::{Callback, NoCallback},
+ HandshakeError,
+};
+
+use crate::protocol::{WebSocket, WebSocketConfig};
+
+use std::io::{Read, Write};
+
+/// Accept the given Stream as a WebSocket.
+///
+/// Uses a configuration provided as an argument. Calling it with `None` will use the default one
+/// used by `accept()`.
+///
+/// This function starts a server WebSocket handshake over the given stream.
+/// If you want TLS support, use `native_tls::TlsStream`, `rustls::Stream` or
+/// `openssl::ssl::SslStream` for the stream here. Any `Read + Write` streams are supported,
+/// including those from `Mio` and others.
+pub fn accept_with_config<S: Read + Write>(
+ stream: S,
+ config: Option<WebSocketConfig>,
+) -> Result<WebSocket<S>, HandshakeError<ServerHandshake<S, NoCallback>>> {
+ accept_hdr_with_config(stream, NoCallback, config)
+}
+
+/// Accept the given Stream as a WebSocket.
+///
+/// This function starts a server WebSocket handshake over the given stream.
+/// If you want TLS support, use `native_tls::TlsStream`, `rustls::Stream` or
+/// `openssl::ssl::SslStream` for the stream here. Any `Read + Write` streams are supported,
+/// including those from `Mio` and others.
+pub fn accept<S: Read + Write>(
+ stream: S,
+) -> Result<WebSocket<S>, HandshakeError<ServerHandshake<S, NoCallback>>> {
+ accept_with_config(stream, None)
+}
+
+/// Accept the given Stream as a WebSocket.
+///
+/// Uses a configuration provided as an argument. Calling it with `None` will use the default one
+/// used by `accept_hdr()`.
+///
+/// This function does the same as `accept()` 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 fn accept_hdr_with_config<S: Read + Write, C: Callback>(
+ stream: S,
+ callback: C,
+ config: Option<WebSocketConfig>,
+) -> Result<WebSocket<S>, HandshakeError<ServerHandshake<S, C>>> {
+ ServerHandshake::start(stream, callback, config).handshake()
+}
+
+/// Accept the given Stream as a WebSocket.
+///
+/// This function does the same as `accept()` 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 fn accept_hdr<S: Read + Write, C: Callback>(
+ stream: S,
+ callback: C,
+) -> Result<WebSocket<S>, HandshakeError<ServerHandshake<S, C>>> {
+ accept_hdr_with_config(stream, callback, None)
+}
diff --git a/src/stream.rs b/src/stream.rs
new file mode 100644
index 0000000..4775230
--- /dev/null
+++ b/src/stream.rs
@@ -0,0 +1,145 @@
+//! Convenience wrapper for streams to switch between plain TCP and TLS at runtime.
+//!
+//! There is no dependency on actual TLS implementations. Everything like
+//! `native_tls` or `openssl` will work as long as there is a TLS stream supporting standard
+//! `Read + Write` traits.
+
+#[cfg(feature = "__rustls-tls")]
+use std::ops::Deref;
+use std::{
+ fmt::{self, Debug},
+ io::{Read, Result as IoResult, Write},
+};
+
+use std::net::TcpStream;
+
+#[cfg(feature = "native-tls")]
+use native_tls_crate::TlsStream;
+#[cfg(feature = "__rustls-tls")]
+use rustls::StreamOwned;
+
+/// Stream mode, either plain TCP or TLS.
+#[derive(Clone, Copy, Debug)]
+pub enum Mode {
+ /// Plain mode (`ws://` URL).
+ Plain,
+ /// TLS mode (`wss://` URL).
+ Tls,
+}
+
+/// Trait to switch TCP_NODELAY.
+pub trait NoDelay {
+ /// Set the TCP_NODELAY option to the given value.
+ fn set_nodelay(&mut self, nodelay: bool) -> IoResult<()>;
+}
+
+impl NoDelay for TcpStream {
+ fn set_nodelay(&mut self, nodelay: bool) -> IoResult<()> {
+ TcpStream::set_nodelay(self, nodelay)
+ }
+}
+
+#[cfg(feature = "native-tls")]
+impl<S: Read + Write + NoDelay> NoDelay for TlsStream<S> {
+ fn set_nodelay(&mut self, nodelay: bool) -> IoResult<()> {
+ self.get_mut().set_nodelay(nodelay)
+ }
+}
+
+#[cfg(feature = "__rustls-tls")]
+impl<S, SD, T> NoDelay for StreamOwned<S, T>
+where
+ S: Deref<Target = rustls::ConnectionCommon<SD>>,
+ SD: rustls::SideData,
+ T: Read + Write + NoDelay,
+{
+ fn set_nodelay(&mut self, nodelay: bool) -> IoResult<()> {
+ self.sock.set_nodelay(nodelay)
+ }
+}
+
+/// A stream that might be protected with TLS.
+#[non_exhaustive]
+pub enum MaybeTlsStream<S: Read + Write> {
+ /// Unencrypted socket stream.
+ Plain(S),
+ #[cfg(feature = "native-tls")]
+ /// Encrypted socket stream using `native-tls`.
+ NativeTls(native_tls_crate::TlsStream<S>),
+ #[cfg(feature = "__rustls-tls")]
+ /// Encrypted socket stream using `rustls`.
+ Rustls(rustls::StreamOwned<rustls::ClientConnection, S>),
+}
+
+impl<S: Read + Write + Debug> Debug for MaybeTlsStream<S> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Plain(s) => f.debug_tuple("MaybeTlsStream::Plain").field(s).finish(),
+ #[cfg(feature = "native-tls")]
+ Self::NativeTls(s) => f.debug_tuple("MaybeTlsStream::NativeTls").field(s).finish(),
+ #[cfg(feature = "__rustls-tls")]
+ Self::Rustls(s) => {
+ struct RustlsStreamDebug<'a, S: Read + Write>(
+ &'a rustls::StreamOwned<rustls::ClientConnection, S>,
+ );
+
+ impl<'a, S: Read + Write + Debug> Debug for RustlsStreamDebug<'a, S> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("StreamOwned")
+ .field("conn", &self.0.conn)
+ .field("sock", &self.0.sock)
+ .finish()
+ }
+ }
+
+ f.debug_tuple("MaybeTlsStream::Rustls").field(&RustlsStreamDebug(s)).finish()
+ }
+ }
+ }
+}
+
+impl<S: Read + Write> Read for MaybeTlsStream<S> {
+ fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
+ match *self {
+ MaybeTlsStream::Plain(ref mut s) => s.read(buf),
+ #[cfg(feature = "native-tls")]
+ MaybeTlsStream::NativeTls(ref mut s) => s.read(buf),
+ #[cfg(feature = "__rustls-tls")]
+ MaybeTlsStream::Rustls(ref mut s) => s.read(buf),
+ }
+ }
+}
+
+impl<S: Read + Write> Write for MaybeTlsStream<S> {
+ fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
+ match *self {
+ MaybeTlsStream::Plain(ref mut s) => s.write(buf),
+ #[cfg(feature = "native-tls")]
+ MaybeTlsStream::NativeTls(ref mut s) => s.write(buf),
+ #[cfg(feature = "__rustls-tls")]
+ MaybeTlsStream::Rustls(ref mut s) => s.write(buf),
+ }
+ }
+
+ fn flush(&mut self) -> IoResult<()> {
+ match *self {
+ MaybeTlsStream::Plain(ref mut s) => s.flush(),
+ #[cfg(feature = "native-tls")]
+ MaybeTlsStream::NativeTls(ref mut s) => s.flush(),
+ #[cfg(feature = "__rustls-tls")]
+ MaybeTlsStream::Rustls(ref mut s) => s.flush(),
+ }
+ }
+}
+
+impl<S: Read + Write + NoDelay> NoDelay for MaybeTlsStream<S> {
+ fn set_nodelay(&mut self, nodelay: bool) -> IoResult<()> {
+ match *self {
+ MaybeTlsStream::Plain(ref mut s) => s.set_nodelay(nodelay),
+ #[cfg(feature = "native-tls")]
+ MaybeTlsStream::NativeTls(ref mut s) => s.set_nodelay(nodelay),
+ #[cfg(feature = "__rustls-tls")]
+ MaybeTlsStream::Rustls(ref mut s) => s.set_nodelay(nodelay),
+ }
+ }
+}
diff --git a/src/tls.rs b/src/tls.rs
new file mode 100644
index 0000000..0bca333
--- /dev/null
+++ b/src/tls.rs
@@ -0,0 +1,235 @@
+//! Connection helper.
+use std::io::{Read, Write};
+
+use crate::{
+ client::{client_with_config, uri_mode, IntoClientRequest},
+ error::UrlError,
+ handshake::client::Response,
+ protocol::WebSocketConfig,
+ stream::MaybeTlsStream,
+ ClientHandshake, Error, HandshakeError, Result, WebSocket,
+};
+
+/// A connector that can be used when establishing connections, allowing to control whether
+/// `native-tls` or `rustls` is used to create a TLS connection. Or TLS can be disabled with the
+/// `Plain` variant.
+#[non_exhaustive]
+#[allow(missing_debug_implementations)]
+pub enum Connector {
+ /// Plain (non-TLS) connector.
+ Plain,
+ /// `native-tls` TLS connector.
+ #[cfg(feature = "native-tls")]
+ NativeTls(native_tls_crate::TlsConnector),
+ /// `rustls` TLS connector.
+ #[cfg(feature = "__rustls-tls")]
+ Rustls(std::sync::Arc<rustls::ClientConfig>),
+}
+
+mod encryption {
+ #[cfg(feature = "native-tls")]
+ pub mod native_tls {
+ use native_tls_crate::{HandshakeError as TlsHandshakeError, TlsConnector};
+
+ use std::io::{Read, Write};
+
+ use crate::{
+ error::TlsError,
+ stream::{MaybeTlsStream, Mode},
+ Error, Result,
+ };
+
+ pub fn wrap_stream<S>(
+ socket: S,
+ domain: &str,
+ mode: Mode,
+ tls_connector: Option<TlsConnector>,
+ ) -> Result<MaybeTlsStream<S>>
+ where
+ S: Read + Write,
+ {
+ match mode {
+ Mode::Plain => Ok(MaybeTlsStream::Plain(socket)),
+ Mode::Tls => {
+ let try_connector = tls_connector.map_or_else(TlsConnector::new, Ok);
+ let connector = try_connector.map_err(TlsError::Native)?;
+ let connected = connector.connect(domain, socket);
+ match connected {
+ Err(e) => match e {
+ TlsHandshakeError::Failure(f) => Err(Error::Tls(f.into())),
+ TlsHandshakeError::WouldBlock(_) => {
+ panic!("Bug: TLS handshake not blocked")
+ }
+ },
+ Ok(s) => Ok(MaybeTlsStream::NativeTls(s)),
+ }
+ }
+ }
+ }
+ }
+
+ #[cfg(feature = "__rustls-tls")]
+ pub mod rustls {
+ use rustls::{ClientConfig, ClientConnection, RootCertStore, ServerName, StreamOwned};
+
+ use std::{
+ convert::TryFrom,
+ io::{Read, Write},
+ sync::Arc,
+ };
+
+ use crate::{
+ error::TlsError,
+ stream::{MaybeTlsStream, Mode},
+ Result,
+ };
+
+ pub fn wrap_stream<S>(
+ socket: S,
+ domain: &str,
+ mode: Mode,
+ tls_connector: Option<Arc<ClientConfig>>,
+ ) -> Result<MaybeTlsStream<S>>
+ where
+ S: Read + Write,
+ {
+ match mode {
+ Mode::Plain => Ok(MaybeTlsStream::Plain(socket)),
+ Mode::Tls => {
+ let config = match tls_connector {
+ Some(config) => config,
+ None => {
+ #[allow(unused_mut)]
+ let mut root_store = RootCertStore::empty();
+
+ #[cfg(feature = "rustls-tls-native-roots")]
+ {
+ for cert in rustls_native_certs::load_native_certs()? {
+ root_store
+ .add(&rustls::Certificate(cert.0))
+ .map_err(TlsError::Rustls)?;
+ }
+ }
+ #[cfg(feature = "rustls-tls-webpki-roots")]
+ {
+ root_store.add_server_trust_anchors(
+ webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
+ rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
+ ta.subject,
+ ta.spki,
+ ta.name_constraints,
+ )
+ })
+ );
+ }
+
+ Arc::new(
+ ClientConfig::builder()
+ .with_safe_defaults()
+ .with_root_certificates(root_store)
+ .with_no_client_auth(),
+ )
+ }
+ };
+ let domain =
+ ServerName::try_from(domain).map_err(|_| TlsError::InvalidDnsName)?;
+ let client = ClientConnection::new(config, domain).map_err(TlsError::Rustls)?;
+ let stream = StreamOwned::new(client, socket);
+
+ Ok(MaybeTlsStream::Rustls(stream))
+ }
+ }
+ }
+ }
+
+ pub mod plain {
+ use std::io::{Read, Write};
+
+ use crate::{
+ error::UrlError,
+ stream::{MaybeTlsStream, Mode},
+ Error, Result,
+ };
+
+ pub fn wrap_stream<S>(socket: S, mode: Mode) -> Result<MaybeTlsStream<S>>
+ where
+ S: Read + Write,
+ {
+ match mode {
+ Mode::Plain => Ok(MaybeTlsStream::Plain(socket)),
+ Mode::Tls => Err(Error::Url(UrlError::TlsFeatureNotEnabled)),
+ }
+ }
+ }
+}
+
+type TlsHandshakeError<S> = HandshakeError<ClientHandshake<MaybeTlsStream<S>>>;
+
+/// Creates a WebSocket handshake from a request and a stream,
+/// upgrading the stream to TLS if required.
+pub fn client_tls<R, S>(
+ request: R,
+ stream: S,
+) -> Result<(WebSocket<MaybeTlsStream<S>>, Response), TlsHandshakeError<S>>
+where
+ R: IntoClientRequest,
+ S: Read + Write,
+{
+ client_tls_with_config(request, stream, None, None)
+}
+
+/// The same as [`client_tls()`] but one can specify a websocket configuration,
+/// and an optional connector. If no connector is specified, a default one will
+/// be created.
+///
+/// Please refer to [`client_tls()`] for more details.
+pub fn client_tls_with_config<R, S>(
+ request: R,
+ stream: S,
+ config: Option<WebSocketConfig>,
+ connector: Option<Connector>,
+) -> Result<(WebSocket<MaybeTlsStream<S>>, Response), TlsHandshakeError<S>>
+where
+ R: IntoClientRequest,
+ S: Read + Write,
+{
+ let request = request.into_client_request()?;
+
+ #[cfg(any(feature = "native-tls", feature = "__rustls-tls"))]
+ let domain = match request.uri().host() {
+ Some(d) => Ok(d.to_string()),
+ None => Err(Error::Url(UrlError::NoHostName)),
+ }?;
+
+ let mode = uri_mode(request.uri())?;
+
+ let stream = match connector {
+ Some(conn) => match conn {
+ #[cfg(feature = "native-tls")]
+ Connector::NativeTls(conn) => {
+ self::encryption::native_tls::wrap_stream(stream, &domain, mode, Some(conn))
+ }
+ #[cfg(feature = "__rustls-tls")]
+ Connector::Rustls(conn) => {
+ self::encryption::rustls::wrap_stream(stream, &domain, mode, Some(conn))
+ }
+ Connector::Plain => self::encryption::plain::wrap_stream(stream, mode),
+ },
+ None => {
+ #[cfg(feature = "native-tls")]
+ {
+ self::encryption::native_tls::wrap_stream(stream, &domain, mode, None)
+ }
+ #[cfg(all(feature = "__rustls-tls", not(feature = "native-tls")))]
+ {
+ self::encryption::rustls::wrap_stream(stream, &domain, mode, None)
+ }
+ #[cfg(not(any(feature = "native-tls", feature = "__rustls-tls")))]
+ {
+ self::encryption::plain::wrap_stream(stream, mode)
+ }
+ }
+ }?;
+
+ client_with_config(request, stream, config)
+}
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..f40ca43
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,58 @@
+//! Helper traits to ease non-blocking handling.
+
+use std::{
+ io::{Error as IoError, ErrorKind as IoErrorKind},
+ result::Result as StdResult,
+};
+
+use crate::error::Error;
+
+/// Non-blocking IO handling.
+pub trait NonBlockingError: Sized {
+ /// Convert WouldBlock to None and don't touch other errors.
+ fn into_non_blocking(self) -> Option<Self>;
+}
+
+impl NonBlockingError for IoError {
+ fn into_non_blocking(self) -> Option<Self> {
+ match self.kind() {
+ IoErrorKind::WouldBlock => None,
+ _ => Some(self),
+ }
+ }
+}
+
+impl NonBlockingError for Error {
+ fn into_non_blocking(self) -> Option<Self> {
+ match self {
+ Error::Io(e) => e.into_non_blocking().map(|e| e.into()),
+ x => Some(x),
+ }
+ }
+}
+
+/// Non-blocking IO wrapper.
+///
+/// This trait is implemented for `Result<T, E: NonBlockingError>`.
+pub trait NonBlockingResult {
+ /// Type of the converted result: `Result<Option<T>, E>`
+ type Result;
+ /// Perform the non-block conversion.
+ fn no_block(self) -> Self::Result;
+}
+
+impl<T, E> NonBlockingResult for StdResult<T, E>
+where
+ E: NonBlockingError,
+{
+ type Result = StdResult<Option<T>, E>;
+ fn no_block(self) -> Self::Result {
+ match self {
+ Ok(x) => Ok(Some(x)),
+ Err(e) => match e.into_non_blocking() {
+ Some(e) => Err(e),
+ None => Ok(None),
+ },
+ }
+ }
+}