Repository: 1c3t3a/rust-socketio Branch: main Commit: a4e52873105c Files: 85 Total size: 375.4 KB Directory structure: gitextract_9tvnxqqn/ ├── .devcontainer/ │ ├── Dockerfile │ ├── devcontainer.json │ └── docker-compose.yaml ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── benchmark.yml │ ├── build.yml │ ├── coverage.yml │ ├── publish-dry-run.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── Roadmap.md ├── ci/ │ ├── .dockerignore │ ├── Dockerfile │ ├── README.md │ ├── engine-io-polling.js │ ├── engine-io-secure.js │ ├── engine-io.js │ ├── keygen.sh │ ├── package.json │ ├── socket-io-auth.js │ ├── socket-io-restart-url-auth.js │ ├── socket-io-restart.js │ ├── socket-io.js │ └── start_test_server.sh ├── codecov.yml ├── engineio/ │ ├── Cargo.toml │ ├── README.md │ ├── benches/ │ │ └── engineio.rs │ └── src/ │ ├── asynchronous/ │ │ ├── async_socket.rs │ │ ├── async_transports/ │ │ │ ├── mod.rs │ │ │ ├── polling.rs │ │ │ ├── websocket.rs │ │ │ ├── websocket_general.rs │ │ │ └── websocket_secure.rs │ │ ├── callback.rs │ │ ├── client/ │ │ │ ├── async_client.rs │ │ │ ├── builder.rs │ │ │ └── mod.rs │ │ ├── generator.rs │ │ ├── mod.rs │ │ └── transport.rs │ ├── callback.rs │ ├── client/ │ │ ├── client.rs │ │ └── mod.rs │ ├── error.rs │ ├── header.rs │ ├── lib.rs │ ├── packet.rs │ ├── socket.rs │ ├── transport.rs │ └── transports/ │ ├── mod.rs │ ├── polling.rs │ ├── websocket.rs │ └── websocket_secure.rs └── socketio/ ├── Cargo.toml ├── examples/ │ ├── async.rs │ ├── callback.rs │ ├── readme.rs │ └── secure.rs └── src/ ├── asynchronous/ │ ├── client/ │ │ ├── ack.rs │ │ ├── builder.rs │ │ ├── callback.rs │ │ ├── client.rs │ │ └── mod.rs │ ├── generator.rs │ ├── mod.rs │ └── socket.rs ├── client/ │ ├── builder.rs │ ├── callback.rs │ ├── client.rs │ ├── mod.rs │ └── raw_client.rs ├── error.rs ├── event.rs ├── lib.rs ├── packet.rs ├── payload.rs └── socket.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM mcr.microsoft.com/vscode/devcontainers/rust:0-1 # Install socat needed for TCP proxy RUN apt update && apt install -y socat COPY ./ci/cert/ca.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/rust { "name": "Rust", "dockerComposeFile": [ "./docker-compose.yaml" ], "service": "rust-client", "workspaceFolder": "/workspace/rust-socketio", "shutdownAction": "stopCompose", "customizations": { "vscode": { // Set *default* container specific settings.json values on container create. "settings": { "lldb.executable": "/usr/bin/lldb", // VS Code don't watch files under ./target "files.watcherExclude": { "**/target/**": true }, "rust-analyzer.cargo.features": [ "async" ] /*, // If you prefer rust-analzyer to be less noisy consider these settings to your settings.json "editor.semanticTokenColorCustomizations": { "rules": { "*.mutable": { "underline": false } } }, "rust-analyzer.inlayHints.parameterHints": false */ }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "rust-lang.rust-analyzer", "bungcip.better-toml", "vadimcn.vscode-lldb", "eamodio.gitlens", "streetsidesoftware.code-spell-checker" ] } }, "remoteUser": "vscode", // Start a TCP proxy from to the testing node-socket-io server so doc tests can pass. "postAttachCommand": { "SocketIOProxy": "socat TCP-LISTEN:4200,fork,reuseaddr TCP:node-socket-io:4200", "EngineIOProxy": "socat TCP-LISTEN:4201,fork,reuseaddr TCP:node-engine-io:4201", "SocketIOAuthProxy": "socat TCP-LISTEN:4204,fork,reuseaddr TCP:node-socket-io-auth:4204" } } ================================================ FILE: .devcontainer/docker-compose.yaml ================================================ version: '3' services: node-engine-io-secure: build: context: ../ci command: [ "node", "/test/engine-io-secure.js" ] ports: - "4202:4202" environment: - "DEBUG=*" node-engine-io: build: context: ../ci command: [ "node", "/test/engine-io.js" ] ports: - "4201:4201" environment: - "DEBUG=*" node-engine-io-polling: build: context: ../ci command: [ "node", "/test/engine-io-polling.js" ] ports: - "4203:4203" environment: - "DEBUG=*" node-socket-io: build: context: ../ci command: [ "node", "/test/socket-io.js" ] ports: - "4200:4200" environment: - "DEBUG=*" node-socket-io-auth: build: context: ../ci command: [ "node", "/test/socket-io-auth.js" ] ports: - "4204:4204" environment: - "DEBUG=*" node-socket-restart: build: context: ../ci command: [ "node", "/test/socket-io-restart.js" ] ports: - "4205:4205" environment: - "DEBUG=*" node-socket-restart-url-auth: build: context: ../ci command: [ "node", "/test/socket-io-restart-url-auth.js" ] ports: - "4206:4206" environment: - "DEBUG=*" rust-client: build: context: .. dockerfile: ./.devcontainer/Dockerfile command: /bin/sh -c "while sleep 10000d; do :; done" security_opt: - seccomp:unconfined volumes: - "..:/workspace/rust-socketio" environment: - "SOCKET_IO_SERVER=http://node-socket-io:4200" - "SOCKET_IO_AUTH_SERVER=http://node-socket-io-auth:4204" - "ENGINE_IO_SERVER=http://node-engine-io:4201" - "ENGINE_IO_SECURE_SERVER=https://node-engine-io-secure:4202" - "ENGINE_IO_SECURE_HOST=node-engine-io-secure" - "ENGINE_IO_POLLING_SERVER=http://node-engine-io-polling:4203" - "SOCKET_IO_RESTART_SERVER=http://node-socket-restart:4205" - "SOCKET_IO_RESTART_URL_AUTH_SERVER=http://node-socket-restart-url-auth:4206" ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "monthly" groups: patches: # Group cargo patch updates together to minimize PR management faff applies-to: version-updates update-types: - patch ================================================ FILE: .github/workflows/benchmark.yml ================================================ on: pull_request: types: [opened] issue_comment: types: [created] name: benchmark engine.io jobs: runBenchmark: name: run benchmark runs-on: ubuntu-latest steps: - uses: khan/pull-request-comment-trigger@master id: check with: trigger: '/benchmark' reaction: rocket env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - name: Checkout repository if: steps.check.outputs.triggered == 'true' uses: actions/checkout@v2 - name: Setup rust environment if: steps.check.outputs.triggered == 'true' uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Setup docker if: steps.check.outputs.triggered == 'true' id: buildx uses: docker/setup-buildx-action@v1 - name: Generate keys if: steps.check.outputs.triggered == 'true' run: make keys - name: Build docker container if: steps.check.outputs.triggered == 'true' run: | cd ci && docker build -t test_suite:latest . docker run -d -p 4200:4200 -p 4201:4201 -p 4202:4202 -p 4203:4203 -p 4204:4204 -p 4205:4205 -p 4206:4206 test_suite:latest - name: Extract branch name if: steps.check.outputs.triggered == 'true' shell: bash run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" id: extract_branch - uses: actions/checkout@master if: steps.check.outputs.triggered == 'true' - uses: boa-dev/criterion-compare-action@v3.2.0 if: steps.check.outputs.triggered == 'true' with: cwd: "engineio" branchName: ${{ steps.extract_branch.outputs.branch }} token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/build.yml ================================================ name: Build and code style on: push: branches: [main, refactoring] pull_request: branches: [main] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup rust environment uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Generate Cargo.lock run: cargo generate-lockfile - uses: actions/cache@v2 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Build run: cargo build --verbose --all-features - name: Linting run: cargo clippy --verbose --all-features - name: Check formatting run: cargo fmt --all -- --check ================================================ FILE: .github/workflows/coverage.yml ================================================ on: push: branches: [main] pull_request: branches: [main] name: generate coverage jobs: check: name: Setup Rust project runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - name: Setup rust environment uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Setup docker id: buildx uses: docker/setup-buildx-action@v1 - name: Generate keys run: make keys - name: Build docker container run: | cd ci && docker build -t test_suite:latest . docker run -d --name test_suite -p 4200:4200 -p 4201:4201 -p 4202:4202 -p 4203:4203 -p 4204:4204 -p 4205:4205 -p 4206:4206 test_suite:latest - uses: actions/cache@v2 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Generate code coverage run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - name: Upload to codecov.io uses: codecov/codecov-action@v1.5.2 with: token: ${{secrets.CODECOV_TOKEN}} files: lcov.info fail_ci_if_error: true - name: Collect docker logs if: always() run: docker logs test_suite > my_logs.txt 2>&1 - name: Upload docker logs uses: actions/upload-artifact@v4 if: always() with: name: docker logs path: my_logs.txt ================================================ FILE: .github/workflows/publish-dry-run.yml ================================================ name: Publish dry run on: workflow_dispatch jobs: publish: name: Publish dry run runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true # Publish the engine.io crate - uses: katyo/publish-crates@v1 with: path: './engineio' dry-run: true # Publish the socket.io crate - uses: katyo/publish-crates@v1 with: path: './socketio' dry-run: true ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: workflow_dispatch jobs: publish: name: Publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true # Publish the engine.io crate - uses: katyo/publish-crates@v1 with: path: './engineio' registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} # Publish the socket.io crate - uses: katyo/publish-crates@v1 with: path: './socketio' registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: [main] pull_request: branches: [main, refactoring] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v2 - name: Setup rust environment uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Setup docker id: buildx uses: docker/setup-buildx-action@v1 - name: Generate keys run: make keys - name: Build docker container run: | cd ci && docker build -t test_suite:latest . docker run -d -p 4200:4200 -p 4201:4201 -p 4202:4202 -p 4203:4203 -p 4204:4204 -p 4205:4205 -p 4206:4206 test_suite:latest - name: Generate Cargo.lock run: cargo generate-lockfile - uses: actions/cache@v2 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run testsuite run: cargo test --verbose --features "async" ================================================ FILE: .gitignore ================================================ target .vscode .idea ci/node_modules ci/package-lock.json ci/cert ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project are documented in this file. The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning]. The file is auto-generated using [Conventional Commits]. [keep a changelog]: https://keepachangelog.com/en/1.0.0/ [semantic versioning]: https://semver.org/spec/v2.0.0.html [conventional commits]: https://www.conventionalcommits.org/en/v1.0.0-beta.4/ ## Overview * [unreleased](#unreleased) * [`0.5.0`](#060) - _2024.04.16_ * [`0.5.0`](#050) - _2024.03.31_ * [`0.4.4`](#044) - _2023.11.18_ * [`0.4.3`](#043) - _2023.07.08_ * [`0.4.2`](#042) - _2023.06.25_ * [`0.4.1-alpha.2`](#041a2) - _2023.03.26_ * [`0.4.1-alpha.1`](#041a1) - _2023.01.15_ * [`0.4.0`](#041) - _2023.01.15_ * [`0.4.0`](#040) - _2022.10.20_ * [`0.3.1`](#031) - _2022.03.19_ * [`0.3.0`](#030) - _2021.12.16_ * [`0.3.0-alpha.2`](#030a3) - _2021.12.04_ * [`0.3.0-alpha.2`](#030a2) - _2021.10.14_ * [`0.3.0-alpha.1`](#030a1) - _2021.09.20_ * [`0.2.4`](#024) - _2021.05.25_ * [`0.2.3`](#023) - _2021.05.24_ * [`0.2.2`](#022) - _2021.05.13 * [`0.2.1`](#021) - _2021.04.27_ * [`0.2.0`](#020) – _2021.03.13_ * [`0.1.1`](#011) – _2021.01.10_ * [`0.1.0`](#010) – _2021.01.05_ ## _[Unreleased]_ _nothing new to show for… yet!_> ## [0.6.0] - _Multi-payload fix and http 1.0_ _2024.04.16_ - Fix issues with processing multi-payload messages ([#392](https://github.com/1c3t3a/rust-socketio/pull/392)). Credits to shenjackyuanjie@. - Bump http to 1.0 and all dependencies that use http to a version that also uses http 1.0 ([#418](https://github.com/1c3t3a/rust-socketio/pull/418)). Bumping those dependencies makes this a breaking change. ## [0.5.0] - _Packed with changes!_ _2024.03.31_ - Support multiple arguments to the payload through a new Payload variant called `Text` that holds a JSON value ([#384](https://github.com/1c3t3a/rust-socketio/pull/384)). Credits to ctrlaltf24@ and SalahaldinBilal@! Please note: This is a breaking change: `Payload::String` is deprecated and will be removed soon. - Async reconnections: Support for automatic reconnection in the async version of the crate! ([#400](https://github.com/1c3t3a/rust-socketio/pull/400)). Credits to rageshkrishna@. - Add an `on_reconnect` callback that allows to change the connection configuration ([#405](https://github.com/1c3t3a/rust-socketio/pull/405)). Credits to rageshkrishna@. - Fix bug that ignored the ping interval ([#359](https://github.com/1c3t3a/rust-socketio/pull/359)). Credits to sirkrypt0@. This is a breaking change that removes the engine.io's stream impl. It is however replaced by a method called `as_stream` on the engine.io socket. - Add macro `async_callback` and `async_any_callback` for async callbacks ([#399](https://github.com/1c3t3a/rust-socketio/pull/399). Credits to shenjackyuanjie@. ## [0.4.4] - _Bump dependencies_ _2023.11.18_ - Bump tungstenite version to v0.20.1 (avoiding security vulnerability) [#368](https://github.com/1c3t3a/rust-socketio/pull/368) - Updating other dependencies ## [0.4.3] - _Bugfix!_ _2023.07.08_ - Fix of [#323](https://github.com/1c3t3a/rust-socketio/issues/323) - Marking the async feature optional ## [0.4.2] - _Stabilizing the async interface!_ _2023.06.25_ - Fix "Error while parsing an incomplete packet socketio" on first heartbeat killing the connection async client ([#311](https://github.com/1c3t3a/rust-socketio/issues/311)). Credits to [@sirkrypt0](https://github.com/sirkrypt0) - Fix allow awaiting async callbacks ([#313](https://github.com/1c3t3a/rust-socketio/issues/313)). Credits to [@felix-gohla](https://github.com/felix-gohla) - Various performance improvements especially in packet parsing. Credits to [@MaxOhn](https://github.com/MaxOhn) - API for setting the reconnect URL on a connected client ([#251](https://github.com/1c3t3a/rust-socketio/issues/251)). Credits to [@tyilo](https://github.com/tyilo) ## [0.4.0-alpha.2] - _Async socket.io fixes_ _2023.03.26_ - Add `on_any` method for async `ClientBuilder`. This adds the capability to react to all incoming events (custom and otherwise). - Add `auth` option to async `ClientBuilder`. This allows for specifying JSON data that is sent with the first open packet, which is commonly used for authentication. - Bump dependencies and remove calls to deprecated library functions. ## [0.4.0-alpha.1] - _Async socket.io version_ _2023.01.05_ - Add an async socket.io interface under the `async` feature flag, relevant PR: [#180](https://github.com/1c3t3a/rust-socketio/pull/180). - See example code under `socketio/examples/async.rs` and in the `async` section of the README. ## [0.4.1] - _Minor enhancements_ _2023.01.05_ - As of [#264](https://github.com/1c3t3a/rust-socketio/pull/264), the callbacks are now allowed to be `?Sync`. - As of [#265](https://github.com/1c3t3a/rust-socketio/pull/265), the `Payload` type now implements `AsRef`. ## [0.4.0] - _Bugfixes and Reconnection feature_ _2022.10.20_ ### Changes - Fix [#214](https://github.com/1c3t3a/rust-socketio/issues/214). - Fix [#215](https://github.com/1c3t3a/rust-socketio/issues/215). - Fix [#219](https://github.com/1c3t3a/rust-socketio/issues/219). - Fix [#221](https://github.com/1c3t3a/rust-socketio/issues/221). - Fix [#222](https://github.com/1c3t3a/rust-socketio/issues/222). - BREAKING: The default Client returned by the builder will automatically reconnect to the server unless stopped manually. The new `ReconnectClient` encapsulates this behaviour. Special thanks to [@SSebo](https://github.com/SSebo) for his major contribution to this release. ## [0.3.1] - _Bugfix_ _2022.03.19_ ### Changes - Fixes regarding [#166](https://github.com/1c3t3a/rust-socketio/issues/166). ## [0.3.0] - _Stabilize alpha version_ _2021.12.16_ ### Changes - Stabilized alpha features. - Fixes regarding [#133](https://github.com/1c3t3a/rust-socketio/issues/133). ## [0.3.0-alpha.3] - _Bugfixes_ _2021.12.04_ ### Changes - fix a bug that resulted in a blocking `emit` method (see [#133](https://github.com/1c3t3a/rust-socketio/issues/133)). - Bump dependencies. ## [0.3.0-alpha.2] - _Further refactoring_ _2021.10.14_ ### Changes * Rename `Socket` to `Client` and `SocketBuilder` to `ClientBuilder` * Removed headermap from pub use, internal type only * Deprecations: * crate::payload (use crate::Payload instead) * crate::error (use crate::Error instead) * crate::event (use crate::Event instead) ## [0.3.0-alpha.1] - _Refactoring_ _2021.09.20_ ### Changes * Refactored Errors * Renamed EmptyPacket to EmptyPacket() * Renamed IncompletePacket to IncompletePacket() * Renamed InvalidPacket to InvalidPacket() * Renamed Utf8Error to InvalidUtf8() * Renamed Base64Error to InvalidBase64 * Renamed InvalidUrl to InvalidUrlScheme * Renamed ReqwestError to IncompleteResponseFromReqwest * Renamed HttpError to IncompleteHttp * Renamed HandshakeError to InvalidHandshake * Renamed ~ActionBeforeOpen to IllegalActionBeforeOpen()~ * Renamed DidNotReceiveProperAck to MissingAck * Renamed PoisonedLockError to InvalidPoisonedLock * Renamed FromWebsocketError to IncompleteResponseFromWebsocket * Renamed FromWebsocketParseError to InvalidWebsocketURL * Renamed FromIoError to IncompleteIo * New error type InvalidUrl(UrlParseError) * New error type InvalidInteger(ParseIntError) * New error type IncompleteResponseFromEngineIo(rust_engineio::Error) * New error type InvalidAttachmentPacketType(u8) * Removed EmptyPacket * Refactored Packet * Renamed encode to From<&Packet> * Renamed decode to TryFrom<&Bytes> * Renamed attachments to attachments_count * New struct member attachments: Option> * Refactor PacketId * Renamed u8_to_packet_id to TryFrom for PacketId * Refactored SocketBuilder * Renamed set_namespace to namespace * Renamed set_tls_config to tls_config * Renamed set_opening_header to opening_header * namespace returns Self rather than Result * opening_header accepts a Into rather than HeaderValue * Allows for pure websocket connections * Refactor EngineIO module ## [0.2.4] - _Bugfixes_ _2021.05.25_ ### Changes * Fixed a bug that prevented the client from receiving data for a message event issued on the server. ## [0.2.3] - _Disconnect methods on the Socket struct_ _2021.05.24_ ### Changes * Added a `disconnect` method to the `Socket` struct as requested in [#43](https://github.com/1c3t3a/rust-socketio/issues/43). ## [0.2.2] - _Safe websockets and custom headers_ _2021.05.13_ ### Changes * Added websocket communication over TLS when either `wss`, or `https` are specified in the URL. * Added the ability to configure the TLS connection by providing an own `TLSConnector`. * Added the ability to set custom headers as requested in [#35](https://github.com/1c3t3a/rust-socketio/issues/35). ## [0.2.1] - _Bugfixes_ _2021.04.27_ ### Changes * Corrected memory ordering issues which might have become an issue on certain platforms. * Added this CHANGELOG to keep track of all changes. * Small stylistic changes to the codebase in general. ## [0.2.0] - _Fully implemented the socket.io protocol 🎉_ _2021.03.13_ ### Changes * Moved from async rust to sync rust. * Implemented the missing protocol features. * Websocket as a transport layer. * Binary payload. * Added a `SocketBuilder` class to easily configure a connected client. * Added a new `Payload` type to manage the binary and string payload. ## [0.1.1] - _Update to tokio 1.0_ _2021.01.10_ ### Changes * Bumped `tokio` to version `1.0.*`, and therefore reqwest to `0.11.*`. * Removed all `unsafe` code. ## [0.1.0] - _First release of rust-socketio 🎉_ _2021.01.05_ * First version of the library written in async rust. The features included: * connecting to a server. * register callbacks for the following event types: * open, close, error, message * custom events like "foo", "on_payment", etc. * send json-data to the server (recommended to use serde_json as it provides safe handling of json data). * send json-data to the server and receive an ack with a possible message. ================================================ FILE: CONTRIBUTING.md ================================================ # Introduction Contributions to this project are welcome! This project is still being developed, our goal is to have a well-designed thought-out project, so when you make a contribution, please think in those terms. That is: - For code: - Is the contribution in the scope of this crate? - Is it well documented? - Is it well tested? - For documentation: - Is it clear and well-written? - Can others understand it easily? - For bugs: - Does it test functionality of this crate? - Do you have a minimal crate that causes the issue that we can use and test? - For feature requests: - Is the request within the scope of this crate? - Is the request clearly explained? ## Licensing and other property rights. All contributions that are made to this project are only accepted under the terms in the [LICENSE](LICENSE) file. That is, if you contribute to this project, you are certifying that you have all the necessary rights to make the contribution under the terms of the [LICENSE](LICENSE) file, and you are in fact licensing them to this project and anyone that uses this project under the terms in the [LICENSE](LICENSE) file. ## Misc - We are looking at adopting the [conventional commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) standard. - This would make it easier for us to use tools like [jilu](https://crates.io/crates/jilu) to create change logs. - Read [keep a changelog](https://keepachangelog.com/en/1.0.0/) for more information. ## Git hooks > Git hooks are scripts that Git executes before or after events such as: commit, push, and receive. Git hooks are a built-in feature - no need to download anything. Git hooks are run locally. - [githooks.com](https://githooks.com/) These example hooks enforce some of these contributing guidelines so you don't need to remember them. ### pre-commit Put the contents below in ./.git/hooks/pre-commit ```bash #!/bin/bash set -e set -o pipefail # Clippy recomendations cargo clippy --verbose # Check formatting cargo fmt --all -- --check || { cargo fmt echo "Formatted some files make sure to check them in." exit 1 } # Make sure our build passes cargo build --verbose ``` ### commit-msg Put the contents below in ./.git/hooks/commit-msg ```bash #!/bin/bash # https://dev.to/craicoverflow/enforcing-conventional-commits-using-git-hooks-1o5p regexp="^(revert: )?(fix|feat|docs|ci|refactor|style|test)(\(.*?\))?(\!)?: .+$" msg="$(head -1 $1)" if [[ ! $msg =~ $regexp ]] then echo -e "INVALID COMMIT MESSAGE" echo -e "------------------------" echo -e "Valid types: fix, feat, docs, ci, style, test, refactor" echo -e "Such as: 'feat: add new feature'" echo -e "See https://www.conventionalcommits.org/en/v1.0.0/ for details" echo # exit with an error exit 1 fi while read line do if [[ $(echo "$line" | wc -c) -gt 51 ]] then echo "Line '$line' is longer than 50 characters." echo "Consider splitting into these two lines" echo "$line" | head -c 50 echo echo "$line" | tail -c +51 echo exit 1 fi done < $1 ``` ================================================ FILE: Cargo.toml ================================================ [workspace] members = ["engineio", "socketio"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Bastian Kersting Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: build test-fast run-test-servers test-all clippy format checks pipeline build: @cargo build --verbose --all-features keys: @./ci/keygen.sh node-engine-io-secure 127.0.0.1 test-fast: keys @cargo test --verbose --package rust_socketio --lib -- engineio::packet && cargo test --verbose --package rust_socketio --lib -- socketio::packet run-test-servers: cd ci && docker build -t test_suite:latest . && cd .. docker run -d -p 4200:4200 -p 4201:4201 -p 4202:4202 -p 4203:4203 -p 4204:4204 -p 4205:4205 -p 4206:4206 --name socketio_test test_suite:latest test-all: keys run-test-servers @cargo test --verbose --all-features docker stop socketio_test clippy: @cargo clippy --verbose --all-features format: @cargo fmt --all -- --check checks: build test-fast clippy format @echo "### Don't forget to add untracked files! ###" @git status @echo "### Awesome work! 😍 ###""" pipeline: build test-all clippy format @echo "### Don't forget to add untracked files! ###" @git status @echo "### Awesome work! 😍 ###""" ================================================ FILE: README.md ================================================ [![Latest Version](https://img.shields.io/crates/v/rust_socketio)](https://crates.io/crates/rust_socketio) [![docs.rs](https://docs.rs/rust_socketio/badge.svg)](https://docs.rs/rust_socketio) [![Build and code style](https://github.com/1c3t3a/rust-socketio/actions/workflows/build.yml/badge.svg)](https://github.com/1c3t3a/rust-socketio/actions/workflows/build.yml) [![Test](https://github.com/1c3t3a/rust-socketio/actions/workflows/test.yml/badge.svg)](https://github.com/1c3t3a/rust-socketio/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/1c3t3a/rust-socketio/branch/main/graph/badge.svg?token=GUF406K0KL)](https://codecov.io/gh/1c3t3a/rust-socketio) # Rust-socketio-client An implementation of a socket.io client written in the rust programming language. This implementation currently supports revision 5 of the socket.io protocol and therefore revision 4 of the engine.io protocol. If you have any connection issues with this client, make sure the server uses at least revision 4 of the engine.io protocol. Information on the [`async`](#async) version can be found below. ## Example usage Add the following to your `Cargo.toml` file: ```toml rust_socketio = "*" ``` Then you're able to run the following example code: ``` rust use rust_socketio::{ClientBuilder, Payload, RawClient}; use serde_json::json; use std::time::Duration; // define a callback which is called when a payload is received // this callback gets the payload as well as an instance of the // socket to communicate with the server let callback = |payload: Payload, socket: RawClient| { match payload { Payload::String(str) => println!("Received: {}", str), Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), } socket.emit("test", json!({"got ack": true})).expect("Server unreachable") }; // get a socket that is connected to the admin namespace let socket = ClientBuilder::new("http://localhost:4200") .namespace("/admin") .on("test", callback) .on("error", |err, _| eprintln!("Error: {:#?}", err)) .connect() .expect("Connection failed"); // emit to the "foo" event let json_payload = json!({"token": 123}); socket.emit("foo", json_payload).expect("Server unreachable"); // define a callback, that's executed when the ack got acked let ack_callback = |message: Payload, _| { println!("Yehaa! My ack got acked?"); println!("Ack data: {:#?}", message); }; let json_payload = json!({"myAckData": 123}); // emit with an ack socket .emit_with_ack("test", json_payload, Duration::from_secs(2), ack_callback) .expect("Server unreachable"); socket.disconnect().expect("Disconnect failed") ``` The main entry point for using this crate is the `ClientBuilder` which provides a way to easily configure a socket in the needed way. When the `connect` method is called on the builder, it returns a connected client which then could be used to emit messages to certain events. One client can only be connected to one namespace. If you need to listen to the messages in different namespaces you need to allocate multiple sockets. ## Documentation Documentation of this crate can be found up on [docs.rs](https://docs.rs/rust_socketio). ## Current features This implementation now supports all of the features of the socket.io protocol mentioned [here](https://github.com/socketio/socket.io-protocol). It generally tries to make use of websockets as often as possible. This means most times only the opening request uses http and as soon as the server mentions that he is able to upgrade to websockets, an upgrade is performed. But if this upgrade is not successful or the server does not mention an upgrade possibility, http-long polling is used (as specified in the protocol specs). Here's an overview of possible use-cases: - connecting to a server. - register callbacks for the following event types: - open - close - error - message - custom events like "foo", "on_payment", etc. - send JSON data to the server (via `serde_json` which provides safe handling). - send JSON data to the server and receive an `ack`. - send and handle Binary data. ## Async version This library provides an ability for being executed in an asynchronous context using `tokio` as the execution runtime. Please note that the current async implementation is still experimental, the interface can be object to changes at any time. The async `Client` and `ClientBuilder` support a similar interface to the sync version and live in the `asynchronous` module. In order to enable the support, you need to enable the `async` feature flag: ```toml rust_socketio = { version = "*", features = ["async"] } ``` The following code shows the example above in async fashion: ``` rust use futures_util::FutureExt; use rust_socketio::{ asynchronous::{Client, ClientBuilder}, Payload, }; use serde_json::json; use std::time::Duration; #[tokio::main] async fn main() { // define a callback which is called when a payload is received // this callback gets the payload as well as an instance of the // socket to communicate with the server let callback = |payload: Payload, socket: Client| { async move { match payload { Payload::String(str) => println!("Received: {}", str), Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), } socket .emit("test", json!({"got ack": true})) .await .expect("Server unreachable"); } .boxed() }; // get a socket that is connected to the admin namespace let socket = ClientBuilder::new("http://localhost:4200/") .namespace("/admin") .on("test", callback) .on("error", |err, _| { async move { eprintln!("Error: {:#?}", err) }.boxed() }) .connect() .await .expect("Connection failed"); // emit to the "foo" event let json_payload = json!({"token": 123}); socket .emit("foo", json_payload) .await .expect("Server unreachable"); // define a callback, that's executed when the ack got acked let ack_callback = |message: Payload, _: Client| { async move { println!("Yehaa! My ack got acked?"); println!("Ack data: {:#?}", message); } .boxed() }; let json_payload = json!({"myAckData": 123}); // emit with an ack socket .emit_with_ack("test", json_payload, Duration::from_secs(2), ack_callback) .await .expect("Server unreachable"); socket.disconnect().await.expect("Disconnect failed"); } ``` ## Content of this repository This repository contains a rust implementation of the socket.io protocol as well as the underlying engine.io protocol. The details about the engine.io protocol can be found here: * The specification for the socket.io protocol here: * Looking at the component chart, the following parts are implemented (Source: https://socket.io/images/dependencies.jpg): ## Licence MIT ================================================ FILE: Roadmap.md ================================================ # Roadmap - 0.2.5 deprecation begins - 0.3.0 refactor with breaking changes (Conform to api recommendations wherever reasonable) - 0.4.0 Release with basic server - 0.5.0 Release with async - 0.6.0 Release with redis - ????? Rooms - ????? Refactor Engine.IO to separate crate - 1.0.0 Stable? ================================================ FILE: ci/.dockerignore ================================================ node_modules ================================================ FILE: ci/Dockerfile ================================================ FROM node:12.18.1 WORKDIR /test COPY . ./ COPY start_test_server.sh ./ RUN cp cert/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates RUN npm install RUN chmod u+x start_test_server.sh CMD ./start_test_server.sh ================================================ FILE: ci/README.md ================================================ # How the CI pipelines are set up This document explains how the CI pipeline is set up. Generally, the pipeline runs on ever push to the `main` branch or on a pull request. There are three different pipelines: * Build and Codestyle: Tries to build the application and checks the code quality and formatting through `cargo clippy` and `cargo fmt`. If you'd like to trigger this manually, run `make checks` in the project root. * Build and test: Builds the code and kicks of a docker container containing the socket.io and engine.io servers. Then the tests run against the servers. The servers code should not be changed as the clients' tests assume certain events to fire e.g. an ack gets acked or a certain namespace exists. Two servers are started: * An engine.io server with some callbacks that send normal string data. * A _safe_ engine.io server with some callbacks that send normal string data. Generate keys for TLS with `./ci/keygen.sh localhost 127.0.0.1`. This server is used for tests using `wss://` and `https://`. * A socket.io server which sends string and binary data, handles acks, etc. * Generate coverage: This action acts like the `Build and test` action, but generates a coverage report as well. Afterward the coverage report is uploaded to codecov.io. This action also collects the docker server logs and uploads them as an artifact. # How to run the tests locally To run the tests locally, simply use `cargo test`, or the various Make targets in the `Makefile`. For example `make pipeline` runs all tests, but also `clippy` and `rustfmt`. As some tests depend on a running engine.io/socket.io server, you will need to provide those locally, too. See the sections below for multiple options to do this. You will also need to create a self-signed certificate for the secure engine.io server. This can be done with the helper script `/ci/keygen.sh`. Please make sure to also import the generated ca and server certificates into your system (i.e. Keychain Access for MacOS, /usr/local/share/ca-certificates/ for linux) and make sure they are "always trusted". ## Running server processes manually via nodejs If you'd like to run the full test suite locally, you need to run the five server instances as well (see files in `ci/` folder). You could do this manually by running them all with node: ``` node engine-io.js node engine-io-polling.js node engine-io-secure.js node socket-io.js node socket-io-auth.js node socket-io-restart.js node socket-io-restart-url-auth.js ``` If you'd like to see debug log as well, export this environment variable beforehand: ``` export DEBUG=* ``` You will need to have the two node packages `socket.io` and `engine.io` installed, if this is not the case, fetch them via: ``` npm install socket.io engine.io ``` ## Running server processes in a Docker container As running all the node scripts manually is pretty tedious, you can also use a prepared docker container, which can be built with the Dockerfile located in the `ci/` folder: ``` docker build -t test_suite:latest ci ``` Then you can run the container and forward all the needed ports with the following command: ``` docker run -d --name test_suite -p 4200:4200 -p 4201:4201 -p 4202:4202 -p 4203:4203 -p 4204:4204 -p 4205:4205 -p 4206:4206 test_suite:latest ``` The docker container runs a shell script that starts the two servers in the background and checks if the processes are still alive. ## Using the Visual Studio Code devcontainer If you are using Visual Studio Code, the easiest method to get up and running would be to simply use the devcontainer prepared in the `.devcontainer/` directory. This will also launch the needed server processes and set up networking etc. Please refer to the vscode [documentation](https://code.visualstudio.com/docs/remote/containers) for more information on how to use devcontainers. # Polling vs. Websockets The underlying engine.io protocol provides two mechanisms for transporting: polling and websockets. In order to test both in the pipeline, the two servers are configured differently. The socket.io test suite always upgrades to websockets as fast as possible while one of the engine.io suites just uses long-polling, the other one uses websockets but is reachable via `https://` and `wss://`. This assures that both the websocket connection code and the long-polling code gets tested (as seen on codecov.io). Keep that in mind while expanding the tests. ================================================ FILE: ci/engine-io-polling.js ================================================ /** * This is an example server, used to test the current code. */ const engine = require('engine.io'); const http = require('http').createServer().listen(4203); // the engine.io client runs on port 4203 const server = engine.attach(http, { allowUpgrades: false, transports: ["polling"] }); console.log("Started") server.on('connection', socket => { console.log("Connected"); socket.on('message', message => { if (message !== undefined) { console.log(message.toString()); if (message == "respond") { socket.send("Roger Roger"); } else if (message == "close") { socket.close(); } } else { console.log("empty message received") } }); socket.on('heartbeat', () => { console.log("heartbeat"); }); socket.on('error', message => { // Notify the client if there is an error so it's tests will fail socket.send("ERROR: Received error") console.log(message.toString()); }); socket.on('close', () => { console.log("Close"); socket.close(); }); socket.send('hello client'); }); ================================================ FILE: ci/engine-io-secure.js ================================================ const fs = require('fs'); const https = require('https'); const eio = require('engine.io'); const serverOpts = { key: fs.readFileSync("cert/server.key"), cert: fs.readFileSync("cert/server.crt"), ca: fs.readFileSync("cert/ca.crt"), }; const http = https.createServer(serverOpts); const server = eio.attach(http); console.log("Started") http.listen(4202, () => { server.on('connection', socket => { console.log("Connected"); socket.on('message', message => { if (message !== undefined) { console.log(message.toString()); if (message == "respond") { socket.send("Roger Roger"); } else if (message == "close") { socket.close(); } } else { console.log("empty message received") } }); socket.on('heartbeat', () => { console.log("heartbeat"); }); socket.on('error', message => { // Notify the client if there is an error so it's tests will fail socket.send("ERROR: Received error") console.log(message.toString()); }); socket.on('close', () => { console.log("Close"); socket.close(); }); socket.send('hello client'); }); }); ================================================ FILE: ci/engine-io.js ================================================ /** * This is an example server, used to test the current code. */ const engine = require('engine.io'); const http = require('http').createServer().listen(4201); // the engine.io client runs on port 4201 const server = engine.attach(http); console.log("Started") server.on('connection', socket => { console.log("Connected"); socket.on('message', message => { if (message !== undefined) { console.log(message.toString()); if (message == "respond") { socket.send("Roger Roger"); } else if (message == "close") { socket.close(); } } else { console.log("empty message received") } }); socket.on('heartbeat', () => { console.log("heartbeat"); }); socket.on('error', message => { // Notify the client if there is an error so it's tests will fail socket.send("ERROR: Received error") console.log(message.toString()); }); socket.on('close', () => { console.log("Close"); socket.close(); }); socket.send('hello client'); }); ================================================ FILE: ci/keygen.sh ================================================ #!/bin/sh cd $(dirname $0) if [ "$1" = "" ] || [ "$2" = "" ] then echo "Usage: keygen.sh [DOMAIN] [IP]" exit 1 fi DOMAIN="$1" IP="$2" CA_NAME=${CA_NAME:-"rust-socketio-dev"} mkdir cert || true cd cert # Credit https://scriptcrunch.com/create-ca-tls-ssl-certificates-keys/ if [ ! -f ca.key ] then echo "Generating CA key" openssl genrsa -out ca.key 4096 fi if [ ! -f "ca.crt" ] then echo "Generating CA cert" openssl req -x509 -new -nodes -key ca.key -subj "/CN=${CA_NAME}/C=??/L=Varius" -out ca.crt fi if [ ! -f "server.key" ] then echo "Generating server key" openssl genrsa -out server.key 4096 fi if [ ! -f "csr.conf" ] then echo """ [ req ] default_bits = 4096 prompt = no default_md = sha256 req_extensions = req_ext distinguished_name = dn [ dn ] C = ?? ST = Varius L = Varius O = ${DOMAIN} OU = ${DOMAIN} CN = ${DOMAIN} [ req_ext ] subjectAltName = @alt_names [ alt_names ] DNS.1 = ${DOMAIN} DNS.2 = localhost IP.1 = ${IP} """ > csr.conf fi if [ ! -f "server.csr" ] then echo "Generating server signing request" openssl req -new -key server.key -out server.csr -config csr.conf fi if [ ! -f "server.crt" ] then echo "Generating signed server certifcicate" openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extfile csr.conf fi ================================================ FILE: ci/package.json ================================================ { "name": "rust-socketio-test", "version": "1.0.0", "description": "A test environment for a socketio client", "author": "Bastian Kersting", "license": "MIT", "dependencies": { "engine.io": "6.4.2", "socket.io": "4.0.0" } } ================================================ FILE: ci/socket-io-auth.js ================================================ const server = require('http').createServer(); const io = require('socket.io')(server); console.log('Started'); var callback = client => { console.log('Connected!'); client.emit('auth', client.handshake.auth.password === '123' ? 'success' : 'failed') }; io.on('connection', callback); io.of('/admin').on('connection', callback); // the socket.io client runs on port 4204 server.listen(4204); ================================================ FILE: ci/socket-io-restart-url-auth.js ================================================ let createServer = require("http").createServer; let server = createServer(); const io = require("socket.io")(server); const port = 4206; const timeout = 200; const TIMESTAMP_SLACK_ALLOWED = 1000; function isValidTimestamp(timestampStr) { if (timestampStr === undefined) return false; const timestamp = parseInt(timestampStr); if (isNaN(timestamp)) return false; const diff = Date.now() - timestamp; return Math.abs(diff) <= TIMESTAMP_SLACK_ALLOWED; } console.log("Started"); var callback = (client) => { const timestamp = client.request._query.timestamp; console.log("Connected, timestamp:", timestamp); if (!isValidTimestamp(timestamp)) { console.log("Invalid timestamp!"); client.disconnect(); return; } client.emit("message", "test"); client.on("restart_server", () => { console.log("will restart in ", timeout, "ms"); io.close(); setTimeout(() => { server = createServer(); server.listen(port); io.attach(server); console.log("do restart"); }, timeout); }); }; io.on("connection", callback); server.listen(port); ================================================ FILE: ci/socket-io-restart.js ================================================ let createServer = require("http").createServer; let server = createServer(); const io = require("socket.io")(server); const port = 4205; const timeout = 2000; console.log("Started"); var callback = (client) => { const headers = client.request.headers; console.log("headers", headers); const message = headers.message_back || "test"; console.log("Connected!"); client.emit("message", message); client.on("restart_server", () => { console.log("will restart in ", timeout, "ms"); io.close(); setTimeout(() => { server = createServer(); server.listen(port); io.attach(server); console.log("do restart"); }, timeout); }); }; io.on("connection", callback); server.listen(port); ================================================ FILE: ci/socket-io.js ================================================ const server = require('http').createServer(); const io = require('socket.io')(server); console.log('Started'); var callback = client => { console.log('Connected!'); client.on('test', data => { // Send a message back to the server to confirm the message was received client.emit('test-received', data); console.log(['test', data]); }); client.on('message', data => { client.emit('message-received', data); console.log(['message', data]); }); client.on('test', function (arg, ack) { console.log('Ack received') if (ack) { ack('woot'); } }); client.on('binary', data => { var bufView = new Uint8Array(data); console.log(['binary', 'Yehaa binary payload!']); for (elem in bufView) { console.log(['binary', elem]); } client.emit('binary-received', data); console.log(['binary', data]); }); client.on('binary', function (arg, ack) { console.log(['binary', 'Ack received, answer with binary']) if (ack) { ack(Buffer.from([1, 2, 3])); } }); // This event allows the test framework to arbitrarily close the underlying connection client.on('close_transport', data => { console.log(['close_transport', 'Request to close transport received']) // Close underlying websocket connection client.client.conn.close(); }) client.emit('Hello from the message event!'); client.emit('test', 'Hello from the test event!'); client.emit(Buffer.from([4, 5, 6])); client.emit('test', Buffer.from([1, 2, 3])); client.emit('This is the first argument', 'This is the second argument', { argCount: 3 }); client.emit('on_abc_event', '', { abc: 0, some_other: 'value', }); }; io.on('connection', callback); io.of('/admin').on('connection', callback); // the socket.io client runs on port 4201 server.listen(4200); ================================================ FILE: ci/start_test_server.sh ================================================ echo "Starting test environment" DEBUG=* node engine-io.js & status=$? if [ $status -ne 0 ]; then echo "Failed to start engine.io: $status" exit $status fi echo "Successfully started engine.io instance" DEBUG=* node engine-io-polling.js & status=$? if [ $status -ne 0 ]; then echo "Failed to start polling engine.io: $status" exit $status fi echo "Successfully started polling engine.io instance" DEBUG=* node socket-io.js & status=$? if [ $status -ne 0 ]; then echo "Failed to start socket.io: $status" exit $status fi echo "Successfully started socket.io instance" DEBUG=* node socket-io-auth.js & status=$? if [ $status -ne 0 ]; then echo "Failed to start socket.io auth: $status" exit $status fi echo "Successfully started socket.io auth instance" DEBUG=* node socket-io-restart.js & status=$? if [ $status -ne 0 ]; then echo "Failed to start socket.io restart: $status" exit $status fi echo "Successfully started socket.io restart instance" DEBUG=* node socket-io-restart-url-auth.js & status=$? if [ $status -ne 0 ]; then echo "Failed to start socket.io restart url auth: $status" exit $status fi echo "Successfully started socket.io restart url auth instance" DEBUG=* node engine-io-secure.js & status=$? if [ $status -ne 0 ]; then echo "Failed to start secure engine.io: $status" exit $status fi echo "Successfully started secure engine.io instance" while sleep 60; do ps aux | grep socket | grep -q -v grep PROCESS_1_STATUS=$? ps aux | grep engine-io.js | grep -q -v grep PROCESS_2_STATUS=$? ps aux | grep engine-io-secure.js | grep -q -v grep PROCESS_3_STATUS=$? # If the greps above find anything, they exit with 0 status # If they are not both 0, then something is wrong if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 -o $PROCESS_3_STATUS -ne 0 ]; then echo "One of the processes has already exited." exit 1 fi done ================================================ FILE: codecov.yml ================================================ coverage: range: 50..90 # coverage lower than 50 is red, higher than 90 green, between color code status: project: # settings affecting project coverage default: target: auto # auto % coverage target threshold: 5% # allow for 5% reduction of coverage without failing # do not run coverage on patch nor changes patch: false ================================================ FILE: engineio/Cargo.toml ================================================ [package] name = "rust_engineio" version = "0.6.0" authors = ["Bastian Kersting "] edition = "2021" description = "An implementation of a engineio client written in rust." readme = "README.md" repository = "https://github.com/1c3t3a/rust-socketio" keywords = ["engineio", "network", "protocol", "client"] categories = ["network-programming", "web-programming", "web-programming::websocket"] license = "MIT" [package.metadata.docs.rs] all-features = true [dependencies] base64 = "0.22.1" bytes = "1" reqwest = { version = "0.12.4", features = ["blocking", "native-tls", "stream"] } adler32 = "1.2.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" http = "1.1.0" tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] } tungstenite = "0.21.0" tokio = "1.40.0" futures-util = { version = "0.3", default-features = false, features = ["sink"] } async-trait = "0.1.83" async-stream = "0.3.6" thiserror = "1.0" native-tls = "0.2.12" url = "2.5.4" [dev-dependencies] criterion = { version = "0.5.1", features = ["async_tokio"] } lazy_static = "1.4.0" [dev-dependencies.tokio] version = "1.40.0" # we need the `#[tokio::test]` macro features = ["macros"] [[bench]] name = "engineio" harness = false # needs to be present in order to support the benchmark # ci job # source: https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options [lib] bench = false [features] default = ["async"] async-callbacks = [] async = ["async-callbacks"] ================================================ FILE: engineio/README.md ================================================ [![Latest Version](https://img.shields.io/crates/v/rust_engineio)](https://crates.io/crates/rust_engineio) [![docs.rs](https://docs.rs/rust_engineio/badge.svg)](https://docs.rs/rust_engineio) # Rust-engineio-client An implementation of a engine.io client written in the rust programming language. This implementation currently supports revision 4 of the engine.io protocol. If you have any connection issues with this client, make sure the server uses at least revision 4 of the engine.io protocol. ## Example usage ``` rust use rust_engineio::{ClientBuilder, Client, packet::{Packet, PacketId}}; use url::Url; use bytes::Bytes; // get a client with an `on_open` callback let client: Client = ClientBuilder::new(Url::parse("http://localhost:4201").unwrap()) .on_open(|_| println!("Connection opened!")) .build() .expect("Connection failed"); // connect to the server client.connect().expect("Connection failed"); // create a packet, in this case a message packet and emit it let packet = Packet::new(PacketId::Message, Bytes::from_static(b"Hello World")); client.emit(packet).expect("Server unreachable"); // disconnect from the server client.disconnect().expect("Disconnect failed") ``` The main entry point for using this crate is the `ClientBuilder` (or `asynchronous::ClientBuilder` respectively) which provides the opportunity to define how you want to connect to a certain endpoint. The following connection methods are available: * `build`: Build websocket if allowed, if not fall back to polling. Standard configuration. * `build_polling`: enforces a `polling` transport. * `build_websocket_with_upgrade`: Build socket with a polling transport then upgrade to websocket transport (if possible). * `build_websocket`: Build socket with only a websocket transport, crashes when websockets are not allowed. ## Current features This implementation now supports all of the features of the engine.io protocol mentioned [here](https://github.com/socketio/engine.io-protocol). This includes various transport options, the possibility of sending engine.io packets and registering the common engine.io event callbacks: * on_open * on_close * on_data * on_error * on_packet It is also possible to pass in custom tls configurations via the `TlsConnector` as well as custom headers for the opening request. ## Documentation Documentation of this crate can be found up on [docs.rs](https://docs.rs/rust_engineio). ## Async version The crate also ships with an asynchronous version that can be enabled with a feature flag. The async version implements the same features mentioned above. The asynchronous version has a similar API, just with async functions. Currently the futures can only be executed with [`tokio`](https://tokio.rs). In the first benchmarks the async version showed improvements of up to 93% in speed. To make use of the async version, import the crate as follows: ```toml [depencencies] rust-engineio = { version = "0.3.1", features = ["async"] } ``` ================================================ FILE: engineio/benches/engineio.rs ================================================ use criterion::{criterion_group, criterion_main}; use native_tls::Certificate; use native_tls::TlsConnector; use rust_engineio::error::Error; use std::fs::File; use std::io::Read; use url::Url; pub use criterion_wrappers::*; pub use tests::*; pub use util::*; pub mod util { use super::*; pub fn engine_io_url() -> Result { let url = std::env::var("ENGINE_IO_SERVER") .unwrap_or_else(|_| "http://localhost:4201".to_owned()); Ok(Url::parse(&url)?) } pub fn engine_io_url_secure() -> Result { let url = std::env::var("ENGINE_IO_SECURE_SERVER") .unwrap_or_else(|_| "https://localhost:4202".to_owned()); Ok(Url::parse(&url)?) } pub fn tls_connector() -> Result { let cert_path = "../".to_owned() + &std::env::var("CA_CERT_PATH").unwrap_or_else(|_| "ci/cert/ca.crt".to_owned()); let mut cert_file = File::open(cert_path)?; let mut buf = vec![]; cert_file.read_to_end(&mut buf)?; let cert: Certificate = Certificate::from_pem(&buf[..]).unwrap(); Ok(TlsConnector::builder() // ONLY USE FOR TESTING! .danger_accept_invalid_hostnames(true) .add_root_certificate(cert) .build() .unwrap()) } } /// sync benches #[cfg(not(feature = "async"))] pub mod tests { use bytes::Bytes; use reqwest::Url; use rust_engineio::{Client, ClientBuilder, Error, Packet, PacketId}; use crate::tls_connector; pub fn engine_io_socket_build(url: Url) -> Result { ClientBuilder::new(url).build() } pub fn engine_io_socket_build_polling(url: Url) -> Result { ClientBuilder::new(url).build_polling() } pub fn engine_io_socket_build_polling_secure(url: Url) -> Result { ClientBuilder::new(url) .tls_config(tls_connector()?) .build_polling() } pub fn engine_io_socket_build_websocket(url: Url) -> Result { ClientBuilder::new(url).build_websocket() } pub fn engine_io_socket_build_websocket_secure(url: Url) -> Result { ClientBuilder::new(url) .tls_config(tls_connector()?) .build_websocket() } pub fn engine_io_packet() -> Packet { Packet::new(PacketId::Message, Bytes::from("hello world")) } pub fn engine_io_emit(socket: &Client, packet: Packet) -> Result<(), Error> { socket.emit(packet) } } #[cfg(not(feature = "async"))] mod criterion_wrappers { use criterion::{black_box, Criterion}; use super::*; pub fn criterion_engine_io_socket_build(c: &mut Criterion) { let url = engine_io_url().unwrap(); c.bench_function("engine io build", |b| { b.iter(|| { engine_io_socket_build(black_box(url.clone())) .unwrap() .close() }) }); } pub fn criterion_engine_io_socket_build_polling(c: &mut Criterion) { let url = engine_io_url().unwrap(); c.bench_function("engine io build polling", |b| { b.iter(|| { engine_io_socket_build_polling(black_box(url.clone())) .unwrap() .close() }) }); } pub fn criterion_engine_io_socket_build_polling_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); c.bench_function("engine io build polling secure", |b| { b.iter(|| { engine_io_socket_build_polling_secure(black_box(url.clone())) .unwrap() .close() }) }); } pub fn criterion_engine_io_socket_build_websocket(c: &mut Criterion) { let url = engine_io_url().unwrap(); c.bench_function("engine io build websocket", |b| { b.iter(|| { engine_io_socket_build_websocket(black_box(url.clone())) .unwrap() .close() }) }); } pub fn criterion_engine_io_socket_build_websocket_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); c.bench_function("engine io build websocket secure", |b| { b.iter(|| { engine_io_socket_build_websocket_secure(black_box(url.clone())) .unwrap() .close() }) }); } pub fn criterion_engine_io_packet(c: &mut Criterion) { c.bench_function("engine io packet", |b| b.iter(|| engine_io_packet())); } pub fn criterion_engine_io_emit_polling(c: &mut Criterion) { let url = engine_io_url().unwrap(); let socket = engine_io_socket_build_polling(url).unwrap(); socket.connect().unwrap(); let packet = engine_io_packet(); c.bench_function("engine io polling emit", |b| { b.iter(|| engine_io_emit(black_box(&socket), black_box(packet.clone())).unwrap()) }); socket.close().unwrap(); } pub fn criterion_engine_io_emit_polling_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); let socket = engine_io_socket_build_polling_secure(url).unwrap(); socket.connect().unwrap(); let packet = engine_io_packet(); c.bench_function("engine io polling secure emit", |b| { b.iter(|| engine_io_emit(black_box(&socket), black_box(packet.clone())).unwrap()) }); socket.close().unwrap(); } pub fn criterion_engine_io_emit_websocket(c: &mut Criterion) { let url = engine_io_url().unwrap(); let socket = engine_io_socket_build_websocket(url).unwrap(); socket.connect().unwrap(); let packet = engine_io_packet(); c.bench_function("engine io websocket emit", |b| { b.iter(|| engine_io_emit(black_box(&socket), black_box(packet.clone())).unwrap()) }); socket.close().unwrap(); } pub fn criterion_engine_io_emit_websocket_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); let socket = engine_io_socket_build_websocket_secure(url).unwrap(); socket.connect().unwrap(); let packet = engine_io_packet(); c.bench_function("engine io websocket secure emit", |b| { b.iter(|| engine_io_emit(black_box(&socket), black_box(packet.clone())).unwrap()) }); socket.close().unwrap(); } } /// async benches #[cfg(feature = "async")] pub mod tests { use bytes::Bytes; use rust_engineio::{ asynchronous::{Client, ClientBuilder}, Error, Packet, PacketId, }; use url::Url; use crate::tls_connector; pub async fn engine_io_socket_build(url: Url) -> Result { ClientBuilder::new(url).build().await } pub async fn engine_io_socket_build_polling(url: Url) -> Result { ClientBuilder::new(url).build_polling().await } pub async fn engine_io_socket_build_polling_secure(url: Url) -> Result { ClientBuilder::new(url) .tls_config(tls_connector()?) .build_polling() .await } pub async fn engine_io_socket_build_websocket(url: Url) -> Result { ClientBuilder::new(url).build_websocket().await } pub async fn engine_io_socket_build_websocket_secure(url: Url) -> Result { ClientBuilder::new(url) .tls_config(tls_connector()?) .build_websocket() .await } pub fn engine_io_packet() -> Packet { Packet::new(PacketId::Message, Bytes::from("hello world")) } pub async fn engine_io_emit(socket: &Client, packet: Packet) -> Result<(), Error> { socket.emit(packet).await } } #[cfg(feature = "async")] mod criterion_wrappers { use std::sync::Arc; use bytes::Bytes; use criterion::{black_box, Criterion}; use lazy_static::lazy_static; use rust_engineio::{Packet, PacketId}; use tokio::runtime::{Builder, Runtime}; use super::tests::{ engine_io_emit, engine_io_packet, engine_io_socket_build, engine_io_socket_build_polling, engine_io_socket_build_polling_secure, engine_io_socket_build_websocket, engine_io_socket_build_websocket_secure, }; use super::util::{engine_io_url, engine_io_url_secure}; lazy_static! { static ref RUNTIME: Arc = Arc::new(Builder::new_multi_thread().enable_all().build().unwrap()); } pub fn criterion_engine_io_socket_build(c: &mut Criterion) { let url = engine_io_url().unwrap(); c.bench_function("engine io build", move |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_socket_build(black_box(url.clone())) .await .unwrap() .close() .await }) }); } pub fn criterion_engine_io_socket_build_polling(c: &mut Criterion) { let url = engine_io_url().unwrap(); c.bench_function("engine io build polling", move |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_socket_build_polling(black_box(url.clone())) .await .unwrap() .close() .await }) }); } pub fn criterion_engine_io_socket_build_polling_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); c.bench_function("engine io build polling secure", move |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_socket_build_polling_secure(black_box(url.clone())) .await .unwrap() .close() .await }) }); } pub fn criterion_engine_io_socket_build_websocket(c: &mut Criterion) { let url = engine_io_url().unwrap(); c.bench_function("engine io build websocket", move |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_socket_build_websocket(black_box(url.clone())) .await .unwrap() .close() .await }) }); } pub fn criterion_engine_io_socket_build_websocket_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); c.bench_function("engine io build websocket secure", move |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_socket_build_websocket_secure(black_box(url.clone())) .await .unwrap() .close() .await }) }); } pub fn criterion_engine_io_packet(c: &mut Criterion) { c.bench_function("engine io packet", move |b| { b.iter(|| Packet::new(PacketId::Message, Bytes::from("hello world"))) }); } pub fn criterion_engine_io_emit_polling(c: &mut Criterion) { let url = engine_io_url().unwrap(); let socket = RUNTIME.block_on(async { let socket = engine_io_socket_build_polling(url).await.unwrap(); socket.connect().await.unwrap(); socket }); let packet = engine_io_packet(); c.bench_function("engine io polling emit", |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_emit(black_box(&socket), black_box(packet.clone())) .await .unwrap() }) }); } pub fn criterion_engine_io_emit_polling_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); let socket = RUNTIME.block_on(async { let socket = engine_io_socket_build_polling_secure(url).await.unwrap(); socket.connect().await.unwrap(); socket }); let packet = engine_io_packet(); c.bench_function("engine io polling secure emit", |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_emit(black_box(&socket), black_box(packet.clone())) .await .unwrap() }) }); } pub fn criterion_engine_io_emit_websocket(c: &mut Criterion) { let url = engine_io_url().unwrap(); let socket = RUNTIME.block_on(async { let socket = engine_io_socket_build_websocket(url).await.unwrap(); socket.connect().await.unwrap(); socket }); let packet = engine_io_packet(); c.bench_function("engine io websocket emit", |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_emit(black_box(&socket), black_box(packet.clone())) .await .unwrap() }) }); } pub fn criterion_engine_io_emit_websocket_secure(c: &mut Criterion) { let url = engine_io_url_secure().unwrap(); let socket = RUNTIME.block_on(async { let socket = engine_io_socket_build_websocket_secure(url).await.unwrap(); socket.connect().await.unwrap(); socket }); let packet = engine_io_packet(); c.bench_function("engine io websocket secure emit", |b| { b.to_async(RUNTIME.as_ref()).iter(|| async { engine_io_emit(black_box(&socket), black_box(packet.clone())) .await .unwrap() }) }); } } criterion_group!( benches, criterion_engine_io_socket_build_polling, criterion_engine_io_socket_build_polling_secure, criterion_engine_io_socket_build_websocket, criterion_engine_io_socket_build_websocket_secure, criterion_engine_io_socket_build, criterion_engine_io_packet, criterion_engine_io_emit_polling, criterion_engine_io_emit_polling_secure, criterion_engine_io_emit_websocket, criterion_engine_io_emit_websocket_secure ); criterion_main!(benches); ================================================ FILE: engineio/src/asynchronous/async_socket.rs ================================================ use std::{ fmt::Debug, pin::Pin, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, }; use async_stream::try_stream; use bytes::Bytes; use futures_util::{stream, Stream, StreamExt}; use tokio::{runtime::Handle, sync::Mutex, time::Instant}; use crate::{ asynchronous::{callback::OptionalCallback, transport::AsyncTransportType}, error::Result, packet::{HandshakePacket, Payload}, Error, Packet, PacketId, }; #[derive(Clone)] pub struct Socket { handle: Handle, transport: Arc>, transport_raw: AsyncTransportType, on_close: OptionalCallback<()>, on_data: OptionalCallback, on_error: OptionalCallback, on_open: OptionalCallback<()>, on_packet: OptionalCallback, connected: Arc, last_ping: Arc>, last_pong: Arc>, connection_data: Arc, max_ping_timeout: u64, } impl Socket { pub(crate) fn new( transport: AsyncTransportType, handshake: HandshakePacket, on_close: OptionalCallback<()>, on_data: OptionalCallback, on_error: OptionalCallback, on_open: OptionalCallback<()>, on_packet: OptionalCallback, ) -> Self { let max_ping_timeout = handshake.ping_interval + handshake.ping_timeout; Socket { handle: Handle::current(), on_close, on_data, on_error, on_open, on_packet, transport: Arc::new(Mutex::new(transport.clone())), transport_raw: transport, connected: Arc::new(AtomicBool::default()), last_ping: Arc::new(Mutex::new(Instant::now())), last_pong: Arc::new(Mutex::new(Instant::now())), connection_data: Arc::new(handshake), max_ping_timeout, } } /// Opens the connection to a specified server. The first Pong packet is sent /// to the server to trigger the Ping-cycle. pub async fn connect(&self) -> Result<()> { // SAFETY: Has valid handshake due to type self.connected.store(true, Ordering::Release); if let Some(on_open) = self.on_open.as_ref() { let on_open = on_open.clone(); self.handle.spawn(async move { on_open(()).await }); } // set the last ping to now and set the connected state *self.last_ping.lock().await = Instant::now(); // emit a pong packet to keep trigger the ping cycle on the server self.emit(Packet::new(PacketId::Pong, Bytes::new())).await?; Ok(()) } /// A helper method that distributes pub(super) async fn handle_incoming_packet(&self, packet: Packet) -> Result<()> { // check for the appropriate action or callback self.handle_packet(packet.clone()); match packet.packet_id { PacketId::MessageBinary => { self.handle_data(packet.data.clone()); } PacketId::Message => { self.handle_data(packet.data.clone()); } PacketId::Close => { self.handle_close(); } PacketId::Upgrade => { // this is already checked during the handshake, so just do nothing here } PacketId::Ping => { self.pinged().await; self.emit(Packet::new(PacketId::Pong, Bytes::new())).await?; } PacketId::Pong | PacketId::Open => { // this will never happen as the pong and open // packets are only sent by the client return Err(Error::InvalidPacket()); } PacketId::Noop => (), } Ok(()) } /// Helper method that parses bytes and returns an iterator over the elements. fn parse_payload(bytes: Bytes) -> impl Stream> { try_stream! { let payload = Payload::try_from(bytes); for elem in payload?.into_iter() { yield elem; } } } /// Creates a stream over the incoming packets, uses the streams provided by the /// underlying transport types. fn stream( mut transport: AsyncTransportType, ) -> Pin> + 'static + Send>> { // map the byte stream of the underlying transport // to a packet stream Box::pin(try_stream! { for await payload in transport.as_pin_box() { for await packet in Self::parse_payload(payload?) { yield packet?; } } }) } pub async fn disconnect(&self) -> Result<()> { if let Some(on_close) = self.on_close.as_ref() { let on_close = on_close.clone(); self.handle.spawn(async move { on_close(()).await }); } self.emit(Packet::new(PacketId::Close, Bytes::new())) .await?; self.connected.store(false, Ordering::Release); Ok(()) } /// Sends a packet to the server. pub async fn emit(&self, packet: Packet) -> Result<()> { if !self.connected.load(Ordering::Acquire) { let error = Error::IllegalActionBeforeOpen(); self.call_error_callback(format!("{}", error)); return Err(error); } let is_binary = packet.packet_id == PacketId::MessageBinary; // send a post request with the encoded payload as body // if this is a binary attachment, then send the raw bytes let data: Bytes = if is_binary { packet.data } else { packet.into() }; let lock = self.transport.lock().await; let fut = lock.as_transport().emit(data, is_binary); if let Err(error) = fut.await { self.call_error_callback(error.to_string()); return Err(error); } Ok(()) } /// Calls the error callback with a given message. #[inline] fn call_error_callback(&self, text: String) { if let Some(on_error) = self.on_error.as_ref() { let on_error = on_error.clone(); self.handle.spawn(async move { on_error(text).await }); } } // Check if the underlying transport client is connected. pub(crate) fn is_connected(&self) -> bool { self.connected.load(Ordering::Acquire) } pub(crate) async fn pinged(&self) { *self.last_ping.lock().await = Instant::now(); } /// Returns the time in milliseconds that is left until a new ping must be received. /// This is used to detect whether we have been disconnected from the server. /// See https://socket.io/docs/v4/how-it-works/#disconnection-detection async fn time_to_next_ping(&self) -> u64 { match Instant::now().checked_duration_since(*self.last_ping.lock().await) { Some(since_last_ping) => { let since_last_ping = since_last_ping.as_millis() as u64; if since_last_ping > self.max_ping_timeout { 0 } else { self.max_ping_timeout - since_last_ping } } None => 0, } } pub(crate) fn handle_packet(&self, packet: Packet) { if let Some(on_packet) = self.on_packet.as_ref() { let on_packet = on_packet.clone(); self.handle.spawn(async move { on_packet(packet).await }); } } pub(crate) fn handle_data(&self, data: Bytes) { if let Some(on_data) = self.on_data.as_ref() { let on_data = on_data.clone(); self.handle.spawn(async move { on_data(data).await }); } } pub(crate) fn handle_close(&self) { if let Some(on_close) = self.on_close.as_ref() { let on_close = on_close.clone(); self.handle.spawn(async move { on_close(()).await }); } self.connected.store(false, Ordering::Release); } /// Returns the packet stream for the client. pub(crate) fn as_stream<'a>( &'a self, ) -> Pin> + Send + 'a>> { stream::unfold( Self::stream(self.transport_raw.clone()), |mut stream| async { // Wait for the next payload or until we should have received the next ping. match tokio::time::timeout( std::time::Duration::from_millis(self.time_to_next_ping().await), stream.next(), ) .await { Ok(result) => result.map(|result| (result, stream)), // We didn't receive a ping in time and now consider the connection as closed. Err(_) => { // Be nice and disconnect properly. if let Err(e) = self.disconnect().await { Some((Err(e), stream)) } else { Some((Err(Error::PingTimeout()), stream)) } } } }, ) .boxed() } } #[cfg_attr(tarpaulin, ignore)] impl Debug for Socket { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Socket") .field("transport", &self.transport) .field("on_close", &self.on_close) .field("on_data", &self.on_data) .field("on_error", &self.on_error) .field("on_open", &self.on_open) .field("on_packet", &self.on_packet) .field("connected", &self.connected) .field("last_ping", &self.last_ping) .field("last_pong", &self.last_pong) .field("connection_data", &self.connection_data) .finish() } } ================================================ FILE: engineio/src/asynchronous/async_transports/mod.rs ================================================ mod polling; mod websocket; mod websocket_general; mod websocket_secure; pub use self::polling::PollingTransport; pub use self::websocket::WebsocketTransport; pub use self::websocket_secure::WebsocketSecureTransport; ================================================ FILE: engineio/src/asynchronous/async_transports/polling.rs ================================================ use adler32::adler32; use async_stream::try_stream; use async_trait::async_trait; use base64::{engine::general_purpose, Engine as _}; use bytes::{BufMut, Bytes, BytesMut}; use futures_util::{Stream, StreamExt}; use http::HeaderMap; use native_tls::TlsConnector; use reqwest::{Client, ClientBuilder, Response}; use std::fmt::Debug; use std::time::SystemTime; use std::{pin::Pin, sync::Arc}; use tokio::sync::RwLock; use url::Url; use crate::asynchronous::generator::StreamGenerator; use crate::{asynchronous::transport::AsyncTransport, error::Result, Error}; /// An asynchronous polling type. Makes use of the nonblocking reqwest types and /// methods. #[derive(Clone)] pub struct PollingTransport { client: Client, base_url: Arc>, generator: StreamGenerator, } impl PollingTransport { pub fn new( base_url: Url, tls_config: Option, opening_headers: Option, ) -> Self { let client = match (tls_config, opening_headers) { (Some(config), Some(map)) => ClientBuilder::new() .use_preconfigured_tls(config) .default_headers(map) .build() .unwrap(), (Some(config), None) => ClientBuilder::new() .use_preconfigured_tls(config) .build() .unwrap(), (None, Some(map)) => ClientBuilder::new().default_headers(map).build().unwrap(), (None, None) => Client::new(), }; let mut url = base_url; url.query_pairs_mut().append_pair("transport", "polling"); PollingTransport { client: client.clone(), base_url: Arc::new(RwLock::new(url.clone())), generator: StreamGenerator::new(Self::stream(url, client)), } } fn address(mut url: Url) -> Result { let reader = format!("{:#?}", SystemTime::now()); let hash = adler32(reader.as_bytes()).unwrap(); url.query_pairs_mut().append_pair("t", &hash.to_string()); Ok(url) } fn send_request(url: Url, client: Client) -> impl Stream> { try_stream! { let address = Self::address(url); yield client .get(address?) .send().await? } } fn stream( url: Url, client: Client, ) -> Pin> + 'static + Send>> { Box::pin(try_stream! { loop { for await elem in Self::send_request(url.clone(), client.clone()) { for await bytes in elem?.bytes_stream() { yield bytes?; } } } }) } } impl Stream for PollingTransport { type Item = Result; fn poll_next( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.generator.poll_next_unpin(cx) } } #[async_trait] impl AsyncTransport for PollingTransport { async fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()> { let data_to_send = if is_binary_att { // the binary attachment gets `base64` encoded let mut packet_bytes = BytesMut::with_capacity(data.len() + 1); packet_bytes.put_u8(b'b'); let encoded_data = general_purpose::STANDARD.encode(data); packet_bytes.put(encoded_data.as_bytes()); packet_bytes.freeze() } else { data }; let status = self .client .post(self.address().await?) .body(data_to_send) .send() .await? .status() .as_u16(); if status != 200 { let error = Error::IncompleteHttp(status); return Err(error); } Ok(()) } async fn base_url(&self) -> Result { Ok(self.base_url.read().await.clone()) } async fn set_base_url(&self, base_url: Url) -> Result<()> { let mut url = base_url; if !url .query_pairs() .any(|(k, v)| k == "transport" && v == "polling") { url.query_pairs_mut().append_pair("transport", "polling"); } *self.base_url.write().await = url; Ok(()) } } impl Debug for PollingTransport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PollingTransport") .field("client", &self.client) .field("base_url", &self.base_url) .finish() } } #[cfg(test)] mod test { use crate::asynchronous::transport::AsyncTransport; use super::*; use std::str::FromStr; #[tokio::test] async fn polling_transport_base_url() -> Result<()> { let url = crate::test::engine_io_server()?.to_string(); let transport = PollingTransport::new(Url::from_str(&url[..]).unwrap(), None, None); assert_eq!( transport.base_url().await?.to_string(), url.clone() + "?transport=polling" ); transport .set_base_url(Url::parse("https://127.0.0.1")?) .await?; assert_eq!( transport.base_url().await?.to_string(), "https://127.0.0.1/?transport=polling" ); assert_ne!(transport.base_url().await?.to_string(), url); transport .set_base_url(Url::parse("http://127.0.0.1/?transport=polling")?) .await?; assert_eq!( transport.base_url().await?.to_string(), "http://127.0.0.1/?transport=polling" ); assert_ne!(transport.base_url().await?.to_string(), url); Ok(()) } } ================================================ FILE: engineio/src/asynchronous/async_transports/websocket.rs ================================================ use std::fmt::Debug; use std::pin::Pin; use std::sync::Arc; use crate::asynchronous::transport::AsyncTransport; use crate::error::Result; use async_trait::async_trait; use bytes::Bytes; use futures_util::stream::StreamExt; use futures_util::Stream; use http::HeaderMap; use tokio::sync::RwLock; use tokio_tungstenite::connect_async; use tungstenite::client::IntoClientRequest; use url::Url; use super::websocket_general::AsyncWebsocketGeneralTransport; /// An asynchronous websocket transport type. /// This type only allows for plain websocket /// connections ("ws://"). #[derive(Clone)] pub struct WebsocketTransport { inner: AsyncWebsocketGeneralTransport, base_url: Arc>, } impl WebsocketTransport { /// Creates a new instance over a request that might hold additional headers and an URL. pub async fn new(base_url: Url, headers: Option) -> Result { let mut url = base_url; url.query_pairs_mut().append_pair("transport", "websocket"); url.set_scheme("ws").unwrap(); let mut req = url.clone().into_client_request()?; if let Some(map) = headers { // SAFETY: this unwrap never panics as the underlying request is just initialized and in proper state req.headers_mut().extend(map); } let (ws_stream, _) = connect_async(req).await?; let (sen, rec) = ws_stream.split(); let inner = AsyncWebsocketGeneralTransport::new(sen, rec).await; Ok(WebsocketTransport { inner, base_url: Arc::new(RwLock::new(url)), }) } /// Sends probe packet to ensure connection is valid, then sends upgrade /// request pub(crate) async fn upgrade(&self) -> Result<()> { self.inner.upgrade().await } pub(crate) async fn poll_next(&self) -> Result> { self.inner.poll_next().await } } #[async_trait] impl AsyncTransport for WebsocketTransport { async fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()> { self.inner.emit(data, is_binary_att).await } async fn base_url(&self) -> Result { Ok(self.base_url.read().await.clone()) } async fn set_base_url(&self, base_url: Url) -> Result<()> { let mut url = base_url; if !url .query_pairs() .any(|(k, v)| k == "transport" && v == "websocket") { url.query_pairs_mut().append_pair("transport", "websocket"); } url.set_scheme("ws").unwrap(); *self.base_url.write().await = url; Ok(()) } } impl Stream for WebsocketTransport { type Item = Result; fn poll_next( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.inner.poll_next_unpin(cx) } } impl Debug for WebsocketTransport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AsyncWebsocketTransport") .field( "base_url", &self .base_url .try_read() .map_or("Currently not available".to_owned(), |url| url.to_string()), ) .finish() } } #[cfg(test)] mod test { use super::*; use crate::ENGINE_IO_VERSION; use std::str::FromStr; async fn new() -> Result { let url = crate::test::engine_io_server()?.to_string() + "engine.io/?EIO=" + &ENGINE_IO_VERSION.to_string(); WebsocketTransport::new(Url::from_str(&url[..])?, None).await } #[tokio::test] async fn websocket_transport_base_url() -> Result<()> { let transport = new().await?; let mut url = crate::test::engine_io_server()?; url.set_path("/engine.io/"); url.query_pairs_mut() .append_pair("EIO", &ENGINE_IO_VERSION.to_string()) .append_pair("transport", "websocket"); url.set_scheme("ws").unwrap(); assert_eq!(transport.base_url().await?.to_string(), url.to_string()); transport .set_base_url(reqwest::Url::parse("https://127.0.0.1")?) .await?; assert_eq!( transport.base_url().await?.to_string(), "ws://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url().await?.to_string(), url.to_string()); transport .set_base_url(reqwest::Url::parse( "http://127.0.0.1/?transport=websocket", )?) .await?; assert_eq!( transport.base_url().await?.to_string(), "ws://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url().await?.to_string(), url.to_string()); Ok(()) } #[tokio::test] async fn websocket_secure_debug() -> Result<()> { let mut transport = new().await?; assert_eq!( format!("{:?}", transport), format!( "AsyncWebsocketTransport {{ base_url: {:?} }}", transport.base_url().await?.to_string() ) ); println!("{:?}", transport.next().await.unwrap()); println!("{:?}", transport.next().await.unwrap()); Ok(()) } } ================================================ FILE: engineio/src/asynchronous/async_transports/websocket_general.rs ================================================ use std::{borrow::Cow, str::from_utf8, sync::Arc, task::Poll}; use crate::{error::Result, Error, Packet, PacketId}; use bytes::{BufMut, Bytes, BytesMut}; use futures_util::{ ready, stream::{SplitSink, SplitStream}, FutureExt, SinkExt, Stream, StreamExt, }; use tokio::{net::TcpStream, sync::Mutex}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tungstenite::Message; type AsyncWebsocketSender = SplitSink>, Message>; type AsyncWebsocketReceiver = SplitStream>>; /// A general purpose asynchronous websocket transport type. Holds /// the sender and receiver stream of a websocket connection /// and implements the common methods `update` and `emit`. This also /// implements `Stream`. #[derive(Clone)] pub(crate) struct AsyncWebsocketGeneralTransport { sender: Arc>, receiver: Arc>, } impl AsyncWebsocketGeneralTransport { pub(crate) async fn new( sender: SplitSink>, Message>, receiver: SplitStream>>, ) -> Self { AsyncWebsocketGeneralTransport { sender: Arc::new(Mutex::new(sender)), receiver: Arc::new(Mutex::new(receiver)), } } /// Sends probe packet to ensure connection is valid, then sends upgrade /// request pub(crate) async fn upgrade(&self) -> Result<()> { let mut receiver = self.receiver.lock().await; let mut sender = self.sender.lock().await; sender .send(Message::text(Cow::Borrowed(from_utf8(&Bytes::from( Packet::new(PacketId::Ping, Bytes::from("probe")), ))?))) .await?; let msg = receiver .next() .await .ok_or(Error::IllegalWebsocketUpgrade())??; if msg.into_data() != Bytes::from(Packet::new(PacketId::Pong, Bytes::from("probe"))) { return Err(Error::InvalidPacket()); } sender .send(Message::text(Cow::Borrowed(from_utf8(&Bytes::from( Packet::new(PacketId::Upgrade, Bytes::from("")), ))?))) .await?; Ok(()) } pub(crate) async fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()> { let mut sender = self.sender.lock().await; let message = if is_binary_att { Message::binary(Cow::Borrowed(data.as_ref())) } else { Message::text(Cow::Borrowed(std::str::from_utf8(data.as_ref())?)) }; sender.send(message).await?; Ok(()) } pub(crate) async fn poll_next(&self) -> Result> { loop { let mut receiver = self.receiver.lock().await; let next = receiver.next().await; match next { Some(Ok(Message::Text(str))) => return Ok(Some(Bytes::from(str))), Some(Ok(Message::Binary(data))) => { let mut msg = BytesMut::with_capacity(data.len() + 1); msg.put_u8(PacketId::Message as u8); msg.put(data.as_ref()); return Ok(Some(msg.freeze())); } // ignore packets other than text and binary Some(Ok(_)) => (), Some(Err(err)) => return Err(err.into()), None => return Ok(None), } } } } impl Stream for AsyncWebsocketGeneralTransport { type Item = Result; fn poll_next( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { loop { let mut lock = ready!(Box::pin(self.receiver.lock()).poll_unpin(cx)); let next = ready!(lock.poll_next_unpin(cx)); match next { Some(Ok(Message::Text(str))) => return Poll::Ready(Some(Ok(Bytes::from(str)))), Some(Ok(Message::Binary(data))) => { let mut msg = BytesMut::with_capacity(data.len() + 1); msg.put_u8(PacketId::Message as u8); msg.put(data.as_ref()); return Poll::Ready(Some(Ok(msg.freeze()))); } // ignore packets other than text and binary Some(Ok(_)) => (), Some(Err(err)) => return Poll::Ready(Some(Err(err.into()))), None => return Poll::Ready(None), } } } } ================================================ FILE: engineio/src/asynchronous/async_transports/websocket_secure.rs ================================================ use std::fmt::Debug; use std::pin::Pin; use std::sync::Arc; use crate::asynchronous::transport::AsyncTransport; use crate::error::Result; use async_trait::async_trait; use bytes::Bytes; use futures_util::Stream; use futures_util::StreamExt; use http::HeaderMap; use native_tls::TlsConnector; use tokio::sync::RwLock; use tokio_tungstenite::connect_async_tls_with_config; use tokio_tungstenite::Connector; use tungstenite::client::IntoClientRequest; use url::Url; use super::websocket_general::AsyncWebsocketGeneralTransport; /// An asynchronous websocket transport type. /// This type only allows for secure websocket /// connections ("wss://"). #[derive(Clone)] pub struct WebsocketSecureTransport { inner: AsyncWebsocketGeneralTransport, base_url: Arc>, } impl WebsocketSecureTransport { /// Creates a new instance over a request that might hold additional headers, a possible /// Tls connector and an URL. pub(crate) async fn new( base_url: Url, tls_config: Option, headers: Option, ) -> Result { let mut url = base_url; url.query_pairs_mut().append_pair("transport", "websocket"); url.set_scheme("wss").unwrap(); let mut req = url.clone().into_client_request()?; if let Some(map) = headers { // SAFETY: this unwrap never panics as the underlying request is just initialized and in proper state req.headers_mut().extend(map); } // `disable_nagle` Sets the value of the TCP_NODELAY option on this socket. // // If set to `true`, this option disables the Nagle algorithm. // This means that segments are always sent as soon as possible, even if there is only a small amount of data. // When `false`, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets. // // See the docs: https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html#method.set_nodelay let (ws_stream, _) = connect_async_tls_with_config( req, None, /*disable_nagle=*/ false, tls_config.map(Connector::NativeTls), ) .await?; let (sen, rec) = ws_stream.split(); let inner = AsyncWebsocketGeneralTransport::new(sen, rec).await; Ok(WebsocketSecureTransport { inner, base_url: Arc::new(RwLock::new(url)), }) } /// Sends probe packet to ensure connection is valid, then sends upgrade /// request pub(crate) async fn upgrade(&self) -> Result<()> { self.inner.upgrade().await } pub(crate) async fn poll_next(&self) -> Result> { self.inner.poll_next().await } } impl Stream for WebsocketSecureTransport { type Item = Result; fn poll_next( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.inner.poll_next_unpin(cx) } } #[async_trait] impl AsyncTransport for WebsocketSecureTransport { async fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()> { self.inner.emit(data, is_binary_att).await } async fn base_url(&self) -> Result { Ok(self.base_url.read().await.clone()) } async fn set_base_url(&self, base_url: Url) -> Result<()> { let mut url = base_url; if !url .query_pairs() .any(|(k, v)| k == "transport" && v == "websocket") { url.query_pairs_mut().append_pair("transport", "websocket"); } url.set_scheme("wss").unwrap(); *self.base_url.write().await = url; Ok(()) } } impl Debug for WebsocketSecureTransport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AsyncWebsocketSecureTransport") .field( "base_url", &self .base_url .try_read() .map_or("Currently not available".to_owned(), |url| url.to_string()), ) .finish() } } #[cfg(test)] mod test { use super::*; use crate::ENGINE_IO_VERSION; use std::str::FromStr; async fn new() -> Result { let url = crate::test::engine_io_server_secure()?.to_string() + "engine.io/?EIO=" + &ENGINE_IO_VERSION.to_string(); WebsocketSecureTransport::new( Url::from_str(&url[..])?, Some(crate::test::tls_connector()?), None, ) .await } #[tokio::test] async fn websocket_secure_transport_base_url() -> Result<()> { let transport = new().await?; let mut url = crate::test::engine_io_server_secure()?; url.set_path("/engine.io/"); url.query_pairs_mut() .append_pair("EIO", &ENGINE_IO_VERSION.to_string()) .append_pair("transport", "websocket"); url.set_scheme("wss").unwrap(); assert_eq!(transport.base_url().await?.to_string(), url.to_string()); transport .set_base_url(reqwest::Url::parse("https://127.0.0.1")?) .await?; assert_eq!( transport.base_url().await?.to_string(), "wss://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url().await?.to_string(), url.to_string()); transport .set_base_url(reqwest::Url::parse( "http://127.0.0.1/?transport=websocket", )?) .await?; assert_eq!( transport.base_url().await?.to_string(), "wss://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url().await?.to_string(), url.to_string()); Ok(()) } #[tokio::test] async fn websocket_secure_debug() -> Result<()> { let transport = new().await?; assert_eq!( format!("{:?}", transport), format!( "AsyncWebsocketSecureTransport {{ base_url: {:?} }}", transport.base_url().await?.to_string() ) ); Ok(()) } } ================================================ FILE: engineio/src/asynchronous/callback.rs ================================================ use bytes::Bytes; use futures_util::future::BoxFuture; use std::{fmt::Debug, ops::Deref, sync::Arc}; use crate::Packet; /// Internal type, provides a way to store futures and return them in a boxed manner. pub(crate) type DynAsyncCallback = dyn 'static + Send + Sync + Fn(I) -> BoxFuture<'static, ()>; /// Internal type, might hold an async callback. #[derive(Clone)] pub(crate) struct OptionalCallback { inner: Option>>, } impl OptionalCallback { pub(crate) fn new(callback: T) -> Self where T: 'static + Send + Sync + Fn(I) -> BoxFuture<'static, ()>, { OptionalCallback { inner: Some(Arc::new(callback)), } } pub(crate) fn default() -> Self { OptionalCallback { inner: None } } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(String)" } else { "None" } )) } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback<()> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(())" } else { "None" } )) } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(Packet)" } else { "None" } )) } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(Bytes)" } else { "None" } )) } } impl Deref for OptionalCallback { type Target = Option>>; fn deref(&self) -> &::Target { &self.inner } } ================================================ FILE: engineio/src/asynchronous/client/async_client.rs ================================================ use std::{fmt::Debug, pin::Pin}; use crate::{ asynchronous::{async_socket::Socket as InnerSocket, generator::StreamGenerator}, error::Result, Packet, }; use async_stream::try_stream; use futures_util::{Stream, StreamExt}; /// An engine.io client that allows interaction with the connected engine.io /// server. This client provides means for connecting, disconnecting and sending /// packets to the server. /// /// ## Note: /// There is no need to put this Client behind an `Arc`, as the type uses `Arc` /// internally and provides a shared state beyond all cloned instances. #[derive(Clone)] pub struct Client { pub(super) socket: InnerSocket, generator: StreamGenerator, } impl Client { pub(super) fn new(socket: InnerSocket) -> Self { Client { socket: socket.clone(), generator: StreamGenerator::new(Self::stream(socket)), } } pub async fn close(&self) -> Result<()> { self.socket.disconnect().await } /// Opens the connection to a specified server. The first Pong packet is sent /// to the server to trigger the Ping-cycle. pub async fn connect(&self) -> Result<()> { self.socket.connect().await } /// Disconnects the connection. pub async fn disconnect(&self) -> Result<()> { self.socket.disconnect().await } /// Sends a packet to the server. pub async fn emit(&self, packet: Packet) -> Result<()> { self.socket.emit(packet).await } /// Static method that returns a generator for each element of the stream. fn stream( socket: InnerSocket, ) -> Pin> + 'static + Send>> { Box::pin(try_stream! { let socket = socket.clone(); for await item in socket.as_stream() { let packet = item?; socket.handle_incoming_packet(packet.clone()).await?; yield packet; } }) } /// Check if the underlying transport client is connected. pub fn is_connected(&self) -> bool { self.socket.is_connected() } } impl Stream for Client { type Item = Result; fn poll_next( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.generator.poll_next_unpin(cx) } } impl Debug for Client { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Client") .field("socket", &self.socket) .finish() } } #[cfg(all(test))] mod test { use super::*; use crate::{asynchronous::ClientBuilder, header::HeaderMap, packet::PacketId, Error}; use bytes::Bytes; use futures_util::StreamExt; use native_tls::TlsConnector; use url::Url; /// The purpose of this test is to check whether the Client is properly cloneable or not. /// As the documentation of the engine.io client states, the object needs to maintain it's internal /// state when cloned and the cloned object should reflect the same state throughout the lifetime /// of both objects (initial and cloned). #[tokio::test] async fn test_client_cloneable() -> Result<()> { let url = crate::test::engine_io_server()?; let mut sut = builder(url).build().await?; let mut cloned = sut.clone(); sut.connect().await?; // when the underlying socket is connected, the // state should also change on the cloned one assert!(sut.is_connected()); assert!(cloned.is_connected()); // both clients should reflect the same messages. assert_eq!( sut.next().await.unwrap()?, Packet::new(PacketId::Message, "hello client") ); sut.emit(Packet::new(PacketId::Message, "respond")).await?; assert_eq!( cloned.next().await.unwrap()?, Packet::new(PacketId::Message, "Roger Roger") ); cloned.disconnect().await?; // when the underlying socket is disconnected, the // state should also change on the cloned one assert!(!sut.is_connected()); assert!(!cloned.is_connected()); Ok(()) } #[tokio::test] async fn test_illegal_actions() -> Result<()> { let url = crate::test::engine_io_server()?; let mut sut = builder(url.clone()).build().await?; assert!(sut .emit(Packet::new(PacketId::Close, Bytes::new())) .await .is_err()); sut.connect().await?; assert!(sut.next().await.unwrap().is_ok()); assert!(builder(Url::parse("fake://fake.fake").unwrap()) .build_websocket() .await .is_err()); sut.disconnect().await?; Ok(()) } use reqwest::header::HOST; use crate::packet::Packet; fn builder(url: Url) -> ClientBuilder { ClientBuilder::new(url) .on_open(|_| { Box::pin(async { println!("Open event!"); }) }) .on_packet(|packet| { Box::pin(async move { println!("Received packet: {:?}", packet); }) }) .on_data(|data| { Box::pin(async move { println!("Received data: {:?}", std::str::from_utf8(&data)); }) }) .on_close(|_| { Box::pin(async { println!("Close event!"); }) }) .on_error(|error| { Box::pin(async move { println!("Error {}", error); }) }) } async fn test_connection(socket: Client) -> Result<()> { let mut socket = socket; socket.connect().await.unwrap(); assert_eq!( socket.next().await.unwrap()?, Packet::new(PacketId::Message, "hello client") ); println!("received msg, about to send"); socket .emit(Packet::new(PacketId::Message, "respond")) .await?; println!("send msg"); assert_eq!( socket.next().await.unwrap()?, Packet::new(PacketId::Message, "Roger Roger") ); println!("received 2"); socket.close().await } #[tokio::test] async fn test_connection_long() -> Result<()> { // Long lived socket to receive pings let url = crate::test::engine_io_server()?; let mut socket = builder(url).build().await?; socket.connect().await?; // hello client assert!(matches!( socket.next().await.unwrap()?, Packet { packet_id: PacketId::Message, .. } )); // Ping assert!(matches!( socket.next().await.unwrap()?, Packet { packet_id: PacketId::Ping, .. } )); socket.disconnect().await?; assert!(!socket.is_connected()); Ok(()) } #[tokio::test] async fn test_connection_dynamic() -> Result<()> { let url = crate::test::engine_io_server()?; let socket = builder(url).build().await?; test_connection(socket).await?; let url = crate::test::engine_io_polling_server()?; let socket = builder(url).build().await?; test_connection(socket).await } #[tokio::test] async fn test_connection_fallback() -> Result<()> { let url = crate::test::engine_io_server()?; let socket = builder(url).build_with_fallback().await?; test_connection(socket).await?; let url = crate::test::engine_io_polling_server()?; let socket = builder(url).build_with_fallback().await?; test_connection(socket).await } #[tokio::test] async fn test_connection_dynamic_secure() -> Result<()> { let url = crate::test::engine_io_server_secure()?; let mut socket_builder = builder(url); socket_builder = socket_builder.tls_config(crate::test::tls_connector()?); let socket = socket_builder.build().await?; test_connection(socket).await } #[tokio::test] async fn test_connection_polling() -> Result<()> { let url = crate::test::engine_io_server()?; let socket = builder(url).build_polling().await?; test_connection(socket).await } #[tokio::test] async fn test_connection_wss() -> Result<()> { let url = crate::test::engine_io_polling_server()?; assert!(builder(url).build_websocket_with_upgrade().await.is_err()); let host = std::env::var("ENGINE_IO_SECURE_HOST").unwrap_or_else(|_| "localhost".to_owned()); let mut url = crate::test::engine_io_server_secure()?; let mut headers = HeaderMap::default(); headers.insert(HOST, host); let mut builder = builder(url.clone()); builder = builder.tls_config(crate::test::tls_connector()?); builder = builder.headers(headers.clone()); let socket = builder.clone().build_websocket_with_upgrade().await?; test_connection(socket).await?; let socket = builder.build_websocket().await?; test_connection(socket).await?; url.set_scheme("wss").unwrap(); let builder = self::builder(url) .tls_config(crate::test::tls_connector()?) .headers(headers); let socket = builder.clone().build_websocket().await?; test_connection(socket).await?; assert!(builder.build_websocket_with_upgrade().await.is_err()); Ok(()) } #[tokio::test] async fn test_connection_ws() -> Result<()> { let url = crate::test::engine_io_polling_server()?; assert!(builder(url.clone()).build_websocket().await.is_err()); assert!(builder(url).build_websocket_with_upgrade().await.is_err()); let mut url = crate::test::engine_io_server()?; let builder = builder(url.clone()); let socket = builder.clone().build_websocket().await?; test_connection(socket).await?; let socket = builder.build_websocket_with_upgrade().await?; test_connection(socket).await?; url.set_scheme("ws").unwrap(); let builder = self::builder(url); let socket = builder.clone().build_websocket().await?; test_connection(socket).await?; assert!(builder.build_websocket_with_upgrade().await.is_err()); Ok(()) } #[tokio::test] async fn test_open_invariants() -> Result<()> { let url = crate::test::engine_io_server()?; let illegal_url = "this is illegal"; assert!(Url::parse(illegal_url).is_err()); let invalid_protocol = "file:///tmp/foo"; assert!(builder(Url::parse(invalid_protocol).unwrap()) .build() .await .is_err()); let sut = builder(url.clone()).build().await?; let _error = sut .emit(Packet::new(PacketId::Close, Bytes::new())) .await .expect_err("error"); assert!(matches!(Error::IllegalActionBeforeOpen(), _error)); // test missing match arm in socket constructor let mut headers = HeaderMap::default(); let host = std::env::var("ENGINE_IO_SECURE_HOST").unwrap_or_else(|_| "localhost".to_owned()); headers.insert(HOST, host); let _ = builder(url.clone()) .tls_config( TlsConnector::builder() .danger_accept_invalid_certs(true) .build() .unwrap(), ) .build() .await?; let _ = builder(url).headers(headers).build().await?; Ok(()) } } ================================================ FILE: engineio/src/asynchronous/client/builder.rs ================================================ use crate::{ asynchronous::{ async_socket::Socket as InnerSocket, async_transports::{PollingTransport, WebsocketSecureTransport, WebsocketTransport}, callback::OptionalCallback, transport::AsyncTransport, }, error::Result, header::HeaderMap, packet::HandshakePacket, Error, Packet, ENGINE_IO_VERSION, }; use bytes::Bytes; use futures_util::{future::BoxFuture, StreamExt}; use native_tls::TlsConnector; use url::Url; use super::Client; #[derive(Clone, Debug)] pub struct ClientBuilder { url: Url, tls_config: Option, headers: Option, handshake: Option, on_error: OptionalCallback, on_open: OptionalCallback<()>, on_close: OptionalCallback<()>, on_data: OptionalCallback, on_packet: OptionalCallback, } impl ClientBuilder { pub fn new(url: Url) -> Self { let mut url = url; url.query_pairs_mut() .append_pair("EIO", &ENGINE_IO_VERSION.to_string()); // No path add engine.io if url.path() == "/" { url.set_path("/engine.io/"); } ClientBuilder { url, headers: None, tls_config: None, handshake: None, on_close: OptionalCallback::default(), on_data: OptionalCallback::default(), on_error: OptionalCallback::default(), on_open: OptionalCallback::default(), on_packet: OptionalCallback::default(), } } /// Specify transport's tls config pub fn tls_config(mut self, tls_config: TlsConnector) -> Self { self.tls_config = Some(tls_config); self } /// Specify transport's HTTP headers pub fn headers(mut self, headers: HeaderMap) -> Self { self.headers = Some(headers); self } /// Registers the `on_close` callback. #[cfg(feature = "async-callbacks")] pub fn on_close(mut self, callback: T) -> Self where T: 'static + Send + Sync + Fn(()) -> BoxFuture<'static, ()>, { self.on_close = OptionalCallback::new(callback); self } /// Registers the `on_data` callback. #[cfg(feature = "async-callbacks")] pub fn on_data(mut self, callback: T) -> Self where T: 'static + Send + Sync + Fn(Bytes) -> BoxFuture<'static, ()>, { self.on_data = OptionalCallback::new(callback); self } /// Registers the `on_error` callback. #[cfg(feature = "async-callbacks")] pub fn on_error(mut self, callback: T) -> Self where T: 'static + Send + Sync + Fn(String) -> BoxFuture<'static, ()>, { self.on_error = OptionalCallback::new(callback); self } /// Registers the `on_open` callback. #[cfg(feature = "async-callbacks")] pub fn on_open(mut self, callback: T) -> Self where T: 'static + Send + Sync + Fn(()) -> BoxFuture<'static, ()>, { self.on_open = OptionalCallback::new(callback); self } /// Registers the `on_packet` callback. #[cfg(feature = "async-callbacks")] pub fn on_packet(mut self, callback: T) -> Self where T: 'static + Send + Sync + Fn(Packet) -> BoxFuture<'static, ()>, { self.on_packet = OptionalCallback::new(callback); self } /// Performs the handshake async fn handshake_with_transport( &mut self, transport: &mut T, ) -> Result<()> { // No need to handshake twice if self.handshake.is_some() { return Ok(()); } let mut url = self.url.clone(); let handshake: HandshakePacket = Packet::try_from(transport.next().await.ok_or(Error::IncompletePacket())??)? .try_into()?; // update the base_url with the new sid url.query_pairs_mut().append_pair("sid", &handshake.sid[..]); self.handshake = Some(handshake); self.url = url; Ok(()) } async fn handshake(&mut self) -> Result<()> { if self.handshake.is_some() { return Ok(()); } let headers = if let Some(map) = self.headers.clone() { Some(map.try_into()?) } else { None }; // Start with polling transport let mut transport = PollingTransport::new(self.url.clone(), self.tls_config.clone(), headers); self.handshake_with_transport(&mut transport).await } /// Build websocket if allowed, if not fall back to polling pub async fn build(mut self) -> Result { self.handshake().await?; if self.websocket_upgrade()? { self.build_websocket_with_upgrade().await } else { self.build_polling().await } } /// Build socket with polling transport pub async fn build_polling(mut self) -> Result { self.handshake().await?; // Make a polling transport with new sid let transport = PollingTransport::new( self.url, self.tls_config, self.headers.map(|v| v.try_into().unwrap()), ); // SAFETY: handshake function called previously. Ok(Client::new(InnerSocket::new( transport.into(), self.handshake.unwrap(), self.on_close, self.on_data, self.on_error, self.on_open, self.on_packet, ))) } /// Build socket with a polling transport then upgrade to websocket transport pub async fn build_websocket_with_upgrade(mut self) -> Result { self.handshake().await?; if self.websocket_upgrade()? { self.build_websocket().await } else { Err(Error::IllegalWebsocketUpgrade()) } } /// Build socket with only a websocket transport pub async fn build_websocket(mut self) -> Result { let headers = if let Some(map) = self.headers.clone() { Some(map.try_into()?) } else { None }; match self.url.scheme() { "http" | "ws" => { let mut transport = WebsocketTransport::new(self.url.clone(), headers).await?; if self.handshake.is_some() { transport.upgrade().await?; } else { self.handshake_with_transport(&mut transport).await?; } // NOTE: Although self.url contains the sid, it does not propagate to the transport // SAFETY: handshake function called previously. Ok(Client::new(InnerSocket::new( transport.into(), self.handshake.unwrap(), self.on_close, self.on_data, self.on_error, self.on_open, self.on_packet, ))) } "https" | "wss" => { let mut transport = WebsocketSecureTransport::new( self.url.clone(), self.tls_config.clone(), headers, ) .await?; if self.handshake.is_some() { transport.upgrade().await?; } else { self.handshake_with_transport(&mut transport).await?; } // NOTE: Although self.url contains the sid, it does not propagate to the transport // SAFETY: handshake function called previously. Ok(Client::new(InnerSocket::new( transport.into(), self.handshake.unwrap(), self.on_close, self.on_data, self.on_error, self.on_open, self.on_packet, ))) } _ => Err(Error::InvalidUrlScheme(self.url.scheme().to_string())), } } /// Build websocket if allowed, if not allowed or errored fall back to polling. /// WARNING: websocket errors suppressed, no indication of websocket success or failure. pub async fn build_with_fallback(self) -> Result { let result = self.clone().build().await; if result.is_err() { self.build_polling().await } else { result } } /// Checks the handshake to see if websocket upgrades are allowed fn websocket_upgrade(&mut self) -> Result { if self.handshake.is_none() { return Ok(false); } Ok(self .handshake .as_ref() .unwrap() .upgrades .iter() .any(|upgrade| upgrade.to_lowercase() == *"websocket")) } } ================================================ FILE: engineio/src/asynchronous/client/mod.rs ================================================ mod async_client; mod builder; pub use async_client::Client; pub use builder::ClientBuilder; ================================================ FILE: engineio/src/asynchronous/generator.rs ================================================ use std::{pin::Pin, sync::Arc}; use crate::error::Result; use futures_util::{ready, FutureExt, Stream, StreamExt}; use tokio::sync::Mutex; /// A generator is an internal type that represents a [`Send`] [`futures_util::Stream`] /// that yields a certain type `T` whenever it's polled. pub(crate) type Generator = Pin + 'static + Send>>; /// An internal type that implements stream by repeatedly calling [`Stream::poll_next`] on an /// underlying stream. Note that the generic parameter will be wrapped in a [`Result`]. #[derive(Clone)] pub(crate) struct StreamGenerator { inner: Arc>>>, } impl Stream for StreamGenerator { type Item = Result; fn poll_next( self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { let mut lock = ready!(Box::pin(self.inner.lock()).poll_unpin(cx)); lock.poll_next_unpin(cx) } } impl StreamGenerator { pub(crate) fn new(generator_stream: Generator>) -> Self { StreamGenerator { inner: Arc::new(Mutex::new(generator_stream)), } } } ================================================ FILE: engineio/src/asynchronous/mod.rs ================================================ pub mod async_transports; pub mod transport; pub(self) mod async_socket; #[cfg(feature = "async-callbacks")] mod callback; #[cfg(feature = "async")] pub mod client; mod generator; #[cfg(feature = "async")] pub use client::Client; #[cfg(feature = "async")] pub use client::ClientBuilder; ================================================ FILE: engineio/src/asynchronous/transport.rs ================================================ use crate::error::Result; use adler32::adler32; use async_trait::async_trait; use bytes::Bytes; use futures_util::Stream; use std::{pin::Pin, time::SystemTime}; use url::Url; use super::async_transports::{PollingTransport, WebsocketSecureTransport, WebsocketTransport}; #[async_trait] pub trait AsyncTransport: Stream> + Unpin { /// Sends a packet to the server. This optionally handles sending of a /// socketio binary attachment via the boolean attribute `is_binary_att`. async fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()>; /// Returns start of the url. ex. http://localhost:2998/engine.io/?EIO=4&transport=polling /// Must have EIO and transport already set. async fn base_url(&self) -> Result; /// Used to update the base path, like when adding the sid. async fn set_base_url(&self, base_url: Url) -> Result<()>; /// Full query address async fn address(&self) -> Result where Self: Sized, { let reader = format!("{:#?}", SystemTime::now()); let hash = adler32(reader.as_bytes()).unwrap(); let mut url = self.base_url().await?; url.query_pairs_mut().append_pair("t", &hash.to_string()); Ok(url) } } #[derive(Debug, Clone)] pub enum AsyncTransportType { Polling(PollingTransport), Websocket(WebsocketTransport), WebsocketSecure(WebsocketSecureTransport), } impl From for AsyncTransportType { fn from(transport: PollingTransport) -> Self { AsyncTransportType::Polling(transport) } } impl From for AsyncTransportType { fn from(transport: WebsocketTransport) -> Self { AsyncTransportType::Websocket(transport) } } impl From for AsyncTransportType { fn from(transport: WebsocketSecureTransport) -> Self { AsyncTransportType::WebsocketSecure(transport) } } #[cfg(feature = "async")] impl AsyncTransportType { pub fn as_transport(&self) -> &(dyn AsyncTransport + Send) { match self { AsyncTransportType::Polling(transport) => transport, AsyncTransportType::Websocket(transport) => transport, AsyncTransportType::WebsocketSecure(transport) => transport, } } pub fn as_pin_box(&mut self) -> Pin> { match self { AsyncTransportType::Polling(transport) => Box::pin(transport), AsyncTransportType::Websocket(transport) => Box::pin(transport), AsyncTransportType::WebsocketSecure(transport) => Box::pin(transport), } } } ================================================ FILE: engineio/src/callback.rs ================================================ use crate::Packet; use bytes::Bytes; use std::fmt::Debug; use std::ops::Deref; use std::sync::Arc; pub(crate) type DynCallback = dyn Fn(I) + 'static + Sync + Send; #[derive(Clone)] /// Internal type, only implements debug on fixed set of generics pub(crate) struct OptionalCallback { inner: Arc>>>, } impl OptionalCallback { pub(crate) fn new(callback: T) -> Self where T: Fn(I) + 'static + Sync + Send, { OptionalCallback { inner: Arc::new(Some(Box::new(callback))), } } pub(crate) fn default() -> Self { OptionalCallback { inner: Arc::new(None), } } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(String)" } else { "None" } )) } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback<()> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(())" } else { "None" } )) } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(Packet)" } else { "None" } )) } } #[cfg_attr(tarpaulin, ignore)] impl Debug for OptionalCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_fmt(format_args!( "Callback({:?})", if self.inner.is_some() { "Fn(Bytes)" } else { "None" } )) } } impl Deref for OptionalCallback { type Target = Option>>; fn deref(&self) -> &::Target { self.inner.as_ref() } } ================================================ FILE: engineio/src/client/client.rs ================================================ use super::super::socket::Socket as InnerSocket; use crate::callback::OptionalCallback; use crate::socket::DEFAULT_MAX_POLL_TIMEOUT; use crate::transport::Transport; use crate::error::{Error, Result}; use crate::header::HeaderMap; use crate::packet::{HandshakePacket, Packet, PacketId}; use crate::transports::{PollingTransport, WebsocketSecureTransport, WebsocketTransport}; use crate::ENGINE_IO_VERSION; use bytes::Bytes; use native_tls::TlsConnector; use std::convert::TryFrom; use std::convert::TryInto; use std::fmt::Debug; use url::Url; /// An engine.io client that allows interaction with the connected engine.io /// server. This client provides means for connecting, disconnecting and sending /// packets to the server. /// /// ## Note: /// There is no need to put this Client behind an `Arc`, as the type uses `Arc` /// internally and provides a shared state beyond all cloned instances. #[derive(Clone, Debug)] pub struct Client { socket: InnerSocket, } #[derive(Clone, Debug)] pub struct ClientBuilder { url: Url, tls_config: Option, headers: Option, handshake: Option, on_error: OptionalCallback, on_open: OptionalCallback<()>, on_close: OptionalCallback<()>, on_data: OptionalCallback, on_packet: OptionalCallback, } impl ClientBuilder { pub fn new(url: Url) -> Self { let mut url = url; url.query_pairs_mut() .append_pair("EIO", &ENGINE_IO_VERSION.to_string()); // No path add engine.io if url.path() == "/" { url.set_path("/engine.io/"); } ClientBuilder { url, headers: None, tls_config: None, handshake: None, on_close: OptionalCallback::default(), on_data: OptionalCallback::default(), on_error: OptionalCallback::default(), on_open: OptionalCallback::default(), on_packet: OptionalCallback::default(), } } /// Specify transport's tls config pub fn tls_config(mut self, tls_config: TlsConnector) -> Self { self.tls_config = Some(tls_config); self } /// Specify transport's HTTP headers pub fn headers(mut self, headers: HeaderMap) -> Self { self.headers = Some(headers); self } /// Registers the `on_close` callback. pub fn on_close(mut self, callback: T) -> Self where T: Fn(()) + 'static + Sync + Send, { self.on_close = OptionalCallback::new(callback); self } /// Registers the `on_data` callback. pub fn on_data(mut self, callback: T) -> Self where T: Fn(Bytes) + 'static + Sync + Send, { self.on_data = OptionalCallback::new(callback); self } /// Registers the `on_error` callback. pub fn on_error(mut self, callback: T) -> Self where T: Fn(String) + 'static + Sync + Send, { self.on_error = OptionalCallback::new(callback); self } /// Registers the `on_open` callback. pub fn on_open(mut self, callback: T) -> Self where T: Fn(()) + 'static + Sync + Send, { self.on_open = OptionalCallback::new(callback); self } /// Registers the `on_packet` callback. pub fn on_packet(mut self, callback: T) -> Self where T: Fn(Packet) + 'static + Sync + Send, { self.on_packet = OptionalCallback::new(callback); self } /// Performs the handshake fn handshake_with_transport(&mut self, transport: &T) -> Result<()> { // No need to handshake twice if self.handshake.is_some() { return Ok(()); } let mut url = self.url.clone(); let handshake: HandshakePacket = Packet::try_from(transport.poll(DEFAULT_MAX_POLL_TIMEOUT)?)?.try_into()?; // update the base_url with the new sid url.query_pairs_mut().append_pair("sid", &handshake.sid[..]); self.handshake = Some(handshake); self.url = url; Ok(()) } fn handshake(&mut self) -> Result<()> { if self.handshake.is_some() { return Ok(()); } // Start with polling transport let transport = PollingTransport::new( self.url.clone(), self.tls_config.clone(), self.headers.clone().map(|v| v.try_into().unwrap()), ); self.handshake_with_transport(&transport) } /// Build websocket if allowed, if not fall back to polling pub fn build(mut self) -> Result { self.handshake()?; if self.websocket_upgrade()? { self.build_websocket_with_upgrade() } else { self.build_polling() } } /// Build socket with polling transport pub fn build_polling(mut self) -> Result { self.handshake()?; // Make a polling transport with new sid let transport = PollingTransport::new( self.url, self.tls_config, self.headers.map(|v| v.try_into().unwrap()), ); // SAFETY: handshake function called previously. Ok(Client { socket: InnerSocket::new( transport.into(), self.handshake.unwrap(), self.on_close, self.on_data, self.on_error, self.on_open, self.on_packet, ), }) } /// Build socket with a polling transport then upgrade to websocket transport pub fn build_websocket_with_upgrade(mut self) -> Result { self.handshake()?; if self.websocket_upgrade()? { self.build_websocket() } else { Err(Error::IllegalWebsocketUpgrade()) } } /// Build socket with only a websocket transport pub fn build_websocket(mut self) -> Result { // SAFETY: Already a Url let url = url::Url::parse(self.url.as_ref())?; let headers: Option = if let Some(map) = self.headers.clone() { Some(map.try_into()?) } else { None }; match url.scheme() { "http" | "ws" => { let transport = WebsocketTransport::new(url, headers)?; if self.handshake.is_some() { transport.upgrade()?; } else { self.handshake_with_transport(&transport)?; } // NOTE: Although self.url contains the sid, it does not propagate to the transport // SAFETY: handshake function called previously. Ok(Client { socket: InnerSocket::new( transport.into(), self.handshake.unwrap(), self.on_close, self.on_data, self.on_error, self.on_open, self.on_packet, ), }) } "https" | "wss" => { let transport = WebsocketSecureTransport::new(url, self.tls_config.clone(), headers)?; if self.handshake.is_some() { transport.upgrade()?; } else { self.handshake_with_transport(&transport)?; } // NOTE: Although self.url contains the sid, it does not propagate to the transport // SAFETY: handshake function called previously. Ok(Client { socket: InnerSocket::new( transport.into(), self.handshake.unwrap(), self.on_close, self.on_data, self.on_error, self.on_open, self.on_packet, ), }) } _ => Err(Error::InvalidUrlScheme(url.scheme().to_string())), } } /// Build websocket if allowed, if not allowed or errored fall back to polling. /// WARNING: websocket errors suppressed, no indication of websocket success or failure. pub fn build_with_fallback(self) -> Result { let result = self.clone().build(); if result.is_err() { self.build_polling() } else { result } } /// Checks the handshake to see if websocket upgrades are allowed fn websocket_upgrade(&mut self) -> Result { // SAFETY: handshake set by above function. Ok(self .handshake .as_ref() .unwrap() .upgrades .iter() .any(|upgrade| upgrade.to_lowercase() == *"websocket")) } } impl Client { pub fn close(&self) -> Result<()> { self.socket.disconnect() } /// Opens the connection to a specified server. The first Pong packet is sent /// to the server to trigger the Ping-cycle. pub fn connect(&self) -> Result<()> { self.socket.connect() } /// Disconnects the connection. pub fn disconnect(&self) -> Result<()> { self.socket.disconnect() } /// Sends a packet to the server. pub fn emit(&self, packet: Packet) -> Result<()> { self.socket.emit(packet) } /// Polls for next payload #[doc(hidden)] pub fn poll(&self) -> Result> { let packet = self.socket.poll()?; if let Some(packet) = packet { // check for the appropriate action or callback self.socket.handle_packet(packet.clone()); match packet.packet_id { PacketId::MessageBinary => { self.socket.handle_data(packet.data.clone()); } PacketId::Message => { self.socket.handle_data(packet.data.clone()); } PacketId::Close => { self.socket.handle_close(); } PacketId::Open => { unreachable!("Won't happen as we open the connection beforehand"); } PacketId::Upgrade => { // this is already checked during the handshake, so just do nothing here } PacketId::Ping => { self.socket.pinged()?; self.emit(Packet::new(PacketId::Pong, Bytes::new()))?; } PacketId::Pong => { // this will never happen as the pong packet is // only sent by the client unreachable!(); } PacketId::Noop => (), } Ok(Some(packet)) } else { Ok(None) } } /// Check if the underlying transport client is connected. pub fn is_connected(&self) -> Result { self.socket.is_connected() } pub fn iter(&self) -> Iter { Iter { socket: self } } } #[derive(Clone)] pub struct Iter<'a> { socket: &'a Client, } impl<'a> Iterator for Iter<'a> { type Item = Result; fn next(&mut self) -> std::option::Option<::Item> { match self.socket.poll() { Ok(Some(packet)) => Some(Ok(packet)), Ok(None) => None, Err(err) => Some(Err(err)), } } } #[cfg(test)] mod test { use crate::packet::PacketId; use super::*; /// The purpose of this test is to check whether the Client is properly cloneable or not. /// As the documentation of the engine.io client states, the object needs to maintain it's internal /// state when cloned and the cloned object should reflect the same state throughout the lifetime /// of both objects (initial and cloned). #[test] fn test_client_cloneable() -> Result<()> { let url = crate::test::engine_io_server()?; let sut = builder(url).build()?; let cloned = sut.clone(); sut.connect()?; // when the underlying socket is connected, the // state should also change on the cloned one assert!(sut.is_connected()?); assert!(cloned.is_connected()?); // both clients should reflect the same messages. let mut iter = sut .iter() .map(|packet| packet.unwrap()) .filter(|packet| packet.packet_id != PacketId::Ping); let mut iter_cloned = cloned .iter() .map(|packet| packet.unwrap()) .filter(|packet| packet.packet_id != PacketId::Ping); assert_eq!( iter.next(), Some(Packet::new(PacketId::Message, "hello client")) ); sut.emit(Packet::new(PacketId::Message, "respond"))?; assert_eq!( iter_cloned.next(), Some(Packet::new(PacketId::Message, "Roger Roger")) ); cloned.disconnect()?; // when the underlying socket is disconnected, the // state should also change on the cloned one assert!(!sut.is_connected()?); assert!(!cloned.is_connected()?); Ok(()) } #[test] fn test_illegal_actions() -> Result<()> { let url = crate::test::engine_io_server()?; let sut = builder(url.clone()).build()?; assert!(sut .emit(Packet::new(PacketId::Close, Bytes::new())) .is_err()); sut.connect()?; assert!(sut.poll().is_ok()); assert!(builder(Url::parse("fake://fake.fake").unwrap()) .build_websocket() .is_err()); Ok(()) } use reqwest::header::HOST; use crate::packet::Packet; fn builder(url: Url) -> ClientBuilder { ClientBuilder::new(url) .on_open(|_| { println!("Open event!"); }) .on_packet(|packet| { println!("Received packet: {:?}", packet); }) .on_data(|data| { println!("Received data: {:?}", std::str::from_utf8(&data)); }) .on_close(|_| { println!("Close event!"); }) .on_error(|error| { println!("Error {}", error); }) } fn test_connection(socket: Client) -> Result<()> { let socket = socket; socket.connect().unwrap(); // TODO: 0.3.X better tests let mut iter = socket .iter() .map(|packet| packet.unwrap()) .filter(|packet| packet.packet_id != PacketId::Ping); assert_eq!( iter.next(), Some(Packet::new(PacketId::Message, "hello client")) ); socket.emit(Packet::new(PacketId::Message, "respond"))?; assert_eq!( iter.next(), Some(Packet::new(PacketId::Message, "Roger Roger")) ); socket.close() } #[test] fn test_connection_long() -> Result<()> { // Long lived socket to receive pings let url = crate::test::engine_io_server()?; let socket = builder(url).build()?; socket.connect()?; let mut iter = socket.iter(); // hello client iter.next(); // Ping iter.next(); socket.disconnect()?; assert!(!socket.is_connected()?); Ok(()) } #[test] fn test_connection_dynamic() -> Result<()> { let url = crate::test::engine_io_server()?; let socket = builder(url).build()?; test_connection(socket)?; let url = crate::test::engine_io_polling_server()?; let socket = builder(url).build()?; test_connection(socket) } #[test] fn test_connection_fallback() -> Result<()> { let url = crate::test::engine_io_server()?; let socket = builder(url).build_with_fallback()?; test_connection(socket)?; let url = crate::test::engine_io_polling_server()?; let socket = builder(url).build_with_fallback()?; test_connection(socket) } #[test] fn test_connection_dynamic_secure() -> Result<()> { let url = crate::test::engine_io_server_secure()?; let mut builder = builder(url); builder = builder.tls_config(crate::test::tls_connector()?); let socket = builder.build()?; test_connection(socket) } #[test] fn test_connection_polling() -> Result<()> { let url = crate::test::engine_io_server()?; let socket = builder(url).build_polling()?; test_connection(socket) } #[test] fn test_connection_wss() -> Result<()> { let url = crate::test::engine_io_polling_server()?; assert!(builder(url).build_websocket_with_upgrade().is_err()); let host = std::env::var("ENGINE_IO_SECURE_HOST").unwrap_or_else(|_| "localhost".to_owned()); let mut url = crate::test::engine_io_server_secure()?; let mut headers = HeaderMap::default(); headers.insert(HOST, host); let mut builder = builder(url.clone()); builder = builder.tls_config(crate::test::tls_connector()?); builder = builder.headers(headers.clone()); let socket = builder.clone().build_websocket_with_upgrade()?; test_connection(socket)?; let socket = builder.build_websocket()?; test_connection(socket)?; url.set_scheme("wss").unwrap(); let builder = self::builder(url) .tls_config(crate::test::tls_connector()?) .headers(headers); let socket = builder.clone().build_websocket()?; test_connection(socket)?; assert!(builder.build_websocket_with_upgrade().is_err()); Ok(()) } #[test] fn test_connection_ws() -> Result<()> { let url = crate::test::engine_io_polling_server()?; assert!(builder(url.clone()).build_websocket().is_err()); assert!(builder(url).build_websocket_with_upgrade().is_err()); let mut url = crate::test::engine_io_server()?; let builder = builder(url.clone()); let socket = builder.clone().build_websocket()?; test_connection(socket)?; let socket = builder.build_websocket_with_upgrade()?; test_connection(socket)?; url.set_scheme("ws").unwrap(); let builder = self::builder(url); let socket = builder.clone().build_websocket()?; test_connection(socket)?; assert!(builder.build_websocket_with_upgrade().is_err()); Ok(()) } #[test] fn test_open_invariants() -> Result<()> { let url = crate::test::engine_io_server()?; let illegal_url = "this is illegal"; assert!(Url::parse(illegal_url).is_err()); let invalid_protocol = "file:///tmp/foo"; assert!(builder(Url::parse(invalid_protocol).unwrap()) .build() .is_err()); let sut = builder(url.clone()).build()?; let _error = sut .emit(Packet::new(PacketId::Close, Bytes::new())) .expect_err("error"); assert!(matches!(Error::IllegalActionBeforeOpen(), _error)); // test missing match arm in socket constructor let mut headers = HeaderMap::default(); let host = std::env::var("ENGINE_IO_SECURE_HOST").unwrap_or_else(|_| "localhost".to_owned()); headers.insert(HOST, host); let _ = builder(url.clone()) .tls_config( TlsConnector::builder() .danger_accept_invalid_certs(true) .build() .unwrap(), ) .build()?; let _ = builder(url).headers(headers).build()?; Ok(()) } } ================================================ FILE: engineio/src/client/mod.rs ================================================ mod client; pub use client::Iter; pub use {client::Client, client::ClientBuilder, client::Iter as SocketIter}; ================================================ FILE: engineio/src/error.rs ================================================ use base64::DecodeError; use reqwest::Error as ReqwestError; use serde_json::Error as JsonError; use std::io::Error as IoError; use std::str::Utf8Error; use thiserror::Error; use tungstenite::Error as TungsteniteError; use url::ParseError as UrlParseError; /// Enumeration of all possible errors in the `socket.io` context. #[derive(Error, Debug)] #[non_exhaustive] #[cfg_attr(tarpaulin, ignore)] pub enum Error { // Conform to https://rust-lang.github.io/api-guidelines/naming.html#names-use-a-consistent-word-order-c-word-order // Negative verb-object #[error("Invalid packet id: {0}")] InvalidPacketId(u8), #[error("Error while parsing an incomplete packet")] IncompletePacket(), #[error("Got an invalid packet which did not follow the protocol format")] InvalidPacket(), #[error("An error occurred while decoding the utf-8 text: {0}")] InvalidUtf8(#[from] Utf8Error), #[error("An error occurred while encoding/decoding base64: {0}")] InvalidBase64(#[from] DecodeError), #[error("Invalid Url during parsing")] InvalidUrl(#[from] UrlParseError), #[error("Invalid Url Scheme: {0}")] InvalidUrlScheme(String), #[error("Error during connection via http: {0}")] IncompleteResponseFromReqwest(#[from] ReqwestError), #[error("Error with websocket connection: {0}")] WebsocketError(#[from] TungsteniteError), #[error("Network request returned with status code: {0}")] IncompleteHttp(u16), #[error("Got illegal handshake response: {0}")] InvalidHandshake(String), #[error("Called an action before the connection was established")] IllegalActionBeforeOpen(), #[error("Error setting up the http request: {0}")] InvalidHttpConfiguration(#[from] http::Error), #[error("string is not json serializable: {0}")] InvalidJson(#[from] JsonError), #[error("A lock was poisoned")] InvalidPoisonedLock(), #[error("Got an IO-Error: {0}")] IncompleteIo(#[from] IoError), #[error("Server did not allow upgrading to websockets")] IllegalWebsocketUpgrade(), #[error("Invalid header name")] InvalidHeaderNameFromReqwest(#[from] reqwest::header::InvalidHeaderName), #[error("Invalid header value")] InvalidHeaderValueFromReqwest(#[from] reqwest::header::InvalidHeaderValue), #[error("The server did not send a PING packet in time")] PingTimeout(), } pub(crate) type Result = std::result::Result; impl From> for Error { fn from(_: std::sync::PoisonError) -> Self { Self::InvalidPoisonedLock() } } impl From for std::io::Error { fn from(err: Error) -> std::io::Error { std::io::Error::new(std::io::ErrorKind::Other, err) } } #[cfg(test)] mod tests { use std::sync::{Mutex, PoisonError}; use super::*; /// This just tests the own implementations and relies on `thiserror` for the others. #[test] fn test_error_conversion() { let mutex = Mutex::new(0); let _error = Error::from(PoisonError::new(mutex.lock())); assert!(matches!(Error::InvalidPoisonedLock(), _error)); let _io_error = std::io::Error::from(Error::IllegalWebsocketUpgrade()); let _error = std::io::Error::new(std::io::ErrorKind::Other, Error::IllegalWebsocketUpgrade()); assert!(matches!(_io_error, _error)); } } ================================================ FILE: engineio/src/header.rs ================================================ use crate::Error; use bytes::Bytes; use http::{ header::HeaderName as HttpHeaderName, HeaderMap as HttpHeaderMap, HeaderValue as HttpHeaderValue, }; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::str::FromStr; #[derive(Eq, PartialEq, Hash, Debug, Clone)] pub struct HeaderName { inner: Box, } #[derive(Eq, PartialEq, Hash, Debug, Clone)] pub struct HeaderValue { inner: Bytes, } #[derive(Eq, PartialEq, Debug, Clone, Default)] pub struct HeaderMap { map: HashMap, } pub struct IntoIter { inner: std::collections::hash_map::IntoIter, } impl Display for HeaderName { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(self.inner.as_ref()) } } impl From for HeaderName { fn from(string: String) -> Self { HeaderName { inner: string.into_boxed_str(), } } } impl TryFrom for HttpHeaderName { type Error = Error; fn try_from( header: HeaderName, ) -> std::result::Result>::Error> { Ok(HttpHeaderName::from_str(header.inner.as_ref())?) } } impl From for HeaderName { fn from(header: HttpHeaderName) -> Self { HeaderName::from(header.to_string()) } } impl From for HeaderValue { fn from(string: String) -> Self { HeaderValue { inner: Bytes::from(string), } } } impl TryFrom for HttpHeaderValue { type Error = Error; fn try_from( value: HeaderValue, ) -> std::result::Result>::Error> { Ok(HttpHeaderValue::from_bytes(&value.inner[..])?) } } impl From for HeaderValue { fn from(value: HttpHeaderValue) -> Self { HeaderValue { inner: Bytes::copy_from_slice(value.as_bytes()), } } } impl From<&str> for HeaderValue { fn from(string: &str) -> Self { Self::from(string.to_owned()) } } impl TryFrom for HttpHeaderMap { type Error = Error; fn try_from( headers: HeaderMap, ) -> std::result::Result>::Error> { headers .into_iter() .map(|(key, value)| { Ok(( HttpHeaderName::try_from(key)?, HttpHeaderValue::try_from(value)?, )) }) .collect() } } impl IntoIterator for HeaderMap { type Item = (HeaderName, HeaderValue); type IntoIter = IntoIter; fn into_iter(self) -> ::IntoIter { IntoIter { inner: self.map.into_iter(), } } } impl HeaderMap { pub fn new() -> Self { HeaderMap { map: HashMap::new(), } } pub fn insert, U: Into>( &mut self, key: T, value: U, ) -> Option { self.map.insert(key.into(), value.into()) } } impl Iterator for IntoIter { type Item = (HeaderName, HeaderValue); fn next(&mut self) -> std::option::Option<::Item> { self.inner.next() } } ================================================ FILE: engineio/src/lib.rs ================================================ //! # Rust-engineio-client //! //! An implementation of a engine.io client written in the rust programming language. This implementation currently //! supports revision 4 of the engine.io protocol. If you have any connection issues with this client, //! make sure the server uses at least revision 4 of the engine.io protocol. //! //! ## Example usage //! //! ``` rust //! use rust_engineio::{ClientBuilder, Client, packet::{Packet, PacketId}}; //! use url::Url; //! use bytes::Bytes; //! //! // get a client with an `on_open` callback //! let client: Client = ClientBuilder::new(Url::parse("http://localhost:4201").unwrap()) //! .on_open(|_| println!("Connection opened!")) //! .build() //! .expect("Creating client failed"); //! //! // connect to the server //! client.connect().expect("Connection failed"); //! //! // create a packet, in this case a message packet and emit it //! let packet = Packet::new(PacketId::Message, Bytes::from_static(b"Hello World")); //! client.emit(packet).expect("Server unreachable"); //! //! // disconnect from the server //! client.disconnect().expect("Disconnect failed") //! ``` //! //! The main entry point for using this crate is the [`ClientBuilder`] (or [`asynchronous::ClientBuilder`] respectively) //! which provides the opportunity to define how you want to connect to a certain endpoint. //! The following connection methods are available: //! * `build`: Build websocket if allowed, if not fall back to polling. Standard configuration. //! * `build_polling`: enforces a `polling` transport. //! * `build_websocket_with_upgrade`: Build socket with a polling transport then upgrade to websocket transport (if possible). //! * `build_websocket`: Build socket with only a websocket transport, crashes when websockets are not allowed. //! //! //! ## Current features //! //! This implementation now supports all of the features of the engine.io protocol mentioned [here](https://github.com/socketio/engine.io-protocol). //! This includes various transport options, the possibility of sending engine.io packets and registering the //! common engine.io event callbacks: //! * on_open //! * on_close //! * on_data //! * on_error //! * on_packet //! //! It is also possible to pass in custom tls configurations via the `TlsConnector` as well //! as custom headers for the opening request. //! //! ## Async version //! //! The crate also ships with an asynchronous version that can be enabled with a feature flag. //! The async version implements the same features mentioned above. //! The asynchronous version has a similar API, just with async functions. Currently the futures //! can only be executed with [`tokio`](https://tokio.rs). In the first benchmarks the async version //! showed improvements of up to 93% in speed. //! To make use of the async version, import the crate as follows: //! ```toml //! [depencencies] //! rust-engineio = { version = "0.3.1", features = ["async"] } //! ``` //! #![allow(clippy::rc_buffer)] #![warn(clippy::complexity)] #![warn(clippy::style)] #![warn(clippy::perf)] #![warn(clippy::correctness)] /// A small macro that spawns a scoped thread. Used for calling the callback /// functions. macro_rules! spawn_scoped { ($e:expr) => { std::thread::scope(|s| { s.spawn(|| $e); }); }; } pub mod asynchronous; mod callback; pub mod client; /// Generic header map pub mod header; pub mod packet; pub(self) mod socket; pub mod transport; pub mod transports; pub const ENGINE_IO_VERSION: i32 = 4; /// Contains the error type which will be returned with every result in this /// crate. Handles all kinds of errors. pub mod error; pub use client::{Client, ClientBuilder}; pub use error::Error; pub use packet::{Packet, PacketId}; #[cfg(test)] pub(crate) mod test { use super::*; use native_tls::TlsConnector; const CERT_PATH: &str = "../ci/cert/ca.crt"; use native_tls::Certificate; use std::fs::File; use std::io::Read; pub(crate) fn tls_connector() -> error::Result { let cert_path = std::env::var("CA_CERT_PATH").unwrap_or_else(|_| CERT_PATH.to_owned()); let mut cert_file = File::open(cert_path)?; let mut buf = vec![]; cert_file.read_to_end(&mut buf)?; let cert: Certificate = Certificate::from_pem(&buf[..]).unwrap(); Ok(TlsConnector::builder() // ONLY USE FOR TESTING! .danger_accept_invalid_hostnames(true) .add_root_certificate(cert) .build() .unwrap()) } /// The `engine.io` server for testing runs on port 4201 const SERVER_URL: &str = "http://localhost:4201"; /// The `engine.io` server that refuses upgrades runs on port 4203 const SERVER_POLLING_URL: &str = "http://localhost:4203"; const SERVER_URL_SECURE: &str = "https://localhost:4202"; use url::Url; pub(crate) fn engine_io_server() -> crate::error::Result { let url = std::env::var("ENGINE_IO_SERVER").unwrap_or_else(|_| SERVER_URL.to_owned()); Ok(Url::parse(&url)?) } pub(crate) fn engine_io_polling_server() -> crate::error::Result { let url = std::env::var("ENGINE_IO_POLLING_SERVER") .unwrap_or_else(|_| SERVER_POLLING_URL.to_owned()); Ok(Url::parse(&url)?) } pub(crate) fn engine_io_server_secure() -> crate::error::Result { let url = std::env::var("ENGINE_IO_SECURE_SERVER") .unwrap_or_else(|_| SERVER_URL_SECURE.to_owned()); Ok(Url::parse(&url)?) } } ================================================ FILE: engineio/src/packet.rs ================================================ use base64::{engine::general_purpose, Engine as _}; use bytes::{BufMut, Bytes, BytesMut}; use serde::{Deserialize, Serialize}; use std::char; use std::convert::TryFrom; use std::convert::TryInto; use std::fmt::{Display, Formatter, Result as FmtResult, Write}; use std::ops::Index; use crate::error::{Error, Result}; /// Enumeration of the `engine.io` `Packet` types. #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum PacketId { Open, Close, Ping, Pong, Message, // A type of message that is base64 encoded MessageBinary, Upgrade, Noop, } impl PacketId { /// Returns the byte that represents the [`PacketId`] as a [`char`]. fn to_string_byte(self) -> u8 { match self { Self::MessageBinary => b'b', _ => u8::from(self) + b'0', } } } impl Display for PacketId { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_char(self.to_string_byte() as char) } } impl From for u8 { fn from(packet_id: PacketId) -> Self { match packet_id { PacketId::Open => 0, PacketId::Close => 1, PacketId::Ping => 2, PacketId::Pong => 3, PacketId::Message => 4, PacketId::MessageBinary => 4, PacketId::Upgrade => 5, PacketId::Noop => 6, } } } impl TryFrom for PacketId { type Error = Error; /// Converts a byte into the corresponding `PacketId`. fn try_from(b: u8) -> Result { match b { 0 | b'0' => Ok(PacketId::Open), 1 | b'1' => Ok(PacketId::Close), 2 | b'2' => Ok(PacketId::Ping), 3 | b'3' => Ok(PacketId::Pong), 4 | b'4' => Ok(PacketId::Message), 5 | b'5' => Ok(PacketId::Upgrade), 6 | b'6' => Ok(PacketId::Noop), _ => Err(Error::InvalidPacketId(b)), } } } /// A `Packet` sent via the `engine.io` protocol. #[derive(Debug, Clone, Eq, PartialEq)] pub struct Packet { pub packet_id: PacketId, pub data: Bytes, } /// Data which gets exchanged in a handshake as defined by the server. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct HandshakePacket { pub sid: String, pub upgrades: Vec, #[serde(rename = "pingInterval")] pub ping_interval: u64, #[serde(rename = "pingTimeout")] pub ping_timeout: u64, } impl TryFrom for HandshakePacket { type Error = Error; fn try_from(packet: Packet) -> Result { Ok(serde_json::from_slice(packet.data[..].as_ref())?) } } impl Packet { /// Creates a new `Packet`. pub fn new>(packet_id: PacketId, data: T) -> Self { Packet { packet_id, data: data.into(), } } } impl TryFrom for Packet { type Error = Error; /// Decodes a single `Packet` from an `u8` byte stream. fn try_from( bytes: Bytes, ) -> std::result::Result>::Error> { if bytes.is_empty() { return Err(Error::IncompletePacket()); } let is_base64 = *bytes.first().ok_or(Error::IncompletePacket())? == b'b'; // only 'messages' packets could be encoded let packet_id = if is_base64 { PacketId::MessageBinary } else { (*bytes.first().ok_or(Error::IncompletePacket())?).try_into()? }; if bytes.len() == 1 && packet_id == PacketId::Message { return Err(Error::IncompletePacket()); } let data: Bytes = bytes.slice(1..); Ok(Packet { packet_id, data: if is_base64 { Bytes::from(general_purpose::STANDARD.decode(data.as_ref())?) } else { data }, }) } } impl From for Bytes { /// Encodes a `Packet` into an `u8` byte stream. fn from(packet: Packet) -> Self { let mut result = BytesMut::with_capacity(packet.data.len() + 1); result.put_u8(packet.packet_id.to_string_byte()); if packet.packet_id == PacketId::MessageBinary { result.extend(general_purpose::STANDARD.encode(packet.data).into_bytes()); } else { result.put(packet.data); } result.freeze() } } #[derive(Debug, Clone)] pub(crate) struct Payload(Vec); impl Payload { // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text const SEPARATOR: char = '\x1e'; #[cfg(test)] pub fn len(&self) -> usize { self.0.len() } } impl TryFrom for Payload { type Error = Error; /// Decodes a `payload` which in the `engine.io` context means a chain of normal /// packets separated by a certain SEPARATOR, in this case the delimiter `\x30`. fn try_from(payload: Bytes) -> Result { payload .split(|&c| c as char == Self::SEPARATOR) .map(|slice| Packet::try_from(payload.slice_ref(slice))) .collect::>>() .map(Self) } } impl TryFrom for Bytes { type Error = Error; /// Encodes a payload. Payload in the `engine.io` context means a chain of /// normal `packets` separated by a SEPARATOR, in this case the delimiter /// `\x30`. fn try_from(packets: Payload) -> Result { let mut buf = BytesMut::new(); for packet in packets { // at the moment no base64 encoding is used buf.extend(Bytes::from(packet.clone())); buf.put_u8(Payload::SEPARATOR as u8); } // remove the last separator let _ = buf.split_off(buf.len() - 1); Ok(buf.freeze()) } } #[derive(Clone, Debug)] pub struct IntoIter { iter: std::vec::IntoIter, } impl Iterator for IntoIter { type Item = Packet; fn next(&mut self) -> std::option::Option<::Item> { self.iter.next() } } impl IntoIterator for Payload { type Item = Packet; type IntoIter = IntoIter; fn into_iter(self) -> ::IntoIter { IntoIter { iter: self.0.into_iter(), } } } impl Index for Payload { type Output = Packet; fn index(&self, index: usize) -> &Packet { &self.0[index] } } #[cfg(test)] mod tests { use super::*; #[test] fn test_packet_error() { let err = Packet::try_from(BytesMut::with_capacity(10).freeze()); assert!(err.is_err()) } #[test] fn test_is_reflexive() { let data = Bytes::from_static(b"1Hello World"); let packet = Packet::try_from(data).unwrap(); assert_eq!(packet.packet_id, PacketId::Close); assert_eq!(packet.data, Bytes::from_static(b"Hello World")); let data = Bytes::from_static(b"1Hello World"); assert_eq!(Bytes::from(packet), data); } #[test] fn test_binary_packet() { // SGVsbG8= is the encoded string for 'Hello' let data = Bytes::from_static(b"bSGVsbG8="); let packet = Packet::try_from(data.clone()).unwrap(); assert_eq!(packet.packet_id, PacketId::MessageBinary); assert_eq!(packet.data, Bytes::from_static(b"Hello")); assert_eq!(Bytes::from(packet), data); } #[test] fn test_decode_payload() -> Result<()> { let data = Bytes::from_static(b"1Hello\x1e1HelloWorld"); let packets = Payload::try_from(data)?; assert_eq!(packets[0].packet_id, PacketId::Close); assert_eq!(packets[0].data, Bytes::from_static(b"Hello")); assert_eq!(packets[1].packet_id, PacketId::Close); assert_eq!(packets[1].data, Bytes::from_static(b"HelloWorld")); let data = "1Hello\x1e1HelloWorld".to_owned().into_bytes(); assert_eq!(Bytes::try_from(packets).unwrap(), data); Ok(()) } #[test] fn test_binary_payload() { let data = Bytes::from_static(b"bSGVsbG8=\x1ebSGVsbG9Xb3JsZA==\x1ebSGVsbG8="); let packets = Payload::try_from(data.clone()).unwrap(); assert!(packets.len() == 3); assert_eq!(packets[0].packet_id, PacketId::MessageBinary); assert_eq!(packets[0].data, Bytes::from_static(b"Hello")); assert_eq!(packets[1].packet_id, PacketId::MessageBinary); assert_eq!(packets[1].data, Bytes::from_static(b"HelloWorld")); assert_eq!(packets[2].packet_id, PacketId::MessageBinary); assert_eq!(packets[2].data, Bytes::from_static(b"Hello")); assert_eq!(Bytes::try_from(packets).unwrap(), data); } #[test] fn test_packet_id_conversion_and_incompl_packet() -> Result<()> { let sut = Packet::try_from(Bytes::from_static(b"4")); assert!(sut.is_err()); let _sut = sut.unwrap_err(); assert!(matches!(Error::IncompletePacket, _sut)); assert_eq!(PacketId::MessageBinary.to_string(), "b"); let sut = PacketId::try_from(b'0')?; assert_eq!(sut, PacketId::Open); assert_eq!(sut.to_string(), "0"); let sut = PacketId::try_from(b'1')?; assert_eq!(sut, PacketId::Close); assert_eq!(sut.to_string(), "1"); let sut = PacketId::try_from(b'2')?; assert_eq!(sut, PacketId::Ping); assert_eq!(sut.to_string(), "2"); let sut = PacketId::try_from(b'3')?; assert_eq!(sut, PacketId::Pong); assert_eq!(sut.to_string(), "3"); let sut = PacketId::try_from(b'4')?; assert_eq!(sut, PacketId::Message); assert_eq!(sut.to_string(), "4"); let sut = PacketId::try_from(b'5')?; assert_eq!(sut, PacketId::Upgrade); assert_eq!(sut.to_string(), "5"); let sut = PacketId::try_from(b'6')?; assert_eq!(sut, PacketId::Noop); assert_eq!(sut.to_string(), "6"); let sut = PacketId::try_from(42); assert!(sut.is_err()); assert!(matches!(sut.unwrap_err(), Error::InvalidPacketId(42))); Ok(()) } #[test] fn test_handshake_packet() { assert!( HandshakePacket::try_from(Packet::new(PacketId::Message, Bytes::from("test"))).is_err() ); let packet = HandshakePacket { ping_interval: 10000, ping_timeout: 1000, sid: "Test".to_owned(), upgrades: vec!["websocket".to_owned(), "test".to_owned()], }; let encoded: String = serde_json::to_string(&packet).unwrap(); assert_eq!( packet, HandshakePacket::try_from(Packet::new(PacketId::Message, Bytes::from(encoded))) .unwrap() ); } } ================================================ FILE: engineio/src/socket.rs ================================================ use crate::callback::OptionalCallback; use crate::transport::TransportType; use crate::error::{Error, Result}; use crate::packet::{HandshakePacket, Packet, PacketId, Payload}; use bytes::Bytes; use std::convert::TryFrom; use std::sync::RwLock; use std::time::Duration; use std::{fmt::Debug, sync::atomic::Ordering}; use std::{ sync::{atomic::AtomicBool, Arc, Mutex}, time::Instant, }; /// The default maximum ping timeout as calculated from the pingInterval and pingTimeout. /// See https://socket.io/docs/v4/server-options/#pinginterval and /// https://socket.io/docs/v4/server-options/#pingtimeout pub const DEFAULT_MAX_POLL_TIMEOUT: Duration = Duration::from_secs(45); /// An `engine.io` socket which manages a connection with the server and allows /// it to register common callbacks. #[derive(Clone)] pub struct Socket { transport: Arc, on_close: OptionalCallback<()>, on_data: OptionalCallback, on_error: OptionalCallback, on_open: OptionalCallback<()>, on_packet: OptionalCallback, connected: Arc, last_ping: Arc>, last_pong: Arc>, connection_data: Arc, /// Since we get packets in payloads it's possible to have a state where only some of the packets have been consumed. remaining_packets: Arc>>, max_ping_timeout: u64, } impl Socket { pub(crate) fn new( transport: TransportType, handshake: HandshakePacket, on_close: OptionalCallback<()>, on_data: OptionalCallback, on_error: OptionalCallback, on_open: OptionalCallback<()>, on_packet: OptionalCallback, ) -> Self { let max_ping_timeout = handshake.ping_interval + handshake.ping_timeout; Socket { on_close, on_data, on_error, on_open, on_packet, transport: Arc::new(transport), connected: Arc::new(AtomicBool::default()), last_ping: Arc::new(Mutex::new(Instant::now())), last_pong: Arc::new(Mutex::new(Instant::now())), connection_data: Arc::new(handshake), remaining_packets: Arc::new(RwLock::new(None)), max_ping_timeout, } } /// Opens the connection to a specified server. The first Pong packet is sent /// to the server to trigger the Ping-cycle. pub fn connect(&self) -> Result<()> { // SAFETY: Has valid handshake due to type self.connected.store(true, Ordering::Release); if let Some(on_open) = self.on_open.as_ref() { spawn_scoped!(on_open(())); } // set the last ping to now and set the connected state *self.last_ping.lock()? = Instant::now(); // emit a pong packet to keep trigger the ping cycle on the server self.emit(Packet::new(PacketId::Pong, Bytes::new()))?; Ok(()) } pub fn disconnect(&self) -> Result<()> { if let Some(on_close) = self.on_close.as_ref() { spawn_scoped!(on_close(())); } // will not succeed when connection to the server is interrupted let _ = self.emit(Packet::new(PacketId::Close, Bytes::new())); self.connected.store(false, Ordering::Release); Ok(()) } /// Sends a packet to the server. pub fn emit(&self, packet: Packet) -> Result<()> { if !self.connected.load(Ordering::Acquire) { let error = Error::IllegalActionBeforeOpen(); self.call_error_callback(format!("{}", error)); return Err(error); } let is_binary = packet.packet_id == PacketId::MessageBinary; // send a post request with the encoded payload as body // if this is a binary attachment, then send the raw bytes let data: Bytes = if is_binary { packet.data } else { packet.into() }; if let Err(error) = self.transport.as_transport().emit(data, is_binary) { self.call_error_callback(error.to_string()); return Err(error); } Ok(()) } /// Polls for next payload pub(crate) fn poll(&self) -> Result> { loop { if self.connected.load(Ordering::Acquire) { if self.remaining_packets.read()?.is_some() { // SAFETY: checked is some above let mut iter = self.remaining_packets.write()?; let iter = iter.as_mut().unwrap(); if let Some(packet) = iter.next() { return Ok(Some(packet)); } } // Iterator has run out of packets, get a new payload. // Make sure that payload is received within time_to_next_ping, as otherwise the heart // stopped beating and we disconnect. let data = self .transport .as_transport() .poll(Duration::from_millis(self.time_to_next_ping()?))?; if data.is_empty() { continue; } let payload = Payload::try_from(data)?; let mut iter = payload.into_iter(); if let Some(packet) = iter.next() { *self.remaining_packets.write()? = Some(iter); return Ok(Some(packet)); } } else { return Ok(None); } } } /// Calls the error callback with a given message. #[inline] fn call_error_callback(&self, text: String) { if let Some(function) = self.on_error.as_ref() { spawn_scoped!(function(text)); } } // Check if the underlying transport client is connected. pub(crate) fn is_connected(&self) -> Result { Ok(self.connected.load(Ordering::Acquire)) } pub(crate) fn pinged(&self) -> Result<()> { *self.last_ping.lock()? = Instant::now(); Ok(()) } /// Returns the time in milliseconds that is left until a new ping must be received. /// This is used to detect whether we have been disconnected from the server. /// See https://socket.io/docs/v4/how-it-works/#disconnection-detection fn time_to_next_ping(&self) -> Result { match Instant::now().checked_duration_since(*self.last_ping.lock()?) { Some(since_last_ping) => { let since_last_ping = since_last_ping.as_millis() as u64; if since_last_ping > self.max_ping_timeout { Ok(0) } else { Ok(self.max_ping_timeout - since_last_ping) } } None => Ok(0), } } pub(crate) fn handle_packet(&self, packet: Packet) { if let Some(on_packet) = self.on_packet.as_ref() { spawn_scoped!(on_packet(packet)); } } pub(crate) fn handle_data(&self, data: Bytes) { if let Some(on_data) = self.on_data.as_ref() { spawn_scoped!(on_data(data)); } } pub(crate) fn handle_close(&self) { if let Some(on_close) = self.on_close.as_ref() { spawn_scoped!(on_close(())); } self.connected.store(false, Ordering::Release); } } #[cfg_attr(tarpaulin, ignore)] impl Debug for Socket { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( "EngineSocket(transport: {:?}, on_error: {:?}, on_open: {:?}, on_close: {:?}, on_packet: {:?}, on_data: {:?}, connected: {:?}, last_ping: {:?}, last_pong: {:?}, connection_data: {:?})", self.transport, self.on_error, self.on_open, self.on_close, self.on_packet, self.on_data, self.connected, self.last_ping, self.last_pong, self.connection_data, )) } } ================================================ FILE: engineio/src/transport.rs ================================================ use super::transports::{PollingTransport, WebsocketSecureTransport, WebsocketTransport}; use crate::error::Result; use adler32::adler32; use bytes::Bytes; use std::time::{Duration, SystemTime}; use url::Url; pub trait Transport { /// Sends a packet to the server. This optionally handles sending of a /// socketio binary attachment via the boolean attribute `is_binary_att`. fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()>; /// Performs the server long polling procedure as long as the client is /// connected. This should run separately at all time to ensure proper /// response handling from the server. fn poll(&self, timeout: Duration) -> Result; /// Returns start of the url. ex. http://localhost:2998/engine.io/?EIO=4&transport=polling /// Must have EIO and transport already set. fn base_url(&self) -> Result; /// Used to update the base path, like when adding the sid. fn set_base_url(&self, base_url: Url) -> Result<()>; /// Full query address fn address(&self) -> Result { let reader = format!("{:#?}", SystemTime::now()); let hash = adler32(reader.as_bytes()).unwrap(); let mut url = self.base_url()?; url.query_pairs_mut().append_pair("t", &hash.to_string()); Ok(url) } } #[derive(Debug)] pub enum TransportType { Polling(PollingTransport), WebsocketSecure(WebsocketSecureTransport), Websocket(WebsocketTransport), } impl From for TransportType { fn from(transport: PollingTransport) -> Self { TransportType::Polling(transport) } } impl From for TransportType { fn from(transport: WebsocketSecureTransport) -> Self { TransportType::WebsocketSecure(transport) } } impl From for TransportType { fn from(transport: WebsocketTransport) -> Self { TransportType::Websocket(transport) } } impl TransportType { pub fn as_transport(&self) -> &dyn Transport { match self { TransportType::Polling(transport) => transport, TransportType::Websocket(transport) => transport, TransportType::WebsocketSecure(transport) => transport, } } } impl std::fmt::Debug for dyn Transport { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!("Transport(base_url: {:?})", self.base_url(),)) } } ================================================ FILE: engineio/src/transports/mod.rs ================================================ mod polling; mod websocket; mod websocket_secure; pub use self::polling::PollingTransport; pub use self::websocket::WebsocketTransport; pub use self::websocket_secure::WebsocketSecureTransport; ================================================ FILE: engineio/src/transports/polling.rs ================================================ use crate::error::{Error, Result}; use crate::transport::Transport; use base64::{engine::general_purpose, Engine as _}; use bytes::{BufMut, Bytes, BytesMut}; use native_tls::TlsConnector; use reqwest::{ blocking::{Client, ClientBuilder}, header::HeaderMap, }; use std::sync::{Arc, RwLock}; use std::time::Duration; use url::Url; #[derive(Debug, Clone)] pub struct PollingTransport { client: Arc, base_url: Arc>, } impl PollingTransport { /// Creates an instance of `PollingTransport`. pub fn new( base_url: Url, tls_config: Option, opening_headers: Option, ) -> Self { let client = match (tls_config, opening_headers) { (Some(config), Some(map)) => ClientBuilder::new() .use_preconfigured_tls(config) .default_headers(map) .build() .unwrap(), (Some(config), None) => ClientBuilder::new() .use_preconfigured_tls(config) .build() .unwrap(), (None, Some(map)) => ClientBuilder::new().default_headers(map).build().unwrap(), (None, None) => Client::new(), }; let mut url = base_url; url.query_pairs_mut().append_pair("transport", "polling"); PollingTransport { client: Arc::new(client), base_url: Arc::new(RwLock::new(url)), } } } impl Transport for PollingTransport { fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()> { let data_to_send = if is_binary_att { // the binary attachment gets `base64` encoded let mut packet_bytes = BytesMut::with_capacity(data.len() + 1); packet_bytes.put_u8(b'b'); let encoded_data = general_purpose::STANDARD.encode(data); packet_bytes.put(encoded_data.as_bytes()); packet_bytes.freeze() } else { data }; let status = self .client .post(self.address()?) .body(data_to_send) .send()? .status() .as_u16(); if status != 200 { let error = Error::IncompleteHttp(status); return Err(error); } Ok(()) } fn poll(&self, timeout: Duration) -> Result { Ok(self .client .get(self.address()?) .timeout(timeout) .send()? .bytes()?) } fn base_url(&self) -> Result { Ok(self.base_url.read()?.clone()) } fn set_base_url(&self, base_url: Url) -> Result<()> { let mut url = base_url; if !url .query_pairs() .any(|(k, v)| k == "transport" && v == "polling") { url.query_pairs_mut().append_pair("transport", "polling"); } *self.base_url.write()? = url; Ok(()) } } #[cfg(test)] mod test { use super::*; use std::str::FromStr; #[test] fn polling_transport_base_url() -> Result<()> { let url = crate::test::engine_io_server()?.to_string(); let transport = PollingTransport::new(Url::from_str(&url[..]).unwrap(), None, None); assert_eq!( transport.base_url()?.to_string(), url.clone() + "?transport=polling" ); transport.set_base_url(Url::parse("https://127.0.0.1")?)?; assert_eq!( transport.base_url()?.to_string(), "https://127.0.0.1/?transport=polling" ); assert_ne!(transport.base_url()?.to_string(), url); transport.set_base_url(Url::parse("http://127.0.0.1/?transport=polling")?)?; assert_eq!( transport.base_url()?.to_string(), "http://127.0.0.1/?transport=polling" ); assert_ne!(transport.base_url()?.to_string(), url); Ok(()) } #[test] fn transport_debug() -> Result<()> { let mut url = crate::test::engine_io_server()?; let transport = PollingTransport::new(Url::from_str(&url.to_string()[..]).unwrap(), None, None); url.query_pairs_mut().append_pair("transport", "polling"); assert_eq!(format!("PollingTransport {{ client: {:?}, base_url: RwLock {{ data: {:?}, poisoned: false, .. }} }}", transport.client, url), format!("{:?}", transport)); let test: Box = Box::new(transport); assert_eq!( format!("Transport(base_url: Ok({:?}))", url), format!("{:?}", test) ); Ok(()) } } ================================================ FILE: engineio/src/transports/websocket.rs ================================================ use crate::{ asynchronous::{ async_transports::WebsocketTransport as AsyncWebsocketTransport, transport::AsyncTransport, }, error::Result, transport::Transport, Error, }; use bytes::Bytes; use http::HeaderMap; use std::{sync::Arc, time::Duration}; use tokio::runtime::Runtime; use url::Url; #[derive(Clone)] pub struct WebsocketTransport { runtime: Arc, inner: Arc, } impl WebsocketTransport { /// Creates an instance of `WebsocketTransport`. pub fn new(base_url: Url, headers: Option) -> Result { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let inner = runtime.block_on(AsyncWebsocketTransport::new(base_url, headers))?; Ok(WebsocketTransport { runtime: Arc::new(runtime), inner: Arc::new(inner), }) } /// Sends probe packet to ensure connection is valid, then sends upgrade /// request pub(crate) fn upgrade(&self) -> Result<()> { self.runtime.block_on(async { self.inner.upgrade().await }) } } impl Transport for WebsocketTransport { fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()> { self.runtime .block_on(async { self.inner.emit(data, is_binary_att).await }) } fn poll(&self, timeout: Duration) -> Result { self.runtime.block_on(async { let r = match tokio::time::timeout(timeout, self.inner.poll_next()).await { Ok(r) => r, Err(_) => return Err(Error::PingTimeout()), }; match r { Ok(b) => b.ok_or(Error::IncompletePacket()), Err(_) => Err(Error::IncompletePacket()), } }) } fn base_url(&self) -> Result { self.runtime.block_on(async { self.inner.base_url().await }) } fn set_base_url(&self, url: url::Url) -> Result<()> { self.runtime .block_on(async { self.inner.set_base_url(url).await }) } } impl std::fmt::Debug for WebsocketTransport { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( "WebsocketTransport(base_url: {:?})", self.base_url(), )) } } #[cfg(test)] mod test { use super::*; use crate::ENGINE_IO_VERSION; use std::str::FromStr; const TIMEOUT_DURATION: Duration = Duration::from_secs(45); fn new() -> Result { let url = crate::test::engine_io_server()?.to_string() + "engine.io/?EIO=" + &ENGINE_IO_VERSION.to_string(); WebsocketTransport::new(Url::from_str(&url[..])?, None) } #[test] fn websocket_transport_base_url() -> Result<()> { let transport = new()?; let mut url = crate::test::engine_io_server()?; url.set_path("/engine.io/"); url.query_pairs_mut() .append_pair("EIO", &ENGINE_IO_VERSION.to_string()) .append_pair("transport", "websocket"); url.set_scheme("ws").unwrap(); assert_eq!(transport.base_url()?.to_string(), url.to_string()); transport.set_base_url(reqwest::Url::parse("https://127.0.0.1")?)?; assert_eq!( transport.base_url()?.to_string(), "ws://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url()?.to_string(), url.to_string()); transport.set_base_url(reqwest::Url::parse( "http://127.0.0.1/?transport=websocket", )?)?; assert_eq!( transport.base_url()?.to_string(), "ws://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url()?.to_string(), url.to_string()); Ok(()) } #[test] fn websocket_secure_debug() -> Result<()> { let transport = new()?; assert_eq!( format!("{:?}", transport), format!("WebsocketTransport(base_url: {:?})", transport.base_url()) ); println!("{:?}", transport.poll(TIMEOUT_DURATION).unwrap()); println!("{:?}", transport.poll(TIMEOUT_DURATION).unwrap()); Ok(()) } } ================================================ FILE: engineio/src/transports/websocket_secure.rs ================================================ use crate::{ asynchronous::{ async_transports::WebsocketSecureTransport as AsyncWebsocketSecureTransport, transport::AsyncTransport, }, error::Result, transport::Transport, Error, }; use bytes::Bytes; use http::HeaderMap; use native_tls::TlsConnector; use std::{sync::Arc, time::Duration}; use tokio::runtime::Runtime; use url::Url; #[derive(Clone)] pub struct WebsocketSecureTransport { runtime: Arc, inner: Arc, } impl WebsocketSecureTransport { /// Creates an instance of `WebsocketSecureTransport`. pub fn new( base_url: Url, tls_config: Option, headers: Option, ) -> Result { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; let inner = runtime.block_on(AsyncWebsocketSecureTransport::new( base_url, tls_config, headers, ))?; Ok(WebsocketSecureTransport { runtime: Arc::new(runtime), inner: Arc::new(inner), }) } /// Sends probe packet to ensure connection is valid, then sends upgrade /// request pub(crate) fn upgrade(&self) -> Result<()> { self.runtime.block_on(async { self.inner.upgrade().await }) } } impl Transport for WebsocketSecureTransport { fn emit(&self, data: Bytes, is_binary_att: bool) -> Result<()> { self.runtime .block_on(async { self.inner.emit(data, is_binary_att).await }) } fn poll(&self, timeout: Duration) -> Result { self.runtime.block_on(async { let r = match tokio::time::timeout(timeout, self.inner.poll_next()).await { Ok(r) => r, Err(_) => return Err(Error::PingTimeout()), }; match r { Ok(b) => b.ok_or(Error::IncompletePacket()), Err(_) => Err(Error::IncompletePacket()), } }) } fn base_url(&self) -> Result { self.runtime.block_on(async { self.inner.base_url().await }) } fn set_base_url(&self, url: url::Url) -> Result<()> { self.runtime .block_on(async { self.inner.set_base_url(url).await }) } } impl std::fmt::Debug for WebsocketSecureTransport { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( "WebsocketSecureTransport(base_url: {:?})", self.base_url(), )) } } #[cfg(test)] mod test { use super::*; use crate::ENGINE_IO_VERSION; use std::str::FromStr; fn new() -> Result { let url = crate::test::engine_io_server_secure()?.to_string() + "engine.io/?EIO=" + &ENGINE_IO_VERSION.to_string(); WebsocketSecureTransport::new( Url::from_str(&url[..])?, Some(crate::test::tls_connector()?), None, ) } #[test] fn websocket_secure_transport_base_url() -> Result<()> { let transport = new()?; let mut url = crate::test::engine_io_server_secure()?; url.set_path("/engine.io/"); url.query_pairs_mut() .append_pair("EIO", &ENGINE_IO_VERSION.to_string()) .append_pair("transport", "websocket"); url.set_scheme("wss").unwrap(); assert_eq!(transport.base_url()?.to_string(), url.to_string()); transport.set_base_url(reqwest::Url::parse("https://127.0.0.1")?)?; assert_eq!( transport.base_url()?.to_string(), "wss://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url()?.to_string(), url.to_string()); transport.set_base_url(reqwest::Url::parse( "http://127.0.0.1/?transport=websocket", )?)?; assert_eq!( transport.base_url()?.to_string(), "wss://127.0.0.1/?transport=websocket" ); assert_ne!(transport.base_url()?.to_string(), url.to_string()); Ok(()) } #[test] fn websocket_secure_debug() -> Result<()> { let transport = new()?; assert_eq!( format!("{:?}", transport), format!( "WebsocketSecureTransport(base_url: {:?})", transport.base_url() ) ); Ok(()) } } ================================================ FILE: socketio/Cargo.toml ================================================ [package] name = "rust_socketio" version = "0.6.0" authors = ["Bastian Kersting "] edition = "2021" description = "An implementation of a socketio client written in rust." readme = "../README.md" repository = "https://github.com/1c3t3a/rust-socketio" keywords = ["socketio", "engineio", "network", "protocol", "client"] categories = ["network-programming", "web-programming", "web-programming::websocket"] license = "MIT" [package.metadata.docs.rs] all-features = true [dependencies] rust_engineio = { version = "0.6.0", path = "../engineio" } base64 = "0.22.1" bytes = "1" backoff = "0.4" rand = "0.8.5" adler32 = "1.2.0" serde_json = "1.0" thiserror = "1.0" native-tls = "0.2.12" url = "2.5.4" tokio = { version = "1.40.0", optional = true } futures-util = { version = "0.3", default-features = false, features = ["sink"], optional = true } async-stream = { version = "0.3.6", optional = true } log = "0.4.22" serde = "1.0.215" [dev-dependencies] cargo-tarpaulin = "0.18.5" serial_test = "3.0.0" [dev-dependencies.tokio] version = "1.40.0" # we need the `#[tokio::test]` macro features = ["macros", "rt-multi-thread"] [features] default = [] async-callbacks = ["rust_engineio/async-callbacks"] async = ["async-callbacks", "rust_engineio/async", "tokio", "futures-util", "async-stream"] [[example]] name = "async" path = "examples/async.rs" required-features = ["async"] ================================================ FILE: socketio/examples/async.rs ================================================ use futures_util::FutureExt; use rust_socketio::{ asynchronous::{Client, ClientBuilder}, Payload, }; use serde_json::json; use std::time::Duration; #[tokio::main] async fn main() { // define a callback which is called when a payload is received // this callback gets the payload as well as an instance of the // socket to communicate with the server let callback = |payload: Payload, socket: Client| { async move { match payload { Payload::Text(values) => println!("Received: {:#?}", values), Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), // Use Payload::Text instead #[allow(deprecated)] Payload::String(str) => println!("Received: {}", str), } socket .emit("test", json!({"got ack": true})) .await .expect("Server unreachable"); } .boxed() }; // get a socket that is connected to the admin namespace let socket = ClientBuilder::new("http://localhost:4200/") .namespace("/admin") .on("test", callback) .on("error", |err, _| { async move { eprintln!("Error: {:#?}", err) }.boxed() }) .connect() .await .expect("Connection failed"); // emit to the "foo" event let json_payload = json!({"token": 123}); socket .emit("foo", json_payload) .await .expect("Server unreachable"); // define a callback, that's executed when the ack got acked let ack_callback = |message: Payload, _: Client| { async move { println!("Yehaa! My ack got acked?"); println!("Ack data: {:#?}", message); } .boxed() }; let json_payload = json!({"myAckData": 123}); // emit with an ack socket .emit_with_ack("test", json_payload, Duration::from_secs(2), ack_callback) .await .expect("Server unreachable"); socket.disconnect().await.expect("Disconnect failed"); } ================================================ FILE: socketio/examples/callback.rs ================================================ use rust_socketio::{ClientBuilder, Event, Payload, RawClient}; use serde_json::json; fn handle_foo(payload: Payload, socket: RawClient) -> () { socket.emit("bar", payload).expect("Server unreachable") } fn main() { // define a callback which is called when a payload is received // this callback gets the payload as well as an instance of the // socket to communicate with the server let handle_test = |payload: Payload, socket: RawClient| { match payload { Payload::Text(text) => println!("Received json: {:#?}", text), Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), #[allow(deprecated)] // Use Payload::Text instead Payload::String(str) => println!("Received string: {}", str), } socket .emit("test", json!({"got ack": true})) .expect("Server unreachable") }; // get a socket that is connected to the admin namespace let socket = ClientBuilder::new("http://localhost:4200") .namespace("/admin") // Saved closure .on("test", handle_test) // Inline closure .on("error", |err, _| eprintln!("Error: {:#?}", err)) // Function call with signature (payload: Payload, socket: RawClient) -> () .on("foo", handle_foo) // Reserved event names are case insensitive .on("oPeN", |_, _| println!("Connected")) // Custom names are case sensitive .on("Test", |_, _| println!("TesT received")) // Event specified by enum .on(Event::Close, |_, socket| { println!("Socket Closed"); socket .emit("message", json!({"foo": "Hello server"})) .expect("Error emitting"); }) .connect() .expect("Connection failed"); // use the socket socket.disconnect().expect("Disconnect failed") } ================================================ FILE: socketio/examples/readme.rs ================================================ use rust_socketio::{ClientBuilder, Payload, RawClient}; use serde_json::json; use std::time::Duration; fn main() { // define a callback which is called when a payload is received // this callback gets the payload as well as an instance of the // socket to communicate with the server let callback = |payload: Payload, socket: RawClient| { match payload { #[allow(deprecated)] Payload::String(str) => println!("Received: {}", str), Payload::Text(text) => println!("Received json: {:#?}", text), Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), } socket .emit("test", json!({"got ack": true})) .expect("Server unreachable") }; // get a socket that is connected to the admin namespace let socket = ClientBuilder::new("http://localhost:4200") .namespace("/admin") .on("test", callback) .on("error", |err, _| eprintln!("Error: {:#?}", err)) .connect() .expect("Connection failed"); // emit to the "foo" event let json_payload = json!({"token": 123}); socket .emit("foo", json_payload) .expect("Server unreachable"); // define a callback, that's executed when the ack got acked let ack_callback = |message: Payload, _| { println!("Yehaa! My ack got acked?"); println!("Ack data: {:#?}", message); }; let json_payload = json!({"myAckData": 123}); // emit with an ack socket .emit_with_ack("test", json_payload, Duration::from_secs(2), ack_callback) .expect("Server unreachable"); socket.disconnect().expect("Disconnect failed") } ================================================ FILE: socketio/examples/secure.rs ================================================ use native_tls::Certificate; use native_tls::TlsConnector; use rust_socketio::ClientBuilder; use std::fs::File; use std::io::Read; fn main() { // In case a trusted CA is needed that isn't in the trust chain. let cert_path = "ca.crt"; let mut cert_file = File::open(cert_path).expect("Failed to open cert"); let mut buf = vec![]; cert_file .read_to_end(&mut buf) .expect("Failed to read cert"); let cert: Certificate = Certificate::from_pem(&buf[..]).unwrap(); let tls_connector = TlsConnector::builder() .add_root_certificate(cert) .build() .expect("Failed to build TLS Connector"); let socket = ClientBuilder::new("https://localhost:4200") .tls_config(tls_connector) // Not strictly required for HTTPS .opening_header("HOST", "localhost") .on("error", |err, _| eprintln!("Error: {:#?}", err)) .connect() .expect("Connection failed"); // use the socket socket.disconnect().expect("Disconnect failed") } ================================================ FILE: socketio/src/asynchronous/client/ack.rs ================================================ use std::time::Duration; use crate::asynchronous::client::callback::Callback; use tokio::time::Instant; use super::callback::DynAsyncCallback; /// Represents an `Ack` as given back to the caller. Holds the internal `id` as /// well as the current ack'ed state. Holds data which will be accessible as /// soon as the ack'ed state is set to true. An `Ack` that didn't get ack'ed /// won't contain data. #[derive(Debug)] pub(crate) struct Ack { pub id: i32, pub timeout: Duration, pub time_started: Instant, pub callback: Callback, } ================================================ FILE: socketio/src/asynchronous/client/builder.rs ================================================ use futures_util::future::BoxFuture; use log::trace; use native_tls::TlsConnector; use rust_engineio::{ asynchronous::ClientBuilder as EngineIoClientBuilder, header::{HeaderMap, HeaderValue}, }; use std::collections::HashMap; use url::Url; use crate::{error::Result, Event, Payload, TransportType}; use super::{ callback::{ Callback, DynAsyncAnyCallback, DynAsyncCallback, DynAsyncReconnectSettingsCallback, }, client::{Client, ReconnectSettings}, }; use crate::asynchronous::socket::Socket as InnerSocket; /// A builder class for a `socket.io` socket. This handles setting up the client and /// configuring the callback, the namespace and metadata of the socket. If no /// namespace is specified, the default namespace `/` is taken. The `connect` method /// acts the `build` method and returns a connected [`Client`]. pub struct ClientBuilder { pub(crate) address: String, pub(crate) on: HashMap>, pub(crate) on_any: Option>, pub(crate) on_reconnect: Option>, pub(crate) namespace: String, tls_config: Option, pub(crate) opening_headers: Option, transport_type: TransportType, pub(crate) auth: Option, pub(crate) reconnect: bool, pub(crate) reconnect_on_disconnect: bool, // None implies infinite attempts pub(crate) max_reconnect_attempts: Option, pub(crate) reconnect_delay_min: u64, pub(crate) reconnect_delay_max: u64, } impl ClientBuilder { /// Create as client builder from a URL. URLs must be in the form /// `[ws or wss or http or https]://[domain]:[port]/[path]`. The /// path of the URL is optional and if no port is given, port 80 /// will be used. /// # Example /// ```rust /// use rust_socketio::{Payload, asynchronous::{ClientBuilder, Client}}; /// use serde_json::json; /// use futures_util::future::FutureExt; /// /// /// #[tokio::main] /// async fn main() { /// let callback = |payload: Payload, socket: Client| { /// async move { /// match payload { /// Payload::Text(values) => println!("Received: {:#?}", values), /// Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), /// // This is deprecated, use Payload::Text instead /// Payload::String(str) => println!("Received: {}", str), /// } /// }.boxed() /// }; /// /// let mut socket = ClientBuilder::new("http://localhost:4200") /// .namespace("/admin") /// .on("test", callback) /// .connect() /// .await /// .expect("error while connecting"); /// /// // use the socket /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload).await; /// /// assert!(result.is_ok()); /// } /// ``` pub fn new>(address: T) -> Self { Self { address: address.into(), on: HashMap::new(), on_any: None, on_reconnect: None, namespace: "/".to_owned(), tls_config: None, opening_headers: None, transport_type: TransportType::Any, auth: None, reconnect: true, reconnect_on_disconnect: false, // None implies infinite attempts max_reconnect_attempts: None, reconnect_delay_min: 1000, reconnect_delay_max: 5000, } } /// Sets the target namespace of the client. The namespace should start /// with a leading `/`. Valid examples are e.g. `/admin`, `/foo`. /// If the String provided doesn't start with a leading `/`, it is /// added manually. pub fn namespace>(mut self, namespace: T) -> Self { let mut nsp = namespace.into(); if !nsp.starts_with('/') { nsp = "/".to_owned() + &nsp; trace!("Added `/` to the given namespace: {}", nsp); } self.namespace = nsp; self } /// Registers a new callback for a certain [`crate::event::Event`]. The event could either be /// one of the common events like `message`, `error`, `open`, `close` or a custom /// event defined by a string, e.g. `onPayment` or `foo`. /// /// # Example /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder, Payload}; /// use futures_util::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("test", |payload: Payload, _| { /// async move { /// match payload { /// Payload::Text(values) => println!("Received: {:#?}", values), /// Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), /// // This is deprecated, use Payload::Text instead /// Payload::String(str) => println!("Received: {}", str), /// } /// } /// .boxed() /// }) /// .on("error", |err, _| async move { eprintln!("Error: {:#?}", err) }.boxed()) /// .connect() /// .await; /// } /// ``` /// /// # Issues with type inference for the callback method /// /// Currently stable Rust does not contain types like `AsyncFnMut`. /// That is why this library uses the type `FnMut(..) -> BoxFuture<_>`, /// which basically represents a closure or function that returns a /// boxed future that can be executed in an async executor. /// The complicated constraints for the callback function /// bring the Rust compiler to it's limits, resulting in confusing error /// messages when passing in a variable that holds a closure (to the `on` method). /// In order to make sure type inference goes well, the [`futures_util::FutureExt::boxed`] /// method can be used on an async block (the future) to make sure the return type /// is conform with the generic requirements. An example can be found here: /// /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder, Payload}; /// use futures_util::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let callback = |payload: Payload, _| { /// async move { /// match payload { /// Payload::Text(values) => println!("Received: {:#?}", values), /// Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), /// // This is deprecated use Payload::Text instead /// Payload::String(str) => println!("Received: {}", str), /// } /// } /// .boxed() // <-- this makes sure we end up with a `BoxFuture<_>` /// }; /// /// let socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("test", callback) /// .connect() /// .await; /// } /// ``` /// #[cfg(feature = "async-callbacks")] pub fn on, F>(mut self, event: T, callback: F) -> Self where F: for<'a> std::ops::FnMut(Payload, Client) -> BoxFuture<'static, ()> + 'static + Send + Sync, { self.on .insert(event.into(), Callback::::new(callback)); self } /// Registers a callback for reconnect events. The event handler must return /// a [ReconnectSettings] struct with the settings that should be updated. /// /// # Example /// ```rust /// use rust_socketio::{asynchronous::{ClientBuilder, ReconnectSettings}}; /// use futures_util::future::FutureExt; /// use serde_json::json; /// /// #[tokio::main] /// async fn main() { /// let client = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on_reconnect(|| { /// async { /// let mut settings = ReconnectSettings::new(); /// settings.address("http://server?test=123"); /// settings.auth(json!({ "token": "abc" })); /// settings.opening_header("TRAIL", "abc-123"); /// settings /// }.boxed() /// }) /// .connect() /// .await; /// } /// ``` pub fn on_reconnect(mut self, callback: F) -> Self where F: for<'a> std::ops::FnMut() -> BoxFuture<'static, ReconnectSettings> + 'static + Send + Sync, { self.on_reconnect = Some(Callback::::new(callback)); self } /// Registers a Callback for all [`crate::event::Event::Custom`] and [`crate::event::Event::Message`]. /// /// # Example /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder, Payload}; /// use futures_util::future::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let client = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on_any(|event, payload, _client| { /// async { /// if let Payload::String(str) = payload { /// println!("{}: {}", String::from(event), str); /// } /// }.boxed() /// }) /// .connect() /// .await; /// } /// ``` pub fn on_any(mut self, callback: F) -> Self where F: for<'a> FnMut(Event, Payload, Client) -> BoxFuture<'static, ()> + 'static + Send + Sync, { self.on_any = Some(Callback::::new(callback)); self } /// Uses a preconfigured TLS connector for secure communication. This configures /// both the `polling` as well as the `websocket` transport type. /// # Example /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder, Payload}; /// use native_tls::TlsConnector; /// use futures_util::future::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let tls_connector = TlsConnector::builder() /// .use_sni(true) /// .build() /// .expect("Found illegal configuration"); /// /// let socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("error", |err, _| async move { eprintln!("Error: {:#?}", err) }.boxed()) /// .tls_config(tls_connector) /// .connect() /// .await; /// } /// ``` pub fn tls_config(mut self, tls_config: TlsConnector) -> Self { self.tls_config = Some(tls_config); self } /// Sets custom http headers for the opening request. The headers will be passed to the underlying /// transport type (either websockets or polling) and then get passed with every request thats made. /// via the transport layer. /// # Example /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder, Payload}; /// use futures_util::future::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("error", |err, _| async move { eprintln!("Error: {:#?}", err) }.boxed()) /// .opening_header("accept-encoding", "application/json") /// .connect() /// .await; /// } /// ``` pub fn opening_header, K: Into>(mut self, key: K, val: T) -> Self { match self.opening_headers { Some(ref mut map) => { map.insert(key.into(), val.into()); } None => { let mut map = HeaderMap::default(); map.insert(key.into(), val.into()); self.opening_headers = Some(map); } } self } /// Sets authentification data sent in the opening request. /// # Example /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder}; /// use serde_json::json; /// use futures_util::future::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let socket = ClientBuilder::new("http://localhost:4204/") /// .namespace("/admin") /// .auth(json!({ "password": "1337" })) /// .on("error", |err, _| async move { eprintln!("Error: {:#?}", err) }.boxed()) /// .connect() /// .await; /// } /// ``` pub fn auth>(mut self, auth: T) -> Self { self.auth = Some(auth.into()); self } /// Specifies which EngineIO [`TransportType`] to use. /// /// # Example /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder, TransportType}; /// /// #[tokio::main] /// async fn main() { /// let socket = ClientBuilder::new("http://localhost:4200/") /// // Use websockets to handshake and connect. /// .transport_type(TransportType::Websocket) /// .connect() /// .await /// .expect("connection failed"); /// } /// ``` pub fn transport_type(mut self, transport_type: TransportType) -> Self { self.transport_type = transport_type; self } /// If set to `false` do not try to reconnect on network errors. Defaults to /// `true` pub fn reconnect(mut self, reconnect: bool) -> Self { self.reconnect = reconnect; self } /// If set to `true` try to reconnect when the server disconnects the /// client. Defaults to `false` pub fn reconnect_on_disconnect(mut self, reconnect_on_disconnect: bool) -> Self { self.reconnect_on_disconnect = reconnect_on_disconnect; self } /// Sets the minimum and maximum delay between reconnection attempts pub fn reconnect_delay(mut self, min: u64, max: u64) -> Self { self.reconnect_delay_min = min; self.reconnect_delay_max = max; self } /// Sets the maximum number of times to attempt reconnections. Defaults to /// an infinite number of attempts pub fn max_reconnect_attempts(mut self, reconnect_attempts: u8) -> Self { self.max_reconnect_attempts = Some(reconnect_attempts); self } /// Connects the socket to a certain endpoint. This returns a connected /// [`Client`] instance. This method returns an [`std::result::Result::Err`] /// value if something goes wrong during connection. Also starts a separate /// thread to start polling for packets. Used with callbacks. /// # Example /// ```rust /// use rust_socketio::{asynchronous::ClientBuilder, Payload}; /// use serde_json::json; /// use futures_util::future::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("error", |err, _| async move { eprintln!("Error: {:#?}", err) }.boxed()) /// .connect() /// .await /// .expect("connection failed"); /// /// // use the socket /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload).await; /// /// assert!(result.is_ok()); /// } /// ``` pub async fn connect(self) -> Result { let mut socket = self.connect_manual().await?; socket.poll_stream().await?; Ok(socket) } /// Creates a new Socket that can be used for reconnections pub(crate) async fn inner_create(&self) -> Result { let mut url = Url::parse(&self.address)?; if url.path() == "/" { url.set_path("/socket.io/"); } let mut builder = EngineIoClientBuilder::new(url); if let Some(tls_config) = &self.tls_config { builder = builder.tls_config(tls_config.to_owned()); } if let Some(headers) = &self.opening_headers { builder = builder.headers(headers.to_owned()); } let engine_client = match self.transport_type { TransportType::Any => builder.build_with_fallback().await?, TransportType::Polling => builder.build_polling().await?, TransportType::Websocket => builder.build_websocket().await?, TransportType::WebsocketUpgrade => builder.build_websocket_with_upgrade().await?, }; let inner_socket = InnerSocket::new(engine_client)?; Ok(inner_socket) } //TODO: 0.3.X stabilize pub(crate) async fn connect_manual(self) -> Result { let inner_socket = self.inner_create().await?; let socket = Client::new(inner_socket, self)?; socket.connect().await?; Ok(socket) } } ================================================ FILE: socketio/src/asynchronous/client/callback.rs ================================================ use futures_util::future::BoxFuture; use std::{ fmt::Debug, ops::{Deref, DerefMut}, }; use crate::{Event, Payload}; use super::client::{Client, ReconnectSettings}; /// Internal type, provides a way to store futures and return them in a boxed manner. pub(crate) type DynAsyncCallback = Box FnMut(Payload, Client) -> BoxFuture<'static, ()> + 'static + Send + Sync>; pub(crate) type DynAsyncAnyCallback = Box< dyn for<'a> FnMut(Event, Payload, Client) -> BoxFuture<'static, ()> + 'static + Send + Sync, >; pub(crate) type DynAsyncReconnectSettingsCallback = Box FnMut() -> BoxFuture<'static, ReconnectSettings> + 'static + Send + Sync>; pub(crate) struct Callback { inner: T, } impl Debug for Callback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Callback") } } impl Deref for Callback { type Target = dyn for<'a> FnMut(Payload, Client) -> BoxFuture<'static, ()> + 'static + Sync + Send; fn deref(&self) -> &Self::Target { self.inner.as_ref() } } impl DerefMut for Callback { fn deref_mut(&mut self) -> &mut Self::Target { self.inner.as_mut() } } impl Callback { pub(crate) fn new(callback: T) -> Self where T: for<'a> FnMut(Payload, Client) -> BoxFuture<'static, ()> + 'static + Sync + Send, { Callback { inner: Box::new(callback), } } } impl Deref for Callback { type Target = dyn for<'a> FnMut(Event, Payload, Client) -> BoxFuture<'static, ()> + 'static + Sync + Send; fn deref(&self) -> &Self::Target { self.inner.as_ref() } } impl DerefMut for Callback { fn deref_mut(&mut self) -> &mut Self::Target { self.inner.as_mut() } } impl Callback { pub(crate) fn new(callback: T) -> Self where T: for<'a> FnMut(Event, Payload, Client) -> BoxFuture<'static, ()> + 'static + Sync + Send, { Callback { inner: Box::new(callback), } } } impl Deref for Callback { type Target = dyn for<'a> FnMut() -> BoxFuture<'static, ReconnectSettings> + 'static + Sync + Send; fn deref(&self) -> &Self::Target { self.inner.as_ref() } } impl DerefMut for Callback { fn deref_mut(&mut self) -> &mut Self::Target { self.inner.as_mut() } } impl Callback { pub(crate) fn new(callback: T) -> Self where T: for<'a> FnMut() -> BoxFuture<'static, ReconnectSettings> + 'static + Sync + Send, { Callback { inner: Box::new(callback), } } } ================================================ FILE: socketio/src/asynchronous/client/client.rs ================================================ use std::{ops::DerefMut, pin::Pin, sync::Arc}; use backoff::{backoff::Backoff, ExponentialBackoffBuilder}; use futures_util::{future::BoxFuture, stream, Stream, StreamExt}; use log::{error, trace}; use rand::{thread_rng, Rng}; use rust_engineio::header::{HeaderMap, HeaderValue}; use serde_json::Value; use tokio::{ sync::RwLock, time::{sleep, Duration, Instant}, }; use super::{ ack::Ack, builder::ClientBuilder, callback::{Callback, DynAsyncCallback}, }; use crate::{ asynchronous::socket::Socket as InnerSocket, error::{Error, Result}, packet::{Packet, PacketId}, CloseReason, Event, Payload, }; #[derive(Default)] enum DisconnectReason { /// There is no known reason for the disconnect; likely a network error #[default] Unknown, /// The user disconnected manually Manual, /// The server disconnected Server, } /// Settings that can be updated before reconnecting to a server #[derive(Default)] pub struct ReconnectSettings { address: Option, auth: Option, headers: Option, } impl ReconnectSettings { pub fn new() -> Self { Self::default() } /// Sets the URL that will be used when reconnecting to the server pub fn address(&mut self, address: T) -> &mut Self where T: Into, { self.address = Some(address.into()); self } /// Sets the authentication data that will be send in the opening request pub fn auth(&mut self, auth: serde_json::Value) { self.auth = Some(auth); } /// Adds an http header to a container that is going to completely replace opening headers on reconnect. /// If there are no headers set in `ReconnectSettings`, client will use headers initially set via the builder. pub fn opening_header, K: Into>( &mut self, key: K, val: T, ) -> &mut Self { self.headers .get_or_insert_with(|| HeaderMap::default()) .insert(key.into(), val.into()); self } } /// A socket which handles communication with the server. It's initialized with /// a specific address as well as an optional namespace to connect to. If `None` /// is given the client will connect to the default namespace `"/"`. #[derive(Clone)] pub struct Client { /// The inner socket client to delegate the methods to. socket: Arc>, outstanding_acks: Arc>>, // namespace, for multiplexing messages nsp: String, // Data send in the opening packet (commonly used as for auth) auth: Option, builder: Arc>, disconnect_reason: Arc>, } impl Client { /// Creates a socket with a certain address to connect to as well as a /// namespace. If `None` is passed in as namespace, the default namespace /// `"/"` is taken. /// ``` pub(crate) fn new(socket: InnerSocket, builder: ClientBuilder) -> Result { Ok(Client { socket: Arc::new(RwLock::new(socket)), nsp: builder.namespace.to_owned(), outstanding_acks: Arc::new(RwLock::new(Vec::new())), auth: builder.auth.clone(), builder: Arc::new(RwLock::new(builder)), disconnect_reason: Arc::new(RwLock::new(DisconnectReason::default())), }) } /// Connects the client to a server. Afterwards the `emit_*` methods can be /// called to interact with the server. pub(crate) async fn connect(&self) -> Result<()> { // Connect the underlying socket self.socket.read().await.connect().await?; // construct the opening packet let auth = self.auth.as_ref().map(|data| data.to_string()); let open_packet = Packet::new(PacketId::Connect, self.nsp.clone(), auth, None, 0, None); self.socket.read().await.send(open_packet).await?; Ok(()) } pub(crate) async fn reconnect(&mut self) -> Result<()> { let mut builder = self.builder.write().await; if let Some(config) = builder.on_reconnect.as_mut() { let reconnect_settings = config().await; if let Some(address) = reconnect_settings.address { builder.address = address; } if let Some(auth) = reconnect_settings.auth { self.auth = Some(auth); } if reconnect_settings.headers.is_some() { builder.opening_headers = reconnect_settings.headers; } } let socket = builder.inner_create().await?; // New inner socket that can be connected let mut client_socket = self.socket.write().await; *client_socket = socket; // Now that we have replaced `self.socket`, we drop the write lock // because the `connect` method we call below will need to use it drop(client_socket); self.connect().await?; Ok(()) } /// Drives the stream using a thread so messages are processed pub(crate) async fn poll_stream(&mut self) -> Result<()> { let builder = self.builder.read().await; let reconnect_delay_min = builder.reconnect_delay_min; let reconnect_delay_max = builder.reconnect_delay_max; let max_reconnect_attempts = builder.max_reconnect_attempts; let reconnect = builder.reconnect; let reconnect_on_disconnect = builder.reconnect_on_disconnect; drop(builder); let mut client_clone = self.clone(); tokio::runtime::Handle::current().spawn(async move { loop { let mut stream = client_clone.as_stream().await; // Consume the stream until it returns None and the stream is closed. while let Some(item) = stream.next().await { if let Err(e) = item { trace!("Network error occurred: {}", e); } } // Drop the stream so we can once again use `socket_clone` as mutable drop(stream); let should_reconnect = match *(client_clone.disconnect_reason.read().await) { DisconnectReason::Unknown => { // If we disconnected for an unknown reason, the client might not have noticed // the closure yet. Hence, fire a transport close event to notify it. // We don't need to do that in the other cases, since proper server close // and manual client close are handled explicitly. if let Some(err) = client_clone .callback(&Event::Close, CloseReason::TransportClose.as_str()) .await .err() { error!("Error while notifying client of transport close: {err}") } reconnect } DisconnectReason::Manual => false, DisconnectReason::Server => reconnect_on_disconnect, }; if should_reconnect { let mut reconnect_attempts = 0; let mut backoff = ExponentialBackoffBuilder::new() .with_initial_interval(Duration::from_millis(reconnect_delay_min)) .with_max_interval(Duration::from_millis(reconnect_delay_max)) .build(); loop { if let Some(max_reconnect_attempts) = max_reconnect_attempts { reconnect_attempts += 1; if reconnect_attempts > max_reconnect_attempts { trace!("Max reconnect attempts reached without success"); break; } } match client_clone.reconnect().await { Ok(_) => { trace!("Reconnected after {reconnect_attempts} attempts"); break; } Err(e) => { trace!("Failed to reconnect: {e:?}"); if let Some(delay) = backoff.next_backoff() { let delay_ms = delay.as_millis(); trace!("Waiting for {delay_ms}ms before reconnecting"); sleep(delay).await; } } } } } else { break; } } }); Ok(()) } /// Sends a message to the server using the underlying `engine.io` protocol. /// This message takes an event, which could either be one of the common /// events like "message" or "error" or a custom event like "foo". But be /// careful, the data string needs to be valid JSON. It's recommended to use /// a library like `serde_json` to serialize the data properly. /// /// # Example /// ``` /// use rust_socketio::{asynchronous::{ClientBuilder, Client}, Payload}; /// use serde_json::json; /// use futures_util::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("test", |payload: Payload, socket: Client| { /// async move { /// println!("Received: {:#?}", payload); /// socket.emit("test", json!({"hello": true})).await.expect("Server unreachable"); /// }.boxed() /// }) /// .connect() /// .await /// .expect("connection failed"); /// /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload).await; /// /// assert!(result.is_ok()); /// } /// ``` #[inline] pub async fn emit(&self, event: E, data: D) -> Result<()> where E: Into, D: Into, { self.socket .read() .await .emit(&self.nsp, event.into(), data.into()) .await } /// Disconnects this client from the server by sending a `socket.io` closing /// packet. /// # Example /// ```rust /// use rust_socketio::{asynchronous::{ClientBuilder, Client}, Payload}; /// use serde_json::json; /// use futures_util::{FutureExt, future::BoxFuture}; /// /// #[tokio::main] /// async fn main() { /// // apparently the syntax for functions is a bit verbose as rust currently doesn't /// // support an `AsyncFnMut` type that conform with async functions /// fn handle_test(payload: Payload, socket: Client) -> BoxFuture<'static, ()> { /// async move { /// println!("Received: {:#?}", payload); /// socket.emit("test", json!({"hello": true})).await.expect("Server unreachable"); /// }.boxed() /// } /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("test", handle_test) /// .connect() /// .await /// .expect("connection failed"); /// /// let json_payload = json!({"token": 123}); /// /// socket.emit("foo", json_payload).await; /// /// // disconnect from the server /// socket.disconnect().await; /// } /// ``` pub async fn disconnect(&self) -> Result<()> { *(self.disconnect_reason.write().await) = DisconnectReason::Manual; let disconnect_packet = Packet::new(PacketId::Disconnect, self.nsp.clone(), None, None, 0, None); self.socket.read().await.send(disconnect_packet).await?; self.socket.read().await.disconnect().await?; Ok(()) } /// Sends a message to the server but `alloc`s an `ack` to check whether the /// server responded in a given time span. This message takes an event, which /// could either be one of the common events like "message" or "error" or a /// custom event like "foo", as well as a data parameter. But be careful, /// in case you send a [`Payload::String`], the string needs to be valid JSON. /// It's even recommended to use a library like serde_json to serialize the data properly. /// It also requires a timeout `Duration` in which the client needs to answer. /// If the ack is acked in the correct time span, the specified callback is /// called. The callback consumes a [`Payload`] which represents the data send /// by the server. /// /// Please note that the requirements on the provided callbacks are similar to the ones /// for [`crate::asynchronous::ClientBuilder::on`]. /// # Example /// ``` /// use rust_socketio::{asynchronous::{ClientBuilder, Client}, Payload}; /// use serde_json::json; /// use std::time::Duration; /// use std::thread::sleep; /// use futures_util::FutureExt; /// /// #[tokio::main] /// async fn main() { /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("foo", |payload: Payload, _| async move { println!("Received: {:#?}", payload) }.boxed()) /// .connect() /// .await /// .expect("connection failed"); /// /// let ack_callback = |message: Payload, socket: Client| { /// async move { /// match message { /// Payload::Text(values) => println!("{:#?}", values), /// Payload::Binary(bytes) => println!("Received bytes: {:#?}", bytes), /// // This is deprecated use Payload::Text instead /// Payload::String(str) => println!("{}", str), /// } /// }.boxed() /// }; /// /// /// let payload = json!({"token": 123}); /// socket.emit_with_ack("foo", payload, Duration::from_secs(2), ack_callback).await.unwrap(); /// /// sleep(Duration::from_secs(2)); /// } /// ``` #[inline] pub async fn emit_with_ack( &self, event: E, data: D, timeout: Duration, callback: F, ) -> Result<()> where F: for<'a> std::ops::FnMut(Payload, Client) -> BoxFuture<'static, ()> + 'static + Send + Sync, E: Into, D: Into, { let id = thread_rng().gen_range(0..999); let socket_packet = Packet::new_from_payload(data.into(), event.into(), &self.nsp, Some(id))?; let ack = Ack { id, time_started: Instant::now(), timeout, callback: Callback::::new(callback), }; // add the ack to the tuple of outstanding acks self.outstanding_acks.write().await.push(ack); self.socket.read().await.send(socket_packet).await } async fn callback>(&self, event: &Event, payload: P) -> Result<()> { let mut builder = self.builder.write().await; let payload = payload.into(); if let Some(callback) = builder.on.get_mut(event) { callback(payload.clone(), self.clone()).await; } // Call on_any for all common and custom events. match event { Event::Message | Event::Custom(_) => { if let Some(callback) = builder.on_any.as_mut() { callback(event.clone(), payload, self.clone()).await; } } _ => (), } Ok(()) } /// Handles the incoming acks and classifies what callbacks to call and how. #[inline] async fn handle_ack(&self, socket_packet: &Packet) -> Result<()> { let mut to_be_removed = Vec::new(); if let Some(id) = socket_packet.id { for (index, ack) in self.outstanding_acks.write().await.iter_mut().enumerate() { if ack.id == id { to_be_removed.push(index); if ack.time_started.elapsed() < ack.timeout { if let Some(ref payload) = socket_packet.data { ack.callback.deref_mut()( Payload::from(payload.to_owned()), self.clone(), ) .await; } if let Some(ref attachments) = socket_packet.attachments { if let Some(payload) = attachments.get(0) { ack.callback.deref_mut()( Payload::Binary(payload.to_owned()), self.clone(), ) .await; } } } else { trace!("Received an Ack that is now timed out (elapsed time was longer than specified duration)"); } } } for index in to_be_removed { self.outstanding_acks.write().await.remove(index); } } Ok(()) } /// Handles a binary event. #[inline] async fn handle_binary_event(&self, packet: &Packet) -> Result<()> { let event = if let Some(string_data) = &packet.data { string_data.replace('\"', "").into() } else { Event::Message }; if let Some(attachments) = &packet.attachments { if let Some(binary_payload) = attachments.get(0) { self.callback(&event, Payload::Binary(binary_payload.to_owned())) .await?; } } Ok(()) } /// A method that parses a packet and eventually calls the corresponding /// callback with the supplied data. async fn handle_event(&self, packet: &Packet) -> Result<()> { let Some(ref data) = packet.data else { return Ok(()); }; // a socketio message always comes in one of the following two flavors (both JSON): // 1: `["event", "msg", ...]` // 2: `["msg"]` // in case 2, the message is ment for the default message event, in case 1 the event // is specified if let Ok(Value::Array(contents)) = serde_json::from_str::(data) { let (event, payloads) = match contents.len() { 0 => return Err(Error::IncompletePacket()), // Incorrect packet, ignore it 1 => (Event::Message, contents.as_slice()), // it's a message event _ => match contents.first() { Some(Value::String(ev)) => (Event::from(ev.as_str()), &contents[1..]), // get rest(1..) of them as data, not just take the 2nd element _ => (Event::Message, contents.as_slice()), // take them all as data }, }; // call the correct callback self.callback(&event, payloads.to_vec()).await?; } Ok(()) } /// Handles the incoming messages and classifies what callbacks to call and how. /// This method is later registered as the callback for the `on_data` event of the /// engineio client. #[inline] async fn handle_socketio_packet(&self, packet: &Packet) -> Result<()> { if packet.nsp == self.nsp { match packet.packet_type { PacketId::Ack | PacketId::BinaryAck => { if let Err(err) = self.handle_ack(packet).await { self.callback(&Event::Error, err.to_string()).await?; return Err(err); } } PacketId::BinaryEvent => { if let Err(err) = self.handle_binary_event(packet).await { self.callback(&Event::Error, err.to_string()).await?; } } PacketId::Connect => { *(self.disconnect_reason.write().await) = DisconnectReason::default(); self.callback(&Event::Connect, "").await?; } PacketId::Disconnect => { *(self.disconnect_reason.write().await) = DisconnectReason::Server; self.callback(&Event::Close, CloseReason::IOServerDisconnect.as_str()) .await?; } PacketId::ConnectError => { self.callback( &Event::Error, String::from("Received an ConnectError frame: ") + packet .data .as_ref() .unwrap_or(&String::from("\"No error message provided\"")), ) .await?; } PacketId::Event => { if let Err(err) = self.handle_event(packet).await { self.callback(&Event::Error, err.to_string()).await?; } } } } Ok(()) } /// Returns the packet stream for the client. pub(crate) async fn as_stream<'a>( &'a self, ) -> Pin> + Send + 'a>> { let socket_clone = (*self.socket.read().await).clone(); stream::unfold(socket_clone, |mut socket| async { // wait for the next payload let packet: Option> = socket.next().await; match packet { // end the stream if the underlying one is closed None => None, Some(Err(err)) => { // call the error callback match self.callback(&Event::Error, err.to_string()).await { Err(callback_err) => Some((Err(callback_err), socket)), Ok(_) => Some((Err(err), socket)), } } Some(Ok(packet)) => match self.handle_socketio_packet(&packet).await { Err(callback_err) => Some((Err(callback_err), socket)), Ok(_) => Some((Ok(packet), socket)), }, } }) .boxed() } } #[cfg(test)] mod test { use std::{ sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, time::Duration, }; use bytes::Bytes; use futures_util::{FutureExt, StreamExt}; use native_tls::TlsConnector; use serde_json::json; use serial_test::serial; use tokio::{ sync::{mpsc, Mutex}, time::{sleep, timeout}, }; use crate::{ asynchronous::{ client::{builder::ClientBuilder, client::Client}, ReconnectSettings, }, error::Result, packet::{Packet, PacketId}, CloseReason, Event, Payload, TransportType, }; #[tokio::test] async fn socket_io_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .on("test", |msg, _| { async { match msg { Payload::Text(values) => println!("Received json: {:#?}", values), #[allow(deprecated)] Payload::String(str) => println!("Received string: {}", str), Payload::Binary(bin) => println!("Received binary data: {:#?}", bin), } } .boxed() }) .connect() .await?; let payload = json!({"token": 123_i32}); let result = socket.emit("test", Payload::from(payload.clone())).await; assert!(result.is_ok()); let ack = socket .emit_with_ack( "test", Payload::from(payload), Duration::from_secs(1), |message: Payload, socket: Client| { async move { let result = socket .emit("test", Payload::from(json!({"got ack": true}))) .await; assert!(result.is_ok()); println!("Yehaa! My ack got acked?"); if let Payload::Text(json) = message { println!("Received json Ack"); println!("Ack data: {:#?}", json); } } .boxed() }, ) .await; assert!(ack.is_ok()); sleep(Duration::from_secs(2)).await; assert!(socket.disconnect().await.is_ok()); Ok(()) } #[tokio::test] async fn socket_io_async_callback() -> Result<()> { // Test whether asynchronous callbacks are fully executed. let url = crate::test::socket_io_server(); // This synchronization mechanism is used to let the test know that the end of the // async callback was reached. let notify = Arc::new(tokio::sync::Notify::new()); let notify_clone = notify.clone(); let socket = ClientBuilder::new(url) .on("test", move |_, _| { let cl = notify_clone.clone(); async move { sleep(Duration::from_secs(1)).await; // The async callback should be awaited and not aborted. // Thus, the notification should be called. cl.notify_one(); } .boxed() }) .connect() .await?; let payload = json!({"token": 123_i32}); let result = socket.emit("test", Payload::from(payload)).await; assert!(result.is_ok()); // If the timeout did not trigger, the async callback was fully executed. let timeout = timeout(Duration::from_secs(5), notify.notified()).await; assert!(timeout.is_ok()); Ok(()) } #[tokio::test] async fn socket_io_builder_integration() -> Result<()> { let url = crate::test::socket_io_server(); // test socket build logic let socket_builder = ClientBuilder::new(url); let tls_connector = TlsConnector::builder() .use_sni(true) .build() .expect("Found illegal configuration"); let socket = socket_builder .namespace("/admin") .tls_config(tls_connector) .opening_header("accept-encoding", "application/json") .on("test", |str, _| { async move { println!("Received: {:#?}", str) }.boxed() }) .on("message", |payload, _| { async move { println!("{:#?}", payload) }.boxed() }) .connect() .await?; assert!(socket.emit("message", json!("Hello World")).await.is_ok()); assert!(socket .emit("binary", Bytes::from_static(&[46, 88])) .await .is_ok()); assert!(socket .emit_with_ack( "binary", json!("pls ack"), Duration::from_secs(1), |payload, _| async move { println!("Yehaa the ack got acked"); println!("With data: {:#?}", payload); } .boxed() ) .await .is_ok()); sleep(Duration::from_secs(2)).await; Ok(()) } #[tokio::test] #[serial(reconnect)] async fn socket_io_reconnect_integration() -> Result<()> { static CONNECT_NUM: AtomicUsize = AtomicUsize::new(0); static MESSAGE_NUM: AtomicUsize = AtomicUsize::new(0); static ON_RECONNECT_CALLED: AtomicUsize = AtomicUsize::new(0); let latest_message = Arc::new(Mutex::new(String::new())); let handler_latest_message = latest_message.clone(); let url = crate::test::socket_io_restart_server(); let socket = ClientBuilder::new(url.clone()) .reconnect(true) .max_reconnect_attempts(100) .reconnect_delay(100, 100) .on_reconnect(move || { let url = url.clone(); async move { ON_RECONNECT_CALLED.fetch_add(1, Ordering::Release); let mut settings = ReconnectSettings::new(); // Try setting the address to what we already have, just // to test. This is not strictly necessary in real usage. settings.address(url.to_string()); settings.opening_header("MESSAGE_BACK", "updated"); settings } .boxed() }) .on("open", |_, socket| { async move { CONNECT_NUM.fetch_add(1, Ordering::Release); let r = socket.emit_with_ack( "message", json!(""), Duration::from_millis(100), |_, _| async move {}.boxed(), ); assert!(r.await.is_ok(), "should emit message success"); } .boxed() }) .on("message", move |payload, _socket| { let latest_message = handler_latest_message.clone(); async move { // test the iterator implementation and make sure there is a constant // stream of packets, even when reconnecting MESSAGE_NUM.fetch_add(1, Ordering::Release); let msg = match payload { Payload::Text(msg) => msg .into_iter() .next() .expect("there should be one text payload"), _ => panic!(), }; let msg = serde_json::from_value(msg).expect("payload should be json string"); *latest_message.lock().await = msg; } .boxed() }) .connect() .await; assert!(socket.is_ok(), "should connect success"); let socket = socket.unwrap(); // waiting for server to emit message sleep(Duration::from_millis(500)).await; assert_eq!(load(&CONNECT_NUM), 1, "should connect once"); assert_eq!(load(&MESSAGE_NUM), 1, "should receive one"); assert_eq!( *latest_message.lock().await, "test", "should receive test message" ); let r = socket.emit("restart_server", json!("")).await; assert!(r.is_ok(), "should emit restart success"); // waiting for server to restart for _ in 0..10 { sleep(Duration::from_millis(400)).await; if load(&CONNECT_NUM) == 2 && load(&MESSAGE_NUM) == 2 { break; } } assert_eq!(load(&CONNECT_NUM), 2, "should connect twice"); assert_eq!(load(&MESSAGE_NUM), 2, "should receive two messages"); assert!( load(&ON_RECONNECT_CALLED) > 1, "should call on_reconnect at least once" ); assert_eq!( *latest_message.lock().await, "updated", "should receive updated message" ); socket.disconnect().await?; Ok(()) } #[tokio::test] async fn socket_io_builder_integration_iterator() -> Result<()> { let url = crate::test::socket_io_server(); // test socket build logic let socket_builder = ClientBuilder::new(url); let tls_connector = TlsConnector::builder() .use_sni(true) .build() .expect("Found illegal configuration"); let socket = socket_builder .namespace("/admin") .tls_config(tls_connector) .opening_header("accept-encoding", "application/json") .on("test", |str, _| { async move { println!("Received: {:#?}", str) }.boxed() }) .on("message", |payload, _| { async move { println!("{:#?}", payload) }.boxed() }) .connect_manual() .await?; assert!(socket.emit("message", json!("Hello World")).await.is_ok()); assert!(socket .emit("binary", Bytes::from_static(&[46, 88])) .await .is_ok()); assert!(socket .emit_with_ack( "binary", json!("pls ack"), Duration::from_secs(1), |payload, _| async move { println!("Yehaa the ack got acked"); println!("With data: {:#?}", payload); } .boxed() ) .await .is_ok()); test_socketio_socket(socket, "/admin".to_owned()).await } #[tokio::test] async fn socket_io_on_any_integration() -> Result<()> { let url = crate::test::socket_io_server(); let (tx, mut rx) = mpsc::channel(2); let mut _socket = ClientBuilder::new(url) .namespace("/") .auth(json!({ "password": "123" })) .on_any(move |event, payload, _| { let clone_tx = tx.clone(); async move { if let Payload::Text(values) = payload { println!("{event}: {values:#?}"); } clone_tx.send(String::from(event)).await.unwrap(); } .boxed() }) .connect() .await?; let event = rx.recv().await.unwrap(); assert_eq!(event, "message"); let event = rx.recv().await.unwrap(); assert_eq!(event, "test"); Ok(()) } #[tokio::test] async fn socket_io_auth_builder_integration() -> Result<()> { let url = crate::test::socket_io_auth_server(); let nsp = String::from("/admin"); let socket = ClientBuilder::new(url) .namespace(nsp.clone()) .auth(json!({ "password": "123" })) .connect_manual() .await?; // open packet let mut socket_stream = socket.as_stream().await; let _ = socket_stream.next().await.unwrap()?; let packet = socket_stream.next().await.unwrap()?; assert_eq!( packet, Packet::new( PacketId::Event, nsp, Some("[\"auth\",\"success\"]".to_owned()), None, 0, None ) ); Ok(()) } #[tokio::test] async fn socket_io_transport_close() -> Result<()> { let url = crate::test::socket_io_server(); let (tx, mut rx) = mpsc::channel(1); let notify = Arc::new(tokio::sync::Notify::new()); let notify_clone = notify.clone(); let socket = ClientBuilder::new(url) .on(Event::Connect, move |_, _| { let cl = notify_clone.clone(); async move { cl.notify_one(); } .boxed() }) .on(Event::Close, move |payload, _| { let clone_tx = tx.clone(); async move { clone_tx.send(payload).await.unwrap() }.boxed() }) .connect() .await?; // Wait until socket is connected let connect_timeout = timeout(Duration::from_secs(1), notify.notified()).await; assert!(connect_timeout.is_ok()); // Instruct server to close transport let result = socket.emit("close_transport", Payload::from("")).await; assert!(result.is_ok()); // Wait for Event::Close let rx_timeout = timeout(Duration::from_secs(1), rx.recv()).await; assert!(rx_timeout.is_ok()); assert_eq!( rx_timeout.unwrap(), Some(Payload::from(CloseReason::TransportClose.as_str())) ); Ok(()) } #[tokio::test] async fn socketio_polling_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url.clone()) .transport_type(TransportType::Polling) .connect_manual() .await?; test_socketio_socket(socket, "/".to_owned()).await } #[tokio::test] async fn socket_io_websocket_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url.clone()) .transport_type(TransportType::Websocket) .connect_manual() .await?; test_socketio_socket(socket, "/".to_owned()).await } #[tokio::test] async fn socket_io_websocket_upgrade_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .transport_type(TransportType::WebsocketUpgrade) .connect_manual() .await?; test_socketio_socket(socket, "/".to_owned()).await } #[tokio::test] async fn socket_io_any_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .transport_type(TransportType::Any) .connect_manual() .await?; test_socketio_socket(socket, "/".to_owned()).await } async fn test_socketio_socket(socket: Client, nsp: String) -> Result<()> { // open packet let mut socket_stream = socket.as_stream().await; let _: Option = Some(socket_stream.next().await.unwrap()?); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some("[\"Hello from the message event!\"]".to_owned()), None, 0, None, ) ); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some("[\"test\",\"Hello from the test event!\"]".to_owned()), None, 0, None ) ); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::BinaryEvent, nsp.clone(), None, None, 1, Some(vec![Bytes::from_static(&[4, 5, 6])]), ) ); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::BinaryEvent, nsp.clone(), Some("\"test\"".to_owned()), None, 1, Some(vec![Bytes::from_static(&[1, 2, 3])]), ) ); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some( serde_json::Value::Array(vec![ serde_json::Value::from("This is the first argument"), serde_json::Value::from("This is the second argument"), serde_json::json!({"argCount":3}) ]) .to_string() ), None, 0, None, ) ); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some( serde_json::json!([ "on_abc_event", "", { "abc": 0, "some_other": "value", } ]) .to_string() ), None, 0, None, ) ); let cb = |message: Payload, _| { async { println!("Yehaa! My ack got acked?"); if let Payload::Text(values) = message { println!("Received json ack"); println!("Ack data: {:#?}", values); } } .boxed() }; assert!(socket .emit_with_ack( "test", Payload::from("123".to_owned()), Duration::from_secs(10), cb ) .await .is_ok()); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some("[\"test-received\",123]".to_owned()), None, 0, None, ) ); let packet: Option = Some(socket_stream.next().await.unwrap()?); assert!(packet.is_some()); let packet = packet.unwrap(); assert!(matches!( packet, Packet { packet_type: PacketId::Ack, nsp: _, data: Some(_), id: Some(_), attachment_count: 0, attachments: None, } )); Ok(()) } fn load(num: &AtomicUsize) -> usize { num.load(Ordering::Acquire) } } ================================================ FILE: socketio/src/asynchronous/client/mod.rs ================================================ mod ack; pub(crate) mod builder; #[cfg(feature = "async-callbacks")] mod callback; pub(crate) mod client; ================================================ FILE: socketio/src/asynchronous/generator.rs ================================================ use std::{pin::Pin, sync::Arc}; use crate::error::Result; use futures_util::{ready, FutureExt, Stream, StreamExt}; use tokio::sync::Mutex; /// A handy type alias for a pinned + boxed Stream trait object that iterates /// over object of a certain type `T`. pub(crate) type Generator = Pin + 'static + Send>>; /// An internal type that implements stream by repeatedly calling [`Stream::poll_next`] on an /// underlying stream. Note that the generic parameter will be wrapped in a [`Result`]. #[derive(Clone)] pub(crate) struct StreamGenerator { inner: Arc>>>, } impl Stream for StreamGenerator { type Item = Result; fn poll_next( self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { let mut lock = ready!(Box::pin(self.inner.lock()).poll_unpin(cx)); lock.poll_next_unpin(cx) } } impl StreamGenerator { pub(crate) fn new(generator_stream: Generator>) -> Self { StreamGenerator { inner: Arc::new(Mutex::new(generator_stream)), } } } ================================================ FILE: socketio/src/asynchronous/mod.rs ================================================ mod client; mod generator; mod socket; #[cfg(feature = "async")] pub use client::builder::ClientBuilder; pub use client::client::{Client, ReconnectSettings}; // re-export the macro pub use crate::{async_any_callback, async_callback}; #[doc = r#" A macro to wrap an async callback function to be used in the client. This macro is used to wrap a callback function that can handle a specific event. ```rust use rust_socketio::async_callback; use rust_socketio::asynchronous::{Client, ClientBuilder}; use rust_socketio::{Event, Payload}; pub async fn callback(payload: Payload, client: Client) {} #[tokio::main] async fn main() { let socket = ClientBuilder::new("http://example.com") .on("message", async_callback!(callback)) .connect() .await; } ``` "#] #[macro_export] macro_rules! async_callback { ($f:expr) => {{ use futures_util::FutureExt; |payload: Payload, client: Client| $f(payload, client).boxed() }}; } #[doc = r#" A macro to wrap an async callback function to be used in the client. This macro is used to wrap a callback function that can handle any event. ```rust use rust_socketio::async_any_callback; use rust_socketio::asynchronous::{Client, ClientBuilder}; use rust_socketio::{Event, Payload}; pub async fn callback_any(event: Event, payload: Payload, client: Client) {} #[tokio::main] async fn main() { let socket = ClientBuilder::new("http://example.com") .on_any(async_any_callback!(callback_any)) .connect() .await; } ``` "#] #[macro_export] macro_rules! async_any_callback { ($f:expr) => {{ use futures_util::FutureExt; |event: Event, payload: Payload, client: Client| $f(event, payload, client).boxed() }}; } ================================================ FILE: socketio/src/asynchronous/socket.rs ================================================ use super::generator::StreamGenerator; use crate::{ error::Result, packet::{Packet, PacketId}, Error, Event, Payload, }; use async_stream::try_stream; use bytes::Bytes; use futures_util::{Stream, StreamExt}; use rust_engineio::{ asynchronous::Client as EngineClient, Packet as EnginePacket, PacketId as EnginePacketId, }; use std::{ fmt::Debug, pin::Pin, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, }; #[derive(Clone)] pub(crate) struct Socket { engine_client: Arc, connected: Arc, generator: StreamGenerator, } impl Socket { /// Creates an instance of `Socket`. pub(super) fn new(engine_client: EngineClient) -> Result { let connected = Arc::new(AtomicBool::default()); Ok(Socket { engine_client: Arc::new(engine_client.clone()), connected: connected.clone(), generator: StreamGenerator::new(Self::stream(engine_client, connected)), }) } /// Connects to the server. This includes a connection of the underlying /// engine.io client and afterwards an opening socket.io request. pub async fn connect(&self) -> Result<()> { self.engine_client.connect().await?; // store the connected value as true, if the connection process fails // later, the value will be updated self.connected.store(true, Ordering::Release); Ok(()) } /// Disconnects from the server by sending a socket.io `Disconnect` packet. This results /// in the underlying engine.io transport to get closed as well. pub async fn disconnect(&self) -> Result<()> { if self.is_engineio_connected() { self.engine_client.disconnect().await?; } if self.connected.load(Ordering::Acquire) { self.connected.store(false, Ordering::Release); } Ok(()) } /// Sends a `socket.io` packet to the server using the `engine.io` client. pub async fn send(&self, packet: Packet) -> Result<()> { if !self.is_engineio_connected() || !self.connected.load(Ordering::Acquire) { return Err(Error::IllegalActionBeforeOpen()); } // the packet, encoded as an engine.io message packet let engine_packet = EnginePacket::new(EnginePacketId::Message, Bytes::from(&packet)); self.engine_client.emit(engine_packet).await?; if let Some(attachments) = packet.attachments { for attachment in attachments { let engine_packet = EnginePacket::new(EnginePacketId::MessageBinary, attachment); self.engine_client.emit(engine_packet).await?; } } Ok(()) } /// Emits to certain event with given data. The data needs to be JSON, /// otherwise this returns an `InvalidJson` error. pub async fn emit(&self, nsp: &str, event: Event, data: Payload) -> Result<()> { let socket_packet = Packet::new_from_payload(data, event, nsp, None)?; self.send(socket_packet).await } fn stream( client: EngineClient, is_connected: Arc, ) -> Pin> + Send>> { Box::pin(try_stream! { for await received_data in client.clone() { let packet = received_data?; if packet.packet_id == EnginePacketId::Message || packet.packet_id == EnginePacketId::MessageBinary { let packet = Self::handle_engineio_packet(packet, client.clone()).await?; Self::handle_socketio_packet(&packet, is_connected.clone()); yield packet; } } }) } /// Handles the connection/disconnection. #[inline] fn handle_socketio_packet(socket_packet: &Packet, is_connected: Arc) { match socket_packet.packet_type { PacketId::Connect => { is_connected.store(true, Ordering::Release); } PacketId::ConnectError => { is_connected.store(false, Ordering::Release); } PacketId::Disconnect => { is_connected.store(false, Ordering::Release); } _ => (), } } /// Handles new incoming engineio packets async fn handle_engineio_packet( packet: EnginePacket, mut client: EngineClient, ) -> Result { let mut socket_packet = Packet::try_from(&packet.data)?; // Only handle attachments if there are any if socket_packet.attachment_count > 0 { let mut attachments_left = socket_packet.attachment_count; let mut attachments = Vec::new(); while attachments_left > 0 { // TODO: This is not nice! Find a different way to peek the next element while mapping the stream let next = client.next().await.unwrap(); match next { Err(err) => return Err(err.into()), Ok(packet) => match packet.packet_id { EnginePacketId::MessageBinary | EnginePacketId::Message => { attachments.push(packet.data); attachments_left -= 1; } _ => { return Err(Error::InvalidAttachmentPacketType( packet.packet_id.into(), )); } }, } } socket_packet.attachments = Some(attachments); } Ok(socket_packet) } fn is_engineio_connected(&self) -> bool { self.engine_client.is_connected() } } impl Stream for Socket { type Item = Result; fn poll_next( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.generator.poll_next_unpin(cx) } } impl Debug for Socket { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Socket") .field("engine_client", &self.engine_client) .field("connected", &self.connected) .finish() } } ================================================ FILE: socketio/src/client/builder.rs ================================================ use super::super::{event::Event, payload::Payload}; use super::callback::Callback; use super::client::Client; use crate::RawClient; use native_tls::TlsConnector; use rust_engineio::client::ClientBuilder as EngineIoClientBuilder; use rust_engineio::header::{HeaderMap, HeaderValue}; use url::Url; use crate::client::callback::{SocketAnyCallback, SocketCallback}; use crate::error::Result; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use crate::socket::Socket as InnerSocket; /// Flavor of Engine.IO transport. #[derive(Clone, Eq, PartialEq)] pub enum TransportType { /// Handshakes with polling, upgrades if possible Any, /// Handshakes with websocket. Does not use polling. Websocket, /// Handshakes with polling, errors if upgrade fails WebsocketUpgrade, /// Handshakes with polling Polling, } /// A builder class for a `socket.io` socket. This handles setting up the client and /// configuring the callback, the namespace and metadata of the socket. If no /// namespace is specified, the default namespace `/` is taken. The `connect` method /// acts the `build` method and returns a connected [`Client`]. #[derive(Clone)] pub struct ClientBuilder { pub(crate) address: String, on: Arc>>>, on_any: Arc>>>, namespace: String, tls_config: Option, opening_headers: Option, transport_type: TransportType, auth: Option, pub(crate) reconnect: bool, pub(crate) reconnect_on_disconnect: bool, // None reconnect attempts represent infinity. pub(crate) max_reconnect_attempts: Option, pub(crate) reconnect_delay_min: u64, pub(crate) reconnect_delay_max: u64, } impl ClientBuilder { /// Create as client builder from a URL. URLs must be in the form /// `[ws or wss or http or https]://[domain]:[port]/[path]`. The /// path of the URL is optional and if no port is given, port 80 /// will be used. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload, RawClient}; /// use serde_json::json; /// /// /// let callback = |payload: Payload, socket: RawClient| { /// match payload { /// Payload::Text(values) => println!("Received: {:#?}", values), /// Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), /// // This payload type is deprecated, use Payload::Text instead /// Payload::String(str) => println!("Received: {}", str), /// } /// }; /// /// let mut socket = ClientBuilder::new("http://localhost:4200") /// .namespace("/admin") /// .on("test", callback) /// .connect() /// .expect("error while connecting"); /// /// // use the socket /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload); /// /// assert!(result.is_ok()); /// ``` pub fn new>(address: T) -> Self { Self { address: address.into(), on: Arc::new(Mutex::new(HashMap::new())), on_any: Arc::new(Mutex::new(None)), namespace: "/".to_owned(), tls_config: None, opening_headers: None, transport_type: TransportType::Any, auth: None, reconnect: true, reconnect_on_disconnect: false, // None means infinity max_reconnect_attempts: None, reconnect_delay_min: 1000, reconnect_delay_max: 5000, } } /// Sets the target namespace of the client. The namespace should start /// with a leading `/`. Valid examples are e.g. `/admin`, `/foo`. pub fn namespace>(mut self, namespace: T) -> Self { let mut nsp = namespace.into(); if !nsp.starts_with('/') { nsp = "/".to_owned() + &nsp; } self.namespace = nsp; self } pub fn reconnect(mut self, reconnect: bool) -> Self { self.reconnect = reconnect; self } /// If set to `true` automatically set try to reconnect when the server /// disconnects the client. /// Defaults to `false`. /// /// # Example /// ```rust /// use rust_socketio::ClientBuilder; /// /// let socket = ClientBuilder::new("http://localhost:4200/") /// .reconnect_on_disconnect(true) /// .connect(); /// ``` pub fn reconnect_on_disconnect(mut self, reconnect_on_disconnect: bool) -> Self { self.reconnect_on_disconnect = reconnect_on_disconnect; self } pub fn reconnect_delay(mut self, min: u64, max: u64) -> Self { self.reconnect_delay_min = min; self.reconnect_delay_max = max; self } pub fn max_reconnect_attempts(mut self, reconnect_attempts: u8) -> Self { self.max_reconnect_attempts = Some(reconnect_attempts); self } /// Registers a new callback for a certain [`crate::event::Event`]. The event could either be /// one of the common events like `message`, `error`, `open`, `close` or a custom /// event defined by a string, e.g. `onPayment` or `foo`. /// /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload}; /// /// let socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("test", |payload: Payload, _| { /// match payload { /// Payload::Text(values) => println!("Received: {:#?}", values), /// Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), /// // This payload type is deprecated, use Payload::Text instead /// Payload::String(str) => println!("Received: {}", str), /// } /// }) /// .on("error", |err, _| eprintln!("Error: {:#?}", err)) /// .connect(); /// /// ``` // While present implementation doesn't require mut, it's reasonable to require mutability. #[allow(unused_mut)] pub fn on, F>(mut self, event: T, callback: F) -> Self where F: FnMut(Payload, RawClient) + 'static + Send, { let callback = Callback::::new(callback); // SAFETY: Lock is held for such amount of time no code paths lead to a panic while lock is held self.on.lock().unwrap().insert(event.into(), callback); self } /// Registers a Callback for all [`crate::event::Event::Custom`] and [`crate::event::Event::Message`]. /// /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload}; /// /// let client = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on_any(|event, payload, _client| { /// if let Payload::String(str) = payload { /// println!("{} {}", String::from(event), str); /// } /// }) /// .connect(); /// /// ``` // While present implementation doesn't require mut, it's reasonable to require mutability. #[allow(unused_mut)] pub fn on_any(mut self, callback: F) -> Self where F: FnMut(Event, Payload, RawClient) + 'static + Send, { let callback = Some(Callback::::new(callback)); // SAFETY: Lock is held for such amount of time no code paths lead to a panic while lock is held *self.on_any.lock().unwrap() = callback; self } /// Uses a preconfigured TLS connector for secure communication. This configures /// both the `polling` as well as the `websocket` transport type. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload}; /// use native_tls::TlsConnector; /// /// let tls_connector = TlsConnector::builder() /// .use_sni(true) /// .build() /// .expect("Found illegal configuration"); /// /// let socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("error", |err, _| eprintln!("Error: {:#?}", err)) /// .tls_config(tls_connector) /// .connect(); /// /// ``` pub fn tls_config(mut self, tls_config: TlsConnector) -> Self { self.tls_config = Some(tls_config); self } /// Sets custom http headers for the opening request. The headers will be passed to the underlying /// transport type (either websockets or polling) and then get passed with every request thats made. /// via the transport layer. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload}; /// /// /// let socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("error", |err, _| eprintln!("Error: {:#?}", err)) /// .opening_header("accept-encoding", "application/json") /// .connect(); /// /// ``` pub fn opening_header, K: Into>(mut self, key: K, val: T) -> Self { match self.opening_headers { Some(ref mut map) => { map.insert(key.into(), val.into()); } None => { let mut map = HeaderMap::default(); map.insert(key.into(), val.into()); self.opening_headers = Some(map); } } self } /// Sets data sent in the opening request. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder}; /// use serde_json::json; /// /// let socket = ClientBuilder::new("http://localhost:4204/") /// .namespace("/admin") /// .auth(json!({ "password": "1337" })) /// .on("error", |err, _| eprintln!("Error: {:#?}", err)) /// .connect() /// .expect("Connection error"); /// /// ``` pub fn auth(mut self, auth: serde_json::Value) -> Self { self.auth = Some(auth); self } /// Specifies which EngineIO [`TransportType`] to use. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, TransportType}; /// use serde_json::json; /// /// let socket = ClientBuilder::new("http://localhost:4200/") /// // Use websockets to handshake and connect. /// .transport_type(TransportType::Websocket) /// .connect() /// .expect("connection failed"); /// /// // use the socket /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload); /// /// assert!(result.is_ok()); /// ``` pub fn transport_type(mut self, transport_type: TransportType) -> Self { self.transport_type = transport_type; self } /// Connects the socket to a certain endpoint. This returns a connected /// [`Client`] instance. This method returns an [`std::result::Result::Err`] /// value if something goes wrong during connection. Also starts a separate /// thread to start polling for packets. Used with callbacks. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload}; /// use serde_json::json; /// /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .namespace("/admin") /// .on("error", |err, _| eprintln!("Client error!: {:#?}", err)) /// .connect() /// .expect("connection failed"); /// /// // use the socket /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload); /// /// assert!(result.is_ok()); /// ``` pub fn connect(self) -> Result { Client::new(self) } pub fn connect_raw(self) -> Result { // Parse url here rather than in new to keep new returning Self. let mut url = Url::parse(&self.address)?; if url.path() == "/" { url.set_path("/socket.io/"); } let mut builder = EngineIoClientBuilder::new(url); if let Some(tls_config) = self.tls_config { builder = builder.tls_config(tls_config); } if let Some(headers) = self.opening_headers { builder = builder.headers(headers); } let engine_client = match self.transport_type { TransportType::Any => builder.build_with_fallback()?, TransportType::Polling => builder.build_polling()?, TransportType::Websocket => builder.build_websocket()?, TransportType::WebsocketUpgrade => builder.build_websocket_with_upgrade()?, }; let inner_socket = InnerSocket::new(engine_client)?; let socket = RawClient::new( inner_socket, &self.namespace, self.on, self.on_any, self.auth, )?; socket.connect()?; Ok(socket) } } ================================================ FILE: socketio/src/client/callback.rs ================================================ use std::{ fmt::Debug, ops::{Deref, DerefMut}, }; use super::RawClient; use crate::{Event, Payload}; pub(crate) type SocketCallback = Box; pub(crate) type SocketAnyCallback = Box; pub(crate) struct Callback { inner: T, } // SocketCallback implementations impl Debug for Callback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Callback") } } impl Deref for Callback { type Target = dyn FnMut(Payload, RawClient) + 'static + Send; fn deref(&self) -> &Self::Target { self.inner.as_ref() } } impl DerefMut for Callback { fn deref_mut(&mut self) -> &mut Self::Target { self.inner.as_mut() } } impl Callback { pub(crate) fn new(callback: T) -> Self where T: FnMut(Payload, RawClient) + 'static + Send, { Callback { inner: Box::new(callback), } } } // SocketAnyCallback implementations impl Debug for Callback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Callback") } } impl Deref for Callback { type Target = dyn FnMut(Event, Payload, RawClient) + 'static + Send; fn deref(&self) -> &Self::Target { self.inner.as_ref() } } impl DerefMut for Callback { fn deref_mut(&mut self) -> &mut Self::Target { self.inner.as_mut() } } impl Callback { pub(crate) fn new(callback: T) -> Self where T: FnMut(Event, Payload, RawClient) + 'static + Send, { Callback { inner: Box::new(callback), } } } ================================================ FILE: socketio/src/client/client.rs ================================================ use std::{ sync::{Arc, Mutex, RwLock}, time::Duration, }; use super::{ClientBuilder, RawClient}; use crate::{ error::Result, packet::{Packet, PacketId}, Error, }; pub(crate) use crate::{event::Event, payload::Payload}; use backoff::ExponentialBackoff; use backoff::{backoff::Backoff, ExponentialBackoffBuilder}; #[derive(Clone)] pub struct Client { builder: Arc>, client: Arc>, backoff: ExponentialBackoff, } impl Client { pub(crate) fn new(builder: ClientBuilder) -> Result { let builder_clone = builder.clone(); let client = builder_clone.connect_raw()?; let backoff = ExponentialBackoffBuilder::new() .with_initial_interval(Duration::from_millis(builder.reconnect_delay_min)) .with_max_interval(Duration::from_millis(builder.reconnect_delay_max)) .build(); let s = Self { builder: Arc::new(Mutex::new(builder)), client: Arc::new(RwLock::new(client)), backoff, }; s.poll_callback(); Ok(s) } /// Updates the URL the client will connect to when reconnecting. /// This is especially useful for updating query parameters. pub fn set_reconnect_url>(&self, address: T) -> Result<()> { self.builder.lock()?.address = address.into(); Ok(()) } /// Sends a message to the server using the underlying `engine.io` protocol. /// This message takes an event, which could either be one of the common /// events like "message" or "error" or a custom event like "foo". But be /// careful, the data string needs to be valid JSON. It's recommended to use /// a library like `serde_json` to serialize the data properly. /// /// # Example /// ``` /// use rust_socketio::{ClientBuilder, RawClient, Payload}; /// use serde_json::json; /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("test", |payload: Payload, socket: RawClient| { /// println!("Received: {:#?}", payload); /// socket.emit("test", json!({"hello": true})).expect("Server unreachable"); /// }) /// .connect() /// .expect("connection failed"); /// /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload); /// /// assert!(result.is_ok()); /// ``` pub fn emit(&self, event: E, data: D) -> Result<()> where E: Into, D: Into, { let client = self.client.read()?; // TODO(#230): like js client, buffer emit, resend after reconnect client.emit(event, data) } /// Sends a message to the server but `alloc`s an `ack` to check whether the /// server responded in a given time span. This message takes an event, which /// could either be one of the common events like "message" or "error" or a /// custom event like "foo", as well as a data parameter. But be careful, /// in case you send a [`Payload::String`], the string needs to be valid JSON. /// It's even recommended to use a library like serde_json to serialize the data properly. /// It also requires a timeout `Duration` in which the client needs to answer. /// If the ack is acked in the correct time span, the specified callback is /// called. The callback consumes a [`Payload`] which represents the data send /// by the server. /// /// # Example /// ``` /// use rust_socketio::{ClientBuilder, Payload, RawClient}; /// use serde_json::json; /// use std::time::Duration; /// use std::thread::sleep; /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("foo", |payload: Payload, _| println!("Received: {:#?}", payload)) /// .connect() /// .expect("connection failed"); /// /// let ack_callback = |message: Payload, socket: RawClient| { /// match message { /// Payload::Text(values) => println!("{:#?}", values), /// Payload::Binary(bytes) => println!("Received bytes: {:#?}", bytes), /// // This is deprecated, use Payload::Text instead. /// Payload::String(str) => println!("{}", str), /// } /// }; /// /// let payload = json!({"token": 123}); /// socket.emit_with_ack("foo", payload, Duration::from_secs(2), ack_callback).unwrap(); /// /// sleep(Duration::from_secs(2)); /// ``` pub fn emit_with_ack( &self, event: E, data: D, timeout: Duration, callback: F, ) -> Result<()> where F: FnMut(Payload, RawClient) + 'static + Send, E: Into, D: Into, { let client = self.client.read()?; // TODO(#230): like js client, buffer emit, resend after reconnect client.emit_with_ack(event, data, timeout, callback) } /// Disconnects this client from the server by sending a `socket.io` closing /// packet. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload, RawClient}; /// use serde_json::json; /// /// fn handle_test(payload: Payload, socket: RawClient) { /// println!("Received: {:#?}", payload); /// socket.emit("test", json!({"hello": true})).expect("Server unreachable"); /// } /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("test", handle_test) /// .connect() /// .expect("connection failed"); /// /// let json_payload = json!({"token": 123}); /// /// socket.emit("foo", json_payload); /// /// // disconnect from the server /// socket.disconnect(); /// /// ``` pub fn disconnect(&self) -> Result<()> { let client = self.client.read()?; client.disconnect() } fn reconnect(&mut self) -> Result<()> { let mut reconnect_attempts = 0; let (reconnect, max_reconnect_attempts) = { let builder = self.builder.lock()?; (builder.reconnect, builder.max_reconnect_attempts) }; if reconnect { loop { if let Some(max_reconnect_attempts) = max_reconnect_attempts { reconnect_attempts += 1; if reconnect_attempts > max_reconnect_attempts { break; } } if let Some(backoff) = self.backoff.next_backoff() { std::thread::sleep(backoff); } if self.do_reconnect().is_ok() { break; } } } Ok(()) } fn do_reconnect(&self) -> Result<()> { let builder = self.builder.lock()?; let new_client = builder.clone().connect_raw()?; let mut client = self.client.write()?; *client = new_client; Ok(()) } pub(crate) fn iter(&self) -> Iter { Iter { socket: self.client.clone(), } } fn poll_callback(&self) { let mut self_clone = self.clone(); // Use thread to consume items in iterator in order to call callbacks std::thread::spawn(move || { // tries to restart a poll cycle whenever a 'normal' error occurs, // it just panics on network errors, in case the poll cycle returned // `Result::Ok`, the server receives a close frame so it's safe to // terminate for packet in self_clone.iter() { let should_reconnect = match packet { Err(Error::IncompleteResponseFromEngineIo(_)) => { //TODO: 0.3.X handle errors //TODO: logging error true } Ok(Packet { packet_type: PacketId::Disconnect, .. }) => match self_clone.builder.lock() { Ok(builder) => builder.reconnect_on_disconnect, Err(_) => false, }, _ => false, }; if should_reconnect { let _ = self_clone.disconnect(); let _ = self_clone.reconnect(); } } }); } } pub(crate) struct Iter { socket: Arc>, } impl Iterator for Iter { type Item = Result; fn next(&mut self) -> Option { let socket = self.socket.read(); match socket { Ok(socket) => match socket.poll() { Err(err) => Some(Err(err)), Ok(Some(packet)) => Some(Ok(packet)), // If the underlying engineIO connection is closed, // throw an error so we know to reconnect Ok(None) => Some(Err(Error::StoppedEngineIoSocket)), }, Err(_) => { // Lock is poisoned, our iterator is useless. None } } } } #[cfg(test)] mod test { use std::{ sync::atomic::{AtomicUsize, Ordering}, time::UNIX_EPOCH, }; use super::*; use crate::error::Result; use crate::ClientBuilder; use serde_json::json; use serial_test::serial; use std::time::{Duration, SystemTime}; use url::Url; #[test] #[serial(reconnect)] fn socket_io_reconnect_integration() -> Result<()> { static CONNECT_NUM: AtomicUsize = AtomicUsize::new(0); static CLOSE_NUM: AtomicUsize = AtomicUsize::new(0); static MESSAGE_NUM: AtomicUsize = AtomicUsize::new(0); let url = crate::test::socket_io_restart_server(); let socket = ClientBuilder::new(url) .reconnect(true) .max_reconnect_attempts(100) .reconnect_delay(100, 100) .on(Event::Connect, move |_, socket| { CONNECT_NUM.fetch_add(1, Ordering::Release); let r = socket.emit_with_ack( "message", json!(""), Duration::from_millis(100), |_, _| {}, ); assert!(r.is_ok(), "should emit message success"); }) .on(Event::Close, move |_, _| { CLOSE_NUM.fetch_add(1, Ordering::Release); }) .on("message", move |_, _socket| { // test the iterator implementation and make sure there is a constant // stream of packets, even when reconnecting MESSAGE_NUM.fetch_add(1, Ordering::Release); }) .connect(); assert!(socket.is_ok(), "should connect success"); let socket = socket.unwrap(); // waiting for server to emit message std::thread::sleep(std::time::Duration::from_millis(500)); assert_eq!(load(&CONNECT_NUM), 1, "should connect once"); assert_eq!(load(&MESSAGE_NUM), 1, "should receive one"); assert_eq!(load(&CLOSE_NUM), 0, "should not close"); let r = socket.emit("restart_server", json!("")); assert!(r.is_ok(), "should emit restart success"); // waiting for server to restart for _ in 0..10 { std::thread::sleep(std::time::Duration::from_millis(400)); if load(&CONNECT_NUM) == 2 && load(&MESSAGE_NUM) == 2 { break; } } assert_eq!(load(&CONNECT_NUM), 2, "should connect twice"); assert_eq!(load(&MESSAGE_NUM), 2, "should receive two messages"); assert_eq!(load(&CLOSE_NUM), 1, "should close once"); socket.disconnect()?; Ok(()) } #[test] fn socket_io_reconnect_url_auth_integration() -> Result<()> { static CONNECT_NUM: AtomicUsize = AtomicUsize::new(0); static CLOSE_NUM: AtomicUsize = AtomicUsize::new(0); static MESSAGE_NUM: AtomicUsize = AtomicUsize::new(0); fn get_url() -> Url { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(); let mut url = crate::test::socket_io_restart_url_auth_server(); url.set_query(Some(&format!("timestamp={timestamp}"))); url } let socket = ClientBuilder::new(get_url()) .reconnect(true) .max_reconnect_attempts(100) .reconnect_delay(100, 100) .on(Event::Connect, move |_, socket| { CONNECT_NUM.fetch_add(1, Ordering::Release); let result = socket.emit_with_ack( "message", json!(""), Duration::from_millis(100), |_, _| {}, ); assert!(result.is_ok(), "should emit message success"); }) .on(Event::Close, move |_, _| { CLOSE_NUM.fetch_add(1, Ordering::Release); }) .on("message", move |_, _| { // test the iterator implementation and make sure there is a constant // stream of packets, even when reconnecting MESSAGE_NUM.fetch_add(1, Ordering::Release); }) .connect(); assert!(socket.is_ok(), "should connect success"); let socket = socket.unwrap(); // waiting for server to emit message std::thread::sleep(std::time::Duration::from_millis(500)); assert_eq!(load(&CONNECT_NUM), 1, "should connect once"); assert_eq!(load(&MESSAGE_NUM), 1, "should receive one"); assert_eq!(load(&CLOSE_NUM), 0, "should not close"); // waiting for timestamp in url to expire std::thread::sleep(std::time::Duration::from_secs(1)); socket.set_reconnect_url(get_url())?; let result = socket.emit("restart_server", json!("")); assert!(result.is_ok(), "should emit restart success"); // waiting for server to restart for _ in 0..10 { std::thread::sleep(std::time::Duration::from_millis(400)); if load(&CONNECT_NUM) == 2 && load(&MESSAGE_NUM) == 2 { break; } } assert_eq!(load(&CONNECT_NUM), 2, "should connect twice"); assert_eq!(load(&MESSAGE_NUM), 2, "should receive two messages"); assert_eq!(load(&CLOSE_NUM), 1, "should close once"); socket.disconnect()?; Ok(()) } #[test] fn socket_io_iterator_integration() -> Result<()> { let url = crate::test::socket_io_server(); let builder = ClientBuilder::new(url); let builder_clone = builder.clone(); let client = Arc::new(RwLock::new(builder_clone.connect_raw()?)); let mut socket = Client { builder: Arc::new(Mutex::new(builder)), client, backoff: Default::default(), }; let socket_clone = socket.clone(); let packets: Arc>> = Default::default(); let packets_clone = packets.clone(); std::thread::spawn(move || { for packet in socket_clone.iter() { { let mut packets = packets_clone.write().unwrap(); if let Ok(packet) = packet { (*packets).push(packet); } } } }); // waiting for client to emit messages std::thread::sleep(Duration::from_millis(100)); let lock = packets.read().unwrap(); let pre_num = lock.len(); drop(lock); let _ = socket.disconnect(); socket.reconnect()?; // waiting for client to emit messages std::thread::sleep(Duration::from_millis(100)); let lock = packets.read().unwrap(); let post_num = lock.len(); drop(lock); assert!( pre_num < post_num, "pre_num {} should less than post_num {}", pre_num, post_num ); Ok(()) } fn load(num: &AtomicUsize) -> usize { num.load(Ordering::Acquire) } } ================================================ FILE: socketio/src/client/mod.rs ================================================ mod builder; mod raw_client; pub use builder::ClientBuilder; pub use builder::TransportType; pub use client::Client; pub use raw_client::RawClient; /// Internal callback type mod callback; mod client; ================================================ FILE: socketio/src/client/raw_client.rs ================================================ use super::callback::Callback; use crate::packet::{Packet, PacketId}; use crate::Error; pub(crate) use crate::{event::CloseReason, event::Event, payload::Payload}; use rand::{thread_rng, Rng}; use serde_json::Value; use crate::client::callback::{SocketAnyCallback, SocketCallback}; use crate::error::Result; use std::collections::HashMap; use std::ops::DerefMut; use std::sync::{Arc, Mutex}; use std::time::Duration; use std::time::Instant; use crate::socket::Socket as InnerSocket; /// Represents an `Ack` as given back to the caller. Holds the internal `id` as /// well as the current ack'ed state. Holds data which will be accessible as /// soon as the ack'ed state is set to true. An `Ack` that didn't get ack'ed /// won't contain data. #[derive(Debug)] pub struct Ack { pub id: i32, timeout: Duration, time_started: Instant, callback: Callback, } /// A socket which handles communication with the server. It's initialized with /// a specific address as well as an optional namespace to connect to. If `None` /// is given the server will connect to the default namespace `"/"`. #[derive(Clone)] pub struct RawClient { /// The inner socket client to delegate the methods to. socket: InnerSocket, on: Arc>>>, on_any: Arc>>>, outstanding_acks: Arc>>, // namespace, for multiplexing messages nsp: String, // Data send in the opening packet (commonly used as for auth) auth: Option, } impl RawClient { /// Creates a socket with a certain address to connect to as well as a /// namespace. If `None` is passed in as namespace, the default namespace /// `"/"` is taken. /// ``` pub(crate) fn new>( socket: InnerSocket, namespace: T, on: Arc>>>, on_any: Arc>>>, auth: Option, ) -> Result { Ok(RawClient { socket, nsp: namespace.into(), on, on_any, outstanding_acks: Arc::new(Mutex::new(Vec::new())), auth, }) } /// Connects the client to a server. Afterwards the `emit_*` methods can be /// called to interact with the server. Attention: it's not allowed to add a /// callback after a call to this method. pub(crate) fn connect(&self) -> Result<()> { // Connect the underlying socket self.socket.connect()?; let auth = self.auth.as_ref().map(|data| data.to_string()); // construct the opening packet let open_packet = Packet::new(PacketId::Connect, self.nsp.clone(), auth, None, 0, None); self.socket.send(open_packet)?; Ok(()) } /// Sends a message to the server using the underlying `engine.io` protocol. /// This message takes an event, which could either be one of the common /// events like "message" or "error" or a custom event like "foo". But be /// careful, the data string needs to be valid JSON. It's recommended to use /// a library like `serde_json` to serialize the data properly. /// /// # Example /// ``` /// use rust_socketio::{ClientBuilder, RawClient, Payload}; /// use serde_json::json; /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("test", |payload: Payload, socket: RawClient| { /// println!("Received: {:#?}", payload); /// socket.emit("test", json!({"hello": true})).expect("Server unreachable"); /// }) /// .connect() /// .expect("connection failed"); /// /// let json_payload = json!({"token": 123}); /// /// let result = socket.emit("foo", json_payload); /// /// assert!(result.is_ok()); /// ``` #[inline] pub fn emit(&self, event: E, data: D) -> Result<()> where E: Into, D: Into, { self.socket.emit(&self.nsp, event.into(), data.into()) } /// Disconnects this client from the server by sending a `socket.io` closing /// packet. /// # Example /// ```rust /// use rust_socketio::{ClientBuilder, Payload, RawClient}; /// use serde_json::json; /// /// fn handle_test(payload: Payload, socket: RawClient) { /// println!("Received: {:#?}", payload); /// socket.emit("test", json!({"hello": true})).expect("Server unreachable"); /// } /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("test", handle_test) /// .connect() /// .expect("connection failed"); /// /// let json_payload = json!({"token": 123}); /// /// socket.emit("foo", json_payload); /// /// // disconnect from the server /// socket.disconnect(); /// /// ``` pub fn disconnect(&self) -> Result<()> { let disconnect_packet = Packet::new(PacketId::Disconnect, self.nsp.clone(), None, None, 0, None); // TODO: logging let _ = self.socket.send(disconnect_packet); self.socket.disconnect()?; let _ = self.callback(&Event::Close, CloseReason::IOClientDisconnect.as_str()); // trigger on_close Ok(()) } /// Sends a message to the server but `alloc`s an `ack` to check whether the /// server responded in a given time span. This message takes an event, which /// could either be one of the common events like "message" or "error" or a /// custom event like "foo", as well as a data parameter. But be careful, /// in case you send a [`Payload::String`], the string needs to be valid JSON. /// It's even recommended to use a library like serde_json to serialize the data properly. /// It also requires a timeout `Duration` in which the client needs to answer. /// If the ack is acked in the correct time span, the specified callback is /// called. The callback consumes a [`Payload`] which represents the data send /// by the server. /// /// # Example /// ``` /// use rust_socketio::{ClientBuilder, Payload, RawClient}; /// use serde_json::json; /// use std::time::Duration; /// use std::thread::sleep; /// /// let mut socket = ClientBuilder::new("http://localhost:4200/") /// .on("foo", |payload: Payload, _| println!("Received: {:#?}", payload)) /// .connect() /// .expect("connection failed"); /// /// let ack_callback = |message: Payload, socket: RawClient| { /// match message { /// Payload::Text(values) => println!("{:#?}", values), /// Payload::Binary(bytes) => println!("Received bytes: {:#?}", bytes), /// // This is deprecated, use Payload::Text instead /// Payload::String(str) => println!("{}", str), /// } /// }; /// /// let payload = json!({"token": 123}); /// socket.emit_with_ack("foo", payload, Duration::from_secs(2), ack_callback).unwrap(); /// /// sleep(Duration::from_secs(2)); /// ``` #[inline] pub fn emit_with_ack( &self, event: E, data: D, timeout: Duration, callback: F, ) -> Result<()> where F: FnMut(Payload, RawClient) + 'static + Send, E: Into, D: Into, { let id = thread_rng().gen_range(0..999); let socket_packet = Packet::new_from_payload(data.into(), event.into(), &self.nsp, Some(id))?; let ack = Ack { id, time_started: Instant::now(), timeout, callback: Callback::::new(callback), }; // add the ack to the tuple of outstanding acks self.outstanding_acks.lock()?.push(ack); self.socket.send(socket_packet)?; Ok(()) } pub(crate) fn poll(&self) -> Result> { loop { match self.socket.poll() { Err(err) => { self.callback(&Event::Error, err.to_string())?; return Err(err); } Ok(Some(packet)) => { if packet.nsp == self.nsp { self.handle_socketio_packet(&packet)?; return Ok(Some(packet)); } else { // Not our namespace continue polling } } Ok(None) => return Ok(None), } } } #[cfg(test)] pub(crate) fn iter(&self) -> Iter { Iter { socket: self } } fn callback>(&self, event: &Event, payload: P) -> Result<()> { let mut on = self.on.lock()?; let mut on_any = self.on_any.lock()?; let lock = on.deref_mut(); let on_any_lock = on_any.deref_mut(); let payload = payload.into(); if let Some(callback) = lock.get_mut(event) { callback(payload.clone(), self.clone()); } match event { Event::Message | Event::Custom(_) => { if let Some(callback) = on_any_lock { callback(event.clone(), payload, self.clone()) } } _ => {} } drop(on); drop(on_any); Ok(()) } /// Handles the incoming acks and classifies what callbacks to call and how. #[inline] fn handle_ack(&self, socket_packet: &Packet) -> Result<()> { let Some(id) = socket_packet.id else { return Ok(()); }; let outstanding_ack = { let mut outstanding_acks = self.outstanding_acks.lock()?; outstanding_acks .iter() .position(|ack| ack.id == id) .map(|pos| outstanding_acks.remove(pos)) }; // If we found a matching ack, call its callback otherwise ignore it. // The official implementation just removes the ack id on timeout: // https://github.com/socketio/socket.io-client/blob/main/lib/socket.ts#L467-L495 if let Some(mut ack) = outstanding_ack { if ack.time_started.elapsed() < ack.timeout { if let Some(ref payload) = socket_packet.data { ack.callback.deref_mut()(Payload::from(payload.to_owned()), self.clone()); } if let Some(ref attachments) = socket_packet.attachments { if let Some(payload) = attachments.first() { ack.callback.deref_mut()(Payload::Binary(payload.to_owned()), self.clone()); } } } } Ok(()) } /// Handles a binary event. #[inline] fn handle_binary_event(&self, packet: &Packet) -> Result<()> { let event = if let Some(string_data) = &packet.data { string_data.replace('\"', "").into() } else { Event::Message }; if let Some(attachments) = &packet.attachments { if let Some(binary_payload) = attachments.first() { self.callback(&event, Payload::Binary(binary_payload.to_owned()))?; } } Ok(()) } /// A method that parses a packet and eventually calls the corresponding /// callback with the supplied data. fn handle_event(&self, packet: &Packet) -> Result<()> { let Some(ref data) = packet.data else { return Ok(()); }; // a socketio message always comes in one of the following two flavors (both JSON): // 1: `["event", "msg", ...]` // 2: `["msg"]` // in case 2, the message is ment for the default message event, in case 1 the event // is specified if let Ok(Value::Array(contents)) = serde_json::from_str::(data) { let (event, payloads) = match contents.len() { 0 => return Err(Error::IncompletePacket()), 1 => (Event::Message, contents.as_slice()), // get rest of data if first is a event _ => match contents.first() { Some(Value::String(ev)) => (Event::from(ev.as_str()), &contents[1..]), // get rest(1..) of them as data, not just take the 2nd element _ => (Event::Message, contents.as_slice()), // take them all as data }, }; // call the correct callback self.callback(&event, payloads.to_vec())?; } Ok(()) } /// Handles the incoming messages and classifies what callbacks to call and how. /// This method is later registered as the callback for the `on_data` event of the /// engineio client. #[inline] fn handle_socketio_packet(&self, packet: &Packet) -> Result<()> { if packet.nsp == self.nsp { match packet.packet_type { PacketId::Ack | PacketId::BinaryAck => { if let Err(err) = self.handle_ack(packet) { self.callback(&Event::Error, err.to_string())?; return Err(err); } } PacketId::BinaryEvent => { if let Err(err) = self.handle_binary_event(packet) { self.callback(&Event::Error, err.to_string())?; } } PacketId::Connect => { self.callback(&Event::Connect, "")?; } PacketId::Disconnect => { self.callback(&Event::Close, CloseReason::IOServerDisconnect.as_str())?; } PacketId::ConnectError => { self.callback( &Event::Error, String::from("Received an ConnectError frame: ") + &packet .clone() .data .unwrap_or_else(|| String::from("\"No error message provided\"")), )?; } PacketId::Event => { if let Err(err) = self.handle_event(packet) { self.callback(&Event::Error, err.to_string())?; } } } } Ok(()) } } pub struct Iter<'a> { socket: &'a RawClient, } impl<'a> Iterator for Iter<'a> { type Item = Result; fn next(&mut self) -> std::option::Option<::Item> { match self.socket.poll() { Err(err) => Some(Err(err)), Ok(Some(packet)) => Some(Ok(packet)), Ok(None) => None, } } } #[cfg(test)] mod test { use std::sync::mpsc; use std::thread::sleep; use super::*; use crate::{client::TransportType, payload::Payload, ClientBuilder}; use bytes::Bytes; use native_tls::TlsConnector; use serde_json::json; use std::time::Duration; #[test] fn socket_io_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .on("test", |msg, _| match msg { #[allow(deprecated)] Payload::String(str) => println!("Received string: {}", str), Payload::Text(text) => println!("Received json: {:#?}", text), Payload::Binary(bin) => println!("Received binary data: {:#?}", bin), }) .connect()?; let payload = json!({"token": 123}); #[allow(deprecated)] let result = socket.emit("test", Payload::String(payload.to_string())); assert!(result.is_ok()); let ack_callback = move |message: Payload, socket: RawClient| { let result = socket.emit("test", Payload::Text(vec![json!({"got ack": true})])); assert!(result.is_ok()); println!("Yehaa! My ack got acked?"); if let Payload::Text(values) = message { println!("Received json Ack"); println!("Ack data: {:#?}", values); } }; let ack = socket.emit_with_ack( "test", Payload::Text(vec![payload]), Duration::from_secs(1), ack_callback, ); assert!(ack.is_ok()); sleep(Duration::from_secs(2)); assert!(socket.disconnect().is_ok()); Ok(()) } #[test] fn socket_io_builder_integration() -> Result<()> { let url = crate::test::socket_io_server(); // test socket build logic let socket_builder = ClientBuilder::new(url); let tls_connector = TlsConnector::builder() .use_sni(true) .build() .expect("Found illegal configuration"); let socket = socket_builder .namespace("/admin") .tls_config(tls_connector) .opening_header("accept-encoding", "application/json") .on("test", |str, _| println!("Received: {:#?}", str)) .on("message", |payload, _| println!("{:#?}", payload)) .connect()?; assert!(socket.emit("message", json!("Hello World")).is_ok()); assert!(socket.emit("binary", Bytes::from_static(&[46, 88])).is_ok()); assert!(socket .emit_with_ack( "binary", json!("pls ack"), Duration::from_secs(1), |payload, _| { println!("Yehaa the ack got acked"); println!("With data: {:#?}", payload); } ) .is_ok()); sleep(Duration::from_secs(2)); Ok(()) } #[test] fn socket_io_builder_integration_iterator() -> Result<()> { let url = crate::test::socket_io_server(); // test socket build logic let socket_builder = ClientBuilder::new(url); let tls_connector = TlsConnector::builder() .use_sni(true) .build() .expect("Found illegal configuration"); let socket = socket_builder .namespace("/admin") .tls_config(tls_connector) .opening_header("accept-encoding", "application/json") .on("test", |str, _| println!("Received: {:#?}", str)) .on("message", |payload, _| println!("{:#?}", payload)) .connect_raw()?; assert!(socket.emit("message", json!("Hello World")).is_ok()); assert!(socket.emit("binary", Bytes::from_static(&[46, 88])).is_ok()); assert!(socket .emit_with_ack( "binary", json!("pls ack"), Duration::from_secs(1), |payload, _| { println!("Yehaa the ack got acked"); println!("With data: {:#?}", payload); } ) .is_ok()); test_socketio_socket(socket, "/admin".to_owned()) } #[test] fn socket_io_on_any_integration() -> Result<()> { let url = crate::test::socket_io_server(); let (tx, rx) = mpsc::sync_channel(1); let _socket = ClientBuilder::new(url) .namespace("/") .auth(json!({ "password": "123" })) .on("auth", |payload, _client| { if let Payload::Text(payload) = payload { println!("{:#?}", payload); } }) .on_any(move |event, payload, _client| { if let Payload::Text(payload) = payload { println!("{event} {payload:#?}"); } tx.send(String::from(event)).unwrap(); }) .connect()?; // Sleep to give server enough time to send 2 events sleep(Duration::from_secs(2)); let event = rx.recv().unwrap(); assert_eq!(event, "message"); let event = rx.recv().unwrap(); assert_eq!(event, "test"); Ok(()) } #[test] fn socket_io_auth_builder_integration() -> Result<()> { let url = crate::test::socket_io_auth_server(); let nsp = String::from("/admin"); let socket = ClientBuilder::new(url) .namespace(nsp.clone()) .auth(json!({ "password": "123" })) .connect_raw()?; let mut iter = socket .iter() .map(|packet| packet.unwrap()) .filter(|packet| packet.packet_type != PacketId::Connect); let packet: Option = iter.next(); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp, Some("[\"auth\",\"success\"]".to_owned()), None, 0, None ) ); Ok(()) } #[test] fn socketio_polling_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .transport_type(TransportType::Polling) .connect_raw()?; test_socketio_socket(socket, "/".to_owned()) } #[test] fn socket_io_websocket_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .transport_type(TransportType::Websocket) .connect_raw()?; test_socketio_socket(socket, "/".to_owned()) } #[test] fn socket_io_websocket_upgrade_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .transport_type(TransportType::WebsocketUpgrade) .connect_raw()?; test_socketio_socket(socket, "/".to_owned()) } #[test] fn socket_io_any_integration() -> Result<()> { let url = crate::test::socket_io_server(); let socket = ClientBuilder::new(url) .transport_type(TransportType::Any) .connect_raw()?; test_socketio_socket(socket, "/".to_owned()) } fn test_socketio_socket(socket: RawClient, nsp: String) -> Result<()> { let mut iter = socket .iter() .map(|packet| packet.unwrap()) .filter(|packet| packet.packet_type != PacketId::Connect); let packet: Option = iter.next(); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some("[\"Hello from the message event!\"]".to_owned()), None, 0, None, ) ); let packet: Option = iter.next(); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some("[\"test\",\"Hello from the test event!\"]".to_owned()), None, 0, None ) ); let packet: Option = iter.next(); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::BinaryEvent, nsp.clone(), None, None, 1, Some(vec![Bytes::from_static(&[4, 5, 6])]), ) ); let packet: Option = iter.next(); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::BinaryEvent, nsp.clone(), Some("\"test\"".to_owned()), None, 1, Some(vec![Bytes::from_static(&[1, 2, 3])]), ) ); let packet: Option = iter.next(); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some( serde_json::Value::Array(vec![ serde_json::Value::from("This is the first argument"), serde_json::Value::from("This is the second argument"), serde_json::json!({"argCount":3}) ]) .to_string() ), None, 0, None, ) ); let packet: Option = iter.next(); assert!(packet.is_some()); let packet = packet.unwrap(); assert_eq!( packet, Packet::new( PacketId::Event, nsp.clone(), Some( serde_json::json!([ "on_abc_event", "", { "abc": 0, "some_other": "value", } ]) .to_string() ), None, 0, None, ) ); assert!(socket .emit_with_ack( "test", Payload::from("123"), Duration::from_secs(10), |message: Payload, _| { println!("Yehaa! My ack got acked?"); if let Payload::Text(values) = message { println!("Received ack"); println!("Ack data: {values:#?}"); } } ) .is_ok()); Ok(()) } // TODO: add secure socketio server } ================================================ FILE: socketio/src/error.rs ================================================ use base64::DecodeError; use serde_json::Error as JsonError; use std::io::Error as IoError; use std::num::ParseIntError; use std::str::Utf8Error; use thiserror::Error; use url::ParseError as UrlParseError; /// Enumeration of all possible errors in the `socket.io` context. /// TODO: 0.4.X Do not expose non-trivial internal errors. Convert error to string. #[derive(Error, Debug)] #[non_exhaustive] #[cfg_attr(tarpaulin, ignore)] pub enum Error { // Conform to https://rust-lang.github.io/api-guidelines/naming.html#names-use-a-consistent-word-order-c-word-order // Negative verb-object #[error("Invalid packet id: {0}")] InvalidPacketId(char), #[error("Error while parsing an incomplete packet")] IncompletePacket(), #[error("Got an invalid packet which did not follow the protocol format")] InvalidPacket(), #[error("An error occurred while decoding the utf-8 text: {0}")] InvalidUtf8(#[from] Utf8Error), #[error("An error occurred while encoding/decoding base64: {0}")] InvalidBase64(#[from] DecodeError), #[error("Invalid Url during parsing")] InvalidUrl(#[from] UrlParseError), #[error("Invalid Url Scheme: {0}")] InvalidUrlScheme(String), #[error("Got illegal handshake response: {0}")] InvalidHandshake(String), #[error("Called an action before the connection was established")] IllegalActionBeforeOpen(), #[error("string is not json serializable: {0}")] InvalidJson(#[from] JsonError), #[error("A lock was poisoned")] InvalidPoisonedLock(), #[error("Got an IO-Error: {0}")] IncompleteIo(#[from] IoError), #[error("Error while parsing an integer")] InvalidInteger(#[from] ParseIntError), #[error("EngineIO Error")] IncompleteResponseFromEngineIo(#[from] rust_engineio::Error), #[error("Invalid packet type while reading attachments")] InvalidAttachmentPacketType(u8), #[error("Underlying Engine.IO connection has closed")] StoppedEngineIoSocket, } pub(crate) type Result = std::result::Result; impl From> for Error { fn from(_: std::sync::PoisonError) -> Self { Self::InvalidPoisonedLock() } } impl From for std::io::Error { fn from(err: Error) -> std::io::Error { std::io::Error::new(std::io::ErrorKind::Other, err) } } #[cfg(test)] mod tests { use std::sync::{Mutex, PoisonError}; use super::*; /// This just tests the own implementations and relies on `thiserror` for the others. #[test] fn test_error_conversion() { let mutex = Mutex::new(0); let _error = Error::from(PoisonError::new(mutex.lock())); assert!(matches!(Error::InvalidPoisonedLock(), _error)); let _io_error = std::io::Error::from(Error::IncompletePacket()); let _error = std::io::Error::new(std::io::ErrorKind::Other, Error::IncompletePacket()); assert!(matches!(_io_error, _error)); } } ================================================ FILE: socketio/src/event.rs ================================================ use std::fmt::{Display, Formatter, Result as FmtResult}; /// An `Event` in `socket.io` could either (`Message`, `Error`) or custom. #[derive(Debug, PartialEq, PartialOrd, Clone, Eq, Hash)] pub enum Event { Message, Error, Custom(String), Connect, Close, } impl Event { pub fn as_str(&self) -> &str { match self { Event::Message => "message", Event::Error => "error", Event::Connect => "connect", Event::Close => "close", Event::Custom(string) => string, } } } impl From for Event { fn from(string: String) -> Self { match &string.to_lowercase()[..] { "message" => Event::Message, "error" => Event::Error, "open" => Event::Connect, "close" => Event::Close, _ => Event::Custom(string), } } } impl From<&str> for Event { fn from(string: &str) -> Self { Event::from(String::from(string)) } } impl From for String { fn from(event: Event) -> Self { match event { Event::Message => Self::from("message"), Event::Connect => Self::from("open"), Event::Close => Self::from("close"), Event::Error => Self::from("error"), Event::Custom(string) => string, } } } impl Display for Event { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(self.as_str()) } } /// A `CloseReason` is the payload of the [`Event::Close`] and specifies the reason for /// why it was fired. /// These are aligned with the official Socket.IO disconnect reasons, see /// https://socket.io/docs/v4/client-socket-instance/#disconnect #[derive(Debug, PartialEq, PartialOrd, Clone, Eq, Hash)] pub enum CloseReason { IOServerDisconnect, IOClientDisconnect, TransportClose, } impl CloseReason { pub fn as_str(&self) -> &str { match self { // Inspired by https://github.com/socketio/socket.io/blob/d0fc72042068e7eaef448941add617f05e1ec236/packages/socket.io-client/lib/socket.ts#L865 CloseReason::IOServerDisconnect => "io server disconnect", // Inspired by https://github.com/socketio/socket.io/blob/d0fc72042068e7eaef448941add617f05e1ec236/packages/socket.io-client/lib/socket.ts#L911 CloseReason::IOClientDisconnect => "io client disconnect", CloseReason::TransportClose => "transport close", } } } impl From for String { fn from(event: CloseReason) -> Self { Self::from(event.as_str()) } } impl Display for CloseReason { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(self.as_str()) } } ================================================ FILE: socketio/src/lib.rs ================================================ //! Rust-socket.io is a socket.io client written in the Rust Programming Language. //! ## Example usage //! //! ``` rust //! use rust_socketio::{ClientBuilder, Payload, RawClient}; //! use serde_json::json; //! use std::time::Duration; //! //! // define a callback which is called when a payload is received //! // this callback gets the payload as well as an instance of the //! // socket to communicate with the server //! let callback = |payload: Payload, socket: RawClient| { //! match payload { //! Payload::Text(values) => println!("Received: {:#?}", values), //! Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), //! // This variant is deprecated, use Payload::Text instead //! Payload::String(str) => println!("Received: {}", str), //! } //! socket.emit("test", json!({"got ack": true})).expect("Server unreachable") //! }; //! //! // get a socket that is connected to the admin namespace //! let mut socket = ClientBuilder::new("http://localhost:4200/") //! .namespace("/admin") //! .on("test", callback) //! .on("error", |err, _| eprintln!("Error: {:#?}", err)) //! .connect() //! .expect("Connection failed"); //! //! // emit to the "foo" event //! let json_payload = json!({"token": 123}); //! //! socket.emit("foo", json_payload).expect("Server unreachable"); //! //! // define a callback, that's executed when the ack got acked //! let ack_callback = |message: Payload, _: RawClient| { //! println!("Yehaa! My ack got acked?"); //! println!("Ack data: {:#?}", message); //! }; //! //! let json_payload = json!({"myAckData": 123}); //! //! // emit with an ack //! let ack = socket //! .emit_with_ack("test", json_payload, Duration::from_secs(2), ack_callback) //! .expect("Server unreachable"); //! ``` //! //! The main entry point for using this crate is the [`ClientBuilder`] which provides //! a way to easily configure a socket in the needed way. When the `connect` method //! is called on the builder, it returns a connected client which then could be used //! to emit messages to certain events. One client can only be connected to one namespace. //! If you need to listen to the messages in different namespaces you need to //! allocate multiple sockets. //! //! ## Current features //! //! This implementation now supports all of the features of the socket.io protocol mentioned //! [here](https://github.com/socketio/socket.io-protocol). //! It generally tries to make use of websockets as often as possible. This means most times //! only the opening request uses http and as soon as the server mentions that he is able to use //! websockets, an upgrade is performed. But if this upgrade is not successful or the server //! does not mention an upgrade possibility, http-long polling is used (as specified in the protocol specs). //! //! Here's an overview of possible use-cases: //! //! - connecting to a server. //! - register callbacks for the following event types: //! - open //! - close //! - error //! - message //! - custom events like "foo", "on_payment", etc. //! - send JSON data to the server (via `serde_json` which provides safe //! handling). //! - send JSON data to the server and receive an `ack`. //! - send and handle Binary data. #![cfg_attr( feature = "async", doc = r#" ## Async version This library provides an ability for being executed in an asynchronous context using `tokio` as the execution runtime. Please note that the current async implementation is in beta, the interface can be object to drastic changes. The async `Client` and `ClientBuilder` support a similar interface to the sync version and live in the [`asynchronous`] module. In order to enable the support, you need to enable the `async` feature flag: ```toml rust_socketio = { version = "^0.4.1", features = ["async"] } ``` The following code shows the example above in async fashion: ``` rust use futures_util::FutureExt; use rust_socketio::{ asynchronous::{Client, ClientBuilder}, Payload, }; use serde_json::json; use std::time::Duration; #[tokio::main] async fn main() { // define a callback which is called when a payload is received // this callback gets the payload as well as an instance of the // socket to communicate with the server let callback = |payload: Payload, socket: Client| { async move { match payload { Payload::Text(values) => println!("Received: {:#?}", values), Payload::Binary(bin_data) => println!("Received bytes: {:#?}", bin_data), // This is deprecated use Payload::Text instead Payload::String(str) => println!("Received: {}", str), } socket .emit("test", json!({"got ack": true})) .await .expect("Server unreachable"); } .boxed() }; // get a socket that is connected to the admin namespace let socket = ClientBuilder::new("http://localhost:4200/") .namespace("/admin") .on("test", callback) .on("error", |err, _| { async move { eprintln!("Error: {:#?}", err) }.boxed() }) .connect() .await .expect("Connection failed"); // emit to the "foo" event let json_payload = json!({"token": 123}); socket .emit("foo", json_payload) .await .expect("Server unreachable"); // define a callback, that's executed when the ack got acked let ack_callback = |message: Payload, _: Client| { async move { println!("Yehaa! My ack got acked?"); println!("Ack data: {:#?}", message); } .boxed() }; let json_payload = json!({"myAckData": 123}); // emit with an ack socket .emit_with_ack("test", json_payload, Duration::from_secs(2), ack_callback) .await .expect("Server unreachable"); socket.disconnect().await.expect("Disconnect failed"); } ```"# )] #![allow(clippy::rc_buffer)] #![warn(clippy::complexity)] #![warn(clippy::style)] #![warn(clippy::perf)] #![warn(clippy::correctness)] /// Defines client only structs pub mod client; /// Deprecated import since 0.3.0-alpha-2, use Event in the crate root instead. /// Defines the events that could be sent or received. pub mod event; pub(crate) mod packet; /// Deprecated import since 0.3.0-alpha-2, use Event in the crate root instead. /// Defines the types of payload (binary or string), that /// could be sent or received. pub mod payload; pub(self) mod socket; /// Deprecated import since 0.3.0-alpha-2, use Error in the crate root instead. /// Contains the error type which will be returned with every result in this /// crate. pub mod error; #[cfg(feature = "async")] /// Asynchronous version of the socket.io client. This module contains the async /// [`crate::asynchronous::Client`] as well as a builder /// ([`crate::asynchronous::ClientBuilder`]) that allows for configuring a client. pub mod asynchronous; pub use error::Error; pub use {event::CloseReason, event::Event, payload::Payload}; pub use client::{ClientBuilder, RawClient, TransportType}; // TODO: 0.4.0 remove #[deprecated(since = "0.3.0-alpha-2", note = "Socket renamed to Client")] pub use client::{ClientBuilder as SocketBuilder, RawClient as Socket}; #[cfg(test)] pub(crate) mod test { use url::Url; /// The socket.io server for testing runs on port 4200 const SERVER_URL: &str = "http://localhost:4200"; pub(crate) fn socket_io_server() -> Url { let url = std::env::var("SOCKET_IO_SERVER").unwrap_or_else(|_| SERVER_URL.to_owned()); let mut url = Url::parse(&url).unwrap(); if url.path() == "/" { url.set_path("/socket.io/"); } url } // The socket.io auth server for testing runs on port 4204 const AUTH_SERVER_URL: &str = "http://localhost:4204"; pub(crate) fn socket_io_auth_server() -> Url { let url = std::env::var("SOCKET_IO_AUTH_SERVER").unwrap_or_else(|_| AUTH_SERVER_URL.to_owned()); let mut url = Url::parse(&url).unwrap(); if url.path() == "/" { url.set_path("/socket.io/"); } url } // The socket.io restart server for testing runs on port 4205 const RESTART_SERVER_URL: &str = "http://localhost:4205"; pub(crate) fn socket_io_restart_server() -> Url { let url = std::env::var("SOCKET_IO_RESTART_SERVER") .unwrap_or_else(|_| RESTART_SERVER_URL.to_owned()); let mut url = Url::parse(&url).unwrap(); if url.path() == "/" { url.set_path("/socket.io/"); } url } // The socket.io restart url auth server for testing runs on port 4206 const RESTART_URL_AUTH_SERVER_URL: &str = "http://localhost:4206"; pub(crate) fn socket_io_restart_url_auth_server() -> Url { let url = std::env::var("SOCKET_IO_RESTART_URL_AUTH_SERVER") .unwrap_or_else(|_| RESTART_URL_AUTH_SERVER_URL.to_owned()); let mut url = Url::parse(&url).unwrap(); if url.path() == "/" { url.set_path("/socket.io/"); } url } } ================================================ FILE: socketio/src/packet.rs ================================================ use crate::error::{Error, Result}; use crate::{Event, Payload}; use bytes::Bytes; use serde::de::IgnoredAny; use std::convert::TryFrom; use std::fmt::Write; use std::str::from_utf8 as str_from_utf8; /// An enumeration of the different `Packet` types in the `socket.io` protocol. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum PacketId { Connect = 0, Disconnect = 1, Event = 2, Ack = 3, ConnectError = 4, BinaryEvent = 5, BinaryAck = 6, } /// A packet which gets sent or received during in the `socket.io` protocol. #[derive(Debug, PartialEq, Eq, Clone)] pub struct Packet { pub packet_type: PacketId, pub nsp: String, pub data: Option, pub id: Option, pub attachment_count: u8, pub attachments: Option>, } impl Packet { /// Returns a packet for a payload, could be used for both binary and non binary /// events and acks. Convenience method. #[inline] pub(crate) fn new_from_payload<'a>( payload: Payload, event: Event, nsp: &'a str, id: Option, ) -> Result { match payload { Payload::Binary(bin_data) => Ok(Packet::new( if id.is_some() { PacketId::BinaryAck } else { PacketId::BinaryEvent }, nsp.to_owned(), Some(serde_json::Value::String(event.into()).to_string()), id, 1, Some(vec![bin_data]), )), #[allow(deprecated)] Payload::String(str_data) => { let payload = if serde_json::from_str::(&str_data).is_ok() { format!("[\"{event}\",{str_data}]") } else { format!("[\"{event}\",\"{str_data}\"]") }; Ok(Packet::new( PacketId::Event, nsp.to_owned(), Some(payload), id, 0, None, )) } Payload::Text(mut data) => { let mut payload_args = vec![serde_json::Value::String(event.to_string())]; payload_args.append(&mut data); drop(data); let payload = serde_json::Value::Array(payload_args).to_string(); Ok(Packet::new( PacketId::Event, nsp.to_owned(), Some(payload), id, 0, None, )) } } } } impl Default for Packet { fn default() -> Self { Self { packet_type: PacketId::Event, nsp: String::from("/"), data: None, id: None, attachment_count: 0, attachments: None, } } } impl TryFrom for PacketId { type Error = Error; fn try_from(b: u8) -> Result { PacketId::try_from(b as char) } } impl TryFrom for PacketId { type Error = Error; fn try_from(b: char) -> Result { match b { '0' => Ok(PacketId::Connect), '1' => Ok(PacketId::Disconnect), '2' => Ok(PacketId::Event), '3' => Ok(PacketId::Ack), '4' => Ok(PacketId::ConnectError), '5' => Ok(PacketId::BinaryEvent), '6' => Ok(PacketId::BinaryAck), _ => Err(Error::InvalidPacketId(b)), } } } impl Packet { /// Creates an instance. pub const fn new( packet_type: PacketId, nsp: String, data: Option, id: Option, attachment_count: u8, attachments: Option>, ) -> Self { Packet { packet_type, nsp, data, id, attachment_count, attachments, } } } impl From for Bytes { fn from(packet: Packet) -> Self { Bytes::from(&packet) } } impl From<&Packet> for Bytes { /// Method for encoding from a `Packet` to a `u8` byte stream. /// The binary payload of a packet is not put at the end of the /// stream as it gets handled and send by it's own logic via the socket. fn from(packet: &Packet) -> Bytes { // first the packet type let mut buffer = String::new(); buffer.push((packet.packet_type as u8 + b'0') as char); // eventually a number of attachments, followed by '-' if let PacketId::BinaryAck | PacketId::BinaryEvent = packet.packet_type { let _ = write!(buffer, "{}-", packet.attachment_count); } // if the namespace is different from the default one append it as well, // followed by ',' if packet.nsp != "/" { buffer.push_str(&packet.nsp); buffer.push(','); } // if an id is present append it... if let Some(id) = packet.id { let _ = write!(buffer, "{id}"); } if packet.attachments.is_some() { let num = packet.attachment_count - 1; // check if an event type is present if let Some(event_type) = packet.data.as_ref() { let _ = write!( buffer, "[{event_type},{{\"_placeholder\":true,\"num\":{num}}}]", ); } else { let _ = write!(buffer, "[{{\"_placeholder\":true,\"num\":{num}}}]"); } } else if let Some(data) = packet.data.as_ref() { buffer.push_str(data); } Bytes::from(buffer) } } impl TryFrom for Packet { type Error = Error; fn try_from(value: Bytes) -> Result { Packet::try_from(&value) } } impl TryFrom<&Bytes> for Packet { type Error = Error; /// Decodes a packet given a `Bytes` type. /// The binary payload of a packet is not put at the end of the /// stream as it gets handled and send by it's own logic via the socket. /// Therefore this method does not return the correct value for the /// binary data, instead the socket is responsible for handling /// this member. This is done because the attachment is usually /// send in another packet. fn try_from(payload: &Bytes) -> Result { let mut payload = str_from_utf8(&payload).map_err(Error::InvalidUtf8)?; let mut packet = Packet::default(); // packet_type let id_char = payload.chars().next().ok_or(Error::IncompletePacket())?; packet.packet_type = PacketId::try_from(id_char)?; payload = &payload[id_char.len_utf8()..]; // attachment_count if let PacketId::BinaryAck | PacketId::BinaryEvent = packet.packet_type { let (prefix, rest) = payload.split_once('-').ok_or(Error::IncompletePacket())?; payload = rest; packet.attachment_count = prefix.parse().map_err(|_| Error::InvalidPacket())?; } // namespace if payload.starts_with('/') { let (prefix, rest) = payload.split_once(',').ok_or(Error::IncompletePacket())?; payload = rest; packet.nsp.clear(); // clearing the default packet.nsp.push_str(prefix); } // id let Some((non_digit_idx, _)) = payload.char_indices().find(|(_, c)| !c.is_ascii_digit()) else { return Ok(packet); }; if non_digit_idx > 0 { let (prefix, rest) = payload.split_at(non_digit_idx); payload = rest; packet.id = Some(prefix.parse().map_err(|_| Error::InvalidPacket())?); } // validate json serde_json::from_str::(payload).map_err(Error::InvalidJson)?; match packet.packet_type { PacketId::BinaryAck | PacketId::BinaryEvent => { if payload.starts_with('[') && payload.ends_with(']') { payload = &payload[1..payload.len() - 1]; } let mut str = payload.replace("{\"_placeholder\":true,\"num\":0}", ""); if str.ends_with(',') { str.pop(); } if !str.is_empty() { packet.data = Some(str); } } _ => packet.data = Some(payload.to_string()), } Ok(packet) } } #[cfg(test)] mod test { use super::*; #[test] /// This test suite is taken from the explanation section here: /// https://github.com/socketio/socket.io-protocol fn test_decode() { let payload = Bytes::from_static(b"0{\"token\":\"123\"}"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::Connect, "/".to_owned(), Some(String::from("{\"token\":\"123\"}")), None, 0, None, ), packet.unwrap() ); let utf8_data = "{\"token™\":\"123\"}".to_owned(); let utf8_payload = format!("0/admin™,{}", utf8_data); let payload = Bytes::from(utf8_payload); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::Connect, "/admin™".to_owned(), Some(utf8_data), None, 0, None, ), packet.unwrap() ); let payload = Bytes::from_static(b"1/admin,"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::Disconnect, "/admin".to_owned(), None, None, 0, None, ), packet.unwrap() ); let payload = Bytes::from_static(b"2[\"hello\",1]"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::Event, "/".to_owned(), Some(String::from("[\"hello\",1]")), None, 0, None, ), packet.unwrap() ); let payload = Bytes::from_static(b"2/admin,456[\"project:delete\",123]"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::Event, "/admin".to_owned(), Some(String::from("[\"project:delete\",123]")), Some(456), 0, None, ), packet.unwrap() ); let payload = Bytes::from_static(b"3/admin,456[]"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::Ack, "/admin".to_owned(), Some(String::from("[]")), Some(456), 0, None, ), packet.unwrap() ); let payload = Bytes::from_static(b"4/admin,{\"message\":\"Not authorized\"}"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::ConnectError, "/admin".to_owned(), Some(String::from("{\"message\":\"Not authorized\"}")), None, 0, None, ), packet.unwrap() ); let payload = Bytes::from_static(b"51-[\"hello\",{\"_placeholder\":true,\"num\":0}]"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::BinaryEvent, "/".to_owned(), Some(String::from("\"hello\"")), None, 1, None, ), packet.unwrap() ); let payload = Bytes::from_static( b"51-/admin,456[\"project:delete\",{\"_placeholder\":true,\"num\":0}]", ); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::BinaryEvent, "/admin".to_owned(), Some(String::from("\"project:delete\"")), Some(456), 1, None, ), packet.unwrap() ); let payload = Bytes::from_static(b"61-/admin,456[{\"_placeholder\":true,\"num\":0}]"); let packet = Packet::try_from(&payload); assert!(packet.is_ok()); assert_eq!( Packet::new( PacketId::BinaryAck, "/admin".to_owned(), None, Some(456), 1, None, ), packet.unwrap() ); } #[test] /// This test suites is taken from the explanation section here: /// https://github.com/socketio/socket.io-protocol fn test_encode() { let packet = Packet::new( PacketId::Connect, "/".to_owned(), Some(String::from("{\"token\":\"123\"}")), None, 0, None, ); assert_eq!( Bytes::from(&packet), "0{\"token\":\"123\"}".to_string().into_bytes() ); let packet = Packet::new( PacketId::Connect, "/admin".to_owned(), Some(String::from("{\"token\":\"123\"}")), None, 0, None, ); assert_eq!( Bytes::from(&packet), "0/admin,{\"token\":\"123\"}".to_string().into_bytes() ); let packet = Packet::new( PacketId::Disconnect, "/admin".to_owned(), None, None, 0, None, ); assert_eq!(Bytes::from(&packet), "1/admin,".to_string().into_bytes()); let packet = Packet::new( PacketId::Event, "/".to_owned(), Some(String::from("[\"hello\",1]")), None, 0, None, ); assert_eq!( Bytes::from(&packet), "2[\"hello\",1]".to_string().into_bytes() ); let packet = Packet::new( PacketId::Event, "/admin".to_owned(), Some(String::from("[\"project:delete\",123]")), Some(456), 0, None, ); assert_eq!( Bytes::from(&packet), "2/admin,456[\"project:delete\",123]" .to_string() .into_bytes() ); let packet = Packet::new( PacketId::Ack, "/admin".to_owned(), Some(String::from("[]")), Some(456), 0, None, ); assert_eq!( Bytes::from(&packet), "3/admin,456[]".to_string().into_bytes() ); let packet = Packet::new( PacketId::ConnectError, "/admin".to_owned(), Some(String::from("{\"message\":\"Not authorized\"}")), None, 0, None, ); assert_eq!( Bytes::from(&packet), "4/admin,{\"message\":\"Not authorized\"}" .to_string() .into_bytes() ); let packet = Packet::new( PacketId::BinaryEvent, "/".to_owned(), Some(String::from("\"hello\"")), None, 1, Some(vec![Bytes::from_static(&[1, 2, 3])]), ); assert_eq!( Bytes::from(&packet), "51-[\"hello\",{\"_placeholder\":true,\"num\":0}]" .to_string() .into_bytes() ); let packet = Packet::new( PacketId::BinaryEvent, "/admin".to_owned(), Some(String::from("\"project:delete\"")), Some(456), 1, Some(vec![Bytes::from_static(&[1, 2, 3])]), ); assert_eq!( Bytes::from(&packet), "51-/admin,456[\"project:delete\",{\"_placeholder\":true,\"num\":0}]" .to_string() .into_bytes() ); let packet = Packet::new( PacketId::BinaryAck, "/admin".to_owned(), None, Some(456), 1, Some(vec![Bytes::from_static(&[3, 2, 1])]), ); assert_eq!( Bytes::from(&packet), "61-/admin,456[{\"_placeholder\":true,\"num\":0}]" .to_string() .into_bytes() ); } #[test] fn test_illegal_packet_id() { let _sut = PacketId::try_from(42).expect_err("error!"); assert!(matches!(Error::InvalidPacketId(42 as char), _sut)) } #[test] fn new_from_payload_binary() { let payload = Payload::Binary(Bytes::from_static(&[0, 4, 9])); let result = Packet::new_from_payload(payload.clone(), "test_event".into(), "namespace", None) .unwrap(); assert_eq!( result, Packet { packet_type: PacketId::BinaryEvent, nsp: "namespace".to_owned(), data: Some("\"test_event\"".to_owned()), id: None, attachment_count: 1, attachments: Some(vec![Bytes::from_static(&[0, 4, 9])]) } ) } #[test] #[allow(deprecated)] fn new_from_payload_string() { let payload = Payload::String("test".to_owned()); let result = Packet::new_from_payload( payload.clone(), "other_event".into(), "other_namespace", Some(10), ) .unwrap(); assert_eq!( result, Packet { packet_type: PacketId::Event, nsp: "other_namespace".to_owned(), data: Some("[\"other_event\",\"test\"]".to_owned()), id: Some(10), attachment_count: 0, attachments: None } ) } #[test] fn new_from_payload_json() { let payload = Payload::Text(vec![ serde_json::json!("String test"), serde_json::json!({"type":"object"}), ]); let result = Packet::new_from_payload(payload.clone(), "third_event".into(), "/", Some(10)).unwrap(); assert_eq!( result, Packet { packet_type: PacketId::Event, nsp: "/".to_owned(), data: Some("[\"third_event\",\"String test\",{\"type\":\"object\"}]".to_owned()), id: Some(10), attachment_count: 0, attachments: None } ) } } ================================================ FILE: socketio/src/payload.rs ================================================ use bytes::Bytes; /// A type which represents a `payload` in the `socket.io` context. /// A payload could either be of the type `Payload::Binary`, which holds /// data in the [`Bytes`] type that represents the payload or of the type /// `Payload::String` which holds a [`std::string::String`]. The enum is /// used for both representing data that's send and data that's received. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Payload { Binary(Bytes), Text(Vec), #[deprecated = "Use `Payload::Text` instead. Continue existing behavior with: Payload::from(String)"] /// String that is sent as JSON if this is a JSON string, or as a raw string if it isn't String(String), } impl Payload { pub(crate) fn string_to_value(string: String) -> serde_json::Value { if let Ok(value) = serde_json::from_str::(&string) { value } else { serde_json::Value::String(string) } } } impl From<&str> for Payload { fn from(string: &str) -> Self { Payload::from(string.to_owned()) } } impl From for Payload { fn from(string: String) -> Self { Self::Text(vec![Payload::string_to_value(string)]) } } impl From> for Payload { fn from(arr: Vec) -> Self { Self::Text(arr.into_iter().map(Payload::string_to_value).collect()) } } impl From> for Payload { fn from(values: Vec) -> Self { Self::Text(values) } } impl From for Payload { fn from(value: serde_json::Value) -> Self { Self::Text(vec![value]) } } impl From> for Payload { fn from(val: Vec) -> Self { Self::Binary(Bytes::from(val)) } } impl From<&'static [u8]> for Payload { fn from(val: &'static [u8]) -> Self { Self::Binary(Bytes::from_static(val)) } } impl From for Payload { fn from(bytes: Bytes) -> Self { Self::Binary(bytes) } } #[cfg(test)] mod tests { use serde_json::json; use super::*; #[test] fn test_from_string() { let sut = Payload::from("foo ™"); assert_eq!( Payload::Text(vec![serde_json::Value::String(String::from("foo ™"))]), sut ); let sut = Payload::from(String::from("foo ™")); assert_eq!( Payload::Text(vec![serde_json::Value::String(String::from("foo ™"))]), sut ); let sut = Payload::from(json!("foo ™")); assert_eq!( Payload::Text(vec![serde_json::Value::String(String::from("foo ™"))]), sut ); } #[test] fn test_from_multiple_strings() { let input = vec![ "one".to_owned(), "two".to_owned(), json!(["foo", "bar"]).to_string(), ]; assert_eq!( Payload::Text(vec![ serde_json::Value::String(String::from("one")), serde_json::Value::String(String::from("two")), json!(["foo", "bar"]) ]), Payload::from(input) ); } #[test] fn test_from_multiple_json() { let input = vec![json!({"foo": "bar"}), json!("foo"), json!(["foo", "bar"])]; assert_eq!(Payload::Text(input.clone()), Payload::from(input.clone())); } #[test] fn test_from_json() { let json = json!({ "foo": "bar" }); let sut = Payload::from(json.clone()); assert_eq!(Payload::Text(vec![json.clone()]), sut); // From JSON encoded string let sut = Payload::from(json.to_string()); assert_eq!(Payload::Text(vec![json]), sut); } #[test] fn test_from_binary() { let sut = Payload::from(vec![1, 2, 3]); assert_eq!(Payload::Binary(Bytes::from_static(&[1, 2, 3])), sut); let sut = Payload::from(&[1_u8, 2_u8, 3_u8][..]); assert_eq!(Payload::Binary(Bytes::from_static(&[1, 2, 3])), sut); let sut = Payload::from(Bytes::from_static(&[1, 2, 3])); assert_eq!(Payload::Binary(Bytes::from_static(&[1, 2, 3])), sut); } } ================================================ FILE: socketio/src/socket.rs ================================================ use crate::error::{Error, Result}; use crate::packet::{Packet, PacketId}; use bytes::Bytes; use rust_engineio::{Client as EngineClient, Packet as EnginePacket, PacketId as EnginePacketId}; use std::convert::TryFrom; use std::sync::{atomic::AtomicBool, Arc}; use std::{fmt::Debug, sync::atomic::Ordering}; use super::{event::Event, payload::Payload}; /// Handles communication in the `socket.io` protocol. #[derive(Clone, Debug)] pub(crate) struct Socket { //TODO: 0.4.0 refactor this engine_client: Arc, connected: Arc, } impl Socket { /// Creates an instance of `Socket`. pub(super) fn new(engine_client: EngineClient) -> Result { Ok(Socket { engine_client: Arc::new(engine_client), connected: Arc::new(AtomicBool::default()), }) } /// Connects to the server. This includes a connection of the underlying /// engine.io client and afterwards an opening socket.io request. pub fn connect(&self) -> Result<()> { self.engine_client.connect()?; // store the connected value as true, if the connection process fails // later, the value will be updated self.connected.store(true, Ordering::Release); Ok(()) } /// Disconnects from the server by sending a socket.io `Disconnect` packet. This results /// in the underlying engine.io transport to get closed as well. pub fn disconnect(&self) -> Result<()> { if self.is_engineio_connected()? { self.engine_client.disconnect()?; } if self.connected.load(Ordering::Acquire) { self.connected.store(false, Ordering::Release); } Ok(()) } /// Sends a `socket.io` packet to the server using the `engine.io` client. pub fn send(&self, packet: Packet) -> Result<()> { if !self.is_engineio_connected()? || !self.connected.load(Ordering::Acquire) { return Err(Error::IllegalActionBeforeOpen()); } // the packet, encoded as an engine.io message packet let engine_packet = EnginePacket::new(EnginePacketId::Message, Bytes::from(&packet)); self.engine_client.emit(engine_packet)?; if let Some(attachments) = packet.attachments { for attachment in attachments { let engine_packet = EnginePacket::new(EnginePacketId::MessageBinary, attachment); self.engine_client.emit(engine_packet)?; } } Ok(()) } /// Emits to certain event with given data. The data needs to be JSON, /// otherwise this returns an `InvalidJson` error. pub fn emit(&self, nsp: &str, event: Event, data: Payload) -> Result<()> { let socket_packet = Packet::new_from_payload(data, event, nsp, None)?; self.send(socket_packet) } pub(crate) fn poll(&self) -> Result> { loop { match self.engine_client.poll() { Ok(Some(packet)) => { if packet.packet_id == EnginePacketId::Message || packet.packet_id == EnginePacketId::MessageBinary { let packet = self.handle_engineio_packet(packet)?; self.handle_socketio_packet(&packet); return Ok(Some(packet)); } else { continue; } } Ok(None) => { return Ok(None); } Err(err) => return Err(err.into()), } } } /// Handles the connection/disconnection. #[inline] fn handle_socketio_packet(&self, socket_packet: &Packet) { match socket_packet.packet_type { PacketId::Connect => { self.connected.store(true, Ordering::Release); } PacketId::ConnectError => { self.connected.store(false, Ordering::Release); } PacketId::Disconnect => { self.connected.store(false, Ordering::Release); } _ => (), } } /// Handles new incoming engineio packets fn handle_engineio_packet(&self, packet: EnginePacket) -> Result { let mut socket_packet = Packet::try_from(&packet.data)?; // Only handle attachments if there are any if socket_packet.attachment_count > 0 { let mut attachments_left = socket_packet.attachment_count; let mut attachments = Vec::new(); while attachments_left > 0 { let next = self.engine_client.poll(); match next { Err(err) => return Err(err.into()), Ok(Some(packet)) => match packet.packet_id { EnginePacketId::MessageBinary | EnginePacketId::Message => { attachments.push(packet.data); attachments_left -= 1; } _ => { return Err(Error::InvalidAttachmentPacketType( packet.packet_id.into(), )); } }, Ok(None) => { // Engineio closed before attachments completed. return Err(Error::IncompletePacket()); } } } socket_packet.attachments = Some(attachments); } Ok(socket_packet) } fn is_engineio_connected(&self) -> Result { Ok(self.engine_client.is_connected()?) } }