├── .github ├── PULL_REQUEST_TEMPLATE.md ├── funding.yml └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Cargo.yml ├── README.md ├── TODO.md ├── build.rs ├── ci ├── clippy.bash ├── doc.bash └── test.bash ├── deny.toml ├── examples ├── close.rs ├── echo.rs ├── echo_tt.rs ├── ssl │ ├── Cargo.toml │ ├── Cargo.yml │ ├── localhost-cert.pem │ ├── localhost-key.pem │ └── src │ │ └── main.rs └── tokio_codec.rs ├── src ├── lib.rs ├── tung_websocket.rs ├── tung_websocket │ ├── closer.rs │ ├── closer │ │ ├── closer_send.rs │ │ ├── no_double_close.rs │ │ └── notify_errors.rs │ └── notifier.rs ├── ws_err.rs ├── ws_event.rs └── ws_stream.rs └── tests ├── buffer_size.rs ├── futures_codec.rs ├── futures_codec_partial.rs ├── ping_pong.rs ├── ping_pong_async_std.rs ├── protocol_error.rs ├── send_text.rs ├── send_text_backpressure.rs └── tokio_codec.rs /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | liberapay: najamelan 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on : [push, pull_request] 3 | 4 | jobs: 5 | 6 | linux-stable: 7 | 8 | name: Linux Rust Stable 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | 13 | - name: Install latest stable Rust 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | override: true 18 | components: clippy 19 | 20 | - name: Install nightly 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: nightly 24 | 25 | 26 | - name: Checkout crate 27 | uses: actions/checkout@v3 28 | 29 | 30 | - name: Run tests 31 | run: bash ci/test.bash 32 | 33 | 34 | - name: Build documentation 35 | run: bash ci/doc.bash 36 | 37 | 38 | linux-nightly: 39 | 40 | name: Linux Rust Nightly 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | 45 | - name: Install latest nightly Rust 46 | uses: actions-rs/toolchain@v1 47 | with: 48 | toolchain: nightly 49 | override: true 50 | components: clippy 51 | 52 | 53 | - name: Checkout crate 54 | uses: actions/checkout@v3 55 | 56 | 57 | - name: Run tests 58 | run : bash ci/test.bash 59 | 60 | 61 | - name: Run clippy 62 | run : bash ci/clippy.bash 63 | 64 | 65 | - name: Build documentation 66 | run : bash ci/doc.bash 67 | 68 | - name: Install cargo-tarpaulin 69 | uses: brndnmtthws/rust-action-cargo-binstall@v1.1.0 70 | with: 71 | packages: cargo-tarpaulin 72 | 73 | - name: Run cargo-tarpaulin 74 | run : | 75 | cargo tarpaulin --out Xml 76 | 77 | - name: Upload to codecov.io 78 | uses: codecov/codecov-action@v1.5.2 79 | 80 | - name: Run cargo-deny 81 | uses: EmbarkStudios/cargo-deny-action@v2 82 | 83 | 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target 4 | /examples/**/target 5 | /endpoint/target 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | **/*.rs.bak 14 | 15 | # Ignore flamegraph output 16 | flamegraph.svg 17 | perf.data 18 | perf.data.old 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | # Need to cache the whole `.cargo` directory to keep .crates.toml for 4 | # cargo-update to work 5 | # 6 | cache: 7 | directories: 8 | - /home/travis/.cargo 9 | 10 | # But don't cache the cargo registry 11 | # and remove wasm-pack binary to avoid the installer asking confirmation for overwriting it. 12 | # 13 | before_cache: 14 | - rm -rf /home/travis/.cargo/git 15 | - rm -rf /home/travis/.cargo/registry 16 | - rm -rf /home/travis/.cargo/bin/cargo-tarpaulin 17 | - rm -rf target/debug/incremental/{ws_stream_tungstenite,build_script_build}-* 18 | - rm -rf target/debug/.fingerprint/ws_stream_tungstenite-* 19 | - rm -rf target/debug/build/ws_stream_tungstenite-* 20 | - rm -rf target/debug/deps/libws_stream_tungstenite-* 21 | - rm -rf target/debug/deps/ws_stream_tungstenite-* 22 | - rm -rf target/debug/{ws_stream_tungstenite,libws_stream_tungstenite}.d 23 | - cargo clean -p ws_stream_tungstenite 24 | 25 | 26 | branches: 27 | only: 28 | - master 29 | - dev 30 | 31 | jobs: 32 | 33 | include: 34 | 35 | - name: linux stable rust 36 | os : linux 37 | rust: stable 38 | 39 | script: 40 | - bash ci/test.bash 41 | 42 | 43 | - name: linux nightly rust 44 | os : linux 45 | dist: bionic # required for tarpaulin binary distribution to work. 46 | rust: nightly 47 | 48 | addons: 49 | apt: 50 | packages: 51 | - libssl-dev # for cargo-tarpaulin 52 | 53 | 54 | script: 55 | - bash ci/test.bash 56 | - bash ci/doc.bash 57 | - bash ci/coverage.bash 58 | 59 | 60 | - name: osx stable rust 61 | os : osx 62 | rust: stable 63 | 64 | script: 65 | - bash ci/test.bash 66 | 67 | 68 | - name: windows stable rust 69 | os : windows 70 | rust: stable 71 | 72 | script: 73 | - bash ci/test.bash 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Design of ws_stream_tungstenite 2 | 3 | ## Obtaining AsyncRead/Write over a tungstenite websocket. 4 | 5 | The WsStream::new function takes a `WebSocketStream` from _async-tungstenite_. Users are to setup their connection with _async-tungstenite_, keeping the implementation of _ws_stream_tungstenite_ free of low level details like transports and tls encryption. 6 | 7 | Note that in order to work correctly, even though WebSocketStream takes `S: AsyncRead + AsyncWrite`, tungstenite itself will work on `S: std::io::Read + std::io::Write`, so the impls of the std traits must use `std::io::ErrorKind::WouldBlock` to indicate pending state and must wake up tasks that are waiting on this pending state. 8 | 9 | The WsStream object implements AsyncRead/Write (futures 0.3). Currently the ecosystem has 2 versions of these traits (futures and tokio-io). We will see how the ecosystem evolves, in order to determine which version(s) to support. For the moment the version from the futures library is implemented. This can be framed with `futures-codec`. Currently there is a bug in 0.2.5 version of futures-codec, which is fixed in the master branch, so use a patch in Cargo.toml until 0.2.6 comes out. 10 | 11 | TODO: impl AsyncRead/Write from tokio 0.2 as well. 12 | 13 | ## Obtaining information about the websocket connection. 14 | 15 | WsStream is observable through pharos. It has an event stream wich will contain: 16 | - errors from underlying layers (tcp, tungstenite) 17 | - errors from ws_stream_tungstenite (we don't accept websocket text messages, only binary) 18 | - Ping event, the remote pinged us and we responded. This contains the data from the ping. 19 | - Close event, when we received a close frame from the remote endpoint. This contains the close frame with code and reason. 20 | 21 | There are several reasons for out of band error notification: 22 | - AsyncRead/Write can only return `std::io::Error`. The variants of `std::io::ErrorKind` don't always allow conveying all meaning of an underlying `tungstenite::Error`. 23 | - A stream framed with a codec (futures-codec) will always return `None` after an error was returned. However some errors are not fatal and we need to perform a close handshake. For that to work with tokio-tungstenite we need to keep polling the stream in order to drive the close handshake to completion. This is not possible if the framed implementation returns `None` prematurely. Thus we can only return fatal errors in band. 24 | - Since we create an AsyncRead/Write for the binary data of the connection, we cannot return websocket control frames in band. These can be close frames that contain a close code and reason which might be relevant for client code or they might want to log them. 25 | 26 | ## Closing the connection 27 | 28 | The WsStream object takes ownership of the WebSocketSteam which in turn takes ownership of the underlying connection, so when dropping WsStream you close the underlying connection, which should only be done if: 29 | - the websocket close handshake is complete (on the server), the client should wait for the server to close the underlying connection as defined by the websocket rfc (6455). 30 | - a fatal error happened 31 | 32 | Dropping the connection earlier will be considered a websocket protocol error by the remote endpoint. 33 | 34 | When a close handshake is in progress, the only way to drive it to completion is by continuing to `poll_read` the WsStream object. In general, you can call `while let Some(msg) = stream.next().await` on a framed and split WsStream. **This means that client code should create a loop over the incoming stream and never break from it unless it returns `None` or an error or the remote endpoint does not close the connection in a timely manner**. 35 | 36 | If you want to close the connection, call `close` on the `futures::io::WriteHalf` (will resolve immediately) and keep polling the `futures::io::ReadHalf` until it returns `None`. You might start a timer at this moment to drop the connection if the remote does not acknowledge the close handshake in a timely manner and break from your read loop to avoid hanging for too long. 37 | 38 | When the `futures::io::ReadHalf` returns `None`, it is always safe to drop WsStream. 39 | 40 | When you try to send data out over the `futures::io::WriteHalf` and the connection is already closed, `std::io::ErrorKind::NotConnected` will be returned and the only sensible thing to do with the `futures::io::WriteHalf` is to drop it. 41 | 42 | When the remote endpoint initiates the close handshake, you can detect this through the event stream, tungstenite will ìmmediately schedule a close acknowledgement (and as long as you keep polling the `futures::io::ReadHalf` it will actually get sent out too). So as soon as we receive the close frame, the close handshake is considered complete and sending will return `std::io::ErrorKind::NotConnected`. 43 | 44 | After the close handshake is complete (your endpoint has both received and sent a close frame), if you are the client, you shall be waiting for the server to close the underlying connection. `WsStream::poll_read` will not return `None` on a client until the server has closed the connection. Here too, you might want to set a timer and drop the connection if the server does not close in a timely manner. 45 | 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ws_stream_tungstenite Changelog 2 | 3 | ## [Unreleased] 4 | 5 | [Unreleased]: https://github.com/najamelan/ws_stream_tungstenite/compare/release...dev 6 | 7 | 8 | ## [0.15] - 2025-06-08 9 | 10 | [0.15.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.14.0...0.15.0 11 | 12 | - **BREAKING_CHANGE**: update async-tungstenite to 0.29 13 | - **BREAKING_CHANGE**: update tungstenite to 0.26 14 | - remove all panics from the library 15 | - add ssl example 16 | 17 | 18 | ## [0.14.0] - 2024-09-08 19 | 20 | [0.14.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.13.0...0.14.0 21 | 22 | - **BREAKING_CHANGE**: update async-tungstenite to 0.28 23 | - **BREAKING_CHANGE**: update tungstenite to 0.24 24 | 25 | 26 | ## [0.13.0] - 2024-02-16 27 | 28 | [0.13.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.12.0...0.13.0 29 | 30 | - **BREAKING_CHANGE**: update async-tungstenite to 0.25 31 | 32 | 33 | ## [0.12.0] - 2024-02-09 34 | 35 | [0.12.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.11.0...0.12.0 36 | 37 | - **BREAKING_CHANGE**: update async-tungstenite to 0.24 38 | - **BREAKING_CHANGE**: update tungstenite to 0.21 39 | 40 | 41 | ## [0.11.0] - 2023-10-07 42 | 43 | [0.11.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.10.0...0.11.0 44 | 45 | - **BREAKING_CHANGE**/**SECURITY UPDATE**: update tungstenite to 0.20.1. 46 | See: [RUSTSEC-2023-0065](https://rustsec.org/advisories/RUSTSEC-2023-0065). 47 | Make sure to check how the new version of tungstenite 48 | [handles buffering](https://docs.rs/tungstenite/latest/tungstenite/protocol/struct.WebSocketConfig.html) 49 | messages before sending them. Having `write_buffer_size` to anything but `0` might cause 50 | messages not to be sent until you flush. _ws_stream_tungstenite_ will make sure to respect 51 | `max_write_buffer_size`, so you shouldn't have to deal with the errors, but note that if 52 | you set it to something really small it might lead to performance issues on throughput. 53 | I wanted to roll this version out fast for the security vulnerability, but note that the 54 | implementation of `AsyncWrite::poll_write_vectored` that handles compliance with `max_write_buffer_size` 55 | currently has no tests. If you want to use it, please review the code. 56 | - **BREAKING_CHANGE**: update async-tungstenite to 0.23 57 | - **BREAKING_CHANGE**: switched to tracing for logging (check out the _tracing-log_ crate 58 | if you need to consume the events with a log consumer) 59 | 60 | 61 | ## [0.10.0] 62 | 63 | [0.10.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.9.0...0.10.0 64 | 65 | - **BREAKING_CHANGE**: update async-tungstenite to 0.22 66 | - **BREAKING_CHANGE**: update tungstenite to 0.19 67 | 68 | 69 | ## [0.9.0] 70 | 71 | [0.9.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.8.0...0.9.0 72 | 73 | - **BREAKING_CHANGE**: update tungstenite to 0.18 74 | 75 | 76 | ## [0.8.0] 77 | 78 | [0.8.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.7.0...0.8.0 79 | 80 | - **BREAKING_CHANGE**: update tungstenite to 0.17 81 | 82 | 83 | ## [0.7.0] 84 | 85 | [0.7.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.6.1...0.7.0 86 | 87 | - **BREAKING_CHANGE**: update dependencies 88 | - In search of the cause of: https://github.com/najamelan/ws_stream_tungstenite/issues/7 error reporting 89 | has been improved. However until now have been unable to reproduce the issue. 90 | 91 | 92 | ## [0.6.2] 93 | 94 | YANKED: has become 0.7.0 because it was a breaking change. 95 | 96 | ## [0.6.1] 97 | 98 | [0.6.1]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.6.0...0.6.1 99 | 100 | ### Updated 101 | - switched to asynchronous-codec from futures-codec. 102 | - fixed external_doc removal in rustdoc 1.54. 103 | - fixed assert_matches ambiguity on nightly. 104 | 105 | 106 | ## [0.6.0] - 2021-02-18 107 | 108 | [0.6.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.5.0...0.6.0 109 | 110 | ### Updated 111 | - **BREAKING_CHANGE**: Update tungstenite and async-tungstenite to 0.13 112 | - **BREAKING_CHANGE**: Update pharos to 0.5 113 | - Update async_io_stream to 0.3 114 | 115 | ## [0.5.0] - 2021-02-11 116 | 117 | [0.5.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.4.0...0.5.0 118 | 119 | ### Updated 120 | - **BREAKING_CHANGE**: Update tungstenite and async-tungstenite to 0.12 121 | 122 | ## [0.4.0] - 2021-01-01 123 | 124 | [0.4.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.4.0-beta.2...0.4.0 125 | 126 | ### Updated 127 | - **BREAKING_CHANGE**: Update tokio to v1 128 | 129 | ## [0.4.0-beta.2] - 2020-11-23 130 | 131 | [0.4.0-beta.2]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.4.0-beta.1...0.4.0-beta.2 132 | 133 | ### Fixed 134 | - **BREAKING_CHANGE**: do not enable default features on tungstenite 135 | - remove thiserror. 136 | 137 | 138 | ## [0.4.0-beta.1] - 2020-11-03 139 | 140 | [0.4.0-beta.1]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.3.0...0.4.0-beta.1 141 | 142 | ### Updated 143 | - **BREAKING_CHANGE**: update tokio to 0.3 and async-tungstenite to 0.10. Will go out of beta when tokio releases 1.0 144 | 145 | 146 | ## [0.3.0] - 2020-10-01 147 | 148 | [0.3.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.2.0...0.3.0 149 | 150 | ### Updated 151 | - **BREAKING_CHANGE**: update async-tungstenite to 0.8 and tungstenite to 0.11 152 | 153 | 154 | ## [0.2.0] - 2020-06-10 155 | 156 | [0.2.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.1.0...0.2.0 157 | 158 | ### Updated 159 | - **BREAKING_CHANGE**: update async-tungstenite to 0.5 160 | 161 | ### Fixed 162 | - correct a documentation mistake 163 | 164 | 165 | ## [0.1.0] - 2020-03-21 166 | 167 | [0.1.0]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.1.0-alpha.5...0.1.0 168 | 169 | ### Added 170 | - **BREAKING_CHANGE**: Switch to async_tungstenite as backend, we are now framework agnostic 171 | - Implement tokio `AsyncRead`/`AsyncWrite` for WsStream (Behind a feature flag). 172 | 173 | ### Fixed 174 | - **BREAKING_CHANGE**: Rename error type to WsErr 175 | - delegate implementation of `AsyncRead`/`AsyncWrite`/`AsyncBufRead` to _async_io_stream_. This allows 176 | sharing the functionality with _ws_stream_wasm_, fleshing it out to always fill and use entire buffers, 177 | polling the underlying stream several times if needed. 178 | - only build for default target on docs.rs. 179 | - exclude unneeded files from package build. 180 | - remove trace and debug statements. 181 | 182 | 183 | ## [0.1.0-alpha.5] - 2019-11-14 184 | 185 | [0.1.0-alpha.5]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.1.0-alpha.4...0.1.0-alpha.5 186 | 187 | ### Updated 188 | - update to futures 0.3.1. 189 | 190 | 191 | ## [0.1.0-alpha.4] - 2019-10-07 192 | 193 | [0.1.0-alpha.4]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.1.0-alpha.2...0.1.0-alpha.4 194 | 195 | ### Fixed 196 | - now handle sending out a close frame correctly when we decide to close, like when we receive a text message. 197 | - Non fatal errors from underlying tokio-tungstenite stream are now returned out of band. This allows to keep 198 | polling the stream until the close handshake is completed and it returns `None`. This is not possible for 199 | errors returned from `poll_read`, since codecs will no longer poll a stream as soon as it has returned an error. 200 | 201 | 202 | ## [0.1.0-alpha.2] - 2019-09-17 203 | 204 | [0.1.0-alpha.2]: https://github.com/najamelan/ws_stream_tungstenite/compare/0.1.0-alpha.1...0.1.0-alpha.2 205 | 206 | ### Fixed 207 | - fix docs.rs readme 208 | - add CI testing 209 | - fix clippy warnings 210 | 211 | ## 0.1.0-alpha.1 - 2019-09-17 Initial release 212 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This repository accepts contributions. Ideas, questions, feature requests and bug reports can be filed through Github issues. 2 | 3 | Pull Requests are welcome on Github. By committing pull requests, you accept that your code might be modified and reformatted to fit the project coding style or to improve the implementation. Contributed code is considered licensed under the same license as the rest of the project unless explicitly agreed otherwise. See the `LICENCE` file. 4 | 5 | Please discuss what you want to see modified before filing a pull request if you don't want to be doing work that might be rejected. 6 | 7 | # git workflow 8 | 9 | Please file PR's against the `dev` branch, don't forget to update the documentation. 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Auto-generated from "Cargo.yml" 2 | [badges] 3 | [badges.maintenance] 4 | status = "actively-developed" 5 | 6 | [badges.travis-ci] 7 | repository = "najamelan/ws_stream_tungstenite" 8 | 9 | [build-dependencies] 10 | rustc_version = "^0.4" 11 | 12 | [dependencies] 13 | [dependencies.async-tungstenite] 14 | default-features = false 15 | features = ["futures-03-sink"] 16 | version = "^0.29" 17 | 18 | [dependencies.async_io_stream] 19 | default-features = false 20 | features = ["map_pharos"] 21 | version = "^0.3" 22 | 23 | [dependencies.bitflags] 24 | default-features = false 25 | version = "^2" 26 | 27 | [dependencies.futures-core] 28 | default-features = false 29 | version = "^0.3" 30 | 31 | [dependencies.futures-io] 32 | default-features = false 33 | version = "^0.3" 34 | 35 | [dependencies.futures-sink] 36 | default-features = false 37 | version = "^0.3" 38 | 39 | [dependencies.futures-util] 40 | default-features = false 41 | version = "^0.3" 42 | 43 | [dependencies.pharos] 44 | default-features = false 45 | version = "^0.5" 46 | 47 | [dependencies.tokio] 48 | default-features = false 49 | optional = true 50 | version = "^1" 51 | 52 | [dependencies.tracing] 53 | version = "^0.1" 54 | 55 | [dependencies.tungstenite] 56 | default-features = false 57 | version = "^0.26" 58 | 59 | [dev-dependencies] 60 | assert_matches = "^1" 61 | async_progress = "^0.2" 62 | asynchronous-codec = "^0.7" 63 | futures = "^0.3" 64 | futures-test = "^0.3" 65 | futures-timer = "^3" 66 | futures_ringbuf = "^0.4" 67 | pin-utils = "^0.1" 68 | tracing-log = "^0.2" 69 | url = "^2" 70 | 71 | [dev-dependencies.async-std] 72 | features = ["attributes"] 73 | version = "^1" 74 | 75 | [dev-dependencies.async-tungstenite] 76 | features = ["tokio-runtime", "async-std-runtime", "url"] 77 | version = "^0.29" 78 | 79 | [dev-dependencies.tokio] 80 | default-features = false 81 | features = ["net", "rt", "rt-multi-thread", "macros"] 82 | version = "^1" 83 | 84 | [dev-dependencies.tokio-util] 85 | default-features = false 86 | features = ["codec"] 87 | version = "^0.7" 88 | 89 | [dev-dependencies.tracing-subscriber] 90 | default-features = false 91 | features = ["ansi", "env-filter", "fmt", "json", "tracing-log"] 92 | version = "^0.3" 93 | 94 | [[example]] 95 | name = "tokio_codec" 96 | path = "examples/tokio_codec.rs" 97 | required-features = ["tokio_io"] 98 | 99 | [features] 100 | default = [] 101 | tokio_io = ["tokio", "async_io_stream/tokio_io"] 102 | 103 | [package] 104 | authors = ["Naja Melan "] 105 | categories = ["asynchronous", "network-programming"] 106 | description = "Provide AsyncRead/AsyncWrite over Tungstenite WebSockets" 107 | documentation = "https://docs.rs/ws_stream_tungstenite" 108 | edition = "2024" 109 | exclude = ["tests", "examples", "ci", ".travis.yml", "TODO.md", "CONTRIBUTING.md", "ARCHITECTURE.md"] 110 | homepage = "https://github.com/najamelan/ws_stream_tungstenite" 111 | keywords = ["websocket", "tokio", "stream", "async", "futures"] 112 | license = "Unlicense" 113 | name = "ws_stream_tungstenite" 114 | readme = "README.md" 115 | repository = "https://github.com/najamelan/ws_stream_tungstenite" 116 | version = "0.15.0" 117 | 118 | [package.metadata] 119 | [package.metadata.docs] 120 | [package.metadata.docs.rs] 121 | all-features = true 122 | targets = [] 123 | 124 | [profile] 125 | [profile.release] 126 | codegen-units = 1 127 | -------------------------------------------------------------------------------- /Cargo.yml: -------------------------------------------------------------------------------- 1 | package: 2 | 3 | # When releasing to crates.io: 4 | # 5 | # - last check for all TODO, FIXME, expect, unwrap. 6 | # - recheck log statements (informative, none left that were just for development, ...) 7 | # - `cargo +nightly doc` and re-read and final polish of documentation. 8 | # 9 | # - Update CHANGELOG.md. 10 | # - Update version numbers in Cargo.yml, Cargo.toml, install section of readme. 11 | # 12 | # - `cargo update --aggressive --verbose` 13 | # - `cargo outdated --root-deps-only` 14 | # - `cargo +nightly udeps --all-targets --all-features` 15 | # - `cargo clippy --tests --examples --benches --all-features` 16 | # - `cargo audit` 17 | # - `cargo crev crate verify --show-all --recursive` and review. 18 | # - 'cargo test --all-targets --all-features' 19 | # 20 | # - push dev and verify CI result 21 | # - `cargo test` on dependent crates 22 | # 23 | # - `cargo publish` 24 | # - `git checkout master && git merge dev --no-ff` 25 | # - `git tag x.x.x` with version number. 26 | # - `git push && git push --tags` 27 | # 28 | version : 0.15.0 29 | name : ws_stream_tungstenite 30 | edition : '2024' 31 | authors : [ Naja Melan ] 32 | description : Provide AsyncRead/AsyncWrite over Tungstenite WebSockets 33 | license : Unlicense 34 | homepage : https://github.com/najamelan/ws_stream_tungstenite 35 | repository : https://github.com/najamelan/ws_stream_tungstenite 36 | documentation : https://docs.rs/ws_stream_tungstenite 37 | readme : README.md 38 | keywords : [ websocket, tokio, stream, async, futures ] 39 | categories : [ asynchronous, network-programming ] 40 | exclude : [ tests, examples, ci, .travis.yml, TODO.md, CONTRIBUTING.md, ARCHITECTURE.md ] 41 | 42 | metadata: 43 | docs: 44 | rs: 45 | all-features: true 46 | targets : [] 47 | 48 | 49 | badges: 50 | 51 | maintenance : { status : actively-developed } 52 | travis-ci : { repository : najamelan/ws_stream_tungstenite } 53 | 54 | 55 | features: 56 | 57 | default: [] 58 | 59 | # Implement AsyncRead/AsyncWrite from tokio 60 | # 61 | tokio_io: [ tokio, async_io_stream/tokio_io ] 62 | 63 | 64 | dependencies: 65 | 66 | # public deps. Bump major version if you change their version number here. 67 | # 68 | futures-core : { version: ^0.3 , default-features: false } 69 | futures-sink : { version: ^0.3 , default-features: false } 70 | futures-io : { version: ^0.3 , default-features: false } 71 | futures-util : { version: ^0.3 , default-features: false } 72 | tungstenite : { version: ^0.26, default-features: false } 73 | pharos : { version: ^0.5 , default-features: false } 74 | async-tungstenite : { version: ^0.29, default-features: false, features: ["futures-03-sink"] } 75 | tokio : { version: ^1 , default-features: false, optional: true } 76 | tracing : { version: ^0.1 } 77 | 78 | # private deps 79 | # 80 | bitflags : { version: ^2, default-features: false } 81 | async_io_stream : { version: ^0.3, features: [ map_pharos ], default-features: false } 82 | 83 | 84 | dev-dependencies: 85 | 86 | async-std : { version: ^1, features: [ attributes ] } 87 | async-tungstenite : { version: ^0.29, features: [ tokio-runtime, async-std-runtime, url ] } 88 | assert_matches : ^1 89 | async_progress : ^0.2 90 | futures : ^0.3 91 | futures-test : ^0.3 92 | futures-timer : ^3 93 | asynchronous-codec : ^0.7 94 | futures_ringbuf : ^0.4 95 | # pretty_assertions : ^0.6 96 | tokio : { version: ^1, default-features: false, features: [ net, rt, rt-multi-thread, macros ] } 97 | tokio-util : { version: ^0.7, default-features: false, features: [ codec ] } 98 | tracing-subscriber : { version: ^0.3, default-features: false, features: [ ansi, env-filter, fmt, json, tracing-log ] } 99 | tracing-log : ^0.2 100 | 101 | # tokio-stream : { version: ^0.1, default-features: false, features: [] } 102 | url : ^2 103 | pin-utils : ^0.1 104 | 105 | 106 | build-dependencies: 107 | 108 | rustc_version: ^0.4 109 | 110 | 111 | profile: 112 | 113 | release: 114 | 115 | codegen-units: 1 116 | 117 | example: 118 | 119 | - name : tokio_codec 120 | path : examples/tokio_codec.rs 121 | required-features: [ tokio_io ] 122 | 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ws_stream_tungstenite 2 | 3 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 4 | [![Build Status](https://github.com/najamelan/ws_stream_tungstenite/workflows/ci/badge.svg?branch=master)](https://github.com/najamelan/ws_stream_tungstenite/actions) 5 | [![Docs](https://docs.rs/ws_stream_tungstenite/badge.svg)](https://docs.rs/ws_stream_tungstenite) 6 | [![crates.io](https://img.shields.io/crates/v/ws_stream_tungstenite.svg)](https://crates.io/crates/ws_stream_tungstenite) 7 | 8 | 9 | > Provide an AsyncRead/Write/AsyncBufRead over websockets that can be framed with a codec. 10 | 11 | This crate provides `AsyncRead`/`AsyncWrite`/`AsyncBufRead` over _async-tungstenite_ websockets. It mainly enables working with rust wasm code and communicating over a framed stream of bytes. This crate provides the functionality for non-WASM targets (eg. server side). 12 | There is a WASM version [available here](https://crates.io/crates/ws_stream_wasm) for the client side. 13 | 14 | NOTE: _async_tungstenite_ now implements `AsyncRead`/`AsyncWrite` through [ByteReader/ByteWriter](https://docs.rs/async-tungstenite/latest/async_tungstenite/bytes/index.html). If this proves to be satisfactory (still have to test), I may deprecate _ws_stream_tungstenite_. 15 | 16 | There are currently 2 versions of the AsyncRead/Write traits. The _futures-rs_ version and the _tokio_ version. You need to enable the features `tokio_io` if you want the _tokio_ version of the traits implemented. 17 | 18 | You might wonder, why not just serialize your struct and send it in websocket messages. First of all, on wasm there wasn't a convenient websocket rust crate before I released _ws_stream_wasm_, even without `AsyncRead`/`AsyncWrite`. Next, this allows you to keep your code generic by just taking `AsyncRead`/`AsyncWrite` instead of adapting it to a specific protocol like websockets, which is especially useful in library crates. Furthermore you don't need to deal with the quirks of a websocket protocol and library. This just works almost like any other async byte stream (exception: [closing the connection](#how-to-close-a-connection)). There is a little bit of extra overhead due to this indirection, but it should be small. 19 | 20 | _ws_stream_tungstenite_ works on top of _async-tungstenite_, so you will have to use the API from _async-tungstenite_ to setup your 21 | connection and pass the [`WebSocketStream`](async_tungstenite::WebSocketStream) to [`WsStream`]. You will need to turn on either the 22 | `tokio-runtime` or the `async-std-runtime` feature on _async-tungstenite_ or you will get a compilation error in _ws_stream_tungstenite_ 23 | because of a missing `Sink` implementation on the underlying stream. 24 | 25 | 26 | ## Table of Contents 27 | 28 | - [Install](#install) 29 | - [Upgrade](#upgrade) 30 | - [Dependencies](#dependencies) 31 | - [Security](#security) 32 | - [Usage](#usage) 33 | - [Example](#example) 34 | - [How to close a connection](#how-to-close-a-connection) 35 | - [Error Handling](#error-handling) 36 | - [Limitations](#limitations) 37 | - [API](#api) 38 | - [References](#references) 39 | - [Contributing](#contributing) 40 | - [Code of Conduct](#code-of-conduct) 41 | - [License](#license) 42 | 43 | 44 | ## Install 45 | 46 | With [cargo add](https://github.com/killercup/cargo-edit): 47 | `cargo add ws_stream_tungstenite` 48 | 49 | With [cargo yaml](https://gitlab.com/storedbox/cargo-yaml): 50 | ```yaml 51 | dependencies: 52 | 53 | ws_stream_tungstenite: ^0.15 54 | ``` 55 | 56 | With raw Cargo.toml 57 | ```toml 58 | [dependencies] 59 | 60 | ws_stream_tungstenite = "0.15" 61 | ``` 62 | 63 | ### Upgrade 64 | 65 | Please check out the [changelog](https://github.com/najamelan/ws_stream_tungstenite/blob/master/CHANGELOG.md) when upgrading. 66 | 67 | ### Dependencies 68 | 69 | This crate has few dependencies. Cargo will automatically handle it's dependencies for you. 70 | 71 | ### Security 72 | 73 | This crate uses `#![ forbid( unsafe_code ) ]`, but our dependencies don't. 74 | 75 | Make sure your codecs have a max message size. 76 | 77 | 78 | ### Features 79 | 80 | The `tokio_io` features enables implementing the `AsyncRead` and `AsyncWrite` traits from _tokio_. 81 | 82 | 83 | ## Usage 84 | 85 | Please have a look in the [examples directory of the repository](https://github.com/najamelan/ws_stream_tungstenite/tree/master/examples). 86 | 87 | The [integration tests](https://github.com/najamelan/ws_stream_tungstenite/tree/master/tests) are also useful. 88 | 89 | 90 | ### Example 91 | 92 | This is the most basic idea (for client code): 93 | 94 | ```rust, no_run 95 | use 96 | { 97 | ws_stream_tungstenite :: { * } , 98 | futures :: { StreamExt } , 99 | tracing :: { * } , 100 | async_tungstenite :: { accept_async } , 101 | asynchronous_codec :: { LinesCodec, Framed } , 102 | async_std :: { net::TcpListener } , 103 | }; 104 | 105 | #[ async_std::main ] 106 | // 107 | async fn main() -> Result<(), std::io::Error> 108 | { 109 | let socket = TcpListener::bind( "127.0.0.1:3012" ).await?; 110 | let mut connections = socket.incoming(); 111 | 112 | let tcp = connections.next().await.expect( "1 connection" ).expect( "tcp connect" ); 113 | let s = accept_async( tcp ).await.expect( "ws handshake" ); 114 | let ws = WsStream::new( s ); 115 | 116 | // ws here is observable with pharos to detect non fatal errors and ping/close events, which cannot 117 | // be represented in the AsyncRead/Write API. See the events example in the repository. 118 | 119 | let (_sink, mut stream) = Framed::new( ws, LinesCodec {} ).split(); 120 | 121 | 122 | while let Some( msg ) = stream.next().await 123 | { 124 | let msg = match msg 125 | { 126 | Err(e) => 127 | { 128 | error!( "Error on server stream: {:?}", e ); 129 | 130 | // Errors returned directly through the AsyncRead/Write API are fatal, generally an error on the underlying 131 | // transport. 132 | // 133 | continue; 134 | } 135 | 136 | Ok(m) => m, 137 | }; 138 | 139 | 140 | info!( "server received: {}", msg.trim() ); 141 | 142 | // ... do something useful 143 | } 144 | 145 | // safe to drop the TCP connection 146 | 147 | Ok(()) 148 | } 149 | ``` 150 | 151 | 152 | ### How to close a connection 153 | 154 | The websocket RFC specifies the close handshake, summarized as follows: 155 | - when an endpoint wants to close the connection, it sends a close frame and after that it sends no more data. 156 | Since the other endpoint might still be sending data, it's best to continue processing incoming data, until: 157 | - the remote sends an acknowledgment of the close frame. 158 | - after an endpoint has both sent and received a close frame, the connection is considered closed and the server 159 | is to close the underlying TCP connection. The client can chose to close it if the server doesn't in a timely manner. 160 | 161 | Properly closing the connection with _ws_stream_tungstenite_ is pretty simple. If the remote endpoint initiates the close, 162 | just polling the stream will make sure the connection is kept until the handshake is finished. When the stream 163 | returns `None`, you're good to drop it. 164 | 165 | If you want to initiate the close, call close on the sink. From then on, the situation is identical to above. 166 | Just poll the stream until it returns `None` and you're good to go. 167 | 168 | Tungstenite will return `None` on the client only when the server closes the underlying connection, so it will 169 | make sure you respect the websocket protocol. 170 | 171 | If you initiate the close handshake, you might want to race a timeout and drop the connection if the remote 172 | endpoint doesn't finish the close handshake in a timely manner. See the close.rs example in 173 | [examples directory of the repository](https://github.com/najamelan/ws_stream_tungstenite/tree/master/examples) 174 | for how to do that. 175 | 176 | 177 | ### Error handling 178 | 179 | _ws_stream_tungstenite_ is about `AsyncRead`/`AsyncWrite`, so we only accept binary messages. If we receive a websocket text message, 180 | that's considered a protocol error. 181 | 182 | For detailed instructions, please have a look at the API docs for [`WsStream`]. Especially at the impls for `AsyncRead`/`AsyncWrite`, which detail all possible errors you can get. 183 | 184 | Since `AsyncRead`/`AsyncWrite` only allow `std::io::Error` to be returned and on the stream some errors might not be fatal, but codecs will often consider any error to be fatal, errors are returned out of band through pharos. You should observe the `WsStream` and in the very least log any errors that are reported. 185 | 186 | 187 | ### Limitations 188 | 189 | - No API is provided to send out Ping messages. Solving this would imply making a `WsMeta` type like 190 | _ws_stream_wasm_. 191 | - Received text messages are considered an error. Another option we could consider is to return 192 | these to client code out of band rather than including them in the data for `AsyncRead`/`AsyncWrite`. 193 | This is also inconsistent with _ws_stream_wasm_ which calls `to_bytes` on them and includes the bytes 194 | in the bytestream. 195 | 196 | 197 | ### API 198 | 199 | Api documentation can be found on [docs.rs](https://docs.rs/ws_stream_tungstenite). 200 | 201 | 202 | ## References 203 | 204 | The reference documents for understanding websockets and how the browser handles them are: 205 | - [RFC 6455 - The WebSocket Protocol](https://tools.ietf.org/html/rfc6455) 206 | - security of ws: [WebSockets not Bound by SOP and CORS? Does this mean…](https://blog.securityevaluators.com/websockets-not-bound-by-cors-does-this-mean-2e7819374acc?gi=e4a712f5f982) 207 | - another: [Cross-Site WebSocket Hijacking (CSWSH)](https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html) 208 | 209 | 210 | ## Contributing 211 | 212 | Please check out the [contribution guidelines](https://github.com/najamelan/ws_stream_tungstenite/blob/master/CONTRIBUTING.md). 213 | 214 | 215 | ### Testing 216 | 217 | `cargo test --all-features` 218 | 219 | 220 | ### Code of conduct 221 | 222 | Any of the behaviors described in [point 4 "Unacceptable Behavior" of the Citizens Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md#4-unacceptable-behavior) are not welcome here and might get you banned. If anyone including maintainers and moderators of the project fail to respect these/your limits, you are entitled to call them out. 223 | 224 | ## License 225 | 226 | [Unlicence](https://unlicense.org/) 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - implement into_inner so people can send a manual websocket message like close with status. https://github.com/najamelan/ws_stream_tungstenite/issues/6 4 | - check crate template for changes. 5 | - ci passes if uploading code coverage fails. 6 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Detect the rustc channel 2 | // 3 | use rustc_version::{ version_meta, Channel }; 4 | 5 | fn main() 6 | { 7 | // Needed to avoid warnings for: 8 | // https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html 9 | // 10 | println!("cargo::rustc-check-cfg=cfg(stable)"); 11 | println!("cargo::rustc-check-cfg=cfg(beta)"); 12 | println!("cargo::rustc-check-cfg=cfg(nightly)"); 13 | println!("cargo::rustc-check-cfg=cfg(rustc_dev)"); 14 | 15 | // Set cfg flags depending on release channel 16 | // 17 | match version_meta().unwrap().channel 18 | { 19 | Channel::Stable => println!( "cargo:rustc-cfg=stable" ), 20 | Channel::Beta => println!( "cargo:rustc-cfg=beta" ), 21 | Channel::Nightly => println!( "cargo:rustc-cfg=nightly" ), 22 | Channel::Dev => println!( "cargo:rustc-cfg=rustc_dev" ), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ci/clippy.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # fail fast 4 | # 5 | set -e 6 | 7 | # print each command before it's executed 8 | # 9 | set -x 10 | 11 | cargo clean 12 | cargo +nightly clippy --tests --examples --benches --all-features -- -D warnings 13 | 14 | -------------------------------------------------------------------------------- /ci/doc.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # fail fast 4 | # 5 | set -e 6 | 7 | # print each command before it's executed 8 | # 9 | set -x 10 | 11 | # only works on nightly because of features like doc_cfg and external_doc 12 | # 13 | cargo +nightly doc --all-features --no-deps 14 | cargo +nightly test --all-features --doc 15 | -------------------------------------------------------------------------------- /ci/test.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # fail fast 4 | # 5 | set -e 6 | 7 | # print each command before it's executed 8 | # 9 | set -x 10 | 11 | cargo test --all-features 12 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | 13 | [graph] 14 | # If 1 or more target triples (and optionally, target_features) are specified, 15 | # only the specified targets will be checked when running `cargo deny check`. 16 | # This means, if a particular package is only ever used as a target specific 17 | # dependency, such as, for example, the `nix` crate only being used via the 18 | # `target_family = "unix"` configuration, that only having windows targets in 19 | # this list would mean the nix crate, as well as any of its exclusive 20 | # dependencies not shared by any other crates, would be ignored, as the target 21 | # list here is effectively saying which targets you are building for. 22 | targets = [ 23 | # The triple can be any string, but only the target triples built in to 24 | # rustc (as of 1.40) can be checked against actual config expressions 25 | #{ triple = "x86_64-unknown-linux-musl" }, 26 | # You can also specify which target_features you promise are enabled for a 27 | # particular target. target_features are currently not validated against 28 | # the actual valid features supported by the target architecture. 29 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 30 | ] 31 | 32 | # This section is considered when running `cargo deny check advisories` 33 | # More documentation for the advisories section can be found here: 34 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 35 | [advisories] 36 | # The path where the advisory database is cloned/fetched into 37 | db-path = "~/.cargo/advisory-db" 38 | # The url of the advisory database to use 39 | db-urls = [ "https://github.com/rustsec/advisory-db" ] 40 | # A list of advisory IDs to ignore. Note that ignored advisories will still 41 | # output a note when they are encountered. 42 | ignore = [ 43 | #"RUSTSEC-0000-0000", 44 | ] 45 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 46 | # lower than the range specified will be ignored. Note that ignored advisories 47 | # will still output a note when they are encountered. 48 | # * None - CVSS Score 0.0 49 | # * Low - CVSS Score 0.1 - 3.9 50 | # * Medium - CVSS Score 4.0 - 6.9 51 | # * High - CVSS Score 7.0 - 8.9 52 | # * Critical - CVSS Score 9.0 - 10.0 53 | #severity-threshold = 54 | 55 | # This section is considered when running `cargo deny check licenses` 56 | # More documentation for the licenses section can be found here: 57 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 58 | [licenses] 59 | # List of explictly allowed licenses 60 | # See https://spdx.org/licenses/ for list of possible licenses 61 | # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. 62 | allow = [ 63 | "MIT", 64 | "Apache-2.0", 65 | # "Apache-2.0 WITH LLVM-exception", 66 | "Unlicense", 67 | "Unicode-3.0", 68 | ] 69 | # The confidence threshold for detecting a license from license text. 70 | # The higher the value, the more closely the license text must be to the 71 | # canonical license text of a valid SPDX license file. 72 | # [possible values: any between 0.0 and 1.0]. 73 | confidence-threshold = 0.8 74 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 75 | # aren't accepted for every possible crate as with the normal allow list 76 | exceptions = [ 77 | # Each entry is the crate and version constraint, and its specific allow 78 | # list 79 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 80 | ] 81 | 82 | # Some crates don't have (easily) machine readable licensing information, 83 | # adding a clarification entry for it allows you to manually specify the 84 | # licensing information 85 | #[[licenses.clarify]] 86 | # The name of the crate the clarification applies to 87 | #name = "ring" 88 | # THe optional version constraint for the crate 89 | #version = "*" 90 | # The SPDX expression for the license requirements of the crate 91 | #expression = "MIT AND ISC AND OpenSSL" 92 | # One or more files in the crate's source used as the "source of truth" for 93 | # the license expression. If the contents match, the clarification will be used 94 | # when running the license check, otherwise the clarification will be ignored 95 | # and the crate will be checked normally, which may produce warnings or errors 96 | # depending on the rest of your configuration 97 | #license-files = [ 98 | # Each entry is a crate relative path, and the (opaque) hash of its contents 99 | #{ path = "LICENSE", hash = 0xbd0eed23 } 100 | #] 101 | 102 | [licenses.private] 103 | # If true, ignores workspace crates that aren't published, or are only 104 | # published to private registries 105 | ignore = false 106 | # One or more private registries that you might publish crates to, if a crate 107 | # is only published to private registries, and ignore is true, the crate will 108 | # not have its license(s) checked 109 | registries = [ 110 | #"https://sekretz.com/registry 111 | ] 112 | 113 | # This section is considered when running `cargo deny check bans`. 114 | # More documentation about the 'bans' section can be found here: 115 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 116 | [bans] 117 | # Lint level for when multiple versions of the same crate are detected 118 | multiple-versions = "warn" 119 | # The graph highlighting used when creating dotgraphs for crates 120 | # with multiple versions 121 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 122 | # * simplest-path - The path to the version with the fewest edges is highlighted 123 | # * all - Both lowest-version and simplest-path are used 124 | highlight = "all" 125 | # List of crates that are allowed. Use with care! 126 | allow = [ 127 | #{ name = "ansi_term", version = "=0.11.0" }, 128 | ] 129 | # List of crates to deny 130 | deny = [ 131 | # Each entry the name of a crate and a version range. If version is 132 | # not specified, all versions will be matched. 133 | 134 | { name = "plutonium" }, # Crate intended to hide unsafe usage. 135 | { name = "fake-static" }, # Crate intended to demonstrate soundness bug in safe code. 136 | ] 137 | # Certain crates/versions that will be skipped when doing duplicate detection. 138 | skip = [ 139 | #{ name = "ansi_term", version = "=0.11.0" }, 140 | ] 141 | # Similarly to `skip` allows you to skip certain crates during duplicate 142 | # detection. Unlike skip, it also includes the entire tree of transitive 143 | # dependencies starting at the specified crate, up to a certain depth, which is 144 | # by default infinite 145 | skip-tree = [ 146 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 147 | ] 148 | 149 | # This section is considered when running `cargo deny check sources`. 150 | # More documentation about the 'sources' section can be found here: 151 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 152 | [sources] 153 | # Lint level for what to happen when a crate from a crate registry that is not 154 | # in the allow list is encountered 155 | unknown-registry = "warn" 156 | # Lint level for what to happen when a crate from a git repository that is not 157 | # in the allow list is encountered 158 | unknown-git = "warn" 159 | # List of URLs for allowed crate registries. Defaults to the crates.io index 160 | # if not specified. If it is specified but empty, no registries are allowed. 161 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 162 | # List of URLs for allowed Git repositories 163 | allow-git = [] 164 | -------------------------------------------------------------------------------- /examples/close.rs: -------------------------------------------------------------------------------- 1 | // This example explores how to properly close a connection. 2 | // 3 | use 4 | { 5 | ws_stream_tungstenite :: { * } , 6 | futures :: { TryFutureExt, StreamExt, SinkExt, join, executor::block_on } , 7 | asynchronous_codec :: { LinesCodec, Framed } , 8 | tokio :: { net::{ TcpListener } } , 9 | futures :: { FutureExt, select, future::{ ok, ready } } , 10 | async_tungstenite :: { accept_async, tokio::{ TokioAdapter, connect_async } } , 11 | url :: { Url } , 12 | tracing :: { * } , 13 | std :: { time::Duration } , 14 | futures_timer :: { Delay } , 15 | pin_utils :: { pin_mut } , 16 | }; 17 | 18 | 19 | 20 | fn main() 21 | { 22 | block_on( async 23 | { 24 | join!( server(), client() ); 25 | 26 | }); 27 | } 28 | 29 | 30 | // Server task. 31 | // Let's assume we are mainly reacting to incoming requests, doing some processing and send a response. 32 | // 33 | async fn server() 34 | { 35 | let socket = TcpListener::bind( "127.0.0.1:3012" ).await.unwrap(); 36 | 37 | let (tcp, _peer_addr) = socket.accept().await.expect( "1 connection" ); 38 | let s = accept_async( TokioAdapter::new(tcp) ).await.expect( "ws handshake" ); 39 | let ws = WsStream::new( s ); 40 | 41 | let (mut sink, mut stream) = Framed::new( ws, LinesCodec {} ).split(); 42 | 43 | 44 | // Loop over incoming websocket messages. When this loop ends, we can safely drop the tcp connection. 45 | // 46 | while let Some( msg ) = stream.next().await 47 | { 48 | let msg = match msg 49 | { 50 | Err(e) => 51 | { 52 | error!( "Error on server stream: {:?}", e ); 53 | 54 | // if possible ws_stream will try to close the connection with a clean handshake, 55 | // but if the error persists, it will just give up and return None next iteration, 56 | // after which we should drop the connection. 57 | // 58 | continue; 59 | } 60 | 61 | Ok(m) => m, 62 | }; 63 | 64 | 65 | info!( "server received: {}", msg.trim() ); 66 | 67 | // Do some actual business logic, maybe send a response. Have a look at the impl of 68 | // AsyncWrite for WsStream for documentation on possible errors. 69 | // 70 | match sink.send( "Response from server\n".to_string() ).await 71 | { 72 | Ok(_) => {} 73 | 74 | Err(e) => 75 | { 76 | error!( "Server error happend on send: {}", e ); 77 | 78 | break; 79 | } 80 | } 81 | 82 | // If we decide to disconnect, we could close `sink`. That will initiate 83 | // the close handshake. We can continue processing incoming data until the client acknowledges 84 | // the close frame, after which this loop will receive None and stop. Note that we should not 85 | // send any more messages after we call sink.close. 86 | // 87 | // Once that happens it's safe to drop the underlying connection. 88 | // 89 | // debug!( "close server side" ); 90 | // sink.close().await.expect( "close out" ); 91 | 92 | // If the remote decides to close, the websocket close handshake will be handled 93 | // automatically for you and the stream will return None once that's finished, so you can 94 | // drop the connection. 95 | } 96 | 97 | // This allows us to test that the client doesn't hang if the server doesn't close the connection 98 | // in a timely matter. When uncommenting this line you will see the order of shutdown reverse. 99 | // 100 | Delay::new( Duration::from_secs(3) ).await; 101 | 102 | info!( "server end" ); 103 | } 104 | 105 | 106 | // We make requests to the server and receive responses. 107 | // 108 | async fn client() 109 | { 110 | let url = Url::parse( "ws://127.0.0.1:3012" ).unwrap(); 111 | let socket = ok( url ).and_then( connect_async ).await.expect( "ws handshake" ); 112 | let ws = WsStream::new( socket.0 ); 113 | 114 | 115 | // This is a bit unfortunate, but the websocket specs say that the server should 116 | // close the underlying tcp connection. Hovever, since are the client, we want to 117 | // use a timeout just in case the server does not close the connection after a reasonable 118 | // amount of time. Thus when we want to initiate the close, we need to keep polling 119 | // the stream to drive the close handshake to completion, but we also want to be able 120 | // to cancel waiting on the stream if it takes to long. Thus we need to break from our 121 | // normal processing loop and thus need to mark here whether we broke because we want 122 | // to close or because the server has already closed and the stream already returned 123 | // None. 124 | // 125 | let mut our_shutdown = false; 126 | 127 | let (mut sink, mut stream) = Framed::new( ws, LinesCodec {} ).split(); 128 | 129 | 130 | // Do some actual business logic 131 | // This could run in a separate task. 132 | // 133 | sink.send( "Hi from client\n".to_string() ).await.expect( "send request" ); 134 | 135 | 136 | while let Some( msg ) = stream.next().await 137 | { 138 | let msg = match msg 139 | { 140 | Err(e) => 141 | { 142 | error!( "Error on client stream: {:?}", e ); 143 | 144 | // if possible ws_stream will try to close the connection with a clean handshake, 145 | // but if the error persists, it will just give up and return None next iteration, 146 | // after which we should drop the connection. 147 | // 148 | continue; 149 | } 150 | 151 | Ok(m) => m, 152 | }; 153 | 154 | 155 | info!( "client received: {}", msg.trim() ); 156 | 157 | // At some point client decides to disconnect. We will still have to poll the stream 158 | // to be sure the close handshake get's driven to completion, but we need to timeout 159 | // if the server never closes the connection. So we need to break from this loop and 160 | // notify the following code that the break was because we close and not because it 161 | // returned None. 162 | // 163 | debug!( "close client side" ); 164 | sink.close().await.expect( "close out" ); 165 | our_shutdown = true; 166 | break; 167 | } 168 | 169 | 170 | // This takes care of the timeout 171 | // 172 | if our_shutdown 173 | { 174 | // We want a future that ends when the stream ends, and that polls it in the mean time. 175 | // We don't want to consider any more messages from the server though. 176 | // 177 | let stream_end = stream.for_each( |_| ready(()) ).fuse(); 178 | let timeout = Delay::new( Duration::from_secs(1) ).fuse(); 179 | 180 | pin_mut!( timeout ); 181 | pin_mut!( stream_end ); 182 | 183 | // select over timer and end of stream 184 | // 185 | select! 186 | { 187 | _ = timeout => {} 188 | _ = stream_end => {} 189 | } 190 | } 191 | 192 | info!( "client end" ); 193 | } 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /examples/echo.rs: -------------------------------------------------------------------------------- 1 | //! This is an echo server that returns all incoming bytes, without framing. It is used for the tests in 2 | //! ws_stream_wasm. 3 | // 4 | use 5 | { 6 | ws_stream_tungstenite :: { * } , 7 | futures :: { AsyncReadExt, io::{ BufReader, copy_buf } } , 8 | std :: { env, net::SocketAddr, io } , 9 | tracing :: { * } , 10 | tokio :: { net::{ TcpListener, TcpStream } } , 11 | async_tungstenite :: { accept_async, tokio::{ TokioAdapter } } , 12 | }; 13 | 14 | 15 | #[tokio::main] 16 | // 17 | async fn main() 18 | { 19 | // flexi_logger::Logger::with_str( "echo=trace, ws_stream_tungstenite=debug, tungstenite=warn, tokio_tungstenite=warn, tokio=warn" ).start().unwrap(); 20 | 21 | let addr: SocketAddr = env::args().nth(1).unwrap_or_else( || "127.0.0.1:3212".to_string() ).parse().unwrap(); 22 | println!( "server task listening at: {}", &addr ); 23 | 24 | let socket = TcpListener::bind(&addr).await.unwrap(); 25 | 26 | loop 27 | { 28 | tokio::spawn( handle_conn( socket.accept().await ) ); 29 | } 30 | } 31 | 32 | 33 | async fn handle_conn( stream: Result< (TcpStream, SocketAddr), io::Error> ) 34 | { 35 | // If the TCP stream fails, we stop processing this connection 36 | // 37 | let (tcp_stream, peer_addr) = match stream 38 | { 39 | Ok( tuple ) => tuple, 40 | 41 | Err(e) => 42 | { 43 | debug!( "Failed TCP incoming connection: {}", e ); 44 | return; 45 | } 46 | }; 47 | 48 | let s = accept_async( TokioAdapter::new(tcp_stream) ).await; 49 | 50 | // If the Ws handshake fails, we stop processing this connection 51 | // 52 | let socket = match s 53 | { 54 | Ok(ws) => ws, 55 | 56 | Err(e) => 57 | { 58 | debug!( "Failed WebSocket HandShake: {}", e ); 59 | return; 60 | } 61 | }; 62 | 63 | 64 | info!( "Incoming connection from: {}", peer_addr ); 65 | 66 | let ws_stream = WsStream::new( socket ); 67 | let (reader, mut writer) = ws_stream.split(); 68 | 69 | // BufReader allows our AsyncRead to work with a bigger buffer than the default 8k. 70 | // This improves performance quite a bit. 71 | // 72 | if let Err(e) = copy_buf( BufReader::with_capacity( 64_000, reader ), &mut writer ).await 73 | { 74 | error!( "{:?}", e.kind() ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/echo_tt.rs: -------------------------------------------------------------------------------- 1 | //! An echo server using just tokio tungstenite. This allows comparing the 2 | //! performance overhead of ws_stream and allows testing ws_stream_wasm for text 3 | //! messages as ws_stream only does binary. 4 | // 5 | use 6 | { 7 | futures :: { StreamExt } , 8 | async_tungstenite :: { accept_async, tokio::TokioAdapter } , 9 | tokio :: { net::{ TcpListener, TcpStream } } , 10 | tracing :: { * } , 11 | std :: { env, net::SocketAddr } , 12 | }; 13 | 14 | 15 | #[tokio::main] 16 | // 17 | async fn main() 18 | { 19 | // flexi_logger::Logger::with_str( "echo_tt=trace, tokio=trace, tungstenite=trace, tokio_tungstenite=trace" ).start().unwrap(); 20 | 21 | let addr: SocketAddr = env::args().nth(1).unwrap_or_else( || "127.0.0.1:3212".to_string() ).parse().unwrap(); 22 | let socket = TcpListener::bind( addr ).await.unwrap(); 23 | 24 | println!( "Listening on: {addr}" ); 25 | 26 | 27 | loop 28 | { 29 | tokio::spawn( handle_conn( socket.accept().await ) ); 30 | } 31 | } 32 | 33 | 34 | async fn handle_conn( conn: Result< (TcpStream, SocketAddr), std::io::Error > ) 35 | { 36 | // If the TCP stream fails, we stop processing this connection 37 | // 38 | let (tcp_stream, peer_addr) = match conn 39 | { 40 | Ok(tuple) => tuple, 41 | Err(_) => 42 | { 43 | debug!( "Failed TCP incoming connection" ); 44 | return; 45 | } 46 | }; 47 | 48 | 49 | let handshake = accept_async( TokioAdapter::new(tcp_stream) ); 50 | 51 | 52 | // If the Ws handshake fails, we stop processing this connection 53 | // 54 | let ttung = match handshake.await 55 | { 56 | Ok( ws ) => ws, 57 | 58 | Err(_) => 59 | { 60 | debug!( "Failed WebSocket HandShake" ); 61 | return; 62 | } 63 | }; 64 | 65 | let (sink, stream) = ttung.split(); 66 | 67 | println!( "New WebSocket connection: {peer_addr}" ); 68 | 69 | 70 | match stream.forward( sink ).await 71 | { 72 | Ok(()) => {}, 73 | 74 | Err(e) => match e 75 | { 76 | // When the client closes the connection, the stream will return None, but then 77 | // `forward` will call poll_close on the sink, which obviously is the same connection, 78 | // and thus already closed. Thus we will always get a ConnectionClosed error at the end of 79 | // this, so we ignore it. 80 | // 81 | // In principle this risks missing the error if it happens before the connection is 82 | // supposed to end, so in production code you should probably manually implement forward 83 | // for an echo server. 84 | // 85 | tungstenite::error::Error::ConnectionClosed | 86 | tungstenite::error::Error::AlreadyClosed => {} 87 | 88 | // Other errors we want to know about 89 | // 90 | _ => { panic!( "{}", e ) } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/ssl/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Auto-generated from "Cargo.yml" 2 | [dependencies] 3 | futures = "^0.3" 4 | pin-utils = "^0.1" 5 | rustls = "^0.23" 6 | tokio-rustls = "^0.26" 7 | url = "^2" 8 | 9 | [dependencies.async-std] 10 | features = ["attributes"] 11 | version = "^1" 12 | 13 | [dependencies.async-tungstenite] 14 | features = ["tokio-runtime", "async-std-runtime", "url"] 15 | version = "^0.29" 16 | 17 | [dependencies.tokio] 18 | default-features = false 19 | features = ["net", "rt", "rt-multi-thread", "macros"] 20 | version = "^1" 21 | 22 | [dependencies.tracing] 23 | version = "^0.1" 24 | 25 | [dependencies.ws_stream_tungstenite] 26 | path = "../../" 27 | 28 | [package] 29 | edition = "2021" 30 | name = "ws_stream_tungstenite_ssl" 31 | version = "0.1.0" 32 | -------------------------------------------------------------------------------- /examples/ssl/Cargo.yml: -------------------------------------------------------------------------------- 1 | package: 2 | name: ws_stream_tungstenite_ssl 3 | version: 0.1.0 4 | edition: "2021" 5 | 6 | dependencies: 7 | async-std : { version: ^1, features: [ attributes ] } 8 | async-tungstenite : { version: ^0.29, features: [ tokio-runtime, async-std-runtime, url ] } 9 | futures : ^0.3 10 | pin-utils : ^0.1 11 | tokio : { version: ^1, default-features: false, features: [ net, rt, rt-multi-thread, macros ] } 12 | tokio-rustls : ^0.26 13 | rustls : ^0.23 14 | tracing : { version: ^0.1 } 15 | url : ^2 16 | ws_stream_tungstenite: { path: ../../ } 17 | -------------------------------------------------------------------------------- /examples/ssl/localhost-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJDCCAoygAwIBAgIQL16/LOTZ8I05jZKsfePcejANBgkqhkiG9w0BAQsFADBf 3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExGjAYBgNVBAsMEXVzZXJA 4 | ZGVza3RvcC5ob21lMSEwHwYDVQQDDBhta2NlcnQgdXNlckBkZXNrdG9wLmhvbWUw 5 | HhcNMjUwMjA0MTgyODQ2WhcNMjcwNTA0MTcyODQ2WjBFMScwJQYDVQQKEx5ta2Nl 6 | cnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxGjAYBgNVBAsMEXVzZXJAZGVza3Rv 7 | cC5ob21lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3EOAeiWkKCV 8 | LBfwv9nnXadcpIjy+xFeG9cxuV/srkWlNmmYJZ8Byk7QQRXG8Ogcd61njiqMLFC8 9 | Iqo01rDONnVYIQ0Qy8nQN6UAUGvSs1a1VBxms/DqjCwKgNm4E4rdtdqJPf/WkdoX 10 | SCiHOa7WIrhR+oKn8Qf4i3wmZDifcMBUpTJMfiZFUDeHJXY1c70Jda5m6x0xFhjY 11 | ujgmPAoSBva6y+JUeE4neEOHKg8i2rc7O9YOzCNC97G3Xsz9HbrVqbJj7rbusnaF 12 | jrh+3Dp2TVJhlr5Trfbg5qEB1pOby7fUPJTZ17Gw0AVA4kj2/ap71Blp1QT6kL9O 13 | RszE0Hs8PQIDAQABo3YwdDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB 14 | BQUHAwEwHwYDVR0jBBgwFoAUa3E6xcfKvUpPeyFij09aDvFFZKUwLAYDVR0RBCUw 15 | I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEB 16 | CwUAA4IBgQCgGUnU4wT2ca/wmPBErneTXU+mKquxF8XaNR4ys6SGflFjjeNO9jDT 17 | ubN0BKMvdWvJ6WkGziBQ6skgFXzX7sHteuIHWOcR/RzXyIMuCKgA2JCnt6MhBion 18 | 9A3dC6IM3dNfwjq0RN7lL6Y8M0kmYwEnP3vrU47mYhmcgBt/p0Mf4THj1wzYHC0n 19 | eIjm3s6TpKpcjrRTet2Qz7iJRXA6fN12rT88wCW/BU6c+OZMCM68JsyBIIY4HXIy 20 | zSWqrdLMvkM8tYRBq15JbOhyEZtt/fbV4dxsjjQnvTVXFMXnLeErBiULJp1EfKTe 21 | yUe75oUwbAvJhLoGF++MT5CFtBivmLn2YxKdlV/2mhDL4z8OlkiMy/ag8Rwua3gF 22 | pEBl/jMmz3CAp5A9ugtDIDfiQbCr5ZY+r33Z7pzOX4K8j88pfACLxOmUTQyEYyNq 23 | 1YkiDKUhNE8vk028eMrg/Lc/DdEN/vgwsANIMvo66tLRHwsBZ/pkTTMCnUJED3zN 24 | /DZtQlWyi9I= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /examples/ssl/localhost-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHcQ4B6JaQoJUs 3 | F/C/2eddp1ykiPL7EV4b1zG5X+yuRaU2aZglnwHKTtBBFcbw6Bx3rWeOKowsULwi 4 | qjTWsM42dVghDRDLydA3pQBQa9KzVrVUHGaz8OqMLAqA2bgTit212ok9/9aR2hdI 5 | KIc5rtYiuFH6gqfxB/iLfCZkOJ9wwFSlMkx+JkVQN4cldjVzvQl1rmbrHTEWGNi6 6 | OCY8ChIG9rrL4lR4Tid4Q4cqDyLatzs71g7MI0L3sbdezP0dutWpsmPutu6ydoWO 7 | uH7cOnZNUmGWvlOt9uDmoQHWk5vLt9Q8lNnXsbDQBUDiSPb9qnvUGWnVBPqQv05G 8 | zMTQezw9AgMBAAECggEAAnx9UTbGEb1+vpL51FpyEO/SLtbqOIkJ5Ez1eLsiZzXF 9 | Dn2YgPePfScoX3dXCI/MMw29Wb9cCf1jqwb2K6r+lx01YQwZpoC4kMkEzPvbH5M4 10 | JXlJKnsZYW/3HY4ZRl0X0HmN9nDnCuBuI98leGmej8Y+d2IDl/Sie5Kro9o02qH1 11 | iPDdRolckhA0FtZnuc3E716tsr2dShY4s2f3SmD4W3STAwlwnKp3CUR7G7WsI1YV 12 | ypAmQT1RYU9gndQHpMxDpCtbXuCGATBttXRuOG7mxz4r+HxpmnCeizSqx1hVba1G 13 | bXd0NokbSvNkR3M2+XOTgDEo6DBbUuP0Xy94LrPqwQKBgQDqfAj39B0rTHRvbTgG 14 | g5+mUmCi+qalTFYu9XnYbjexeLi4pHC30i4FZmx+RrO+Axgr5MIboMgFZRIKL6TI 15 | k7LYxtVF9hjxv2WbCQLZ1dAEKs7OZK9SKF2EXxSV13bUzX/WEsI1vZW+B6fqV6Wo 16 | u+D0Qs5uRuHNqVkb+MqqD4A5bQKBgQDZvd+gjjkCYYWhQ4Zjg4sjfGnx3oEwswor 17 | 7AP19dkKQN5K+J1Gwp2QIulndSEo/cdw5ksnsFjaXC2kqNgnvwPG2wfgK2liP7Ly 18 | FDx7tOin+/0VpyDWo+xm7LEWc1Ou3Mi5TyXG4txk0i1TJPXlFC4gjQMyR93EK57U 19 | Qiamcz2cEQKBgF28vf0ZinufscBFoisAfVcJAXbFys6zyJQ3L8F0tjDtOLMfkav6 20 | isk/28lTTFF+fTA2394ZlTyK3f5Sc4Z3fPyp5+Jy8h/aSq0CmjApCGJSFqBtoaSv 21 | XEspk1ofa3LAwAT9NMQ1COKRvu+woBnnGZOsDUgKcAQ+WdAT0jjPv4u1AoGAbmu2 22 | w4I8cPOpw70torurzOQkCg9vbpXtG5cF7y6s0WFSGaz4fVDmfJjnypqApXwFL0Dq 23 | bgclGB9U2kLx0z4wGSEsXkdFmxh4lAElJwr4TXAhyWBG6/KXSR7RM3RqxKucczUr 24 | EDAt2kcnyxlcRb61IbbBHzeIxBnm4vdlCFY6sTECgYBnpRV11foe0SM7QXYm467B 25 | vieK70UMoMvDM+UTNCJ5jPxNKSUBfF1VR/mukl2Fs8XEuhtaGNn79X48b9w7Ma+I 26 | N5wUJcETDnCMLyRiy4Sc2/ujCU55TfsUBZ1OPySsUBHTucfCxicrtwEN4zdfBi87 27 | TFDX88m7xHMXBz48FONzJA== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /examples/ssl/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This is an echo server that returns all incoming bytes, without framing. 2 | //! 3 | //! This demonstrates how to set up an ssl websocket. 4 | // 5 | use 6 | { 7 | async_tungstenite :: { accept_async, tokio::{ TokioAdapter } } , 8 | futures :: { AsyncReadExt, io::{ BufReader, copy_buf } } , 9 | std :: { env, net::SocketAddr, io, sync::Arc } , 10 | tracing :: { * } , 11 | tokio :: { net::{ TcpListener, TcpStream } } , 12 | tokio_rustls :: { rustls::{ ServerConfig },TlsAcceptor } , 13 | rustls::pki_types :: { CertificateDer, PrivateKeyDer, pem::PemObject } , 14 | ws_stream_tungstenite :: { * } , 15 | }; 16 | 17 | 18 | #[tokio::main] 19 | // 20 | async fn main() 21 | { 22 | // flexi_logger::Logger::with_str( "echo=trace, ws_stream_tungstenite=debug, tungstenite=warn, tokio_tungstenite=warn, tokio=warn" ).start().unwrap(); 23 | 24 | // Load self-signed cert and private key 25 | let certs = CertificateDer::pem_file_iter("localhost-cert.pem") 26 | .expect("load certs") 27 | .collect::, _>>() 28 | .expect("load certs2"); 29 | 30 | let key = PrivateKeyDer::from_pem_file("localhost-key.pem") 31 | .expect("load key"); 32 | 33 | // Set up Rustls server config 34 | let config = ServerConfig::builder() 35 | .with_no_client_auth() 36 | .with_single_cert(certs, key) 37 | .expect("invalid key or certificate"); 38 | 39 | let acceptor = TlsAcceptor::from(Arc::new(config)); 40 | 41 | let addr: SocketAddr = env::args().nth(1).unwrap_or_else( || "127.0.0.1:8443".to_string() ).parse().unwrap(); 42 | println!( "server task listening at: {}", &addr ); 43 | 44 | let socket = TcpListener::bind(&addr).await.unwrap(); 45 | 46 | loop 47 | { 48 | let stream = socket.accept().await; 49 | let task = handle_conn( stream, acceptor.clone() ); 50 | tokio::spawn( task ); 51 | } 52 | } 53 | 54 | 55 | async fn handle_conn( stream: Result< (TcpStream, SocketAddr), io::Error>, acceptor: TlsAcceptor ) 56 | { 57 | // If the TCP stream fails, we stop processing this connection 58 | // 59 | let (tcp_stream, peer_addr) = match stream 60 | { 61 | Ok( tuple ) => tuple, 62 | 63 | Err(e) => 64 | { 65 | debug!( "Failed TCP incoming connection: {}", e ); 66 | return; 67 | } 68 | }; 69 | 70 | info!( "Incoming connection from: {}", peer_addr ); 71 | 72 | let tls_stream = match acceptor.accept(tcp_stream).await 73 | { 74 | Ok(s) => s, 75 | 76 | Err(e) => { 77 | eprintln!("TLS accept error from {}: {}", peer_addr, e); 78 | return; 79 | } 80 | }; 81 | 82 | println!("TLS handshake successful: {}", peer_addr); 83 | 84 | let s = accept_async( TokioAdapter::new(tls_stream) ).await; 85 | 86 | 87 | // If the Ws handshake fails, we stop processing this connection 88 | // 89 | let socket = match s 90 | { 91 | Ok(ws) => ws, 92 | 93 | Err(e) => 94 | { 95 | debug!( "Failed WebSocket HandShake: {}", e ); 96 | return; 97 | } 98 | }; 99 | 100 | 101 | let ws_stream = WsStream::new( socket ); 102 | let (reader, mut writer) = ws_stream.split(); 103 | 104 | // BufReader allows our AsyncRead to work with a bigger buffer than the default 8k. 105 | // This improves performance quite a bit. 106 | // 107 | if let Err(e) = copy_buf( BufReader::with_capacity( 64_000, reader ), &mut writer ).await 108 | { 109 | error!( "{:?}", e.kind() ) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/tokio_codec.rs: -------------------------------------------------------------------------------- 1 | #![ cfg( feature = "tokio_io" ) ] 2 | //! 3 | //! Example showing how the stream can be framed with tokio codec. 4 | // 5 | use 6 | { 7 | tungstenite :: { protocol::Role } , 8 | ws_stream_tungstenite :: { * } , 9 | futures :: { StreamExt, SinkExt, future::join } , 10 | tokio_util::codec :: { LinesCodec, Framed } , 11 | futures_ringbuf :: { Endpoint } , 12 | tracing :: { * } , 13 | }; 14 | 15 | 16 | #[ tokio::main ] 17 | // 18 | async fn main() 19 | { 20 | let (server_con, client_con) = Endpoint::pair( 64, 64 ); 21 | 22 | let server = async move 23 | { 24 | let s = async_tungstenite::WebSocketStream::from_raw_socket( server_con, Role::Server, None ).await; 25 | let server = WsStream::new( s ); 26 | 27 | let mut framed = Framed::new( server, LinesCodec::new() ); 28 | 29 | framed.send( "A line" .to_string() ).await.expect( "Send a line" ); 30 | framed.send( "A second line".to_string() ).await.expect( "Send a line" ); 31 | 32 | debug!( "closing server side" ); 33 | , LinesCodec> as futures::SinkExt>::close( &mut framed ).await.expect( "close server" ); 34 | debug!( "closed server side" ); 35 | 36 | let read = framed.next().await.transpose().expect( "close connection" ); 37 | 38 | assert!( read.is_none() ); 39 | debug!( "Server task ended" ); 40 | }; 41 | 42 | 43 | let client = async move 44 | { 45 | let socket = async_tungstenite::WebSocketStream::from_raw_socket( client_con, Role::Client, None ).await; 46 | 47 | let client = WsStream::new( socket ); 48 | let mut framed = Framed::new( client, LinesCodec::new() ); 49 | 50 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a line" ); 51 | assert_eq!( "A line".to_string(), res ); 52 | 53 | 54 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a second line" ); 55 | assert_eq!( "A second line".to_string(), res ); 56 | 57 | let res = framed.next().await; 58 | 59 | assert!( res.is_none() ); 60 | debug!( "Client task ended" ); 61 | }; 62 | 63 | join( server, client ).await; 64 | } 65 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![ cfg_attr( nightly, feature(doc_cfg) ) ] 2 | #![ doc = include_str!("../README.md") ] 3 | 4 | #![ doc ( html_root_url = "https://docs.rs/ws_stream_tungstenite" ) ] 5 | #![ deny ( missing_docs ) ] 6 | #![ forbid ( unsafe_code ) ] 7 | #![ allow ( clippy::suspicious_else_formatting, clippy::too_long_first_doc_paragraph ) ] 8 | 9 | #![ warn 10 | ( 11 | missing_debug_implementations , 12 | missing_docs , 13 | nonstandard_style , 14 | rust_2018_idioms , 15 | trivial_casts , 16 | trivial_numeric_casts , 17 | unused_extern_crates , 18 | unused_qualifications , 19 | single_use_lifetimes , 20 | unreachable_pub , 21 | variant_size_differences , 22 | )] 23 | 24 | 25 | mod ws_stream ; 26 | mod ws_event ; 27 | mod ws_err ; 28 | 29 | pub(crate) mod tung_websocket; 30 | 31 | pub use 32 | { 33 | self::ws_stream :: { WsStream } , 34 | self::ws_event :: { WsEvent } , 35 | self::ws_err :: { WsErr } , 36 | }; 37 | 38 | 39 | mod import 40 | { 41 | pub(crate) use 42 | { 43 | bitflags :: { bitflags } , 44 | futures_core :: { ready, Stream } , 45 | futures_sink :: { Sink } , 46 | futures_io :: { AsyncRead, AsyncWrite, AsyncBufRead } , 47 | futures_util :: { FutureExt } , 48 | tracing :: { error } , 49 | std :: { io, io::{ IoSlice, IoSliceMut }, pin::Pin, fmt } , 50 | std :: { collections::VecDeque, sync::Arc, task::{ Context, Poll } } , 51 | async_tungstenite :: { WebSocketStream as ATungSocket } , 52 | tungstenite :: { Bytes, Message as TungMessage, Error as TungErr, Utf8Bytes } , 53 | tungstenite :: { protocol::{ CloseFrame, frame::coding::CloseCode } } , 54 | pharos :: { Observable, ObserveConfig, Observe, Pharos, PharErr } , 55 | async_io_stream :: { IoStream } , 56 | }; 57 | 58 | 59 | 60 | #[ cfg( feature = "tokio" ) ] 61 | // 62 | pub(crate) use 63 | { 64 | tokio::io::{ AsyncRead as TokAsyncRead, AsyncWrite as TokAsyncWrite }, 65 | }; 66 | 67 | 68 | 69 | #[ cfg( test ) ] 70 | // 71 | pub(crate) use 72 | { 73 | futures :: { executor::block_on, SinkExt, StreamExt } , 74 | futures_test :: { task::noop_waker } , 75 | pharos :: { Channel } , 76 | assert_matches :: { assert_matches } , 77 | futures_ringbuf :: { Endpoint } , 78 | futures :: { future::{ join } } , 79 | tungstenite :: { protocol::{ Role } } , 80 | tracing :: { info, trace } , 81 | }; 82 | } 83 | 84 | -------------------------------------------------------------------------------- /src/tung_websocket.rs: -------------------------------------------------------------------------------- 1 | mod notifier; 2 | mod closer ; 3 | 4 | use 5 | { 6 | crate :: { import::*, WsEvent, WsErr } , 7 | notifier :: { Notifier } , 8 | closer :: { Closer } , 9 | }; 10 | 11 | 12 | bitflags! 13 | { 14 | /// Tasks that are woken up always come from either the poll_read (Stream) method or poll_ready and 15 | /// poll_flush, poll_close methods (Sink). 16 | /// 17 | /// However we inject extra work (Sending a close frame from read and Sending events on pharos). 18 | /// When these tasks return pending, we want to return pending from our user facing poll methods. 19 | /// However when progress can be made these will wake up the task and it's our user facing methods 20 | /// that get called first. This state allows tracking which sub tasks are in progress and need to be resumed. 21 | /// 22 | /// SINK_CLOSED is used to keep track of any state where we should no longer send anything into the sink 23 | /// (eg. it returned an error). In that case, we might still poll the stream to drive a close handshake 24 | /// to completion. 25 | // 26 | struct State: u8 27 | { 28 | const NOTIFIER_PEND = 0x01; 29 | const CLOSER_PEND = 0x02; 30 | const PHAROS_CLOSED = 0x04; 31 | const SINK_CLOSED = 0x08; 32 | const STREAM_CLOSED = 0x10; 33 | } 34 | } 35 | 36 | 37 | 38 | 39 | /// A wrapper around a WebSocket provided by tungstenite. This provides Stream/Sink Vec to 40 | /// simplify implementing AsyncRead/AsyncWrite on top of async-tungstenite. 41 | // 42 | pub(crate) struct TungWebSocket where S: AsyncRead + AsyncWrite + Send + Unpin 43 | { 44 | inner: ATungSocket , 45 | 46 | state : State , 47 | notifier : Notifier , 48 | closer : Closer , 49 | } 50 | 51 | 52 | impl TungWebSocket where S: AsyncRead + AsyncWrite + Send + Unpin 53 | { 54 | /// Create a new Wrapper for a WebSocket provided by Tungstenite 55 | // 56 | pub(crate) fn new( inner: ATungSocket ) -> Self 57 | { 58 | Self 59 | { 60 | inner , 61 | state : State ::empty() , 62 | notifier : Notifier::new() , 63 | closer : Closer ::new() , 64 | } 65 | } 66 | 67 | 68 | // Check whether there is messages queued up for notification. 69 | // Returns Pending until all of them are processed. 70 | // 71 | fn check_notify( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll<()> 72 | { 73 | if !self.state.contains( State::NOTIFIER_PEND ) 74 | { 75 | return ().into(); 76 | } 77 | 78 | match ready!( self.notifier.run( cx ) ) 79 | { 80 | Ok (_) => {} 81 | Err(_) => self.state.insert( State::PHAROS_CLOSED ), 82 | } 83 | 84 | self.state.remove( State::NOTIFIER_PEND ); 85 | 86 | ().into() 87 | } 88 | 89 | 90 | // Queue a new event to be delivered to observers. 91 | // 92 | fn queue_event( &mut self, evt: WsEvent ) 93 | { 94 | // It should only happen if we call close on it, and we should never do that. 95 | // 96 | debug_assert! 97 | ( 98 | !self.state.contains( State::PHAROS_CLOSED ), 99 | "If this happens, it's a bug in ws_stream_tungstenite, please report." 100 | ); 101 | 102 | self.notifier.queue( evt ); 103 | 104 | self.state.insert( State::NOTIFIER_PEND ); 105 | } 106 | 107 | 108 | // Take care of sending a close frame to tungstenite. 109 | // 110 | // Will return pending until the entire sending operation is finished. We still need to poll 111 | // the stream to drive the handshake to completion. 112 | // 113 | fn send_closeframe( &mut self, code: CloseCode, reason: Utf8Bytes, cx: &mut Context<'_> ) -> Poll<()> 114 | { 115 | // If the sink is already closed, don't try to send any more close frames. 116 | // 117 | if !self.state.contains( State::SINK_CLOSED ) 118 | { 119 | // As soon as we are closing, accept no more messages for writing. 120 | // 121 | self.state.insert( State::SINK_CLOSED ); 122 | self.state.insert( State::CLOSER_PEND ); 123 | 124 | let _ = self.closer.queue( CloseFrame{ code, reason } ) 125 | 126 | .map_err( |_| { error!("ws_stream_tungstenite should not queue 2 close frames."); } ) 127 | ; 128 | } 129 | 130 | self.check_closer( cx ) 131 | } 132 | 133 | 134 | // Check whether there is a close frame in progress of being sent. 135 | // Returns Pending the underlying sink is flushed. 136 | // 137 | fn check_closer( &mut self, cx: &mut Context<'_> ) -> Poll<()> 138 | { 139 | if !self.state.contains( State::CLOSER_PEND ) 140 | { 141 | return ().into(); 142 | } 143 | 144 | 145 | if ready!( Pin::new( &mut self.closer ).run( &mut self.inner, &mut self.notifier, cx) ).is_err() 146 | { 147 | self.state.insert( State::SINK_CLOSED ); 148 | } 149 | 150 | self.state.remove( State::CLOSER_PEND ); 151 | 152 | 153 | // Since closer might have queued events, before returning, make sure they are flushed. 154 | // 155 | Pin::new( self ).check_notify( cx ) 156 | } 157 | } 158 | 159 | 160 | 161 | impl Stream for TungWebSocket where S: AsyncRead + AsyncWrite + Send 162 | { 163 | type Item = Result; 164 | 165 | 166 | /// Get the next websocket message and convert it to a Vec. 167 | /// 168 | /// When None is returned, it means it is safe to drop the underlying connection. Even after calling 169 | /// close on the sink, this should be polled until it returns None to drive the close handshake to completion. 170 | /// 171 | /// ### Errors 172 | /// 173 | /// Errors are mostly returned out of band as events through pharos::Observable. Only fatal errors are returned 174 | /// directly from this method. 175 | /// 176 | /// The following errors can be returned from this method: 177 | /// 178 | /// - std::io::Error generally mean something went wrong on the underlying transport. Consider these fatal 179 | /// and just drop the connection. 180 | // 181 | fn poll_next( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll< Option > 182 | { 183 | // Events can provide back pressure with bounded channels. If this is pending, we don't 184 | // do anything else that might generate more events before these have been delivered. 185 | // 186 | ready!( self.as_mut().check_notify( cx ) ); 187 | 188 | // If we are in the middle of sending out a close frame, make sure that is finished before 189 | // doing any further polling for incoming messages. 190 | // 191 | // After check_closer finishes it's own work, it calls check_notify because it might have 192 | // added new events to the queue. 193 | // 194 | ready!( self.as_mut().check_closer( cx ) ); 195 | 196 | // Never poll tungstenite if we already got note that the connection is ready to be dropped. 197 | // 198 | if self.state.contains( State::STREAM_CLOSED ) 199 | { 200 | return None.into(); 201 | } 202 | 203 | 204 | // Do actual reading from stream. 205 | // 206 | let res = ready!( Pin::new( &mut self.inner ).poll_next( cx ) ); 207 | 208 | 209 | match res 210 | { 211 | None => 212 | { 213 | // if tungstenite is returning None here, we should no longer try to send a pending close frame. 214 | // 215 | self.state.remove( State::CLOSER_PEND ); 216 | self.state.insert( State::STREAM_CLOSED ); 217 | 218 | None.into() 219 | } 220 | 221 | 222 | Some(Ok( msg )) => 223 | { 224 | match msg 225 | { 226 | TungMessage::Binary(bytes) => Some(Ok( bytes )).into(), 227 | 228 | 229 | TungMessage::Text(_) => 230 | { 231 | self.queue_event( WsEvent::Error(Arc::new( WsErr::ReceivedText )) ); 232 | 233 | let string = "Text messages are not supported."; 234 | 235 | // If this returns pending, we don't want to recurse, the task will be woken up. 236 | // 237 | ready!( self.as_mut().send_closeframe( CloseCode::Unsupported, string.into(), cx ) ); 238 | 239 | // Continue to drive the event and the close handshake before returning. 240 | // 241 | self.poll_next( cx ) 242 | } 243 | 244 | 245 | TungMessage::Close(opt) => 246 | { 247 | self.queue_event( WsEvent::CloseFrame( opt )); 248 | 249 | // Tungstenite will keep this stream around until the underlying connection closes. 250 | // It's important we don't return None here so clients don't drop the underlying connection 251 | // while the other end is still processing stuff, otherwise they receive a connection reset 252 | // error and can't read any more data waiting to be processed. 253 | // 254 | self.poll_next( cx ) 255 | } 256 | 257 | 258 | // Tungstenite will have answered it already 259 | // 260 | TungMessage::Ping(data) => 261 | { 262 | self.queue_event( WsEvent::Ping(data) ); 263 | self.poll_next( cx ) 264 | } 265 | 266 | TungMessage::Pong(data) => 267 | { 268 | self.queue_event( WsEvent::Pong(data) ); 269 | self.poll_next( cx ) 270 | } 271 | 272 | TungMessage::Frame(_) => 273 | { 274 | error!( "A Message::Frame(..) should be never occur from a read" ); 275 | Poll::Ready(Some(Err(io::ErrorKind::Other.into()))) 276 | } 277 | } 278 | } 279 | 280 | 281 | 282 | Some(Err( err )) => 283 | { 284 | // See the wildcard at the bottom for why we need this. 285 | // 286 | #[ allow( clippy::wildcard_in_or_patterns )] 287 | // 288 | match err 289 | { 290 | // Just return None, as no more data will come in. 291 | // This can mean tungstenite state is Terminated and we can safely drop the underlying connection. 292 | // Note that tungstenite only set's this on the client after the server has closed the underlying 293 | // connection, to comply with the RFC. 294 | // 295 | TungErr::ConnectionClosed | 296 | TungErr::AlreadyClosed => 297 | { 298 | self.state.insert( State::STREAM_CLOSED ); 299 | 300 | self.queue_event( WsEvent::Closed ); 301 | self.poll_next( cx ) 302 | } 303 | 304 | 305 | // This generally means the underlying transport is broken. Tungstenite will keep bubbling up the 306 | // same error over and over, consider this fatal. 307 | // 308 | TungErr::Io(e) => 309 | { 310 | self.state.insert( State::STREAM_CLOSED ); 311 | self.queue_event( WsEvent::Error(Arc::new( WsErr::from( io::Error::from(e.kind()) ) )) ); 312 | 313 | Some(Err(e)).into() 314 | } 315 | 316 | 317 | // In principle this can fail. If the sendqueue of tungstenite is full, it will return 318 | // an error and the close frame will stay in the Send future, or in the buffer of the 319 | // compat sink, but the point is that it's impossible to create a full send queue with 320 | // the API we provide. 321 | // 322 | // On every call to write on WsStream, we create a full ws message and the poll_write 323 | // only 324 | // 325 | TungErr::Protocol( ref proto_err ) => 326 | { 327 | // If this returns pending, we don't want to recurse, the task will be woken up. 328 | // 329 | ready!( self.as_mut().send_closeframe( CloseCode::Protocol, proto_err.to_string().into(), cx ) ); 330 | 331 | 332 | self.queue_event( WsEvent::Error( Arc::new( WsErr::from(err) )) ); 333 | 334 | 335 | // Continue to drive the event and the close handshake before returning. 336 | // 337 | self.poll_next( cx ) 338 | } 339 | 340 | // This also means the remote sent a text message which isn't supported anyway, so we don't much care 341 | // for the utf errors 342 | // 343 | TungErr::Utf8 => 344 | { 345 | let string = "Text messages are not supported"; 346 | 347 | self.queue_event( WsEvent::Error( Arc::new( WsErr::from(err) )) ); 348 | 349 | // If this returns pending, we don't want to recurse, the task will be woken up. 350 | // 351 | ready!( self.as_mut().send_closeframe( CloseCode::Unsupported, string.into(), cx ) ); 352 | 353 | // Continue to drive the event and the close handshake before returning. 354 | // 355 | self.poll_next( cx ) 356 | } 357 | 358 | 359 | // The capacity for the tungstenite read buffer is currently usize::max, and there is 360 | // no way for clients to change that, so this should never happen. 361 | // 362 | TungErr::Capacity(_) => 363 | { 364 | self.queue_event( WsEvent::Error( Arc::new( WsErr::from(err) )) ); 365 | self.poll_next( cx ) 366 | } 367 | 368 | 369 | // I hope none of these can occur here because they are either handshake errors 370 | // or buffer capacity errors. 371 | // 372 | // This should only happen in the write side: 373 | // 374 | TungErr::WriteBufferFull(_) | 375 | 376 | // These are handshake errors: 377 | // 378 | TungErr::Url (_) | 379 | 380 | // I'd rather have this match exhaustive, but tungstenite has a Tls variant that 381 | // is only there if they have a feature enabled. Since we cannot check whether 382 | // a feature is enabled on a dependency, we have to go for wildcard here. 383 | // As of tungstenite 0.19 Http and HttpFormat are also behind a feature flag. 384 | // 385 | _ => { 386 | error!( "{:?}", err ); 387 | Poll::Ready(Some(Err(io::ErrorKind::Other.into()))) 388 | } 389 | } 390 | } 391 | } 392 | } 393 | } 394 | 395 | 396 | 397 | impl Sink for TungWebSocket where S: AsyncRead + AsyncWrite + Send + Unpin 398 | { 399 | type Error = io::Error; 400 | 401 | 402 | fn poll_ready( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 403 | { 404 | // If we were busy closing, first finish that. Will return on pending or OK. 405 | // 406 | ready!( self.as_mut().check_closer( cx ) ); 407 | 408 | // Are there any events waiting for which we should inform observers? 409 | // 410 | ready!( self.as_mut().check_notify( cx ) ); 411 | 412 | 413 | if self.state.contains( State::SINK_CLOSED ) 414 | { 415 | return Err( io::ErrorKind::NotConnected.into() ).into() 416 | } 417 | 418 | 419 | Pin::new( &mut self.inner ).poll_ready( cx ).map_err( |e| 420 | { 421 | // TODO: It's not quite clear whether the stream can remain functional when we get a sink error, 422 | // but since this is a duplex connection, and poll_next also tries to send out close frames 423 | // through the stream, just consider sink errors fatal. 424 | // 425 | self.state.insert( State::STREAM_CLOSED ); 426 | to_io_error( e ) 427 | }) 428 | } 429 | 430 | 431 | /// ### Errors 432 | /// 433 | /// The following errors can be returned when writing to the stream: 434 | /// 435 | /// - [`io::ErrorKind::NotConnected`]: This means that the connection is already closed. You should 436 | /// no longer write to it. It is safe to drop the underlying connection when `poll_next` returns None. 437 | /// 438 | /// TODO: if error capacity get's returned, is the socket still usable? 439 | /// 440 | /// - [`io::ErrorKind::InvalidData`]: This means that a tungstenite::error::Capacity occurred. This means that 441 | /// you send in a buffer bigger than the maximum message size configured on the underlying websocket connection. 442 | /// If you did not set it manually, the default for tungstenite is 64MB. 443 | /// 444 | /// - other std::io::Error's generally mean something went wrong on the underlying transport. Consider these fatal 445 | /// and just drop the connection as soon as `poll_next` returns None. 446 | // 447 | fn start_send( mut self: Pin<&mut Self>, item: Bytes ) -> Result<(), Self::Error> 448 | { 449 | if self.state.contains( State::SINK_CLOSED ) 450 | { 451 | return Err( io::ErrorKind::NotConnected.into() ) 452 | } 453 | 454 | 455 | Pin::new( &mut self.inner ).start_send( TungMessage::Binary(item) ).map_err( |e| 456 | { 457 | // TODO: It's not quite clear whether the stream can remain functional when we get a sink error, 458 | // but since this is a duplex connection, and poll_next also tries to send out close frames 459 | // through the stream, just consider sink errors fatal. 460 | // 461 | self.state.insert( State::STREAM_CLOSED ); 462 | to_io_error( e ) 463 | }) 464 | } 465 | 466 | /// This will do a send under the hood, so the same errors as from start_send can occur here. 467 | // 468 | fn poll_flush( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 469 | { 470 | Pin::new( &mut self.inner ).poll_flush( cx ).map_err( |e| 471 | { 472 | // TODO: It's not quite clear whether the stream can remain functional when we get a sink error, 473 | // but since this is a duplex connection, and poll_next also tries to send out close frames 474 | // through the stream, just consider sink errors fatal. 475 | // 476 | self.state.insert( State::STREAM_CLOSED ); 477 | to_io_error( e ) 478 | }) 479 | } 480 | 481 | 482 | /// Will resolve immediately. Keep polling the stream until it returns None. To make sure 483 | /// to keep the underlying connection alive until the close handshake is finished. 484 | /// 485 | /// This will do a send under the hood, so the same errors as from start_send can occur here, 486 | /// except InvalidData. 487 | // 488 | fn poll_close( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 489 | { 490 | self.state.insert( State::SINK_CLOSED ); 491 | 492 | // We ignore closed errors since that's what we want, and because after calling this method 493 | // the sender task can in any case be dropped, and verifying that the connection can actually 494 | // be closed should be done through the reader task. 495 | // 496 | Pin::new( &mut self.inner ).poll_close( cx ).map_err( |e| 497 | { 498 | // TODO: It's not quite clear whether the stream can remain functional when we get a sink error, 499 | // but since this is a duplex connection, and poll_next also tries to send out close frames 500 | // through the stream, just consider sink errors fatal. 501 | // 502 | self.state.insert( State::STREAM_CLOSED ); 503 | to_io_error( e ) 504 | }) 505 | } 506 | } 507 | 508 | 509 | 510 | // Convert tungstenite errors that can happen during sending into io::Error. 511 | // 512 | fn to_io_error( err: TungErr ) -> io::Error 513 | { 514 | // See the wildcard at the bottom for why we need this. 515 | // 516 | #[ allow( clippy::wildcard_in_or_patterns )] 517 | // 518 | match err 519 | { 520 | // Mainly on the underlying stream. Fatal 521 | // 522 | TungErr::Io(err) => err, 523 | 524 | 525 | // Connection is closed, does not indicate something went wrong. 526 | // 527 | TungErr::ConnectionClosed | 528 | 529 | // Connection is closed, in principle this indicates that the user tries to keep using it 530 | // after ConnectionClosed has already been returned. 531 | // 532 | TungErr::AlreadyClosed => io::ErrorKind::NotConnected.into() , 533 | 534 | 535 | // This shouldn't happen, we should not cause any protocol errors, since we abstract 536 | // away the websocket protocol for users. They shouldn't be able to trigger this through our API. 537 | // AFAICT the only one you can trigger on send is SendAfterClose unless you create control 538 | // frames yourself, which we don't. 539 | // 540 | TungErr::Protocol(source) => 541 | { 542 | error!( "protocol error from tungstenite on send is a bug in ws_stream_tungstenite, please report at http://github.com/najamelan/ws_stream_tungstenite/issues. Especially if you find a way to reproduce it. The error from tungstenite is {}", source ); 543 | io::ErrorKind::Other.into() 544 | } 545 | 546 | 547 | // This can happen when we create a message bigger than max message size in tungstenite. 548 | // 549 | TungErr::Capacity(string) => io::Error::new( io::ErrorKind::InvalidData, string ), 550 | 551 | 552 | // This can happen if we send a message bigger than the tungstenite `max_write_buffer_len`. 553 | // However `WsStream` looks at the size of this buffer and only sends up to `max_write_buffer_len` 554 | // bytes in one message. 555 | // 556 | TungErr::WriteBufferFull(_) => { 557 | error!( "TungErr::WriteBufferFull" ); 558 | io::ErrorKind::Other.into() 559 | } 560 | 561 | // These are handshake errors 562 | // 563 | TungErr::Url(_) => { 564 | error!( "TungErr::Url" ); 565 | io::ErrorKind::Other.into() 566 | } 567 | 568 | // This is an error specific to Text Messages that we don't use 569 | // 570 | TungErr::Utf8 => { 571 | error!( "TungErr::Utf8" ); 572 | io::ErrorKind::Other.into() 573 | } 574 | 575 | // I'd rather have this match exhaustive, but tungstenite has a Tls variant that 576 | // is only there if they have a feature enabled. Since we cannot check whether 577 | // a feature is enabled on a dependency, we have to go for wildcard here. 578 | // As of tungstenite 0.19 Http and HttpFormat are also behind a feature flag. 579 | // 580 | x => { 581 | error!( "unmatched tungstenite error: {x}" ); 582 | io::ErrorKind::Other.into() 583 | } 584 | } 585 | } 586 | 587 | 588 | impl Observable< WsEvent > for TungWebSocket where S: AsyncRead + AsyncWrite + Send + Unpin 589 | { 590 | type Error = WsErr; 591 | 592 | fn observe( &mut self, options: ObserveConfig< WsEvent > ) -> Observe< '_, WsEvent, Self::Error > 593 | { 594 | self.notifier.observe( options ) 595 | } 596 | } 597 | 598 | 599 | 600 | 601 | -------------------------------------------------------------------------------- /src/tung_websocket/closer.rs: -------------------------------------------------------------------------------- 1 | use 2 | { 3 | crate :: { import::*, WsEvent, WsErr } , 4 | super :: { notifier::Notifier } , 5 | }; 6 | 7 | 8 | // Unit tests for closer. Sending in several times when there is back pressure is tested in the 9 | // integration test send_text_backpressure in this crate, so I haven't made a specific test here. 10 | // 11 | #[ cfg(test) ] mod closer_send ; 12 | #[ cfg(test) ] mod notify_errors ; 13 | #[ cfg(test) ] mod no_double_close ; 14 | 15 | 16 | // Keep track of our state so we can progress through it if the sink returns pending. 17 | // 18 | #[ derive( Debug, Clone ) ] 19 | // 20 | enum State 21 | { 22 | Ready, 23 | 24 | // When we receive protocol errors or text messages, we want to close, and we want to be able to 25 | // send a close frame so the remote can debug their issues. 26 | // However it's an async operation to send this and we are in a reader task atm, so if the sink 27 | // is not ready to receive more data, store it here for now and try again to do this first on each 28 | // read or write from the user. 29 | // 30 | Closing(CloseFrame), 31 | 32 | // When we are closing, and the sink says yes to poll_ready, but it says Pending to flush, we store that 33 | // fact, so we will continue trying to flush on subsequent operations. 34 | // 35 | Flushing, 36 | 37 | 38 | SinkError, 39 | 40 | // We have finished sending a close frame, so we shouldn't do anything anymore. 41 | // 42 | Closed, 43 | } 44 | 45 | 46 | 47 | 48 | 49 | 50 | impl PartialEq for State 51 | { 52 | fn eq( &self, other: &Self ) -> bool 53 | { 54 | std::mem::discriminant( self ) == std::mem::discriminant( other ) 55 | } 56 | } 57 | 58 | 59 | pub(super) struct Closer 60 | { 61 | state: State, 62 | } 63 | 64 | 65 | impl Closer 66 | { 67 | pub(super) fn new() -> Self 68 | { 69 | Self{ state: State::Ready } 70 | } 71 | 72 | 73 | 74 | pub(super) fn queue( &mut self, frame: CloseFrame ) -> Result<(), ()> 75 | { 76 | if self.state != State::Ready 77 | { 78 | return Err(()) 79 | } 80 | 81 | self.state = State::Closing( frame ); 82 | Ok(()) 83 | } 84 | 85 | 86 | 87 | // Will try to send out a close frame to the websocket. It will then poll that send for completion 88 | // saving it's state and returning pending if no more progress can be made. 89 | // 90 | // Any errors that happen will be returned out of band as pharos events through the Notifier. 91 | // 92 | pub(super) fn run 93 | ( 94 | mut self : Pin<&mut Self> , 95 | mut socket : impl Sink + Unpin , 96 | ph : &mut Notifier , 97 | cx : &mut Context<'_> , 98 | ) 99 | -> Poll< Result<(), ()> > 100 | 101 | { 102 | match &self.state 103 | { 104 | State::Ready => Ok (()).into() , 105 | State::SinkError => Err(()).into() , 106 | State::Closed => Err(()).into() , 107 | 108 | State::Closing( frame ) => 109 | { 110 | let ready = Pin::new( &mut socket ).as_mut().poll_ready( cx ); 111 | 112 | match ready 113 | { 114 | Poll::Pending => 115 | { 116 | Poll::Pending 117 | } 118 | 119 | Poll::Ready(Err(e)) => 120 | { 121 | ph.queue( WsEvent::Error( Arc::new( e.into() )) ); 122 | 123 | self.state = State::SinkError; 124 | Err(()).into() 125 | } 126 | 127 | Poll::Ready(Ok(())) => 128 | { 129 | // Send the frame 130 | // 131 | if let Err(e) = Pin::new( &mut socket ).as_mut().start_send( TungMessage::Close( Some(frame.clone()) ) ) 132 | { 133 | ph.queue( WsEvent::Error( Arc::new( e.into() )) ); 134 | 135 | self.state = State::SinkError; 136 | } 137 | 138 | // Flush 139 | // 140 | match Pin::new( &mut socket ).as_mut().poll_flush( cx ) 141 | { 142 | Poll::Pending => 143 | { 144 | self.state = State::Flushing; 145 | 146 | Poll::Pending 147 | } 148 | 149 | // We are really done 150 | // 151 | Poll::Ready(Ok(())) => 152 | { 153 | self.state = State::Closed; 154 | 155 | Ok(()).into() 156 | } 157 | 158 | 159 | Poll::Ready(Err(e)) => 160 | { 161 | ph.queue( WsEvent::Error( Arc::new( e.into() )) ); 162 | 163 | self.state = State::SinkError; 164 | 165 | Err(()).into() 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | 173 | State::Flushing => 174 | { 175 | // Flush 176 | // 177 | match Pin::new( &mut socket ).as_mut().poll_flush( cx ) 178 | { 179 | Poll::Pending => 180 | { 181 | self.state = State::Flushing; 182 | 183 | Poll::Pending 184 | } 185 | 186 | // We are really done 187 | // 188 | Poll::Ready(Ok(())) => 189 | { 190 | self.state = State::Closed; 191 | 192 | Err(()).into() 193 | } 194 | 195 | 196 | Poll::Ready(Err(e)) => 197 | { 198 | ph.queue( WsEvent::Error( Arc::new( WsErr::from(e) )) ); 199 | 200 | self.state = State::SinkError; 201 | 202 | Err(()).into() 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | 210 | -------------------------------------------------------------------------------- /src/tung_websocket/closer/closer_send.rs: -------------------------------------------------------------------------------- 1 | // Tested: 2 | // 3 | // ✔ closer actually sends out on sink 4 | // 5 | use crate :: { import::{ *, assert_matches }, tung_websocket::{ notifier::Notifier, closer::Closer } }; 6 | 7 | 8 | #[ test ] 9 | // 10 | fn send_closeframe() 11 | { 12 | // flexi_logger::Logger::with_str( "send_text_backpressure=trace, tungstenite=trace, tokio_tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 13 | 14 | let (sc, cs) = Endpoint::pair( 100, 100 ); 15 | 16 | 17 | let test = async 18 | { 19 | let mut sink = ATungSocket::from_raw_socket( sc, Role::Server, None ).await.split().0; 20 | let mut stream = ATungSocket::from_raw_socket( cs, Role::Client, None ).await.split().1; 21 | 22 | let mut notif = Notifier::new(); 23 | let mut closer = Closer::new(); 24 | let waker = noop_waker(); 25 | let mut cx = Context::from_waker( &waker ); 26 | 27 | let writer = async 28 | { 29 | closer.queue( CloseFrame{ code: CloseCode::Unsupported, reason: "tada".into() } ) 30 | 31 | .expect( "no double close" ) 32 | ; 33 | 34 | let p = Pin::new( &mut closer ).run( &mut sink, &mut notif, &mut cx ); 35 | 36 | assert_matches!( p, Poll::Ready( Ok(()) ) ); 37 | 38 | trace!( "server: writer end" ); 39 | }; 40 | 41 | 42 | let reader = async 43 | { 44 | let frame = CloseFrame 45 | { 46 | code : CloseCode::Unsupported, 47 | reason: "tada".into(), 48 | }; 49 | 50 | trace!( "client: waiting on close frame" ); 51 | 52 | assert_eq!( Some( tungstenite::Message::Close( Some(frame) )), stream.next().await.transpose().expect( "close" ) ); 53 | 54 | trace!( "client: reader end" ); 55 | }; 56 | 57 | join( reader, writer ).await; 58 | 59 | trace!( "client: drop websocket" ); 60 | }; 61 | 62 | block_on( test ); 63 | info!( "end test" ); 64 | } 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/tung_websocket/closer/no_double_close.rs: -------------------------------------------------------------------------------- 1 | 2 | // Tested: 3 | // 4 | // ✔ do not accept a second close frame 5 | // 6 | use crate :: { import::*, tung_websocket::{ closer::Closer } }; 7 | 8 | 9 | #[ test ] 10 | // 11 | fn no_double_close() 12 | { 13 | // flexi_logger::Logger::with_str( "send_text_backpressure=trace, tungstenite=trace, tokio_tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 14 | 15 | let test = async 16 | { 17 | let mut closer = Closer::new(); 18 | 19 | closer.queue( CloseFrame{ code: CloseCode::Unsupported, reason: "tada" .into() } ).expect( "queue close" ); 20 | 21 | assert!( closer.queue( CloseFrame{ code: CloseCode::Unsupported, reason: "second".into() } ).is_err() ); 22 | 23 | trace!( "server: drop websocket" ); 24 | }; 25 | 26 | block_on( test ); 27 | info!( "end test" ); 28 | } 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/tung_websocket/closer/notify_errors.rs: -------------------------------------------------------------------------------- 1 | // Tested: 2 | // 3 | // ✔ notifiying errors through pharos 4 | // 5 | use crate :: { import::{ *, assert_matches }, WsEvent, WsErr, tung_websocket::{ notifier::Notifier, closer::Closer } }; 6 | 7 | 8 | #[ async_std::test ] 9 | // 10 | async fn notify_errors() 11 | { 12 | // flexi_logger::Logger::with_str( "send_text_backpressure=trace, tungstenite=trace, tokio_tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 13 | 14 | let (sc, cs) = Endpoint::pair( 100, 100 ); 15 | 16 | 17 | let test = async 18 | { 19 | let mut sink = ATungSocket::from_raw_socket( sc, Role::Server, None ).await.split().0; 20 | let mut stream = ATungSocket::from_raw_socket( cs, Role::Client, None ).await.split().1; 21 | 22 | let mut notif = Notifier::new(); 23 | let mut events = notif.observe( ObserveConfig::default() ).await.expect( "observe server" ); 24 | let mut closer = Closer::new(); 25 | let waker = noop_waker(); 26 | let mut cx = Context::from_waker( &waker ); 27 | 28 | 29 | let writer = async 30 | { 31 | sink.close().await.expect( "close sink" ); 32 | 33 | closer.queue( CloseFrame{ code: CloseCode::Unsupported, reason: "tada".into() } ).expect( "queue close" ); 34 | 35 | // this will encounter an error since the sink is already closed 36 | // 37 | let p = Pin::new( &mut closer ).run( &mut sink, &mut notif, &mut cx ); 38 | 39 | assert_matches!( p, Poll::Ready( Err(()) ) ); 40 | 41 | let n = notif.run( &mut cx ); 42 | 43 | assert_matches!( n, Poll::Ready( Ok(()) ) ); 44 | 45 | trace!( "server: writer end" ); 46 | }; 47 | 48 | 49 | let reader = async 50 | { 51 | trace!( "client: waiting on close frame" ); 52 | 53 | assert_eq!( Some( tungstenite::Message::Close( None )), stream.next().await.transpose().expect( "close" ) ); 54 | 55 | // Verify the error can be observed here 56 | // 57 | match events.next().await.expect( "error" ) 58 | { 59 | WsEvent::Error( e ) => 60 | { 61 | assert!( matches!( *e, WsErr::Protocol )); 62 | } 63 | 64 | _ => unreachable!( "expect closeframe" ), 65 | } 66 | 67 | trace!( "client: reader end" ); 68 | }; 69 | 70 | join( reader, writer ).await; 71 | 72 | trace!( "client: drop websocket" ); 73 | }; 74 | 75 | block_on( test ); 76 | info!( "end test" ); 77 | } 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/tung_websocket/notifier.rs: -------------------------------------------------------------------------------- 1 | use crate:: { import::*, WsEvent, WsErr }; 2 | 3 | 4 | // The different states we can be in. 5 | // 6 | #[ derive( Debug, Clone, Copy ) ] 7 | // 8 | enum State 9 | { 10 | Ready, 11 | 12 | // Something is in the queue 13 | // 14 | Pending, 15 | 16 | // Pharos is closed 17 | // 18 | Closed, 19 | 20 | // Pharos returns pending on flush, try flushing before sending anything else 21 | // 22 | Flushing, 23 | } 24 | 25 | 26 | 27 | impl PartialEq for State 28 | { 29 | fn eq( &self, other: &Self ) -> bool 30 | { 31 | std::mem::discriminant( self ) == std::mem::discriminant( other ) 32 | } 33 | } 34 | 35 | 36 | 37 | pub(super) struct Notifier 38 | { 39 | pharos: Pharos < WsEvent > , 40 | events: VecDeque< WsEvent > , 41 | state : State , 42 | } 43 | 44 | 45 | 46 | impl Notifier 47 | { 48 | pub(crate) fn new() -> Self 49 | { 50 | Self 51 | { 52 | // Most of the time there will probably not be many observers 53 | // this keeps memory consumption down 54 | // 55 | pharos: Pharos::new( 2 ) , 56 | state : State::Ready , 57 | events: VecDeque::new() , 58 | } 59 | } 60 | 61 | 62 | pub(crate) fn queue( &mut self, evt: WsEvent ) 63 | { 64 | // It should only happen if we call close on it, and we should never do that. 65 | // 66 | debug_assert!( self.state != State::Closed ); 67 | 68 | self.events.push_back( evt ); 69 | 70 | self.state = State::Pending; 71 | } 72 | 73 | 74 | // try to send out queued events. 75 | // 76 | pub(crate) fn run( &mut self, cx: &mut Context<'_> ) -> Poll< Result<(), ()> > 77 | { 78 | let mut pharos = Pin::new( &mut self.pharos ); 79 | 80 | match self.state 81 | { 82 | State::Ready => Ok (()).into(), 83 | State::Closed => Err(()).into(), 84 | 85 | State::Pending => 86 | { 87 | while !self.events.is_empty() 88 | { 89 | match ready!( pharos.as_mut().poll_ready( cx ) ) 90 | { 91 | Err(e) => 92 | { 93 | error!( "pharos returned an error, this could be a bug in ws_stream_tungstenite, please report: {:?}", e ); 94 | 95 | self.state = State::Closed; 96 | 97 | return Err(()).into(); 98 | } 99 | 100 | Ok(()) => 101 | { 102 | // note we can only get here if the queue isn't empty, shouldn't happen 103 | // 104 | let event = match self.events.pop_front() 105 | { 106 | Some(e) => e, 107 | None => return Poll::Ready(Err(())), 108 | }; 109 | 110 | if let Err(_e) = pharos.as_mut().start_send( event ) 111 | { 112 | self.state = State::Closed; 113 | 114 | return Err(()).into(); 115 | } 116 | 117 | // Flush 118 | // 119 | match pharos.as_mut().poll_flush( cx ) 120 | { 121 | Poll::Pending => 122 | { 123 | self.state = State::Flushing; 124 | 125 | return Poll::Pending 126 | } 127 | 128 | 129 | Poll::Ready(Err(_e)) => 130 | { 131 | self.state = State::Closed; 132 | 133 | return Err(()).into(); 134 | } 135 | 136 | // We are really done 137 | // 138 | Poll::Ready(Ok(())) => {} 139 | } 140 | } 141 | } 142 | } 143 | 144 | self.state = State::Ready; 145 | Ok(()).into() 146 | } 147 | 148 | 149 | State::Flushing => 150 | { 151 | // Flush 152 | // 153 | match ready!( pharos.as_mut().poll_flush( cx ) ) 154 | { 155 | Err(e) => 156 | { 157 | error!( "pharos returned an error, this could be a bug in ws_stream_tungstenite, please report: {:?}", e ); 158 | 159 | self.state = State::Closed; 160 | 161 | Err(()).into() 162 | } 163 | 164 | // We are really done 165 | // 166 | Ok(()) => 167 | { 168 | self.state = State::Ready; 169 | 170 | Ok(()).into() 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | 179 | 180 | impl Observable< WsEvent > for Notifier 181 | { 182 | type Error = WsErr; 183 | 184 | fn observe( &mut self, options: ObserveConfig< WsEvent > ) -> Observe< '_, WsEvent, Self::Error > 185 | { 186 | async move 187 | { 188 | self.pharos.observe( options ).await.map_err( Into::into ) 189 | 190 | }.boxed() 191 | } 192 | } 193 | 194 | 195 | 196 | #[ cfg( test ) ] 197 | // 198 | mod tests 199 | { 200 | // Tested: 201 | // 202 | // ✔ state gets updated correctly 203 | // ✔ queue get's filled up and emptied 204 | // ✔ verify everything get's delivered correctly after pharos gives back pressure 205 | // 206 | use super::{ *, assert_matches }; 207 | 208 | 209 | // verify state becomes pending when queing something and get's reset after calling run without observers. 210 | // 211 | #[ test ] 212 | // 213 | fn notifier_state() 214 | { 215 | 216 | let mut not = Notifier::new(); 217 | 218 | assert_eq!( State::Ready, not.state ); 219 | 220 | 221 | not.queue( WsEvent::Ping( vec![ 1, 2, 3].into() ) ); 222 | 223 | assert_eq!( State::Pending, not.state ); 224 | 225 | 226 | let w = noop_waker(); 227 | let mut cx = Context::from_waker( &w ); 228 | let res = not.run( &mut cx ); 229 | 230 | assert_eq!( Poll::Ready( Ok(()) ), res ); 231 | assert_eq!( State::Ready, not.state ); 232 | 233 | 234 | 235 | not.queue( WsEvent::Closed ); 236 | 237 | assert_eq!( State::Pending, not.state ); 238 | } 239 | 240 | 241 | // verify state changes using an observer that provides back pressure 242 | // 243 | #[ async_std::test ] 244 | // 245 | async fn notifier_state_observers() 246 | { 247 | let mut not = Notifier::new(); 248 | let mut evts = not.observe( Channel::Bounded( 1 ).into() ).await.expect( "observe" ); 249 | 250 | assert_eq!( State::Ready, not.state ); 251 | assert_eq!( 0, not.events.len() ); 252 | 253 | 254 | // Queue 2 so the channel gives back pressure. 255 | // 256 | not.queue( WsEvent::Ping( vec![ 1, 2, 3].into() ) ); 257 | not.queue( WsEvent::Ping( vec![ 1, 2, 3].into() ) ); 258 | 259 | assert_eq!( State::Pending, not.state ); 260 | assert_eq!( 2, not.events.len() ); 261 | 262 | 263 | // delivers 1 and blocks on back pressure 264 | // 265 | let w = noop_waker(); 266 | let mut cx = Context::from_waker( &w ); 267 | let res = not.run( &mut cx ); 268 | 269 | assert_eq!( Poll::Pending, res ); 270 | assert_eq!( State::Pending, not.state ); 271 | assert_eq!( 1, not.events.len() ); 272 | 273 | 274 | // Make more space 275 | // 276 | let evt = block_on( evts.next() ); 277 | assert_matches!( evt, Some( WsEvent::Ping(_) ) ); 278 | 279 | 280 | // now there should be place for the second one 281 | // 282 | let w = noop_waker(); 283 | let mut cx = Context::from_waker( &w ); 284 | let res = not.run( &mut cx ); 285 | 286 | assert_eq!( Poll::Ready( Ok(()) ), res ); 287 | assert_eq!( State::Ready, not.state ); 288 | assert_eq!( 0, not.events.len() ); 289 | 290 | 291 | // read the second one 292 | // 293 | let evt = block_on( evts.next() ); 294 | 295 | assert_matches!( evt, Some( WsEvent::Ping(_) ) ); 296 | } 297 | 298 | 299 | 300 | #[ test ] 301 | // 302 | fn queue() 303 | { 304 | let mut not = Notifier::new(); 305 | 306 | assert_eq!( 0, not.events.len() ); 307 | 308 | 309 | not.queue( WsEvent::Ping( vec![ 1, 2, 3].into() ) ); 310 | 311 | assert_eq!( 1, not.events.len() ); 312 | 313 | 314 | let w = noop_waker(); 315 | let mut cx = Context::from_waker( &w ); 316 | let _ = not.run( &mut cx ); 317 | 318 | assert_eq!( 0, not.events.len() ); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/ws_err.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::* }; 2 | 3 | 4 | /// The error type for errors happening in _ws_stream_tungstenite_. 5 | // 6 | #[ derive( Debug ) ] 7 | #[ non_exhaustive ] 8 | #[ allow( variant_size_differences ) ] 9 | // 10 | pub enum WsErr 11 | { 12 | /// A tungstenite error. 13 | // 14 | Tungstenite 15 | { 16 | /// The underlying error. 17 | // 18 | source: tungstenite::Error 19 | }, 20 | 21 | /// An error from the underlying connection. 22 | // 23 | Io 24 | { 25 | /// The underlying error. 26 | // 27 | source: io::Error 28 | }, 29 | 30 | /// A websocket protocol error. On read it means the remote didn't respect the websocket protocol. 31 | /// On write this means there's a bug in ws_stream_tungstenite and it will return [`std::io::ErrorKind::Other`]. 32 | // 33 | Protocol, 34 | 35 | /// We received a websocket text message. As we are about turning the websocket connection into a 36 | /// bytestream, this is probably unintended, and thus unsupported. 37 | // 38 | ReceivedText, 39 | 40 | /// Trying to work with an connection that is closed. Only happens on writing. On reading 41 | /// `poll_read` will just return `None`. 42 | // 43 | Closed, 44 | 45 | /// Unreachable. This shouldn't happen but we need to match pharos error and want avoid panics. 46 | // 47 | Unreachable, 48 | } 49 | 50 | 51 | 52 | impl std::error::Error for WsErr 53 | { 54 | fn source( &self ) -> Option<&(dyn std::error::Error + 'static)> 55 | { 56 | match &self 57 | { 58 | WsErr::Tungstenite{ source } => Some(source), 59 | WsErr::Io { source } => Some(source), 60 | 61 | WsErr::Protocol | 62 | WsErr::ReceivedText | 63 | WsErr::Closed | 64 | WsErr::Unreachable => None 65 | } 66 | } 67 | } 68 | 69 | 70 | 71 | impl fmt::Display for WsErr 72 | { 73 | fn fmt( &self, f: &mut fmt::Formatter<'_> ) -> fmt::Result 74 | { 75 | match &self 76 | { 77 | WsErr::Tungstenite{ source } => 78 | 79 | write!( f, "A tungstenite error happened: {source}" ), 80 | 81 | WsErr::Io{ source } => 82 | 83 | write!( f, "An io error happened: {source}" ), 84 | 85 | WsErr::Protocol => 86 | 87 | write!( f, "The remote committed a websocket protocol violation." ), 88 | 89 | WsErr::ReceivedText => 90 | 91 | write!( f, "The remote sent a Text message. Only Binary messages are accepted." ), 92 | 93 | WsErr::Closed => 94 | 95 | write!( f, "The connection is already closed." ), 96 | 97 | WsErr::Unreachable => 98 | 99 | write!( f, "A bug in ws_stream_tungstenite caused an error variant that should be unreachable. Please report at github.com/najamelan/ws_stream_tungstenite/issues." ), 100 | } 101 | } 102 | } 103 | 104 | 105 | 106 | impl From< TungErr > for WsErr 107 | { 108 | fn from( inner: TungErr ) -> WsErr 109 | { 110 | match inner 111 | { 112 | TungErr::Protocol(_) => WsErr::Protocol , 113 | source => WsErr::Tungstenite{ source } , 114 | } 115 | } 116 | } 117 | 118 | 119 | 120 | impl From< io::Error > for WsErr 121 | { 122 | fn from( source: io::Error ) -> WsErr 123 | { 124 | WsErr::Io { source } 125 | } 126 | } 127 | 128 | 129 | 130 | impl From< PharErr > for WsErr 131 | { 132 | fn from( source: PharErr ) -> WsErr 133 | { 134 | match source.kind() 135 | { 136 | pharos::ErrorKind::Closed => WsErr::Closed, 137 | _ => WsErr::Unreachable, 138 | } 139 | } 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/ws_event.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::*, WsErr }; 2 | 3 | 4 | /// Events that can happen on the websocket. These are returned through the stream you can obtain 5 | /// from `WsStream::observe`. These include close, ping and pong events which can not be returned 6 | /// through AsyncRead/AsyncWrite, and non-fatal errors. 7 | // 8 | #[ derive( Debug, Clone ) ] 9 | // 10 | pub enum WsEvent 11 | { 12 | /// Non fatal error that happened on the websocket. Non-fatal here doesn't mean the websocket is still 13 | /// usable, but at least is still usable enough to initiate a close handshake. If we bubble up errors 14 | /// through `AsyncRead`/`AsyncWrite`, codecs will always return `None` on subsequent polls, which would prevent 15 | /// from driving the close handshake to completion. Hence they are returned out of band. 16 | // 17 | Error( Arc ), 18 | 19 | /// We received a close frame from the remote. Just keep polling the stream. The close handshake will be 20 | /// completed for you. Once the stream returns `None`, you can drop the [`WsStream`](crate::WsStream). 21 | /// This is mainly useful in order to recover the close code and reason for debugging purposes. 22 | // 23 | CloseFrame( Option< CloseFrame > ), 24 | 25 | /// The remote sent a Ping message. It will automatically be answered as long as you keep polling the 26 | /// `AsyncRead`. This is returned as an event in case you want to analyze the payload, since only bytes 27 | /// from Binary websocket messages are passed through the `AsyncRead`. 28 | // 29 | Ping(Bytes), 30 | 31 | /// The remote send us a Pong. Since we never send Pings, this is a unidirectional heartbeat. 32 | // 33 | Pong(Bytes), 34 | 35 | /// The connection is closed. Polling `WsStream` will return `None` on read and `io::ErrorKind::NotConnected` 36 | /// on write soon. It's provided here for convenience so the task listening to these events know that 37 | /// the connection closed. 38 | /// You should not see any events after this one, so you can drop the Events stream. 39 | // 40 | Closed, 41 | } 42 | -------------------------------------------------------------------------------- /src/ws_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::*, tung_websocket::TungWebSocket, WsEvent, WsErr }; 2 | 3 | 4 | /// Takes a [`WebSocketStream`](async_tungstenite::WebSocketStream) and implements futures `AsyncRead`/`AsyncWrite`/`AsyncBufRead`. 5 | /// 6 | /// Will always create an entire Websocket message from every write. Tungstenite buffers messages up to 7 | /// `write_buffer_size` in their [`tungstenite::protocol::WebSocketConfig`]. If you want small messages to be sent out, 8 | /// either make sure this buffer is small enough or flush the writer. 9 | /// 10 | /// On the other hand the `max_write_buffer_size` from tokio is the maximum size we can send in one go, otherwise 11 | /// _tungstenite_ returns an error. Our [`AsyncWrite`] implementation never sends data that exceeds this buffer or 12 | /// `max_message_size`. 13 | /// 14 | /// However you still must respect the `max_message_size` of the receiving end. 15 | /// 16 | /// ## Errors 17 | /// 18 | /// Errors returned directly are generally io errors from the underlying stream. Only fatal errors are returned in 19 | /// band, so consider them fatal and drop the WsStream object. 20 | /// 21 | /// Other errors are returned out of band through [_pharos_](https://crates.io/crates/pharos): 22 | /// 23 | /// On reading, eg. `AsyncRead::poll_read`: 24 | /// - [`WsErr::Protocol`]: The remote made a websocket protocol violation. The connection will be closed 25 | /// gracefully indicating to the remote what went wrong. You can just keep calling `poll_read` until `None` 26 | /// is returned. 27 | /// - tungstenite returned a utf8 error. Pharos will return it as a Tungstenite error. This means the remote 28 | /// send a text message, which is not supported, so the connection will be gracefully closed. You can just keep calling 29 | /// `poll_read` until `None` is returned. 30 | /// - [`WsErr::ReceivedText`]: This means the remote send a text message, which is not supported, so the connection will 31 | /// be gracefully closed. You can just keep calling `poll_read` until `None` is returned. 32 | /// 33 | /// On writing, eg. `AsyncWrite::*` all errors are fatal. 34 | /// 35 | /// When a Protocol error is encountered during writing, it indicates that either _ws_stream_tungstenite_ or _tungstenite_ have 36 | /// a bug. 37 | // 38 | pub struct WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 39 | { 40 | inner: IoStream< TungWebSocket, Bytes >, 41 | buffer_size: usize, 42 | } 43 | 44 | 45 | 46 | impl WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 47 | { 48 | /// Create a new WsStream. 49 | // 50 | pub fn new( inner: ATungSocket ) -> Self 51 | { 52 | let c = inner.get_config(); 53 | let buffer_size = std::cmp::min( c.max_write_buffer_size, c.max_message_size.unwrap_or(usize::MAX) ); 54 | 55 | Self 56 | { 57 | buffer_size, 58 | inner : IoStream::new( TungWebSocket::new( inner ) ), 59 | } 60 | } 61 | } 62 | 63 | 64 | 65 | impl fmt::Debug for WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 66 | { 67 | fn fmt( &self, f: &mut fmt::Formatter<'_> ) -> fmt::Result 68 | { 69 | write!( f, "WsStream over Tungstenite" ) 70 | } 71 | } 72 | 73 | 74 | 75 | impl AsyncWrite for WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 76 | { 77 | fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8] ) -> Poll< io::Result > 78 | { 79 | let buffer_size = std::cmp::min(self.buffer_size, buf.len()); 80 | AsyncWrite::poll_write( Pin::new( &mut self.inner ), cx, &buf[..buffer_size] ) 81 | } 82 | 83 | 84 | fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[ IoSlice<'_> ] ) -> Poll< io::Result > 85 | { 86 | let mut take_size = 0; 87 | let mut seen_size = 0; 88 | let mut next = 1; 89 | 90 | for (i, buf) in bufs.iter().enumerate() 91 | { 92 | let len = buf.len(); 93 | seen_size += len; 94 | 95 | // If this buffer does not fit entirely 96 | // 97 | if take_size + len > self.buffer_size { break; } 98 | 99 | take_size += len; 100 | next = i+1; 101 | } 102 | 103 | // TODO: scenarios to test: 104 | // - 1 buffer, empty 105 | // - 1 buffer, smaller than self.buffer_size 106 | // - 1 buffer, too big 107 | // - several buffers, some early ones are empty 108 | // - several buffers, first one is smaller than self.buffer_size 109 | 110 | // There is no data at all 111 | // 112 | if seen_size == 0 { return Poll::Ready(Ok(0)); } 113 | 114 | // If the first non-empty buffer is too big, let poll_write take the right amount out of it. 115 | // 116 | if take_size == 0 117 | { 118 | return AsyncWrite::poll_write( self, cx, bufs[next-1].get(0..).expect("index 0 not to be out of bounds") ); 119 | } 120 | 121 | // If we can fill from multiple buffers, we don't try to split any buffer, just take buffers as long as they 122 | // fit entirely. 123 | // 124 | AsyncWrite::poll_write_vectored( Pin::new( &mut self.inner ), cx, &bufs[0..next] ) 125 | } 126 | 127 | 128 | fn poll_flush( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll< io::Result<()> > 129 | { 130 | AsyncWrite::poll_flush( Pin::new( &mut self.inner ), cx ) 131 | } 132 | 133 | 134 | fn poll_close( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll< io::Result<()> > 135 | { 136 | Pin::new( &mut self.inner ).poll_close( cx ) 137 | } 138 | } 139 | 140 | 141 | 142 | #[ cfg( feature = "tokio_io" ) ] 143 | // 144 | #[ cfg_attr( nightly, doc(cfg( feature = "tokio_io" )) ) ] 145 | // 146 | impl TokAsyncWrite for WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 147 | { 148 | /// Will always flush the underlying socket. Will always create an entire Websocket message from every write, 149 | /// so call with a sufficiently large buffer if you have performance problems. Don't call with a buffer larger 150 | /// than the max message size accepted by the remote endpoint. 151 | // 152 | fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8] ) -> Poll< io::Result > 153 | { 154 | TokAsyncWrite::poll_write( Pin::new( &mut self.inner ), cx, buf ) 155 | } 156 | 157 | 158 | fn poll_flush( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll< io::Result<()> > 159 | { 160 | TokAsyncWrite::poll_flush( Pin::new( &mut self.inner ), cx ) 161 | } 162 | 163 | 164 | fn poll_shutdown( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll< io::Result<()> > 165 | { 166 | Pin::new( &mut self.inner ).poll_close( cx ) 167 | } 168 | } 169 | 170 | 171 | 172 | impl AsyncRead for WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 173 | { 174 | fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8] ) -> Poll< io::Result > 175 | { 176 | AsyncRead::poll_read( Pin::new( &mut self.inner), cx, buf ) 177 | } 178 | 179 | fn poll_read_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &mut [IoSliceMut<'_>] ) -> Poll< io::Result > 180 | { 181 | AsyncRead::poll_read_vectored( Pin::new( &mut self.inner), cx, bufs ) 182 | } 183 | } 184 | 185 | 186 | #[ cfg( feature = "tokio_io" ) ] 187 | // 188 | #[ cfg_attr( nightly, doc(cfg( feature = "tokio_io" )) ) ] 189 | // 190 | impl TokAsyncRead for WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 191 | { 192 | fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut tokio::io::ReadBuf<'_> ) -> Poll< io::Result<()> > 193 | { 194 | TokAsyncRead::poll_read( Pin::new( &mut self.inner), cx, buf ) 195 | } 196 | } 197 | 198 | 199 | 200 | impl AsyncBufRead for WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 201 | { 202 | fn poll_fill_buf( self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll< io::Result<&[u8]> > 203 | { 204 | Pin::new( &mut self.get_mut().inner ).poll_fill_buf( cx ) 205 | } 206 | 207 | 208 | fn consume( mut self: Pin<&mut Self>, amount: usize ) 209 | { 210 | Pin::new( &mut self.inner ).consume( amount ) 211 | } 212 | } 213 | 214 | 215 | 216 | impl Observable< WsEvent > for WsStream where S: AsyncRead + AsyncWrite + Send + Unpin 217 | { 218 | type Error = WsErr; 219 | 220 | fn observe( &mut self, options: ObserveConfig< WsEvent > ) -> Observe< '_, WsEvent, Self::Error > 221 | { 222 | self.inner.observe( options ) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /tests/buffer_size.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | 3 | // Verify the correct error is returned when sending a text message. 4 | // 5 | use 6 | { 7 | ws_stream_tungstenite :: { * } , 8 | std :: { future::Future } , 9 | futures :: { StreamExt, SinkExt, executor::block_on, future::join } , 10 | asynchronous_codec :: { LinesCodec, Framed } , 11 | async_tungstenite :: { WebSocketStream } , 12 | tungstenite :: { Bytes, protocol::{ WebSocketConfig, CloseFrame, frame::coding::CloseCode, Role }, Message } , 13 | pharos :: { Observable, ObserveConfig } , 14 | assert_matches :: { assert_matches } , 15 | async_progress :: { Progress } , 16 | futures_ringbuf :: { Endpoint } , 17 | tracing :: { * } , 18 | }; 19 | 20 | 21 | // Test tungstenite buffer implementation with async_tungstenite: 22 | // 1. fill up part of `write_buffer_size` 23 | // 2. send a second message. The combination of both messages exceeds `max_write_buffer_size`. 24 | // 3. verify that it gives proper backpressure and we don't get an error for exceeding the max buffer size. 25 | // 26 | #[ test ] 27 | // 28 | fn buffer_size() 29 | { 30 | let (sc, cs) = Endpoint::pair( 50, 50 ); 31 | 32 | let server = server( sc ); 33 | let client = client( cs ); 34 | 35 | block_on( join( server, client ) ); 36 | info!( "end test" ); 37 | } 38 | 39 | 40 | async fn server( sc: Endpoint ) 41 | { 42 | let mut conf = WebSocketConfig::default(); 43 | 44 | conf.write_buffer_size = 6; 45 | conf.max_write_buffer_size = 8; 46 | 47 | let mut tws = WebSocketStream::from_raw_socket( sc, Role::Server, Some(conf) ).await; 48 | let msg = Message::Binary(Bytes::from("hello".as_bytes())); 49 | 50 | let writer = async 51 | { 52 | info!( "start sending first message" ); 53 | 54 | tws.send( msg.clone() ).await.expect( "Send first line" ); 55 | info!( "finished sending first message" ); 56 | 57 | tws.send( msg ).await.expect( "Send first line" ); 58 | info!( "finished sending second message" ); 59 | 60 | tws.close(None).await.expect("close sink"); 61 | trace!( "server: writer end" ); 62 | }; 63 | 64 | writer.await; 65 | 66 | trace!( "server: drop websocket" ); 67 | } 68 | 69 | 70 | 71 | async fn client( cs: Endpoint ) 72 | { 73 | let conf = WebSocketConfig::default(); 74 | let mut ws = WebSocketStream::from_raw_socket( cs, Role::Client, Some(conf) ).await; 75 | 76 | info!( "wait for client_read" ); 77 | 78 | let test = ws.next().await.unwrap().unwrap(); 79 | assert_matches!( test, Message::Binary(x) if x == "hello".as_bytes() ); 80 | 81 | info!( "client: received first binary message" ); 82 | 83 | let test = ws.next().await.unwrap().unwrap(); 84 | assert_matches!( test, Message::Binary(x) if x == "hello".as_bytes() ); 85 | 86 | info!( "client: received second binary message" ); 87 | 88 | let close = ws.next().await; 89 | assert_matches!( close, Some(Ok(Message::Close(None)))); 90 | 91 | trace!( "client: drop websocket" ); 92 | } 93 | -------------------------------------------------------------------------------- /tests/futures_codec.rs: -------------------------------------------------------------------------------- 1 | // Test using the AsyncRead/AsyncWrite from futures 0.3 2 | // 3 | // ✔ frame with futures-codec 4 | // 5 | use 6 | { 7 | ws_stream_tungstenite :: { * } , 8 | futures :: { StreamExt, SinkExt, future::join } , 9 | asynchronous_codec :: { LinesCodec, Framed } , 10 | tokio :: { net::{ TcpListener } } , 11 | async_tungstenite :: { accept_async, tokio::{ connect_async, TokioAdapter } } , 12 | url :: { Url } , 13 | tracing :: { * } , 14 | }; 15 | 16 | 17 | #[ tokio::test ] 18 | // 19 | async fn futures_codec() 20 | { 21 | // flexi_logger::Logger::with_str( "futures_codec=trace, ws_stream=trace, tokio=warn" ).start().expect( "flexi_logger"); 22 | 23 | let server = async 24 | { 25 | let socket: TcpListener = TcpListener::bind( "127.0.0.1:3012" ).await.expect( "bind to port" ); 26 | 27 | let (tcp_stream, _peer_addr) = socket.accept().await.expect( "1 connection" ); 28 | let s = accept_async( TokioAdapter::new(tcp_stream) ).await.expect("Error during the websocket handshake occurred"); 29 | let server = WsStream::new( s ); 30 | 31 | let (mut sink, mut stream) = Framed::new( server, LinesCodec {} ).split(); 32 | 33 | sink.send( "A line\n" .to_string() ).await.expect( "Send a line" ); 34 | sink.send( "A second line\n".to_string() ).await.expect( "Send a line" ); 35 | 36 | sink.close().await.expect( "close server" ); 37 | 38 | let read = stream.next().await.transpose().expect( "close connection" ); 39 | 40 | assert!( read.is_none() ); 41 | debug!( "Server task ended" ); 42 | }; 43 | 44 | 45 | let client = async 46 | { 47 | let url = Url::parse( "ws://127.0.0.1:3012" ).unwrap(); 48 | let socket = connect_async( url ).await.expect( "ws handshake" ); 49 | 50 | let client = WsStream::new( socket.0 ); 51 | let mut framed = Framed::new( client, LinesCodec {} ); 52 | 53 | 54 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a line" ); 55 | assert_eq!( "A line\n".to_string(), res ); 56 | 57 | 58 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a second line" ); 59 | assert_eq!( "A second line\n".to_string(), res ); 60 | 61 | let res = framed.next().await; 62 | 63 | assert!( res.is_none() ); 64 | debug!( "Client task ended" ); 65 | }; 66 | 67 | join( server, client ).await; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /tests/futures_codec_partial.rs: -------------------------------------------------------------------------------- 1 | // Test using the AsyncRead/AsyncWrite from futures 0.3 2 | // 3 | // ✔ send/receive half a frame 4 | // 5 | use 6 | { 7 | ws_stream_tungstenite :: { * } , 8 | futures :: { StreamExt, SinkExt, future::join, channel::oneshot } , 9 | asynchronous_codec :: { LinesCodec, Framed } , 10 | tokio :: { net::{ TcpListener } } , 11 | async_tungstenite :: { accept_async, tokio::{ connect_async, TokioAdapter } } , 12 | url :: { Url } , 13 | tracing :: { * } , 14 | }; 15 | 16 | 17 | 18 | // Receive half a frame 19 | // 20 | #[ tokio::test ] 21 | // 22 | async fn partial() 23 | { 24 | // flexi_logger::Logger::with_str( "futures_codec=trace, tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 25 | 26 | let (tx, rx) = oneshot::channel(); 27 | 28 | let server = async move 29 | { 30 | let socket = TcpListener::bind( "127.0.0.1:3013" ).await.expect( "bind to port" ); 31 | let (tcp_stream, _sock_addr) = socket.accept().await.expect( "1 connection" ); 32 | let s = accept_async(TokioAdapter::new(tcp_stream)).await.expect("Error during the websocket handshake occurred"); 33 | let server = WsStream::new( s ); 34 | 35 | 36 | let (mut sink, mut stream) = Framed::new( server, LinesCodec {} ).split(); 37 | 38 | sink.send( "A ".to_string() ).await.expect( "Send a line" ); 39 | 40 | // Make sure the client tries to read on a partial line first. 41 | // 42 | rx.await.expect( "read channel" ); 43 | 44 | sink.send( "line\n" .to_string() ).await.expect( "Send a line" ); 45 | sink.send( "A second line\n".to_string() ).await.expect( "Send a line" ); 46 | 47 | sink.close().await.expect( "close server" ); 48 | 49 | let read = stream.next().await.transpose().expect( "close connection" ); 50 | 51 | assert!( read.is_none() ); 52 | debug!( "Server task ended" ); 53 | }; 54 | 55 | 56 | let client = async move 57 | { 58 | let url = Url::parse( "ws://127.0.0.1:3013" ).unwrap(); 59 | let socket = connect_async( url ).await.expect( "ws handshake" ); 60 | 61 | let client = WsStream::new( socket.0 ); 62 | let mut framed = Framed::new( client, LinesCodec {} ); 63 | 64 | // This will not return pending, so we will call framed.next() before the server task will send 65 | // the rest of the line. 66 | // 67 | tx.send(()).expect( "trigger channel" ); 68 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a line" ); 69 | assert_eq!( "A line\n".to_string(), dbg!( res ) ); 70 | 71 | 72 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a second line" ); 73 | assert_eq!( "A second line\n".to_string(), dbg!( res ) ); 74 | 75 | 76 | let res = framed.next().await; 77 | assert!( res.is_none() ); 78 | debug!( "Client task ended" ); 79 | }; 80 | 81 | join( server, client ).await; 82 | } 83 | -------------------------------------------------------------------------------- /tests/ping_pong.rs: -------------------------------------------------------------------------------- 1 | // Verify whether we handle ping and pong frames correctly 2 | // Note that they can hold data, but as we provide AsyncRead/AsyncWrite, we generally don't 3 | // want to produce any data on ping/pong. Just on binary messages. 4 | // This is to check whether tokio-tungstenite will swallow them, or if we need to handle them. 5 | // 6 | // The answer is yes! we have to handle them. Still found an issue with tungstenite thanks to this 7 | // test, PR filed! 8 | // 9 | use 10 | { 11 | ws_stream_tungstenite :: { * } , 12 | futures :: { StreamExt, future::join } , 13 | asynchronous_codec :: { LinesCodec, Framed } , 14 | tokio :: { net::{ TcpListener } } , 15 | async_tungstenite :: { accept_async, tokio::{ connect_async, TokioAdapter } } , 16 | url :: { Url } , 17 | tracing :: { * } , 18 | }; 19 | 20 | 21 | #[ tokio::test ] 22 | // 23 | async fn ping_pong() 24 | { 25 | // flexi_logger::Logger::with_str( "ping_pong=trace, tungstenite=trace, async_tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 26 | 27 | let server = async 28 | { 29 | let socket = TcpListener::bind( "127.0.0.1:3015" ).await.expect( "bind to port" ); 30 | let (tcp_stream, _peer_addr) = socket.accept().await.expect( "tcp connect" ); 31 | let s = accept_async(TokioAdapter::new(tcp_stream)).await.expect("Error during the websocket handshake occurred"); 32 | let server = WsStream::new( s ); 33 | 34 | let mut framed = Framed::new( server, LinesCodec {} ); 35 | 36 | assert_eq!( None, framed.next().await.transpose().expect( "receive on framed" ) ); 37 | }; 38 | 39 | 40 | let client = async 41 | { 42 | let url = Url::parse( "ws://127.0.0.1:3015" ).unwrap(); 43 | let (mut socket, _) = connect_async( url ).await.expect( "ws handshake" ); 44 | 45 | socket.send( tungstenite::Message::Ping( vec![1, 2, 3].into() ) ).await.expect( "send ping" ); 46 | 47 | socket.close( None ).await.expect( "close client end" ); 48 | 49 | assert_eq!( Some( tungstenite::Message::Pong( vec![1, 2, 3].into() ) ), socket.next().await.transpose().expect( "pong" ) ); 50 | assert_eq!( Some( tungstenite::Message::Close(None) ), socket.next().await.transpose().expect( "close" ) ); 51 | 52 | trace!( "drop websocket" ); 53 | }; 54 | 55 | join( server, client ).await; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /tests/ping_pong_async_std.rs: -------------------------------------------------------------------------------- 1 | // Verify whether we handle ping and pong frames correctly 2 | // Note that they can hold data, but as we provide AsyncRead/AsyncWrite, we generally don't 3 | // want to produce any data on ping/pong. Just on binary messages. 4 | // This is to check whether tokio-tungstenite will swallow them, or if we need to handle them. 5 | // 6 | // The answer is yes! we have to handle them. Still found an issue with tungstenite thanks to this 7 | // test, PR filed! 8 | // 9 | use 10 | { 11 | ws_stream_tungstenite :: { * } , 12 | futures :: { StreamExt, future::join } , 13 | asynchronous_codec :: { LinesCodec, Framed } , 14 | async_std :: { net::{ TcpListener } } , 15 | async_tungstenite :: { accept_async, async_std::connect_async } , 16 | url :: { Url } , 17 | tracing :: { * } , 18 | }; 19 | 20 | 21 | #[ async_std::test ] 22 | // 23 | async fn ping_pong_async_std() 24 | { 25 | // flexi_logger::Logger::with_str( "ping_pong=trace, tungstenite=trace, async_tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 26 | 27 | let server = async 28 | { 29 | let socket = TcpListener::bind( "127.0.0.1:3015" ).await.expect( "bind to port" ); 30 | let mut connections = socket.incoming(); 31 | 32 | let tcp_stream = connections.next().await.expect( "1 connection" ).expect( "tcp connect" ); 33 | let s = accept_async( tcp_stream ).await.expect("Error during the websocket handshake occurred"); 34 | let server = WsStream::new( s ); 35 | 36 | let mut framed = Framed::new( server, LinesCodec {} ); 37 | 38 | assert_eq!( None, framed.next().await.transpose().expect( "receive on framed" ) ); 39 | }; 40 | 41 | 42 | let client = async 43 | { 44 | let url = Url::parse( "ws://127.0.0.1:3015" ).unwrap(); 45 | let (mut socket, _) = connect_async( url ).await.expect( "ws handshake" ); 46 | 47 | socket.send( tungstenite::Message::Ping( vec![1, 2, 3].into() ) ).await.expect( "send ping" ); 48 | 49 | socket.close( None ).await.expect( "close client end" ); 50 | 51 | assert_eq!( Some( tungstenite::Message::Pong( vec![1, 2, 3].into() ) ), socket.next().await.transpose().expect( "pong" ) ); 52 | assert_eq!( Some( tungstenite::Message::Close(None) ), socket.next().await.transpose().expect( "close" ) ); 53 | 54 | trace!( "drop websocket" ); 55 | }; 56 | 57 | join( server, client ).await; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /tests/protocol_error.rs: -------------------------------------------------------------------------------- 1 | // Verify the correct error is returned when sending a protocol error. 2 | // 3 | use 4 | { 5 | ws_stream_tungstenite :: { * } , 6 | futures :: { StreamExt, future::join } , 7 | asynchronous_codec :: { LinesCodec, Framed } , 8 | tokio :: { net::{ TcpListener } } , 9 | async_tungstenite :: { accept_async, tokio::{ connect_async, TokioAdapter } } , 10 | url :: { Url } , 11 | pharos :: { Observable, ObserveConfig } , 12 | tungstenite :: { protocol::{ CloseFrame, frame::coding::CloseCode } } , 13 | tracing :: { * } , 14 | }; 15 | 16 | 17 | #[ tokio::test ] 18 | // 19 | async fn protocol_error() 20 | { 21 | // flexi_logger::Logger::with_str( "ping_pong=trace, tungstenite=trace, tokio_tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 22 | 23 | 24 | let server = async 25 | { 26 | let socket: TcpListener = TcpListener::bind( "127.0.0.1:3016" ).await.expect( "bind to port" ); 27 | 28 | let (tcp_stream, _peer_addr) = socket.accept().await.expect( "tcp connect" ); 29 | let s = accept_async(TokioAdapter::new(tcp_stream)).await.expect("Error during the websocket handshake occurred"); 30 | let mut server = WsStream::new( s ); 31 | 32 | let mut events = server.observe( ObserveConfig::default() ).await.expect( "observe" ); 33 | let mut framed = Framed::new( server, LinesCodec {} ); 34 | 35 | 36 | assert!( framed.next().await.is_none() ); 37 | 38 | match events.next().await.expect( "protocol error" ) 39 | { 40 | WsEvent::Error( e ) => assert!(matches!( *e, WsErr::Protocol )), 41 | evt => unreachable!( "{:?}", evt ), 42 | } 43 | }; 44 | 45 | 46 | let client = async 47 | { 48 | let url = Url::parse( "ws://127.0.0.1:3016" ).unwrap(); 49 | let (mut socket, _) = connect_async( url ).await.expect( "ws handshake" ); 50 | 51 | socket.send( tungstenite::Message::Ping( vec![1;126].into() ) ).await.expect( "send ping" ); 52 | 53 | socket.close( None ).await.expect( "close client end" ); 54 | 55 | let frame = CloseFrame 56 | { 57 | code : CloseCode::Protocol , 58 | reason: "Control frame too big (payload must be 125 bytes or less)".into() , 59 | }; 60 | 61 | assert_eq! 62 | ( 63 | Some( tungstenite::Message::Close( Some(frame) )), 64 | socket.next().await.transpose().expect( "close" ) 65 | ); 66 | 67 | trace!( "drop websocket" ); 68 | }; 69 | 70 | join( server, client ).await; 71 | } 72 | 73 | -------------------------------------------------------------------------------- /tests/send_text.rs: -------------------------------------------------------------------------------- 1 | // Verify the correct error is returned when sending a text message. 2 | // 3 | use 4 | { 5 | ws_stream_tungstenite :: { * } , 6 | futures :: { StreamExt, future::join } , 7 | asynchronous_codec :: { LinesCodec, Framed } , 8 | tokio :: { net::{ TcpListener } } , 9 | async_tungstenite :: { accept_async, tokio::{ connect_async, TokioAdapter } } , 10 | url :: { Url } , 11 | pharos :: { Observable, ObserveConfig } , 12 | tungstenite :: { protocol::{ CloseFrame, frame::coding::CloseCode } } , 13 | tracing :: { * } , 14 | }; 15 | 16 | 17 | #[ tokio::test ] 18 | // 19 | async fn send_text() 20 | { 21 | // flexi_logger::Logger::with_str( "ping_pong=trace, tungstenite=trace, tokio_tungstenite=trace, ws_stream_tungstenite=trace, tokio=warn" ).start().expect( "flexi_logger"); 22 | 23 | let server = async 24 | { 25 | let socket = TcpListener::bind( "127.0.0.1:3017" ).await.expect( "bind to port" ); 26 | 27 | let (tcp_stream, _peer_addr) = socket.accept().await.expect( "tcp connect" ); 28 | let s = accept_async(TokioAdapter::new(tcp_stream)).await.expect("Error during the websocket handshake occurred"); 29 | let mut server = WsStream::new( s ); 30 | let mut events = server.observe( ObserveConfig::default() ).await.expect( "observe server" ); 31 | 32 | let mut framed = Framed::new( server, LinesCodec {} ); 33 | 34 | let res = framed.next().await; 35 | 36 | assert!( res.is_none() ); 37 | 38 | match events.next().await.expect( "protocol error" ) 39 | { 40 | WsEvent::Error( e ) => assert!(matches!( *e, WsErr::ReceivedText )), 41 | evt => unreachable!( "{:?}", evt ), 42 | } 43 | 44 | assert_eq!( None, framed.next().await.transpose().expect( "receive close stream" ) ); 45 | }; 46 | 47 | 48 | let client = async 49 | { 50 | let url = Url::parse( "ws://127.0.0.1:3017" ).unwrap(); 51 | let (mut socket, _) = connect_async( url ).await.expect( "ws handshake" ); 52 | 53 | socket.send( tungstenite::Message::Text( "Hi".into() ) ).await.expect( "send text" ); 54 | 55 | socket.close( None ).await.expect( "close client end" ); 56 | 57 | let frame = CloseFrame 58 | { 59 | code : CloseCode::Unsupported, 60 | reason: "Text messages are not supported.".into(), 61 | }; 62 | 63 | assert_eq!( Some( tungstenite::Message::Close( Some(frame) )), socket.next().await.transpose().expect( "close" ) ); 64 | 65 | trace!( "drop websocket" ); 66 | }; 67 | 68 | join( server, client ).await; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /tests/send_text_backpressure.rs: -------------------------------------------------------------------------------- 1 | // Verify the correct error is returned when sending a text message. 2 | // 3 | use 4 | { 5 | ws_stream_tungstenite :: { * } , 6 | std :: { future::Future } , 7 | futures :: { StreamExt, SinkExt, executor::block_on, future::join } , 8 | asynchronous_codec :: { LinesCodec, Framed } , 9 | async_tungstenite :: { WebSocketStream } , 10 | tungstenite :: { protocol::{ WebSocketConfig, CloseFrame, frame::coding::CloseCode, Role }, Message } , 11 | pharos :: { Observable, ObserveConfig } , 12 | assert_matches :: { assert_matches } , 13 | async_progress :: { Progress } , 14 | futures_ringbuf :: { Endpoint } , 15 | tracing :: { * } , 16 | }; 17 | 18 | pub fn setup_tracing() 19 | { 20 | tracing_log::LogTracer::init().expect( "setup tracing_log" ); 21 | 22 | let _ = tracing_subscriber::fmt::Subscriber::builder() 23 | 24 | .with_env_filter( "trace" ) 25 | // .with_timer( tracing_subscriber::fmt::time::ChronoLocal::rfc3339() ) 26 | .json() 27 | .try_init() 28 | ; 29 | } 30 | 31 | 32 | // Make sure the close handshake gets completed correctly if the read end detects a protocol error and 33 | // tries to initiate a close handshake while the send queue from tungstenite is full. Eg, does the handshake 34 | // continue when that send queue opens up. 35 | // 36 | // Progress: 37 | // 38 | // server fills it's send queue 39 | // client sends a text message (which is protocol error) 40 | // server reads text message, initiates close handshake, but queue is full 41 | // client starts reading, should get the messages hanging on server, followed by the close handshake. 42 | // 43 | #[ test ] 44 | // 45 | fn send_text_backpressure() 46 | { 47 | // setup_tracing(); 48 | 49 | let (sc, cs) = Endpoint::pair( 37, 50 ); 50 | 51 | let steps = Progress::new( Step::FillQueue ); 52 | let send_text = steps.once( Step::SendText ); 53 | let read_text = steps.once( Step::ReadText ); 54 | let client_read = steps.once( Step::ClientRead ); 55 | 56 | let server = server( read_text, steps.clone(), sc ); 57 | let client = client( send_text, client_read, steps, cs ).instrument( tracing::info_span!( "client_span" ) ); 58 | 59 | block_on( join( server, client ) ); 60 | info!( "end test" ); 61 | } 62 | 63 | 64 | async fn server 65 | ( 66 | read_text : impl Future , 67 | steps : Progress , 68 | sc : Endpoint , 69 | ) 70 | { 71 | let mut conf = WebSocketConfig::default(); 72 | conf.write_buffer_size = 0; 73 | 74 | let tws = WebSocketStream::from_raw_socket( sc, Role::Server, Some(conf) ).await; 75 | let mut ws = WsStream::new( tws ); 76 | let mut events = ws.observe( ObserveConfig::default() ).await.expect( "observe server" ); 77 | 78 | let (mut sink, mut stream) = Framed::new( ws, LinesCodec {} ).split(); 79 | 80 | let writer = async 81 | { 82 | info!( "start sending first message" ); 83 | 84 | sink.send( "hi this is like 35 characters long\n".to_string() ).await.expect( "Send first line" ); 85 | info!( "finished sending first message" ); 86 | 87 | // The next step will block, so set progress 88 | // 89 | steps.set_state( Step::SendText ).await; 90 | 91 | // With the message from above, this is bigger than 37 bytes, so it will block on the underlying transport. 92 | sink.send( "ho block\n".to_string() ).await.expect( "Send second line" ); 93 | 94 | trace!( "server: writer end" ); 95 | }; 96 | 97 | 98 | let reader = async 99 | { 100 | info!( "wait for read_text" ); 101 | read_text.await; 102 | 103 | // The next step will block, so set progress 104 | // 105 | steps.set_state( Step::ClientRead ).await; 106 | 107 | warn!( "server: read the text, should return None" ); 108 | 109 | let res = stream.next().await; 110 | 111 | assert!(res.is_none()); 112 | 113 | info!( "server: assert protocol error" ); 114 | 115 | match events.next().await.expect( "protocol error" ) 116 | { 117 | WsEvent::Error( e ) => assert!(matches!( *e, WsErr::ReceivedText )), 118 | evt => unreachable!( "{:?}", evt ), 119 | } 120 | 121 | trace!( "server: reader end" ); 122 | }; 123 | 124 | let reader = reader.instrument( tracing::info_span!( "server_reader" ) ); 125 | let writer = writer.instrument( tracing::info_span!( "server_writer" ) ); 126 | 127 | join( reader, writer ).await; 128 | 129 | trace!( "server: drop websocket" ); 130 | } 131 | 132 | 133 | 134 | async fn client 135 | ( 136 | send_text : impl Future , 137 | client_read : impl Future , 138 | steps : Progress , 139 | cs : Endpoint , 140 | ) 141 | { 142 | let mut conf = WebSocketConfig::default(); 143 | conf.write_buffer_size = 0; 144 | 145 | let (mut sink, mut stream) = WebSocketStream::from_raw_socket( cs, Role::Client, Some(conf) ).await.split(); 146 | 147 | info!( "wait for send_text" ); 148 | send_text.await; 149 | sink.send( tungstenite::Message::Text( "Text from client".into() ) ).await.expect( "send text" ); 150 | 151 | steps.set_state( Step::ReadText ).await; 152 | 153 | info!( "wait for client_read" ); 154 | client_read.await; 155 | 156 | let test = stream.next().await.unwrap().unwrap(); 157 | assert_matches!( test, Message::Binary(_) ); 158 | 159 | info!( "client: received first binary message" ); 160 | 161 | 162 | let test = stream.next().await.unwrap().unwrap(); 163 | assert_matches!( test, Message::Binary(_) ); 164 | 165 | info!( "client: received second binary message" ); 166 | 167 | 168 | let frame = CloseFrame 169 | { 170 | code : CloseCode::Unsupported, 171 | reason: "Text messages are not supported.".into(), 172 | }; 173 | 174 | warn!( "client: waiting on close frame" ); 175 | 176 | assert_eq!( Some( tungstenite::Message::Close( Some(frame) )), stream.next().await.transpose().expect( "client: receive close frame" ) ); 177 | 178 | // As tungstenite needs input to send out the response to the close frame, we need to keep polling 179 | // 180 | assert_eq!( None, stream.next().await.transpose().expect( "client: stream closed" ) ); 181 | 182 | trace!( "client: drop websocket" ); 183 | } 184 | 185 | 186 | 187 | // Progress: 188 | // 189 | // server fills it's send queue 190 | // client sends a text message (which is protocol error) 191 | // server reads text message, initiates close handshake, but queue is full 192 | // client starts reading, should get the messages hanging on server, followed by the close handshake. 193 | // 194 | #[ derive( Debug, Clone, PartialEq, Eq )] 195 | // 196 | enum Step 197 | { 198 | FillQueue , 199 | SendText , 200 | ReadText , 201 | ClientRead , 202 | } 203 | -------------------------------------------------------------------------------- /tests/tokio_codec.rs: -------------------------------------------------------------------------------- 1 | #![ cfg( feature = "tokio_io" ) ] 2 | // 3 | // Test using the AsyncRead/AsyncWrite from tokio 4 | // 5 | // ✔ frame with tokio_util::codec 6 | // 7 | use 8 | { 9 | ws_stream_tungstenite :: { * } , 10 | futures :: { StreamExt, SinkExt, future::join } , 11 | tokio_util::codec :: { LinesCodec, Framed } , 12 | tokio :: { net::{ TcpListener } } , 13 | async_tungstenite :: { accept_async, tokio::{ connect_async, TokioAdapter } } , 14 | url :: { Url } , 15 | tracing :: { * } , 16 | }; 17 | 18 | 19 | #[ tokio::test ] 20 | // 21 | async fn tokio_codec() 22 | { 23 | let server = async 24 | { 25 | let socket = TcpListener::bind( "127.0.0.1:3012" ).await.expect( "bind to port" ); 26 | let (tcp_stream, _peer_addr) = socket.accept().await.expect( "tcp connect" ); 27 | let s = accept_async( TokioAdapter::new(tcp_stream) ).await.expect("Error during the websocket handshake occurred"); 28 | let server = WsStream::new( s ); 29 | 30 | let (mut sink, mut stream) = Framed::new( server, LinesCodec::new() ).split(); 31 | 32 | sink.send( "A line" .to_string() ).await.expect( "Send a line" ); 33 | sink.send( "A second line".to_string() ).await.expect( "Send a line" ); 34 | 35 | sink.close().await.expect( "close server" ); 36 | 37 | let read = stream.next().await.transpose().expect( "close connection" ); 38 | 39 | assert!( read.is_none() ); 40 | debug!( "Server task ended" ); 41 | }; 42 | 43 | 44 | let client = async 45 | { 46 | let url = Url::parse( "ws://127.0.0.1:3012" ).unwrap(); 47 | let socket = connect_async( url ).await.expect( "ws handshake" ); 48 | 49 | let client = WsStream::new( socket.0 ); 50 | let mut framed = Framed::new( client, LinesCodec::new() ); 51 | 52 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a line" ); 53 | assert_eq!( "A line".to_string(), res ); 54 | 55 | 56 | let res = framed.next().await.expect( "Receive some" ).expect( "Receive a second line" ); 57 | assert_eq!( "A second line".to_string(), res ); 58 | 59 | let res = framed.next().await; 60 | 61 | assert!( res.is_none() ); 62 | debug!( "Client task ended" ); 63 | }; 64 | 65 | join( server, client ).await; 66 | } 67 | 68 | --------------------------------------------------------------------------------