Repository: vi/websocat Branch: master Commit: 29c8c54d6f27 Files: 60 Total size: 559.8 KB Directory structure: gitextract_scxsgr_6/ ├── .dockerignore ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yaml │ └── container-image-buildah.yml ├── .gitignore ├── 1234.pkcs12 ├── CHANGELOG.md ├── Cargo.lock.legacy ├── Cargo.toml ├── Dockerfile ├── Dockerfile.debian ├── LICENSE ├── README.md ├── doc.md ├── misc/ │ └── prebuilt_release_settings.sh ├── moreexamples.md ├── src/ │ ├── all_peers.rs │ ├── broadcast_reuse_peer.rs │ ├── crypto_peer.rs │ ├── file_peer.rs │ ├── foreachmsg_peer.rs │ ├── help.rs │ ├── http_peer.rs │ ├── http_serve.rs │ ├── jsonrpc_peer.rs │ ├── lengthprefixed_peer.rs │ ├── lib.rs │ ├── line_peer.rs │ ├── lints.rs │ ├── main.rs │ ├── mirror_peer.rs │ ├── my_copy.rs │ ├── net_peer.rs │ ├── options.rs │ ├── primitive_reuse_peer.rs │ ├── process_peer.rs │ ├── prometheus_peer.rs │ ├── readdebt.rs │ ├── reconnect_peer.rs │ ├── sessionserve.rs │ ├── socks5_peer.rs │ ├── specifier.rs │ ├── specparse.rs │ ├── ssl_peer.rs │ ├── stdio_peer.rs │ ├── stdio_threaded_peer.rs │ ├── timestamp_peer.rs │ ├── trivial_peer.rs │ ├── unix_peer.rs │ ├── unix_seqpacket_peer.rs │ ├── util.rs │ ├── windows_np_peer.rs │ ├── ws_client_peer.rs │ ├── ws_lowlevel_peer.rs │ ├── ws_peer.rs │ └── ws_server_peer.rs ├── test.pkcs12 ├── test.sh ├── test_help.sh └── tests/ └── test.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ target ================================================ FILE: .github/dependabot.yml ================================================ # see https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" day: friday # Maintain dependencies for Bundler - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" day: wednesday ================================================ FILE: .github/workflows/ci.yaml ================================================ name: CI on: push: branches: [ "master" ] pull_request: branches: [ "master" ] env: CARGO_TERM_COLOR: always jobs: clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Run clippy run: cargo clippy build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose ================================================ FILE: .github/workflows/container-image-buildah.yml ================================================ name: Container Image on: workflow_dispatch: inputs: platforms: description: "comma-separated list of platforms to build for, e.g. linux/amd64,linux/arm64,linux/s390x,linux/ppc64le,linux/riscv64 (leave empty for defaults)" default: '' custom_tag: description: optional custom tag on remote repo you want image to be tagged with default: scratch workflow_call: inputs: platforms: required: false default: '' type: string custom_tag: required: false default: '' type: string #schedule: # # every Wednesday morning # - cron: 7 7 * * 3 push: branches: [ master ] tags: - '*' # Push events to every tag not containing / #pull_request: # types: [opened, reopened, synchronize] concurrency: group: ci-container-build-${{ github.ref }}-1 cancel-in-progress: true env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} # Sets permissions of the GITHUB_TOKEN to allow deployment to ghcr.io permissions: contents: read packages: write id-token: write jobs: buildah: strategy: matrix: include: - architecture: amd64 runner: ubuntu-latest - architecture: arm64 runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} steps: - name: Sanitize Platforms id: platforms run: | platforms="${{ inputs.platforms == '' && 'linux/amd64,linux/arm64' || inputs.platforms }}" if [ "${{ matrix.architecture }}" = "arm64" ]; then platforms="linux/arm64" else platforms="$( sed -e 's#linux/arm64,##g' -e 's#,linux/arm64##g' -e 's#linux/arm64##g' <<< $platforms)" archs="$( sed -e 's#linux/##g' <<< $platforms )" echo "archs=$archs" >> $GITHUB_OUTPUT fi echo "platforms=$platforms" >> $GITHUB_OUTPUT - name: Install Podman on ubuntu-24.04-arm if: matrix.architecture == 'arm64' run: | sudo apt-get update sudo apt-get install -y podman echo -e "[registries.search]\nregistries = ['docker.io']" | sudo tee /etc/containers/registries.conf # Allow multi-target builds - name: Set up QEMU if: matrix.architecture == 'amd64' uses: docker/setup-qemu-action@v3 with: platforms: ${{ steps.platforms.outputs.archs }} # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: redhat-actions/podman-login@v1 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.architecture }} tags: | type=schedule type=raw,value=latest,enable=${{ github.ref_name == 'master' }} ${{ github.ref_name == 'master' && 'type=raw,value=nightly' }} type=ref,event=branch,enable=${{ github.ref_name != 'master' && inputs.custom_tag == '' }} ${{ inputs.custom_tag }} type=ref,event=tag type=ref,event=pr # https://github.com/actions/checkout - uses: actions/checkout@v4 - name: Build image id: build-image uses: redhat-actions/buildah-build@v2 with: tags: ${{ steps.meta.outputs.tags }} platforms: ${{ steps.platforms.outputs.platforms }} labels: ${{ steps.meta.outputs.labels }} layers: false oci: true tls-verify: true extra-args: | --squash --jobs=3 containerfiles: | Dockerfile - name: Echo Outputs run: | echo "Image: ${{ steps.build-image.outputs.image }}" echo "Tags: ${{ steps.build-image.outputs.tags }}" echo "Tagged Image: ${{ steps.build-image.outputs.image-with-tag }}" - name: Check images created run: buildah images - name: Smoke test the images run: | set -ex # accessing a mapped port from a container did not work so lets # create a pod where both - server and client have same localhost podman pod create > podid platforms="${{ steps.platforms.outputs.platforms }}" test_tag () { podman run -d --pod-id-file=podid --name=listener -u 14:0 "${{ steps.build-image.outputs.image-with-tag }}$1" -s 0.0.0.0:1234 sleep 3 podman logs listener podman run --pod-id-file=podid --rm -i "${{ steps.build-image.outputs.image-with-tag }}$1" ws://127.0.0.1:1234/ <<< "Test Message $1" echo Expecting "\"Test Message $1\"" in listener log.. podman logs listener | tee /dev/stderr | grep -q "Test Message $1" podman rm -f listener } if [ x$( sed -E -e 's#[^/]##g' <<< $platforms ) != "x/" ]; then # if we are here, user has selected more than one build platform arch_tags=$( tr ',' ' ' <<< $platforms | tr -d '/' ) # removed slashes to produce "linuxamd64 linuxs390x linuxppc64le" for tag in $arch_tags; do test_tag -$tag; done else # if we are here, user has selected a single build platform test_tag fi - name: Push To Container Registry id: push-to-container-registry uses: redhat-actions/push-to-registry@v2 if: github.event_name != 'pull_request' with: tags: ${{ steps.build-image.outputs.tags }} - name: Print image url run: echo "Image pushed to ${{ steps.push-to-container-registry.outputs.registry-paths }}" merge-archs: runs-on: ubuntu-latest needs: buildah steps: - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: redhat-actions/podman-login@v1 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=schedule type=raw,value=latest,enable=${{ github.ref_name == 'master' }} ${{ github.ref_name == 'master' && 'type=raw,value=nightly' }} type=ref,event=branch,enable=${{ github.ref_name != 'master' && inputs.custom_tag == '' }} ${{ inputs.custom_tag }} type=ref,event=tag type=ref,event=pr - name: Create and push manifest run: | set -x img="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" podman manifest create $img archs="amd64 arm64" for arch in $archs; do podman manifest add $img "${img}-${arch}:latest" done tags="${{ steps.meta.outputs.tags }}" for tag in $tags; do podman tag $img $tag podman manifest push --all $tag done ================================================ FILE: .gitignore ================================================ target ================================================ FILE: CHANGELOG.md ================================================ # [Sunset (v1.14.0)](https://github.com/vi/websocat/releases/tag/v1.14.0) - 12 Nov 2024 * Attempt to priorize ping requests and replies over normal traffic * More options to supply password for basic auth: `--basic-auth-file` and `WEBSOCAT_BASIC_AUTH` * `drop_on_backpressure:` overlay * `--ua` shortcut for `-H User-Agent:` * SOCKS5 authentication # [Happy eyeballs (v1.13.0)](https://github.com/vi/websocat/releases/tag/v1.13.0) - 31 Mar 2024 * `waitfordata:` overlay to delay connection initiation until first data is attempted to be written to it * Dockerfile updates * `lengthprefixed:` overlay - alternative to base64 mode * Fix for #23 - "happy eyeballs" for ws:// and wss:// URLs. # [Maintainance release (v1.12.0)](https://github.com/vi/websocat/releases/tag/v1.12.0) - 17 Sep 2023 * Option to stop sending or replying to WebSocket pings after specified amount of sent or received pings (for testing idling disconnection behaviour of counterparts). * `--exec-exit-on-disconnect` * Print `Location:` header value in error message when facing a redirect instead of a WebSocket connection. * Other minor fixes [Changes][v1.12.0] # [Still keeping v1 afloat instead of concentrating on v3 (v1.11.0)](https://github.com/vi/websocat/releases/tag/v1.11.0) - 24 Sep 2022 * `--preamble` (`-p`) options to prepend static text to Websocat sessions. For use to authenticate and subscribe to something over WebSocket. Note that specifying passwords on command line may be insecure. Also command line handling around `-p` is finicky. There is also `--preamble-reverse` (`-P`) option to prepend similar chunk in the reverse direction. * `--compress-{zlib,deflate,gzip}` and respective `--uncompress-...` options to modify binary WebSocket messages going to/from a WebSocket. Note that it is not related to [permessage-deflate](https://www.rfc-editor.org/rfc/rfc7692.html), which does similar thing, but on lower level. * `exit_on_specific_byte:` overlay to trigger exit when specific byte is encountered. For interactive tty usage. * `--client-pkcs12-der` to specify client identity certificate for connecting to `wss://` or `ssl:` that requires mutual authentication. * `openssl-probe` is now active by default on Linux, to support for overriding CA lists using environment variables. * Incoming WebSocket frames and message are now limited by default, to prevent memory stuffing denial of service. But the default limit is big (100 megabytes). Use `--max-ws-frame-length` and `--max-ws-message-length` options to override. * `Cargo.lock` is now oriented for building with modern Rust compiler. There is `Cargo.lock.legacy` with dependencies manually locked to versions that support Rust 1.46.0. [Changes][v1.11.0] # [Some fixes, some features. (v1.10.0)](https://github.com/vi/websocat/releases/tag/v1.10.0) - 17 May 2022 * Add `--close-status-code` and ` --close-reason` * Fix `--queue-len` option that took no effect * Fix racing to connect to multiple resolved addresses in `tcp:` specifier (i.e. "happy eyeballs") - now it skips errors if there is a working connection. This does not fix `ws://localhost` unfortunately. * `crypto:` overlay and associated options * `prometheus:` overlay and associated options * `random:` specifier [Changes][v1.10.0] # [Supposedly without yanked crates (v1.9.0)](https://github.com/vi/websocat/releases/tag/v1.9.0) - 30 Oct 2021 * `ssl` Cargo feature is now enabled by default * `vendored_openssl` Cargo feature is now not enabled by default * `--stdout-announce-listening-ports` option to print message when server port is ready to accept clients. * `--no-close` option now also affects Websocket server mode, not just client * `timestamp:` overlay to mangle message, prepending current timestamp as text * `--print-ping-rtts` option * Updated deps for [#138](https://github.com/vi/websocat/issues/138) (not checked whether all yanks are resolved although). [Changes][v1.9.0] # [Fix some bugs (v1.8.0)](https://github.com/vi/websocat/releases/tag/v1.8.0) - 15 Apr 2021 * `--accept-from-fd` option for better systemd intergration * `exec:`/`cmd:`/`sh-c:` specifiers now don't terminate process prematurely * `--foreachmsg-wait-read` for better `foreachmsg:` overlay behaviour. Now `foreachmsg:exec:./myscript` is more meaningul. * ` --basic-auth` option to insert basic authentication header more easily * Websocket close message is now logged in debug mode [Changes][v1.8.0] # [Default threaded stdio, `log:` filter (v1.7.0)](https://github.com/vi/websocat/releases/tag/v1.7.0) - 22 Feb 2021 * Websocat now does not set terminal to nonblocking mode if isatty by default. This should help with [#76](https://github.com/vi/websocat/issues/76). * New overlay `log:` that prints bytes as they travel though Websocat, for debugging. [Changes][v1.7.0] # [A heartbeat release (v1.6.0)](https://github.com/vi/websocat/releases/tag/v1.6.0) - 08 Jul 2020 * UDP multicast options * `foreachmsg:` overlay - run specifier (i.e. connect somewhere or execute a program) on each WebSocket message instead of on each WebSocket connection. * Various minor options like `--max-messages` or zero-length message handling. * Low-level Websocket features: `--just-generate-key` and `--just-generate-accept` options which help generating HTTP headers for WebSockets. `ws-lowlevel-server:` and `ws-lowlevel-client:` overlays to use expose WebSocket's data encoder/decoder without HTTP part. * Basic `http://` client with arbitrary method, uri and so on. * Delay for `autoreconnect:` overlay * More pre-built release assets * Base64 mode for binary WebSocket messages * Prefixes for text and binary WebSocket messages, allowing to discriminate incoming binary and text WebSocket messages and intermix outgoing binary and text WebSocket messages. * Sort-of-unfinished `http-post-sse:` specifier allowing to use HTTP server-sent events (in one direction) and POST request bodies (in the other direction) instead of (or in addition to) a WebSocket and to bridge them together. This mode is not tested properly although. [Changes][v1.6.0] # [Client basic auth, header-to-env (v1.5.0)](https://github.com/vi/websocat/releases/tag/v1.5.0) - 18 Aug 2019 * Using client URI's like `websocat ws://user:password@host/` now adds basic authentication HTTP header to request * New command-line option: `--header-to-env` * Minor dependencies update * Built with newer Rust on newer Debian [Changes][v1.5.0] # [WebSocket ping and Sec-WebSocket-Protocol improvements (v1.4.0)](https://github.com/vi/websocat/releases/tag/v1.4.0) - 21 Mar 2019 * New options: `--server-protocol`, `--ping-timeout`, `--ping-interval`, `--server-header` * Fixed replying to WebSocket pings * Fixed replying to requests with `Sec-WebSocket-Protocol`. [Changes][v1.4.0] # [tokio, conncap, pkcs12-passwd, typos (v1.3.0)](https://github.com/vi/websocat/releases/tag/v1.3.0) - 06 Mar 2019 [Changes][v1.3.0] # [-k (--insecure), native-tls (v1.2.0)](https://github.com/vi/websocat/releases/tag/v1.2.0) - 01 Nov 2018 [Changes][v1.2.0] # [More features (v1.1.0)](https://github.com/vi/websocat/releases/tag/v1.1.0) - 30 Aug 2018 * Static files aside from the websocket for easy prototyping * SOCKS5 proxy client * wss:// listener * Setting environment variables for `exec:` * Sending SIGHUP signal to child process on client disconnect * `--jsonrpc` mode [Changes][v1.1.0] # [Preview of 1.1 (v1.1-pre)](https://github.com/vi/websocat/releases/tag/v1.1-pre) - 13 Jul 2018 * --set-environment option and --static-file [Changes][v1.1-pre] # [The release. Finally. (v1.0.0)](https://github.com/vi/websocat/releases/tag/v1.0.0) - 04 Jul 2018 [Changes][v1.0.0] # [Refactor and more features (v1.0.0-beta)](https://github.com/vi/websocat/releases/tag/v1.0.0-beta) - 20 Jun 2018 [Changes][v1.0.0-beta] # [Async alpha (v1.0.0-alpha)](https://github.com/vi/websocat/releases/tag/v1.0.0-alpha) - 10 May 2018 [Changes][v1.0.0-alpha] # [Async preview (v0.5.1-alpha)](https://github.com/vi/websocat/releases/tag/v0.5.1-alpha) - 14 Mar 2018 [Changes][v0.5.1-alpha] # [Forked rust-websocket (v0.4.0)](https://github.com/vi/websocat/releases/tag/v0.4.0) - 18 Jan 2017 [Changes][v0.4.0] # [More features (v0.3.0)](https://github.com/vi/websocat/releases/tag/v0.3.0) - 22 Dec 2016 - Unix sockets - Executing programs and command lines - Unidirectional mode - Text mode (don't rely on it) [Changes][v0.3.0] # [First actual release (v0.2.0)](https://github.com/vi/websocat/releases/tag/v0.2.0) - 24 Nov 2016 [Changes][v0.2.0] [v1.12.0]: https://github.com/vi/websocat/compare/v1.11.0...v1.12.0 [v1.11.0]: https://github.com/vi/websocat/compare/v1.10.0...v1.11.0 [v1.10.0]: https://github.com/vi/websocat/compare/v3.0.0-prealpha0...v1.10.0 [v1.9.0]: https://github.com/vi/websocat/compare/v1.8.0...v1.9.0 [v1.8.0]: https://github.com/vi/websocat/compare/v1.7.0...v1.8.0 [v1.7.0]: https://github.com/vi/websocat/compare/v1.6.0...v1.7.0 [v1.6.0]: https://github.com/vi/websocat/compare/v2.0.0-alpha0...v1.6.0 [v1.5.0]: https://github.com/vi/websocat/compare/v1.4.0...v1.5.0 [v1.4.0]: https://github.com/vi/websocat/compare/v1.3.0...v1.4.0 [v1.3.0]: https://github.com/vi/websocat/compare/v1.2.0...v1.3.0 [v1.2.0]: https://github.com/vi/websocat/compare/v1.1.0...v1.2.0 [v1.1.0]: https://github.com/vi/websocat/compare/v1.1-pre...v1.1.0 [v1.1-pre]: https://github.com/vi/websocat/compare/v1.0.0...v1.1-pre [v1.0.0]: https://github.com/vi/websocat/compare/v1.0.0-beta...v1.0.0 [v1.0.0-beta]: https://github.com/vi/websocat/compare/v1.0.0-alpha...v1.0.0-beta [v1.0.0-alpha]: https://github.com/vi/websocat/compare/v0.5.1-alpha...v1.0.0-alpha [v0.5.1-alpha]: https://github.com/vi/websocat/compare/v0.4.0...v0.5.1-alpha [v0.4.0]: https://github.com/vi/websocat/compare/v0.3.0...v0.4.0 [v0.3.0]: https://github.com/vi/websocat/compare/v0.2.0...v0.3.0 [v0.2.0]: https://github.com/vi/websocat/tree/v0.2.0 ================================================ FILE: Cargo.lock.legacy ================================================ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ "generic-array 0.14.5", ] [[package]] name = "anymap" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" [[package]] name = "argon2" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" dependencies = [ "base64ct", "blake2", "password-hash", ] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", "winapi 0.3.9", ] [[package]] name = "autocfg" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" dependencies = [ "autocfg 1.1.0", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" dependencies = [ "byteorder", "safemem", ] [[package]] name = "base64" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" dependencies = [ "byteorder", ] [[package]] name = "base64ct" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "blake2" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" dependencies = [ "digest 0.10.3", ] [[package]] name = "block-buffer" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ "block-padding", "byte-tools", "byteorder", "generic-array 0.12.4", ] [[package]] name = "block-buffer" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array 0.14.5", ] [[package]] name = "block-padding" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" dependencies = [ "byte-tools", ] [[package]] name = "byte-tools" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" dependencies = [ "byteorder", "iovec", ] [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" dependencies = [ "cfg-if 1.0.0", "cipher", "cpufeatures", "zeroize", ] [[package]] name = "chacha20poly1305" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" dependencies = [ "aead", "chacha20", "cipher", "poly1305", "zeroize", ] [[package]] name = "cipher" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ "generic-array 0.14.5", ] [[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", "textwrap", "unicode-width", ] [[package]] name = "cloudabi" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ "bitflags", ] [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if 1.0.0", ] [[package]] name = "crossbeam-deque" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" dependencies = [ "crossbeam-epoch", "crossbeam-utils 0.7.2", "maybe-uninit", ] [[package]] name = "crossbeam-epoch" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ "autocfg 1.1.0", "cfg-if 0.1.10", "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-queue" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" dependencies = [ "crossbeam-utils 0.6.6", ] [[package]] name = "crossbeam-queue" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ "cfg-if 0.1.10", "crossbeam-utils 0.7.2", "maybe-uninit", ] [[package]] name = "crossbeam-utils" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" dependencies = [ "cfg-if 0.1.10", "lazy_static", ] [[package]] name = "crossbeam-utils" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ "autocfg 1.1.0", "cfg-if 0.1.10", "lazy_static", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.5", "typenum", ] [[package]] name = "derivative" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6d883546668a3e2011b6a716a7330b82eabb0151b138217f632c8243e17135" dependencies = [ "proc-macro2 0.4.30", "quote 0.6.13", "syn 0.15.44", ] [[package]] name = "digest" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ "generic-array 0.12.4", ] [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer 0.10.2", "crypto-common", "subtle", ] [[package]] name = "env_logger" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" dependencies = [ "log 0.4.17", ] [[package]] name = "fake-simd" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fastrand" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] [[package]] name = "flate2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "fuchsia-cprng" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "fuchsia-zircon" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ "bitflags", "fuchsia-zircon-sys", ] [[package]] name = "fuchsia-zircon-sys" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "generic-array" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" dependencies = [ "typenum", ] [[package]] name = "generic-array" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check 0.9.4", ] [[package]] name = "getrandom" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "heck" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-bytes" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3332986b24440d485a8c31e5a823c098f9c1616e7c60945e09dbc6c8d45bed55" dependencies = [ "base64 0.10.1", "bytes", "http", "httparse", "percent-encoding", ] [[package]] name = "httparse" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] name = "hyper" version = "0.10.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" dependencies = [ "base64 0.9.3", "httparse", "language-tags", "log 0.3.9", "mime", "num_cpus", "time", "traitobject", "typeable", "unicase", "url", ] [[package]] name = "idna" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" dependencies = [ "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", ] [[package]] name = "iovec" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" dependencies = [ "libc", ] [[package]] name = "itoa" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "kernel32-sys" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" dependencies = [ "winapi 0.2.8", "winapi-build", ] [[package]] name = "language-tags" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "lock_api" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" dependencies = [ "scopeguard", ] [[package]] name = "lock_api" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg 1.1.0", "scopeguard", ] [[package]] name = "log" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" dependencies = [ "log 0.4.17", ] [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", ] [[package]] name = "matches" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "maybe-uninit" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" dependencies = [ "autocfg 1.1.0", ] [[package]] name = "mime" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" dependencies = [ "log 0.3.9", ] [[package]] name = "miniz_oxide" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" dependencies = [ "cfg-if 0.1.10", "fuchsia-zircon", "fuchsia-zircon-sys", "iovec", "kernel32-sys", "libc", "log 0.4.17", "miow 0.2.2", "net2", "slab", "winapi 0.2.8", ] [[package]] name = "mio-named-pipes" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" dependencies = [ "log 0.4.17", "mio", "miow 0.3.7", "winapi 0.3.9", ] [[package]] name = "mio-uds" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" dependencies = [ "iovec", "libc", "mio", ] [[package]] name = "miow" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" dependencies = [ "kernel32-sys", "net2", "winapi 0.2.8", "ws2_32-sys", ] [[package]] name = "miow" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ "winapi 0.3.9", ] [[package]] name = "native-tls" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" dependencies = [ "lazy_static", "libc", "log 0.4.17", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "net2" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" dependencies = [ "cfg-if 0.1.10", "libc", "winapi 0.3.9", ] [[package]] name = "num_cpus" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "once_cell" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "opaque-debug" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" version = "0.10.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", "openssl-sys", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" version = "111.22.0+1.1.1q" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853" dependencies = [ "cc", ] [[package]] name = "openssl-sys" version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ "autocfg 1.1.0", "cc", "libc", "openssl-src", "pkg-config", "vcpkg", ] [[package]] name = "parking_lot" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" dependencies = [ "lock_api 0.3.4", "parking_lot_core 0.6.2", "rustc_version", ] [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api 0.4.7", "parking_lot_core 0.9.3", ] [[package]] name = "parking_lot_core" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" dependencies = [ "cfg-if 0.1.10", "cloudabi", "libc", "redox_syscall 0.1.57", "rustc_version", "smallvec 0.6.14", "winapi 0.3.9", ] [[package]] name = "parking_lot_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall 0.2.16", "smallvec 1.9.0", "windows-sys", ] [[package]] name = "password-hash" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", "rand_core 0.6.3", "subtle", ] [[package]] name = "percent-encoding" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" [[package]] name = "pkg-config" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "poly1305" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ "cpufeatures", "opaque-debug 0.3.0", "universal-hash", ] [[package]] name = "ppv-lite86" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ "unicode-xid", ] [[package]] name = "proc-macro2" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" dependencies = [ "unicode-ident", ] [[package]] name = "prometheus" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cface98dfa6d645ea4c789839f176e4b072265d085bfcc48eaa8d137f58d3c39" dependencies = [ "cfg-if 1.0.0", "fnv", "lazy_static", "memchr", "parking_lot 0.12.1", "thiserror", ] [[package]] name = "prometheus-metric-storage" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3282447ea0b07baa9011e45de96794c5963db0162c1001d2867750715d63ff14" dependencies = [ "lazy_static", "prometheus", "prometheus-metric-storage-derive", ] [[package]] name = "prometheus-metric-storage-derive" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239221aa10cd35277c58b8f6509280b2613321760b49584d7a7351a6aacb6963" dependencies = [ "proc-macro2 1.0.42", "quote 1.0.20", "syn 1.0.98", ] [[package]] name = "quote" version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" dependencies = [ "proc-macro2 0.4.30", ] [[package]] name = "quote" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2 1.0.42", ] [[package]] name = "rand" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ "autocfg 0.1.8", "libc", "rand_chacha 0.1.1", "rand_core 0.4.2", "rand_hc", "rand_isaac", "rand_jitter", "rand_os", "rand_pcg", "rand_xorshift", "winapi 0.3.9", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.3", ] [[package]] name = "rand_chacha" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" dependencies = [ "autocfg 0.1.8", "rand_core 0.3.1", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.3", ] [[package]] name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ "rand_core 0.4.2", ] [[package]] name = "rand_core" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rand_core" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" dependencies = [ "rand_core 0.3.1", ] [[package]] name = "rand_isaac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" dependencies = [ "rand_core 0.3.1", ] [[package]] name = "rand_jitter" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ "libc", "rand_core 0.4.2", "winapi 0.3.9", ] [[package]] name = "rand_os" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" dependencies = [ "cloudabi", "fuchsia-cprng", "libc", "rand_core 0.4.2", "rdrand", "winapi 0.3.9", ] [[package]] name = "rand_pcg" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" dependencies = [ "autocfg 0.1.8", "rand_core 0.4.2", ] [[package]] name = "rand_xorshift" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" dependencies = [ "rand_core 0.3.1", ] [[package]] name = "rdrand" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ "rand_core 0.3.1", ] [[package]] name = "readwrite" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73891b98dabbe836d23a094941e6ec891bc4880e771faea98813f2ff27ede473" dependencies = [ "futures", "tokio-io", ] [[package]] name = "redox_syscall" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi 0.3.9", ] [[package]] name = "rustc_version" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ "semver", ] [[package]] name = "safemem" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" [[package]] name = "schannel" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", "windows-sys", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "semver" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ "semver-parser", ] [[package]] name = "semver-parser" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "sha-1" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ "block-buffer 0.7.3", "digest 0.8.1", "fake-simd", "opaque-debug 0.2.3", ] [[package]] name = "signal-hook-registry" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" dependencies = [ "autocfg 1.1.0", ] [[package]] name = "slab_typesafe" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e1a2062526abda41283046a3149dc589cb760c8c6672dd88e209f7fba0c0c1" dependencies = [ "slab", ] [[package]] name = "smallvec" version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" dependencies = [ "maybe-uninit", ] [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "smart-default" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e5c02ddada494809d36623d38050f3bd63446750abd21e7e13c01aa3a79b69" dependencies = [ "proc-macro2 0.4.30", "quote 0.6.13", "syn 0.15.44", ] [[package]] name = "structopt" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa19a5a708e22bb5be31c1b6108a2a902f909c4b9ba85cba44c06632386bc0ff" dependencies = [ "clap", "structopt-derive", ] [[package]] name = "structopt-derive" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6d59d0ae8ef8de16e49e3ca7afa16024a3e0dfd974a75ef93fdc5464e34523f" dependencies = [ "heck", "proc-macro2 0.4.30", "quote 0.6.13", "syn 0.15.44", ] [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ "proc-macro2 0.4.30", "quote 0.6.13", "unicode-xid", ] [[package]] name = "syn" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2 1.0.42", "quote 1.0.20", "unicode-ident", ] [[package]] name = "tempfile" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", "fastrand", "libc", "redox_syscall 0.2.16", "remove_dir_all", "winapi 0.3.9", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "unicode-width", ] [[package]] name = "thiserror" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2 1.0.42", "quote 1.0.20", "syn 1.0.98", ] [[package]] name = "time" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", "winapi 0.3.9", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tk-listen" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5462b0f968c0457efe38fcd2df7e487096b992419e4f5337b06775a614bbda4b" dependencies = [ "futures", "log 0.4.17", "tokio", "tokio-io", ] [[package]] name = "tokio" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" dependencies = [ "bytes", "futures", "mio", "num_cpus", "tokio-codec", "tokio-current-thread", "tokio-executor", "tokio-fs", "tokio-io", "tokio-reactor", "tokio-sync", "tokio-tcp", "tokio-threadpool", "tokio-timer", "tokio-udp", "tokio-uds", ] [[package]] name = "tokio-codec" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" dependencies = [ "bytes", "futures", "tokio-io", ] [[package]] name = "tokio-current-thread" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" dependencies = [ "futures", "tokio-executor", ] [[package]] name = "tokio-executor" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" dependencies = [ "crossbeam-utils 0.7.2", "futures", ] [[package]] name = "tokio-file-unix" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7742e2a421379472607d46e2641e66ee2d135f5274d12ad0aba1c1fbbcea2e8c" dependencies = [ "bytes", "libc", "mio", "tokio-io", "tokio-reactor", ] [[package]] name = "tokio-fs" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" dependencies = [ "futures", "tokio-io", "tokio-threadpool", ] [[package]] name = "tokio-io" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" dependencies = [ "bytes", "futures", "log 0.4.17", ] [[package]] name = "tokio-named-pipes" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d282d483052288b2308ba5ee795f5673b159c9bdf63c385a05609da782a5eae" dependencies = [ "bytes", "futures", "mio", "mio-named-pipes", "tokio", ] [[package]] name = "tokio-process" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "382d90f43fa31caebe5d3bc6cfd854963394fff3b8cb59d5146607aaae7e7e43" dependencies = [ "crossbeam-queue 0.1.2", "futures", "lazy_static", "libc", "log 0.4.17", "mio", "mio-named-pipes", "tokio-io", "tokio-reactor", "tokio-signal", "winapi 0.3.9", ] [[package]] name = "tokio-reactor" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" dependencies = [ "crossbeam-utils 0.7.2", "futures", "lazy_static", "log 0.4.17", "mio", "num_cpus", "parking_lot 0.9.0", "slab", "tokio-executor", "tokio-io", "tokio-sync", ] [[package]] name = "tokio-signal" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0c34c6e548f101053321cba3da7cbb87a610b85555884c41b07da2eb91aff12" dependencies = [ "futures", "libc", "mio", "mio-uds", "signal-hook-registry", "tokio-executor", "tokio-io", "tokio-reactor", "winapi 0.3.9", ] [[package]] name = "tokio-stdin-stdout" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc480d205310fa52f8ea65e7f9443568b6b342f326e86431d2aeb176d720c17" dependencies = [ "futures", "tokio-io", ] [[package]] name = "tokio-sync" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" dependencies = [ "fnv", "futures", ] [[package]] name = "tokio-tcp" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" dependencies = [ "bytes", "futures", "iovec", "mio", "tokio-io", "tokio-reactor", ] [[package]] name = "tokio-threadpool" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" dependencies = [ "crossbeam-deque", "crossbeam-queue 0.2.3", "crossbeam-utils 0.7.2", "futures", "lazy_static", "log 0.4.17", "num_cpus", "slab", "tokio-executor", ] [[package]] name = "tokio-timer" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" dependencies = [ "crossbeam-utils 0.7.2", "futures", "slab", "tokio-executor", ] [[package]] name = "tokio-tls" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "354b8cd83825b3c20217a9dc174d6a0c67441a2fae5c41bcb1ea6679f6ae0f7c" dependencies = [ "futures", "native-tls", "tokio-io", ] [[package]] name = "tokio-udp" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" dependencies = [ "bytes", "futures", "log 0.4.17", "mio", "tokio-codec", "tokio-io", "tokio-reactor", ] [[package]] name = "tokio-uds" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" dependencies = [ "bytes", "futures", "iovec", "libc", "log 0.4.17", "mio", "mio-uds", "tokio-codec", "tokio-io", "tokio-reactor", ] [[package]] name = "traitobject" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" [[package]] name = "typeable" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicase" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" dependencies = [ "version_check 0.1.5", ] [[package]] name = "unicode-bidi" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unicode-normalization" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" [[package]] name = "universal-hash" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ "generic-array 0.14.5", "subtle", ] [[package]] name = "url" version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ "idna", "matches", "percent-encoding", ] [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "websocat" version = "1.11.0" dependencies = [ "anymap", "argon2", "atty", "base64 0.10.1", "chacha20poly1305", "derivative", "env_logger", "flate2", "futures", "hex", "http-bytes", "hyper", "libc", "log 0.4.17", "native-tls", "net2", "openssl-probe", "openssl-sys", "prometheus", "prometheus-metric-storage", "rand 0.8.5", "readwrite", "slab_typesafe", "smart-default", "structopt", "structopt-derive", "tempfile", "tk-listen", "tokio", "tokio-codec", "tokio-current-thread", "tokio-file-unix", "tokio-io", "tokio-named-pipes", "tokio-process", "tokio-reactor", "tokio-signal", "tokio-stdin-stdout", "tokio-tcp", "tokio-timer", "tokio-tls", "tokio-udp", "tokio-uds", "url", "websocket", "websocket-base", ] [[package]] name = "websocket" version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92aacab060eea423e4036820ddd28f3f9003b2c4d8048cbda985e5a14e18038d" dependencies = [ "bytes", "futures", "hyper", "native-tls", "rand 0.6.5", "tokio-codec", "tokio-io", "tokio-reactor", "tokio-tcp", "tokio-tls", "unicase", "url", "websocket-base", ] [[package]] name = "websocket-base" version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aec794b07318993d1db16156d5a9c750120597a5ee40c6b928d416186cb138" dependencies = [ "base64 0.10.1", "bitflags", "byteorder", "bytes", "futures", "native-tls", "rand 0.6.5", "sha-1", "tokio-codec", "tokio-io", "tokio-tcp", "tokio-tls", ] [[package]] name = "winapi" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "ws2_32-sys" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" dependencies = [ "winapi 0.2.8", "winapi-build", ] [[package]] name = "zeroize" version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" ================================================ FILE: Cargo.toml ================================================ [package] name = "websocat" version = "1.14.1" authors = ["Vitaly \"_Vi\" Shukela "] license = "MIT" repository = "https://github.com/vi/websocat" description = "Command-line client for web sockets, like netcat/curl/socat for ws://." keywords = ["WebSocket", "socat", "rfc6455", "netcat", "cli"] include = ["src","Cargo.toml","LICENSE","README.md"] readme = "README.md" edition = "2018" #msrv = "1.48.0" [package.metadata.deb] section = "utility" extended-description = """\ A tool allows you to interconnect two specifiers, like in socat, \ but with Websocket and some other additional functions.""" features = ["ssl", "workaround1", "seqpacket", "unix_stdio"] #depends = "$auto" depends = "libssl1.1, libc6 (>= 2.19), libgcc1 (>= 1:4.9.0)" [dependencies] websocket = { version="0.27.1", default-features = false, features=["async"] } websocket-base = { version="0.26.5", default-features = false, features=["async"] } http-bytes = {version = "0.1.0"} env_logger = { version = "0.6.0", default-features = false } log = {version="0.4.1", default-features = false, features=["release_max_level_debug"]} futures = {version = "0.1.17" } tokio-io = "0.1.5" tokio-stdin-stdout = "0.1.5" structopt = { version = "=0.2.16", default-features = false } structopt-derive = { version = "=0.2.16", default-features = false } tokio-process = { version = "0.2.3", optional = true } slab_typesafe = "0.1" hyper="0.10.13" url="1.7.1" openssl-probe = { version = "0.1.2", optional = true } smart-default = "0.3.0" tokio-tls = {version = "0.2.0", optional = true} native-tls = {version = "0.2.1", optional = true} readwrite = {version = "0.1.1", optional = true, features = ["tokio"]} derivative="1.0.0" tokio-codec = "0.1.1" tokio-tcp = "0.1.2" tokio-udp = "0.1.3" tokio-reactor = "0.1.7" tokio = "0.1.11" tokio-current-thread = "0.1.4" tk-listen = "0.2.1" tokio-timer = "0.2.0" tempfile = "3.0.8" net2 = "0.2.33" anymap = "0.12.1" base64 = "0.10" atty = "0.2.14" #anymap = { path = "/mnt/src/git/anymap"} hex = "0.4.2" chacha20poly1305 = {version="0.9.0",optional=true} rand = { version = "0.8.4", optional = false } argon2 = { version = "0.4.0", optional = true } prometheus = { version = "0.13.0", optional = true, default-features = false } prometheus-metric-storage = { version = "0.5.0", optional = true } flate2 = {version = "1", optional = true} # Rust 1.30.1 compat: #cfg-if="=0.1.9" #unicode-width="=0.1.5" #tokio-reactor = "=0.1.9" #cc="=1.0.41" [target.'cfg(unix)'.dependencies] tokio-file-unix = "0.5.1" tokio-signal = { version = "0.2.7", optional = true } tokio-uds = "0.2.3" libc = { version = "0.2" } [target.'cfg(windows)'.dependencies] tokio-named-pipes = {version="0.1.0", optional=true} [features] default = ["signal_handler", "tokio-process", "unix_stdio", "windows_named_pipes", "ssl", "compression"] unix_stdio = [] ssl = ["websocket/async-ssl", "tokio-tls", "native-tls", "readwrite", "openssl-sys"] signal_handler = ["tokio-signal"] workaround1=[] seqpacket=[] windows_named_pipes=["tokio-named-pipes"] vendored_openssl = ["openssl-sys/vendored"] crypto_peer = ["chacha20poly1305","argon2"] prometheus_peer=["prometheus","prometheus-metric-storage"] compression=["flate2"] [target.'arm-linux-androideabi'.dependencies] openssl-sys = { version="0.9", features=[], optional=true } #[patch."crates-io"] #"tokio-process:0.1.6" = {path="/mnt/src/git/tokio-process"} #"tokio-core:0.1.17" = {path="/mnt/src/git/tokio-core"} #"websocket" = {path="/home/vi/src/rust-websocket"} #"websocket-base" = {path="/home/vi/src/rust-websocket/websocket-base"} ================================================ FILE: Dockerfile ================================================ # Build stage FROM rust:1.87.0-alpine3.20 AS cargo-build RUN apk add --no-cache musl-dev pkgconfig openssl-dev WORKDIR /src/websocat ENV RUSTFLAGS='-Ctarget-feature=-crt-static' COPY Cargo.toml Cargo.toml ARG CARGO_OPTS="--features=workaround1,seqpacket,prometheus_peer,prometheus/process,crypto_peer" RUN mkdir src/ &&\ echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \ cargo build --release $CARGO_OPTS && \ rm -f target/release/deps/websocat* COPY src src RUN cargo build --release $CARGO_OPTS && \ strip target/release/websocat # Final stage FROM alpine:3.20 RUN apk add --no-cache libgcc WORKDIR / COPY --from=cargo-build /src/websocat/target/release/websocat /usr/local/bin/ ENTRYPOINT ["/usr/local/bin/websocat"] ================================================ FILE: Dockerfile.debian ================================================ # Build stage FROM rust:1.87.0-bookworm AS cargo-build RUN apt-get update RUN apt-get install -y libgcc-12-dev libc6-dev pkg-config libssl-dev WORKDIR /src/websocat ENV RUSTFLAGS='-Ctarget-feature=-crt-static' COPY Cargo.toml Cargo.toml # ARG CARGO_OPTS="--features=workaround1,seqpacket,prometheus_peer,prometheus/process,crypto_peer" ARG CARGO_OPTS="--features=ssl,workaround1,seqpacket,unix_stdio,prometheus_peer,prometheus/process,crypto_peer" RUN mkdir src/ &&\ echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \ cargo build --release $CARGO_OPTS && \ rm -f target/release/deps/websocat* COPY src src RUN cargo build --release $CARGO_OPTS && \ strip target/release/websocat # Final stage FROM debian:bookworm RUN apt-get update RUN apt-get install -y libssl3 RUN apt-get clean && \ rm -rf /var/lib/apt/lists/* WORKDIR / COPY --from=cargo-build /src/websocat/target/release/websocat /usr/local/bin/ ENTRYPOINT ["/usr/local/bin/websocat"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Vitaly Shukela 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: README.md ================================================ # websocat Netcat, curl and socat for [WebSockets](https://en.wikipedia.org/wiki/WebSocket). [![Gitter](https://badges.gitter.im/websocat.svg)](https://gitter.im/websocat/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) [![image-build](https://github.com/vi/websocat/actions/workflows/container-image-buildah.yml/badge.svg)](https://github.com/vi/websocat/pkgs/container/websocat) ## Examples ### Connect to public echo server (Ctrl+d to quit) ``` $ websocat ws://ws.vi-server.org/mirror 123 123 ABC ABC $ docker run --rm -ti ghcr.io/vi/websocat:nightly wss://ws.vi-server.org/mirror 123 123 ABC ABC ``` ### Serve and connect ``` A$ websocat -s 1234 Listening on ws://127.0.0.1:1234/ ABC 123 B$ websocat ws://127.0.0.1:1234/ ABC 123 ``` ### Open a tab in Chromium using remote debugging. ``` $ chromium --remote-debugging-port=9222& $ curl -sg http://127.0.0.1:9222/json/new | grep webSocketDebuggerUrl | cut -d'"' -f4 | head -1 ws://127.0.0.1:9222/devtools/page/A331E56CCB8615EB4FCB720425A82259 $ echo 'Page.navigate {"url":"https://example.com"}' | websocat -n1 --jsonrpc --jsonrpc-omit-jsonrpc ws://127.0.0.1:9222/devtools/page/A331E56CCB8615EB4FCB720425A82259 {"id":2,"result":{"frameId":"A331E56CCB8615EB4FCB720425A82259","loaderId":"EF5AAD19F2F8BB27FAF55F94FFB27DF9"}} ``` ### Proxy TCP connections to WebSocket connections and back. ``` $ websocat --oneshot -b ws-l:127.0.0.1:1234 tcp:127.0.0.1:22& $ websocat --oneshot -b tcp-l:127.0.0.1:1236 ws://127.0.0.1:1234/& $ nc 127.0.0.1 1236 SSH-2.0-OpenSSH_7.4p1 Debian-10+deb9u3 qwertyu Protocol mismatch. ``` ### Broadcast all messages between connected WebSocket clients ``` A$ websocat -t ws-l:127.0.0.1:1234 broadcast:mirror: B$ websocat ws://127.0.0.1:1234 C$ websocat ws://127.0.0.1:1234 ``` (if you like this one, you may actually want https://github.com/vi/wsbroad/ instead) See [moreexamples.md](./moreexamples.md) for further examples. ## Features * Connecting to and serving WebSockets from command line. * Executing external program and making it communicate to WebSocket using stdin/stdout. * Text and binary modes, converting between lines (or null-terminated records) and messages. * Inetd mode, UNIX sockets (including abstract namespaced on Linux). * Integration with Nginx using TCP or UNIX sockets. * Directly using unauthenticated SOCKS5 servers for connecting to WebSockets and listening WebSocket connection. * Auto-reconnect and connection-reuse modes. * Linux, Windows and Mac support, with [pre-built executables][releases]. * Low-level WebSocket clients and servers with overridable underlying transport connection, e.g. calling external program to serve as a transport for websocat (for SSL, proxying, etc.). [releases]:https://github.com/vi/websocat/releases # Installation There are multiple options for installing WebSocat. From easy to hard: ## FreeBSD `pkg install websocat` ## Debian / Ubuntu * Download a pre-build executable from [GitHub releases][releases]. * Websocat is not yet officially packaged for Debian. Some older versions of Websocat may also have Debian package files available on Github releases. ## macOS [Homebrew](https://brew.sh): `brew install websocat` [MacPorts](https://www.macports.org): `sudo port install websocat` ## From source * Install the [Rust toolchain](https://rustup.rs/) and do `cargo install websocat`. If something fails with a `-sys` crate, try with `--no-default-features`; * Build Websocat from source code (see below), then move `target/release/websocat` somewhere to the PATH. ## Pre-built binaries Pre-built binaries for Linux (usual and musl), Windows, OS X and Android are available on the [releases page](https://github.com/vi/websocat/releases). Building from source code --- 1. Install the [Rust toolchain](https://rustup.rs/). Note that Websocat v1 (i.e. the current stable version) may fail to support too new Rust due to its old dependencies which can be broken by e.g. [this](https://github.com/rust-lang/rust/pull/78802). 2. `cargo build --release`. 3. Find the executable somewhere under `target/`, e.g. in `target/release/websocat`. ### Rust versions |Websocat versions|Minimal Rust version|Maximal Rust version| |----|----|----| | 4.0.0-alpha1 | 1.84 | ? | | 1.12+ | ? | ? | | 1.9 - 1.11| 1.46 | maybe 1.63 | | 1.6 - 1.8 | 1.34 | maybe 1.63 | | 1.3 - 1.5 | 1.31 | 1.47? | | 1.2 | 1.28 | 1.47? | | 1.0-1.1 | 1.23 | 1.47? | | 1.2 | ? | ? | Note that building with legacy Rust version (e.g. 1.46) may require manually copying `Cargo.lock.legacy` to `Cargo.lock` prior to the building. Early non-async versions of Websocat should be buildable by even older rustc. Note that old versions of Websocat may misbehave if built by Rust 1.48 or later due to https://github.com/rust-lang/rust/pull/71274/. It may be not a good idea to build v1.x line of Websocat with Rust 1.64 due to [IP address representation refactor]. It may expose previously hidden undefined behaviour in legacy dependencies. (In practice, it seems to just work though - but a lot of time passed since I checked Websocat properly and in detail). [ipaddr]:https://github.com/rust-lang/rust/pull/78802 SSL on Android --- websocat's `wss://` may fail on Android. As a workaround, download certificate bundle, for example, from https://curl.haxx.se/ca/cacert.pem and specify it explicitly: SSL_CERT_FILE=cacert.pem /data/local/tmp/websocat wss://echo.websocket.org Or just use `--insecure` option. Documentation --- Basic usage examples are provided at the top of this README and in `--help` message. More tricks are described in [moreexamples.md](./moreexamples.md). There is a [list of all address types and overlays](doc.md).
`websocat --help=long` output ``` websocat 1.13.0 Vitaly "_Vi" Shukela Command-line client for web sockets, like netcat/curl/socat for ws://. USAGE: websocat ws://URL | wss://URL (simple client) websocat -s port (simple server) websocat [FLAGS] [OPTIONS] (advanced mode) FLAGS: --stdout-announce-listening-ports [A] Print a line to stdout for each port being listened --async-stdio [A] On UNIX, set stdin and stdout to nonblocking mode instead of spawning a thread. This should improve performance, but may break other programs running on the same console. --compress-deflate [A] Compress data coming to a WebSocket using deflate method. Affects only binary WebSocket messages. --compress-gzip [A] Compress data coming to a WebSocket using gzip method. Affects only binary WebSocket messages. --compress-zlib [A] Compress data coming to a WebSocket using zlib method. Affects only binary WebSocket messages. --crypto-reverse [A] Swap encryption and decryption operations in `crypto:` specifier - encrypt on read, decrypto on write. --dump-spec [A] Instead of running, dump the specifiers representation to stdout -e, --set-environment Set WEBSOCAT_* environment variables when doing exec:/cmd:/sh-c: Currently it's WEBSOCAT_URI and WEBSOCAT_CLIENT for request URI and client address (if TCP) Beware of ShellShock or similar security problems. -E, --exit-on-eof Close a data transfer direction if the other one reached EOF --foreachmsg-wait-read [A] Wait for reading to finish before closing foreachmsg:'s peer --jsonrpc Format messages you type as JSON RPC 2.0 method calls. First word becomes method name, the rest becomes parameters, possibly automatically wrapped in []. --jsonrpc-omit-jsonrpc [A] Omit `jsonrpc` field when using `--jsonrpc`, e.g. for Chromium --just-generate-key [A] Just a Sec-WebSocket-Key value without running main Websocat --lengthprefixed-little-endian [A] Use little-endian framing headers instead of big-endian for `lengthprefixed:` overlay. --lengthprefixed-skip-read-direction [A] Only affect one direction of the `lengthprefixed:` overlay, bypass tranformation for the other one. --lengthprefixed-skip-write-direction [A] Only affect one direction of the `lengthprefixed:` overlay, bypass tranformation for the other one. --linemode-strip-newlines [A] Don't include trailing \n or \r\n coming from streams in WebSocket messages -0, --null-terminated Use \0 instead of \n for linemode --no-line [A] Don't automatically insert line-to-message transformation --no-exit-on-zeromsg [A] Don't exit when encountered a zero message. Zero messages are used internally in Websocat, so it may fail to close connection at all. --no-fixups [A] Don't perform automatic command-line fixups. May destabilize websocat operation. Use --dump-spec without --no-fixups to discover what is being inserted automatically and read the full manual about Websocat internal workings. --no-async-stdio [A] Inhibit using stdin/stdout in a nonblocking way if it is not a tty -1, --one-message Send and/or receive only one message. Use with --no-close and/or -u/-U. --oneshot Serve only once. Not to be confused with -1 (--one-message) --print-ping-rtts Print measured round-trip-time to stderr after each received WebSocket pong. --exec-exit-on-disconnect [A] Make exec: or sh-c: or cmd: immediately exit when connection is closed, don't wait for termination. --exec-sighup-on-stdin-close [A] Make exec: or sh-c: or cmd: send SIGHUP on UNIX when input is closed. --exec-sighup-on-zero-msg [A] Make exec: or sh-c: or cmd: send SIGHUP on UNIX when facing incoming zero-length message. -q Suppress all diagnostic messages, except of startup errors --reuser-send-zero-msg-on-disconnect [A] Make reuse-raw: send a zero-length message to the peer when some clients disconnects. -s, --server-mode Simple server mode: specify TCP port or addr:port as single argument -S, --strict strict line/message mode: drop too long messages instead of splitting them, drop incomplete lines. --timestamp-monotonic [A] Use monotonic clock for `timestamp:` overlay -k, --insecure Accept invalid certificates and hostnames while connecting to TLS --udp-broadcast [A] Set SO_BROADCAST --udp-multicast-loop [A] Set IP[V6]_MULTICAST_LOOP --udp-oneshot [A] udp-listen: replies only one packet per client --udp-reuseaddr [A] Set SO_REUSEADDR for UDP socket. Listening TCP sockets are always reuseaddr. --uncompress-deflate [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. --uncompress-gzip [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. --uncompress-zlib [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. -u, --unidirectional Inhibit copying data in one direction -U, --unidirectional-reverse Inhibit copying data in the other direction (or maybe in both directions if combined with -u) --accept-from-fd [A] Do not call `socket(2)` in UNIX socket listener peer, start with `accept(2)` using specified file descriptor number as argument instead of filename --unlink [A] Unlink listening UNIX socket before binding to it -V, --version Prints version information -v Increase verbosity level to info or further -b, --binary Send message to WebSockets as binary messages -n, --no-close Don't send Close message to websocket on EOF --websocket-ignore-zeromsg [A] Silently drop incoming zero-length WebSocket messages. They may cause connection close due to usage of zero-len message as EOF flag inside Websocat. -t, --text Send message to WebSockets as text messages --base64 Encode incoming binary WebSocket messages in one-line Base64 If `--binary-prefix` (see `--help=full`) is set, outgoing WebSocket messages that start with the prefix are decoded from base64 prior to sending. --base64-text [A] Encode incoming text WebSocket messages in one-line Base64. I don't know whether it can be ever useful, but it's for symmetry with `--base64`. OPTIONS: --socks5 Use specified address:port as a SOCKS5 proxy. Note that proxy authentication is not supported yet. Example: --socks5 127.0.0.1:9050 --autoreconnect-delay-millis [A] Delay before reconnect attempt for `autoreconnect:` overlay. [default: 20] --basic-auth Add `Authorization: Basic` HTTP request header with this base64-encoded parameter --queue-len [A] Number of pending queued messages for broadcast reuser [default: 16] -B, --buffer-size Maximum message size, in bytes [default: 65536] --byte-to-exit-on [A] Override the byte which byte_to_exit_on: overlay looks for [default: 28] --client-pkcs12-der [A] Client identity TLS certificate --client-pkcs12-passwd [A] Password for --client-pkcs12-der pkcs12 archive. Required on Mac. --close-reason Close connection with a reason message. This option only takes effect if --close-status-code option is provided as well. --close-status-code Close connection with a status code. --crypto-key [A] Specify encryption/decryption key for `crypto:` specifier. Requires `base64:`, `file:` or `pwd:` prefix. -H, --header ... Add custom HTTP header to websocket client request. Separate header name and value with a colon and optionally a single space. Can be used multiple times. Note that single -H may eat multiple further arguments, leading to confusing errors. Specify headers at the end or with equal sign like -H='X: y'. --server-header ... Add custom HTTP header to websocket upgrade reply. Separate header name and value with a colon and optionally a single space. Can be used multiple times. Note that single -H may eat multiple further arguments, leading to confusing errors. --exec-args ... [A] Arguments for the `exec:` specifier. Must be the last option, everything after it gets into the exec args list. --header-to-env ... Forward specified incoming request header to H_* environment variable for `exec:`-like specifiers. -h, --help See the help. --help=short is the list of easy options and address types --help=long lists all options and types (see [A] markers) --help=doc also shows longer description and examples. --inhibit-pongs [A] Stop replying to incoming WebSocket pings after specified number of replies --just-generate-accept [A] Just a Sec-WebSocket-Accept value based on supplied Sec-WebSocket-Key value without running main Websocat --lengthprefixed-nbytes [A] Use this number of length header bytes for `lengthprefixed:` overlay. [default: 4] --max-messages Maximum number of messages to copy in one direction. --max-messages-rev Maximum number of messages to copy in the other direction. --conncap Maximum number of simultaneous connections for listening mode --max-sent-pings [A] Stop sending pings after this number of sent pings --max-ws-frame-length [A] Maximum size of incoming WebSocket frames, to prevent memory overflow [default: 104857600] --max-ws-message-length [A] Maximum size of incoming WebSocket messages (sans of one data frame), to prevent memory overflow [default: 209715200] --origin Add Origin HTTP header to websocket client request --pkcs12-der Pkcs12 archive needed to accept SSL connections, certificate and key. A command to output it: openssl pkcs12 -export -out output.pkcs12 -inkey key.pem -in cert.pem Use with -s (--server-mode) option or with manually specified TLS overlays. See moreexamples.md for more info. --pkcs12-passwd Password for --pkcs12-der pkcs12 archive. Required on Mac. -p, --preamble ... Prepend copied data with a specified string. Can be specified multiple times. -P, --preamble-reverse ... Prepend copied data with a specified string (reverse direction). Can be specified multiple times. --prometheus Expose Prometheus metrics on specified IP address and port in addition to running usual Websocat session --request-header ... [A] Specify HTTP request headers for `http-request:` specifier. -X, --request-method [A] Method to use for `http-request:` specifier --request-uri [A] URI to use for `http-request:` specifier --restrict-uri When serving a websocket, only accept the given URI, like `/ws` This liberates other URIs for things like serving static files or proxying. -F, --static-file ... Serve a named static file for non-websocket connections. Argument syntax: :: Argument example: /index.html:text/html:index.html Directories are not and will not be supported for security reasons. Can be specified multiple times. Recommended to specify them at the end or with equal sign like `-F=...`, otherwise this option may eat positional arguments --socks5-bind-script [A] Execute specified script in `socks5-bind:` mode when remote port number becomes known. --socks5-destination [A] Examples: 1.2.3.4:5678 2600:::80 hostname:5678 --tls-domain [A] Specify domain for SNI or certificate verification when using tls-connect: overlay --udp-multicast ... [A] Issue IP[V6]_ADD_MEMBERSHIP for specified multicast address. Can be specified multiple times. --udp-multicast-iface-v4 ... [A] IPv4 address of multicast network interface. Has to be either not specified or specified the same number of times as multicast IPv4 addresses. Order matters. --udp-multicast-iface-v6 ... [A] Index of network interface for IPv6 multicast. Has to be either not specified or specified the same number of times as multicast IPv6 addresses. Order matters. --udp-ttl [A] Set IP_TTL, also IP_MULTICAST_TTL if applicable --protocol Specify this Sec-WebSocket-Protocol: header when connecting --server-protocol Force this Sec-WebSocket-Protocol: header when accepting a connection --websocket-version Override the Sec-WebSocket-Version value --binary-prefix [A] Prepend specified text to each received WebSocket binary message. Also strip this prefix from outgoing messages, explicitly marking them as binary even if `--text` is specified --ws-c-uri [A] URI to use for ws-c: overlay [default: ws://0.0.0.0/] --ping-interval Send WebSocket pings each this number of seconds --ping-timeout Drop WebSocket connection if Pong message not received for this number of seconds --text-prefix [A] Prepend specified text to each received WebSocket text message. Also strip this prefix from outgoing messages, explicitly marking them as text even if `--binary` is specified ARGS: In simple mode, WebSocket URL to connect. In advanced mode first address (there are many kinds of addresses) to use. See --help=types for info about address types. If this is an address for listening, it will try serving multiple connections. In advanced mode, second address to connect. If this is an address for listening, it will accept only one connection. Basic examples: Command-line websocket client: websocat ws://ws.vi-server.org/mirror/ WebSocket server websocat -s 8080 WebSocket-to-TCP proxy: websocat --binary ws-l:127.0.0.1:8080 tcp:127.0.0.1:5678 Full list of address types: ws:// Insecure (ws://) WebSocket client. Argument is host and URL. wss:// Secure (wss://) WebSocket client. Argument is host and URL. ws-listen: WebSocket server. Argument is host and port to listen. inetd-ws: WebSocket inetd server. [A] l-ws-unix: WebSocket UNIX socket-based server. [A] l-ws-abstract: WebSocket abstract-namespaced UNIX socket server. [A] ws-lowlevel-client: [A] Low-level HTTP-independent WebSocket client connection without associated HTTP upgrade. ws-lowlevel-server: [A] Low-level HTTP-independent WebSocket server connection without associated HTTP upgrade. wss-listen: Listen for secure WebSocket connections on a TCP port http: [A] Issue HTTP request, receive a 1xx or 2xx reply, then pass asyncstdio: [A] Set stdin and stdout to nonblocking mode, then use it as a communication counterpart. UNIX-only. inetd: Like `asyncstdio:`, but intended for inetd(8) usage. [A] tcp: Connect to specified TCP host and port. Argument is a socket address. tcp-listen: Listen TCP port on specified address. ssl-listen: Listen for SSL connections on a TCP port sh-c: Start specified command line using `sh -c` (even on Windows) cmd: Start specified command line using `sh -c` or `cmd /C` (depending on platform) exec: Execute a program directly (without a subshell), providing array of arguments on Unix [A] readfile: Synchronously read a file. Argument is a file path. writefile: Synchronously truncate and write a file. appendfile: Synchronously append a file. udp: Send and receive packets to specified UDP socket, from random UDP port udp-listen: Bind an UDP socket to specified host:port, receive packet open-async: Open file for read and write and use it like a socket. [A] open-fd: Use specified file descriptor like a socket. [A] threadedstdio: [A] Stdin/stdout, spawning a thread (threaded version). - Read input from console, print to console. Uses threaded implementation even on UNIX unless requested by `--async-stdio` CLI option. unix: Connect to UNIX socket. Argument is filesystem path. [A] unix-listen: Listen for connections on a specified UNIX socket [A] unix-dgram: Send packets to one path, receive from the other. [A] abstract: Connect to UNIX abstract-namespaced socket. Argument is some string used as address. [A] abstract-listen: Listen for connections on a specified abstract UNIX socket [A] abstract-dgram: Send packets to one address, receive from the other. [A] mirror: Simply copy output to input. No arguments needed. literalreply: Reply with a specified string for each input packet. clogged: Do nothing. Don't read or write any bytes. Keep connections in "hung" state. [A] literal: Output a string, discard input. assert: Check the input. [A] assert2: Check the input. [A] seqpacket: Connect to AF_UNIX SOCK_SEQPACKET socket. Argument is a filesystem path. [A] seqpacket-listen: Listen for connections on a specified AF_UNIX SOCK_SEQPACKET socket [A] random: Generate random bytes when being read from, discard written bytes. Full list of overlays: ws-upgrade: WebSocket upgrader / raw server. Specify your own protocol instead of usual TCP. [A] http-request: [A] Issue HTTP request, receive a 1xx or 2xx reply, then pass http-post-sse: [A] Accept HTTP/1 request. Then, if it is GET, ssl-connect: Overlay to add TLS encryption atop of existing connection [A] ssl-accept: Accept an TLS connection using arbitrary backing stream. [A] reuse-raw: Reuse subspecifier for serving multiple clients: unpredictable mode. [A] broadcast: Reuse this connection for serving multiple clients, sending replies to all clients. autoreconnect: Re-establish underlying connection on any error or EOF ws-c: Low-level WebSocket connector. Argument is a some another address. [A] msg2line: Line filter: Turns messages from packet stream into lines of byte stream. [A] line2msg: Line filter: turn lines from byte stream into messages as delimited by '\\n' or '\\0' [A] lengthprefixed: Turn stream of bytes to/from data packets with length-prefixed framing. [A] foreachmsg: Execute something for each incoming message. log: Log each buffer as it pass though the underlying connector. jsonrpc: [A] Turns messages like `abc 1,2` into `{"jsonrpc":"2.0","id":412, "method":"abc", "params":[1,2]}`. timestamp: [A] Prepend timestamp to each incoming message. socks5-connect: SOCKS5 proxy client (raw) [A] socks5-bind: SOCKS5 proxy client (raw, bind command) [A] crypto: [A] Encrypts written messages and decrypts (and verifies) read messages with a static key, using ChaCha20-Poly1305 algorithm. prometheus: [A] Account connections, messages, bytes and other data and expose Prometheus metrics on a separate port. exit_on_specific_byte: [A] Turn specific byte into a EOF, allowing user to escape interactive Websocat session waitfordata: Wait for some data to pending being written before starting connecting. [A] ```
Some notes --- * IPv6 is supported, surround your IP in square brackets or use it as is, depending on context. * Web socket usage is not obligatory, you can use any specs on both sides. * Typically one line in binary stream correspond to one WebSocket text message. This is adjustable with options. Limitations --- * It only connects (or serves) HTTP/1. [RFC 8441](https://www.rfc-editor.org/rfc/rfc8441) or [RFC 9220](https://www.rfc-editor.org/rfc/rfc9220.html) are not currently supported. * It is not convenient when text and binary WebSocket messages are mixed. This affects `mirror:` specifier, making it a bit different from ws://echo.websocket.org. There are `--binary-prefix`, `--text-prefix` and `--base64` options to handle mixture of binary and text. * Current version of Websocat don't receive notification about closed sockets. This makes serving without `-E` or `-u` options or in backpressure scenarios prone to socket leak. * Readline is not integrated. Users are advices to wrap websocat using [`rlwrap`](https://linux.die.net/man/1/rlwrap) tool for more convenient CLI. * Build process of current version of Websocat is not properly automated and is fragile. * Main version (v1) is based on obsolete dependency versions that trigger security warnings and may become tricky to build. There is [new version (v4)](https://github.com/vi/websocat/tree/websocat4), but is not yet considered stable and [has many missing features](https://github.com/vi/websocat/issues/276). Bindings --- * Node.js: [`seamapi/node-websocat`](https://github.com/seamapi/node-websocat) See also --- * [wstunnel](https://github.com/erebe/wstunnel) * [wscat](https://github.com/websockets/wscat) * [websocketd](https://github.com/joewalnes/websocketd) * [wsd](https://github.com/alexanderGugel/wsd) ================================================ FILE: doc.md ================================================ # Websocat Reference (in progress) Websocat has many command-line options and special format for positional arguments. There are three main modes of websocat invocation: * Simple client mode: `websocat wss://your.server/url` * Simple server mode: `websocat -s 127.0.0.1:8080` * Advanced [socat][1]-like mode: `websocat -t ws-l:127.0.0.1:8080 mirror:` Ultimately in any of those modes websocat creates two connections and exchanges data between them. If one of the connections is bytestream-oriented (for example the terminal stdin/stdout or a TCP connection), but the other is message-oriented (for example, a WebSocket or UDP) then websocat operates in lines: each line correspond to a message. Details of this are configurable by various options. `ws-l:` or `mirror:` above are examples of address types. With the exception of special cases like WebSocket URL `ws://1.2.3.4/` or stdio `-`, websocat's positional argument is defined by this rule: ``` ::= ( ":" )* ":" [address] ``` Some address types may be "aliases" to other address types or combinations of overlays and address types. [1]:http://www.dest-unreach.org/socat/doc/socat.html # `--help=long` "Advanced" options and flags are denoted by `[A]` marker. ``` websocat 1.14.0 Vitaly "_Vi" Shukela Command-line client for web sockets, like netcat/curl/socat for ws://. USAGE: websocat ws://URL | wss://URL (simple client) websocat -s port (simple server) websocat [FLAGS] [OPTIONS] (advanced mode) FLAGS: --stdout-announce-listening-ports [A] Print a line to stdout for each port being listened --async-stdio [A] On UNIX, set stdin and stdout to nonblocking mode instead of spawning a thread. This should improve performance, but may break other programs running on the same console. --compress-deflate [A] Compress data coming to a WebSocket using deflate method. Affects only binary WebSocket messages. --compress-gzip [A] Compress data coming to a WebSocket using gzip method. Affects only binary WebSocket messages. --compress-zlib [A] Compress data coming to a WebSocket using zlib method. Affects only binary WebSocket messages. --crypto-reverse [A] Swap encryption and decryption operations in `crypto:` specifier - encrypt on read, decrypto on write. --dump-spec [A] Instead of running, dump the specifiers representation to stdout -e, --set-environment Set WEBSOCAT_* environment variables when doing exec:/cmd:/sh-c: Currently it's WEBSOCAT_URI and WEBSOCAT_CLIENT for request URI and client address (if TCP) Beware of ShellShock or similar security problems. -E, --exit-on-eof Close a data transfer direction if the other one reached EOF --foreachmsg-wait-read [A] Wait for reading to finish before closing foreachmsg:'s peer --jsonrpc Format messages you type as JSON RPC 2.0 method calls. First word becomes method name, the rest becomes parameters, possibly automatically wrapped in []. --jsonrpc-omit-jsonrpc [A] Omit `jsonrpc` field when using `--jsonrpc`, e.g. for Chromium --just-generate-key [A] Just a Sec-WebSocket-Key value without running main Websocat --lengthprefixed-little-endian [A] Use little-endian framing headers instead of big-endian for `lengthprefixed:` overlay. --lengthprefixed-skip-read-direction [A] Only affect one direction of the `lengthprefixed:` overlay, bypass tranformation for the other one. --lengthprefixed-skip-write-direction [A] Only affect one direction of the `lengthprefixed:` overlay, bypass tranformation for the other one. --linemode-strip-newlines [A] Don't include trailing \n or \r\n coming from streams in WebSocket messages -0, --null-terminated Use \0 instead of \n for linemode --no-line [A] Don't automatically insert line-to-message transformation --no-exit-on-zeromsg [A] Don't exit when encountered a zero message. Zero messages are used internally in Websocat, so it may fail to close connection at all. --no-fixups [A] Don't perform automatic command-line fixups. May destabilize websocat operation. Use --dump-spec without --no-fixups to discover what is being inserted automatically and read the full manual about Websocat internal workings. --no-async-stdio [A] Inhibit using stdin/stdout in a nonblocking way if it is not a tty -1, --one-message Send and/or receive only one message. Use with --no-close and/or -u/-U. --oneshot Serve only once. Not to be confused with -1 (--one-message) --print-ping-rtts Print measured round-trip-time to stderr after each received WebSocket pong. --exec-exit-on-disconnect [A] Make exec: or sh-c: or cmd: immediately exit when connection is closed, don't wait for termination. --exec-sighup-on-stdin-close [A] Make exec: or sh-c: or cmd: send SIGHUP on UNIX when input is closed. --exec-sighup-on-zero-msg [A] Make exec: or sh-c: or cmd: send SIGHUP on UNIX when facing incoming zero-length message. -q Suppress all diagnostic messages, except of startup errors --reuser-send-zero-msg-on-disconnect [A] Make reuse-raw: send a zero-length message to the peer when some clients disconnects. -s, --server-mode Simple server mode: specify TCP port or addr:port as single argument -S, --strict strict line/message mode: drop too long messages instead of splitting them, drop incomplete lines. --timestamp-monotonic [A] Use monotonic clock for `timestamp:` overlay -k, --insecure Accept invalid certificates and hostnames while connecting to TLS --udp-broadcast [A] Set SO_BROADCAST --udp-multicast-loop [A] Set IP[V6]_MULTICAST_LOOP --udp-oneshot [A] udp-listen: replies only one packet per client --udp-reuseaddr [A] Set SO_REUSEADDR for UDP socket. Listening TCP sockets are always reuseaddr. --uncompress-deflate [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. --uncompress-gzip [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. --uncompress-zlib [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. -u, --unidirectional Inhibit copying data in one direction -U, --unidirectional-reverse Inhibit copying data in the other direction (or maybe in both directions if combined with -u) --accept-from-fd [A] Do not call `socket(2)` in UNIX socket listener peer, start with `accept(2)` using specified file descriptor number as argument instead of filename --unlink [A] Unlink listening UNIX socket before binding to it -V, --version Prints version information -v Increase verbosity level to info or further -b, --binary Send message to WebSockets as binary messages -n, --no-close Don't send Close message to websocket on EOF --websocket-ignore-zeromsg [A] Silently drop incoming zero-length WebSocket messages. They may cause connection close due to usage of zero-len message as EOF flag inside Websocat. -t, --text Send message to WebSockets as text messages --base64 Encode incoming binary WebSocket messages in one-line Base64 If `--binary-prefix` (see `--help=full`) is set, outgoing WebSocket messages that start with the prefix are decoded from base64 prior to sending. --base64-text [A] Encode incoming text WebSocket messages in one-line Base64. I don't know whether it can be ever useful, but it's for symmetry with `--base64`. OPTIONS: --socks5 Use specified address:port as a SOCKS5 proxy. Example: --socks5 127.0.0.1:9050 --autoreconnect-delay-millis [A] Delay before reconnect attempt for `autoreconnect:` overlay. [default: 20] --basic-auth Add `Authorization: Basic` HTTP request header with this base64-encoded parameter. Also available as `WEBSOCAT_BASIC_AUTH` environment variable --basic-auth-file Add `Authorization: Basic` HTTP request header base64-encoded content of the specified file --queue-len [A] Number of pending queued messages for broadcast reuser [default: 16] -B, --buffer-size Maximum message size, in bytes [default: 65536] --byte-to-exit-on [A] Override the byte which byte_to_exit_on: overlay looks for [default: 28] --client-pkcs12-der [A] Client identity TLS certificate --client-pkcs12-passwd [A] Password for --client-pkcs12-der pkcs12 archive. Required on Mac. --close-reason Close connection with a reason message. This option only takes effect if --close-status-code option is provided as well. --close-status-code Close connection with a status code. --crypto-key [A] Specify encryption/decryption key for `crypto:` specifier. Requires `base64:`, `file:` or `pwd:` prefix. -H, --header ... Add custom HTTP header to websocket client request. Separate header name and value with a colon and optionally a single space. Can be used multiple times. Note that single -H may eat multiple further arguments, leading to confusing errors. Specify headers at the end or with equal sign like -H='X: y'. --server-header ... Add custom HTTP header to websocket upgrade reply. Separate header name and value with a colon and optionally a single space. Can be used multiple times. Note that single -H may eat multiple further arguments, leading to confusing errors. --exec-args ... [A] Arguments for the `exec:` specifier. Must be the last option, everything after it gets into the exec args list. --header-to-env ... Forward specified incoming request header to H_* environment variable for `exec:`-like specifiers. -h, --help See the help. --help=short is the list of easy options and address types --help=long lists all options and types (see [A] markers) --help=doc also shows longer description and examples. --inhibit-pongs [A] Stop replying to incoming WebSocket pings after specified number of replies --just-generate-accept [A] Just a Sec-WebSocket-Accept value based on supplied Sec-WebSocket-Key value without running main Websocat --lengthprefixed-nbytes [A] Use this number of length header bytes for `lengthprefixed:` overlay. [default: 4] --max-messages Maximum number of messages to copy in one direction. --max-messages-rev Maximum number of messages to copy in the other direction. --conncap Maximum number of simultaneous connections for listening mode --max-sent-pings [A] Stop sending pings after this number of sent pings --max-ws-frame-length [A] Maximum size of incoming WebSocket frames, to prevent memory overflow [default: 104857600] --max-ws-message-length [A] Maximum size of incoming WebSocket messages (sans of one data frame), to prevent memory overflow [default: 209715200] --origin Add Origin HTTP header to websocket client request --pkcs12-der Pkcs12 archive needed to accept SSL connections, certificate and key. A command to output it: openssl pkcs12 -export -out output.pkcs12 -inkey key.pem -in cert.pem Use with -s (--server-mode) option or with manually specified TLS overlays. See moreexamples.md for more info. --pkcs12-passwd Password for --pkcs12-der pkcs12 archive. Required on Mac. -p, --preamble ... Prepend copied data with a specified string. Can be specified multiple times. -P, --preamble-reverse ... Prepend copied data with a specified string (reverse direction). Can be specified multiple times. --prometheus Expose Prometheus metrics on specified IP address and port in addition to running usual Websocat session --request-header ... [A] Specify HTTP request headers for `http-request:` specifier. -X, --request-method [A] Method to use for `http-request:` specifier --request-uri [A] URI to use for `http-request:` specifier --restrict-uri When serving a websocket, only accept the given URI, like `/ws` This liberates other URIs for things like serving static files or proxying. -F, --static-file ... Serve a named static file for non-websocket connections. Argument syntax: :: Argument example: /index.html:text/html:index.html Directories are not and will not be supported for security reasons. Can be specified multiple times. Recommended to specify them at the end or with equal sign like `-F=...`, otherwise this option may eat positional arguments --socks5-bind-script [A] Execute specified script in `socks5-bind:` mode when remote port number becomes known. --socks5-user-pass [A] Specify username:password for SOCKS5 proxy. If not specified, the default is to use no authentication. --socks5-destination [A] Examples: 1.2.3.4:5678 2600:::80 hostname:5678 --tls-domain [A] Specify domain for SNI or certificate verification when using tls-connect: overlay --udp-multicast ... [A] Issue IP[V6]_ADD_MEMBERSHIP for specified multicast address. Can be specified multiple times. --udp-multicast-iface-v4 ... [A] IPv4 address of multicast network interface. Has to be either not specified or specified the same number of times as multicast IPv4 addresses. Order matters. --udp-multicast-iface-v6 ... [A] Index of network interface for IPv6 multicast. Has to be either not specified or specified the same number of times as multicast IPv6 addresses. Order matters. --udp-ttl [A] Set IP_TTL, also IP_MULTICAST_TTL if applicable --ua Set `User-Agent` request header to this value. Similar to setting it with `-H`. --protocol Specify this Sec-WebSocket-Protocol: header when connecting --server-protocol Force this Sec-WebSocket-Protocol: header when accepting a connection --websocket-version Override the Sec-WebSocket-Version value --binary-prefix [A] Prepend specified text to each received WebSocket binary message. Also strip this prefix from outgoing messages, explicitly marking them as binary even if `--text` is specified --ws-c-uri [A] URI to use for ws-c: overlay [default: ws://0.0.0.0/] --ping-interval Send WebSocket pings each this number of seconds --ping-timeout Drop WebSocket connection if Pong message not received for this number of seconds --text-prefix [A] Prepend specified text to each received WebSocket text message. Also strip this prefix from outgoing messages, explicitly marking them as text even if `--binary` is specified ARGS: In simple mode, WebSocket URL to connect. In advanced mode first address (there are many kinds of addresses) to use. See --help=types for info about address types. If this is an address for listening, it will try serving multiple connections. In advanced mode, second address to connect. If this is an address for listening, it will accept only one connection. Basic examples: Command-line websocket client: websocat ws://ws.vi-server.org/mirror/ WebSocket server websocat -s 8080 WebSocket-to-TCP proxy: websocat --binary ws-l:127.0.0.1:8080 tcp:127.0.0.1:5678 ``` # Full list of address types "Advanced" address types are denoted by `[A]` marker. ### `ws://` Internal name for --dump-spec: WsClient Insecure (ws://) WebSocket client. Argument is host and URL. Example: connect to public WebSocket loopback and copy binary chunks from stdin to the websocket. websocat - ws://echo.websocket.org/ ### `wss://` Internal name for --dump-spec: WsClientSecure Secure (wss://) WebSocket client. Argument is host and URL. Example: forward TCP port 4554 to a websocket websocat tcp-l:127.0.0.1:4554 wss://127.0.0.1/some_websocket ### `ws-listen:` Aliases: `ws-l:`, `l-ws:`, `listen-ws:` Internal name for --dump-spec: WsTcpServer WebSocket server. Argument is host and port to listen. Example: Dump all incoming websocket data to console websocat ws-l:127.0.0.1:8808 - Example: the same, but more verbose: websocat ws-l:tcp-l:127.0.0.1:8808 reuse:- ### `inetd-ws:` Aliases: `ws-inetd:` Internal name for --dump-spec: WsInetdServer WebSocket inetd server. [A] TODO: transfer the example here ### `l-ws-unix:` Internal name for --dump-spec: WsUnixServer WebSocket UNIX socket-based server. [A] ### `l-ws-abstract:` Internal name for --dump-spec: WsAbstractUnixServer WebSocket abstract-namespaced UNIX socket server. [A] ### `ws-lowlevel-client:` Aliases: `ws-ll-client:`, `ws-ll-c:` Internal name for --dump-spec: WsLlClient [A] Low-level HTTP-independent WebSocket client connection without associated HTTP upgrade. Example: TODO ### `ws-lowlevel-server:` Aliases: `ws-ll-server:`, `ws-ll-s:` Internal name for --dump-spec: WsLlServer [A] Low-level HTTP-independent WebSocket server connection without associated HTTP upgrade. Example: TODO ### `wss-listen:` Aliases: `wss-l:`, `l-wss:`, `wss-listen:` Internal name for --dump-spec: WssListen Listen for secure WebSocket connections on a TCP port Example: wss:// echo server + client for testing websocat -E -t --pkcs12-der=q.pkcs12 wss-listen:127.0.0.1:1234 mirror: websocat --ws-c-uri=wss://localhost/ -t - ws-c:cmd:'socat - ssl:127.0.0.1:1234,verify=0' See [moreexamples.md](./moreexamples.md) for info about generation of `q.pkcs12`. ### `http:` Internal name for --dump-spec: Http [A] Issue HTTP request, receive a 1xx or 2xx reply, then pass the torch to outer peer, if any - highlevel version. Content you write becomes body, content you read is body that server has sent. URI is specified inline. Example: websocat -b - http://example.com < /dev/null ### `asyncstdio:` Internal name for --dump-spec: AsyncStdio [A] Set stdin and stdout to nonblocking mode, then use it as a communication counterpart. UNIX-only. May cause problems with programs running at the same terminal. This specifier backs the `--async-stdio` CLI option. Typically this specifier can be specified only one time. Example: simulate `cat(1)`. This is an exception from "only one time" rule above: websocat - - Example: SSH transport ssh -c ProxyCommand='websocat asyncstdio: ws://myserver/mywebsocket' user@myserver ### `inetd:` Internal name for --dump-spec: Inetd Like `asyncstdio:`, but intended for inetd(8) usage. [A] Automatically enables `-q` (`--quiet`) mode. `inetd-ws:` - is of `ws-l:inetd:` Example of inetd.conf line that makes it listen for websocket connections on port 1234 and redirect the data to local SSH server. 1234 stream tcp nowait myuser /opt/websocat websocat inetd-ws: tcp:127.0.0.1:22 ### `tcp:` Aliases: `tcp-connect:`, `connect-tcp:`, `tcp-c:`, `c-tcp:` Internal name for --dump-spec: TcpConnect Connect to specified TCP host and port. Argument is a socket address. Example: simulate netcat netcat websocat - tcp:127.0.0.1:22 Example: redirect websocket connections to local SSH server over IPv6 websocat ws-l:0.0.0.0:8084 tcp:[::1]:22 ### `tcp-listen:` Aliases: `listen-tcp:`, `tcp-l:`, `l-tcp:` Internal name for --dump-spec: TcpListen Listen TCP port on specified address. Example: echo server websocat tcp-l:0.0.0.0:1441 mirror: Example: redirect TCP to a websocket websocat tcp-l:0.0.0.0:8088 ws://echo.websocket.org ### `ssl-listen:` Aliases: `ssl-l:`, `tls-l:`, `tls-listen:`, `l-ssl:`, `listen-ssl:`, `listen-tls:`, `listen-tls:` Internal name for --dump-spec: TlsListen Listen for SSL connections on a TCP port Example: Non-websocket SSL echo server websocat -E -b --pkcs12-der=q.pkcs12 ssl-listen:127.0.0.1:1234 mirror: socat - ssl:127.0.0.1:1234,verify=0 ### `sh-c:` Internal name for --dump-spec: ShC Start specified command line using `sh -c` (even on Windows) Example: serve a counter websocat -U ws-l:127.0.0.1:8008 sh-c:'for i in 0 1 2 3 4 5 6 7 8 9 10; do echo $i; sleep 1; done' Example: unauthenticated shell websocat --exit-on-eof ws-l:127.0.0.1:5667 sh-c:'bash -i 2>&1' ### `cmd:` Internal name for --dump-spec: Cmd Start specified command line using `sh -c` or `cmd /C` (depending on platform) Otherwise should be the the same as `sh-c:` (see examples from there). ### `exec:` Internal name for --dump-spec: Exec Execute a program directly (without a subshell), providing array of arguments on Unix [A] Example: Serve current date websocat -U ws-l:127.0.0.1:5667 exec:date Example: pinger websocat -U ws-l:127.0.0.1:5667 exec:ping --exec-args 127.0.0.1 -c 1 ### `readfile:` Internal name for --dump-spec: ReadFile Synchronously read a file. Argument is a file path. Blocking on operations with the file pauses the whole process Example: Serve the file once per connection, ignore all replies. websocat ws-l:127.0.0.1:8000 readfile:hello.json ### `writefile:` Internal name for --dump-spec: WriteFile Synchronously truncate and write a file. Blocking on operations with the file pauses the whole process Example: websocat ws-l:127.0.0.1:8000 writefile:data.txt ### `appendfile:` Internal name for --dump-spec: AppendFile Synchronously append a file. Blocking on operations with the file pauses the whole process Example: Logging all incoming data from WebSocket clients to one file websocat -u ws-l:127.0.0.1:8000 reuse:appendfile:log.txt ### `udp:` Aliases: `udp-connect:`, `connect-udp:`, `udp-c:`, `c-udp:` Internal name for --dump-spec: UdpConnect Send and receive packets to specified UDP socket, from random UDP port ### `udp-listen:` Aliases: `listen-udp:`, `udp-l:`, `l-udp:` Internal name for --dump-spec: UdpListen Bind an UDP socket to specified host:port, receive packet from any remote UDP socket, send replies to recently observed remote UDP socket. Note that it is not a multiconnect specifier like e.g. `tcp-listen`: entire lifecycle of the UDP socket is the same connection. File a feature request on Github if you want proper DNS-like request-reply UDP mode here. ### `open-async:` Internal name for --dump-spec: OpenAsync Open file for read and write and use it like a socket. [A] Not for regular files, see readfile/writefile instead. Example: Serve big blobs of random data to clients websocat -U ws-l:127.0.0.1:8088 open-async:/dev/urandom ### `open-fd:` Internal name for --dump-spec: OpenFdAsync Use specified file descriptor like a socket. [A] Example: Serve random data to clients v2 websocat -U ws-l:127.0.0.1:8088 reuse:open-fd:55 55< /dev/urandom ### `threadedstdio:` Internal name for --dump-spec: ThreadedStdio [A] Stdin/stdout, spawning a thread (threaded version). Like `-`, but forces threaded mode instead of async mode Use when standard input is not `epoll(7)`-able or you want to avoid setting it to nonblocking mode. ### `-` Aliases: `stdio:` Internal name for --dump-spec: Stdio Read input from console, print to console. Uses threaded implementation even on UNIX unless requested by `--async-stdio` CLI option. Typically this specifier can be specified only one time. Example: simulate `cat(1)`. This is an exception from "only one time" rule above: websocat - - Example: SSH transport ssh -c ProxyCommand='websocat - ws://myserver/mywebsocket' user@myserver ### `unix:` Aliases: `unix-connect:`, `connect-unix:`, `unix-c:`, `c-unix:` Internal name for --dump-spec: UnixConnect Connect to UNIX socket. Argument is filesystem path. [A] Example: forward connections from websockets to a UNIX stream socket websocat ws-l:127.0.0.1:8088 unix:the_socket ### `unix-listen:` Aliases: `listen-unix:`, `unix-l:`, `l-unix:` Internal name for --dump-spec: UnixListen Listen for connections on a specified UNIX socket [A] Example: forward connections from a UNIX socket to a WebSocket websocat --unlink unix-l:the_socket ws://127.0.0.1:8089 Example: Accept forwarded WebSocket connections from Nginx umask 0000 websocat --unlink -b -E ws-u:unix-l:/tmp/wstest tcp:[::]:22 Nginx config: location /ws { proxy_read_timeout 7d; proxy_send_timeout 7d; #proxy_pass http://localhost:3012; proxy_pass http://unix:/tmp/wstest; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } This configuration allows to make Nginx responsible for SSL and also it can choose which connections to forward to websocat based on URLs. Obviously, Nginx can also redirect to TCP-listening websocat just as well - UNIX sockets are not a requirement for this feature. See `moreexamples.md` for SystemD usage (untested). TODO: --chmod option? ### `unix-dgram:` Internal name for --dump-spec: UnixDgram Send packets to one path, receive from the other. [A] A socket for sending must be already opened. I don't know if this mode has any use, it is here just for completeness. Example: socat unix-recv:./sender -& websocat - unix-dgram:./receiver:./sender ### `abstract:` Aliases: `abstract-connect:`, `connect-abstract:`, `abstract-c:`, `c-abstract:` Internal name for --dump-spec: AbstractConnect Connect to UNIX abstract-namespaced socket. Argument is some string used as address. [A] Too long addresses may be silently chopped off. Example: forward connections from websockets to an abstract stream socket websocat ws-l:127.0.0.1:8088 abstract:the_socket Note that abstract-namespaced Linux sockets may not be normally supported by Rust, so non-prebuilt versions may have problems with them. ### `abstract-listen:` Aliases: `listen-abstract:`, `abstract-l:`, `l-abstract:` Internal name for --dump-spec: AbstractListen Listen for connections on a specified abstract UNIX socket [A] Example: forward connections from an abstract UNIX socket to a WebSocket websocat abstract-l:the_socket ws://127.0.0.1:8089 Note that abstract-namespaced Linux sockets may not be normally supported by Rust, so non-prebuilt versions may have problems with them. ### `abstract-dgram:` Internal name for --dump-spec: AbstractDgram Send packets to one address, receive from the other. [A] A socket for sending must be already opened. I don't know if this mode has any use, it is here just for completeness. Example (untested): websocat - abstract-dgram:receiver_addr:sender_addr Note that abstract-namespaced Linux sockets may not be normally supported by Rust, so non-prebuilt versions may have problems with them. In particular, this mode may fail to work without `workaround1` Cargo feature. ### `mirror:` Internal name for --dump-spec: Mirror Simply copy output to input. No arguments needed. Example: emulate echo.websocket.org websocat -t ws-l:127.0.0.1:1234 mirror: ### `literalreply:` Internal name for --dump-spec: LiteralReply Reply with a specified string for each input packet. Example: websocat ws-l:0.0.0.0:1234 literalreply:'{"status":"OK"}' ### `clogged:` Internal name for --dump-spec: Clogged Do nothing. Don't read or write any bytes. Keep connections in "hung" state. [A] ### `literal:` Internal name for --dump-spec: Literal Output a string, discard input. Example: websocat ws-l:127.0.0.1:8080 literal:'{ "hello":"world"} ' ### `assert:` Internal name for --dump-spec: Assert Check the input. [A] Read entire input and panic the program if the input is not equal to the specified string. Used in tests. ### `assert2:` Internal name for --dump-spec: Assert2 Check the input. [A] Read entire input and emit an error if the input is not equal to the specified string. ### `seqpacket:` Aliases: `seqpacket-connect:`, `connect-seqpacket:`, `seqpacket-c:`, `c-seqpacket:` Internal name for --dump-spec: SeqpacketConnect Connect to AF_UNIX SOCK_SEQPACKET socket. Argument is a filesystem path. [A] Start the path with `@` character to make it connect to abstract-namespaced socket instead. Too long paths are silently truncated. Example: forward connections from websockets to a UNIX seqpacket abstract socket websocat ws-l:127.0.0.1:1234 seqpacket:@test ### `seqpacket-listen:` Aliases: `listen-seqpacket:`, `seqpacket-l:`, `l-seqpacket:` Internal name for --dump-spec: SeqpacketListen Listen for connections on a specified AF_UNIX SOCK_SEQPACKET socket [A] Start the path with `@` character to make it connect to abstract-namespaced socket instead. Too long (>=108 bytes) paths are silently truncated. Example: forward connections from a UNIX seqpacket socket to a WebSocket websocat --unlink seqpacket-l:the_socket ws://127.0.0.1:8089 ### `random:` Internal name for --dump-spec: Random Generate random bytes when being read from, discard written bytes. websocat -b random: ws://127.0.0.1/flood # Full list of overlays "Advanced" overlays denoted by `[A]` marker. ### `ws-upgrade:` Aliases: `upgrade-ws:`, `ws-u:`, `u-ws:` Internal name for --dump-spec: WsServer WebSocket upgrader / raw server. Specify your own protocol instead of usual TCP. [A] All other WebSocket server modes actually use this overlay under the hood. Example: serve incoming connection from socat socat tcp-l:1234,fork,reuseaddr exec:'websocat -t ws-u\:stdio\: mirror\:' ### `http-request:` Internal name for --dump-spec: HttpRequest [A] Issue HTTP request, receive a 1xx or 2xx reply, then pass the torch to outer peer, if any - lowlevel version. Content you write becomes body, content you read is body that server has sent. URI is specified using a separate command-line parameter Example: websocat -Ub - http-request:tcp:example.com:80 --request-uri=http://example.com/ --request-header 'Connection: close' ### `http-post-sse:` Internal name for --dump-spec: HttpPostSse [A] Accept HTTP/1 request. Then, if it is GET, unidirectionally return incoming messages as server-sent events (SSE). If it is POST then, also unidirectionally, write body upstream. Example - turn SSE+POST pair into a client WebSocket connection: websocat -E -t http-post-sse:tcp-l:127.0.0.1:8080 reuse:ws://127.0.0.1:80/websock `curl -dQQQ http://127.0.0.1:8080/` would send into it and `curl -N http://127.0.0.1:8080/` would recv from it. ### `ssl-connect:` Aliases: `ssl-c:`, `ssl:`, `tls:`, `tls-connect:`, `tls-c:`, `c-ssl:`, `connect-ssl:`, `c-tls:`, `connect-tls:` Internal name for --dump-spec: TlsConnect Overlay to add TLS encryption atop of existing connection [A] Example: manually connect to a secure websocket websocat -t - ws-c:tls-c:tcp:174.129.224.73:1080 --ws-c-uri ws://echo.websocket.org --tls-domain echo.websocket.org For a user-friendly solution, see --socks5 command-line option ### `ssl-accept:` Aliases: `ssl-a:`, `tls-a:`, `tls-accept:`, `a-ssl:`, `accept-ssl:`, `accept-tls:`, `accept-tls:` Internal name for --dump-spec: TlsAccept Accept an TLS connection using arbitrary backing stream. [A] Example: The same as in TlsListenClass's example, but with manual acceptor websocat -E -b --pkcs12-der=q.pkcs12 tls-a:tcp-l:127.0.0.1:1234 mirror: ### `reuse-raw:` Aliases: `raw-reuse:` Internal name for --dump-spec: Reuser Reuse subspecifier for serving multiple clients: unpredictable mode. [A] Better used with --unidirectional, otherwise replies get directed to random connected client. Example: Forward multiple parallel WebSocket connections to a single persistent TCP connection websocat -u ws-l:0.0.0.0:8800 reuse:tcp:127.0.0.1:4567 Example (unreliable): don't disconnect SSH when websocket reconnects websocat ws-l:[::]:8088 reuse:tcp:127.0.0.1:22 ### `broadcast:` Aliases: `reuse:`, `reuse-broadcast:`, `broadcast-reuse:` Internal name for --dump-spec: BroadcastReuser Reuse this connection for serving multiple clients, sending replies to all clients. Messages from any connected client get directed to inner connection, replies from the inner connection get duplicated across all connected clients (and are dropped if there are none). If WebSocket client is too slow for accepting incoming data, messages get accumulated up to the configurable --broadcast-buffer, then dropped. Example: Simple data exchange between connected WebSocket clients websocat -E ws-l:0.0.0.0:8800 reuse-broadcast:mirror: ### `autoreconnect:` Internal name for --dump-spec: AutoReconnect Re-establish underlying connection on any error or EOF Example: keep connecting to the port or spin 100% CPU trying if it is closed. websocat - autoreconnect:tcp:127.0.0.1:5445 Example: keep remote logging connection open (or flood the host if port is closed): websocat -u ws-l:0.0.0.0:8080 reuse:autoreconnect:tcp:192.168.0.3:1025 TODO: implement delays between reconnect attempts ### `ws-c:` Aliases: `c-ws:`, `ws-connect:`, `connect-ws:` Internal name for --dump-spec: WsConnect Low-level WebSocket connector. Argument is a some another address. [A] URL and Host: header being sent are independent from the underlying connection. Example: connect to echo server in more explicit way websocat --ws-c-uri=ws://echo.websocket.org/ - ws-c:tcp:174.129.224.73:80 Example: connect to echo server, observing WebSocket TCP packet exchange websocat --ws-c-uri=ws://echo.websocket.org/ - ws-c:cmd:"socat -v -x - tcp:174.129.224.73:80" ### `msg2line:` Internal name for --dump-spec: Message2Line Line filter: Turns messages from packet stream into lines of byte stream. [A] Ensure each message (a chunk from one read call from underlying connection) contains no inner newlines (or zero bytes) and terminates with one newline. Reverse of the `line2msg:`. Unless --null-terminated, replaces both newlines (\x0A) and carriage returns (\x0D) with spaces (\x20) for each read. Does not affect writing at all. Use this specifier on both ends to get bi-directional behaviour. Automatically inserted by --line option on top of the stack containing a websocket. Example: TODO ### `line2msg:` Internal name for --dump-spec: Line2Message Line filter: turn lines from byte stream into messages as delimited by '\\n' or '\\0' [A] Ensure that each message (a successful read call) is obtained from a line [A] coming from underlying specifier, buffering up or splitting content as needed. Reverse of the `msg2line:`. Does not affect writing at all. Use this specifier on both ends to get bi-directional behaviour. Automatically inserted by --line option at the top of the stack opposite to websocket-containing stack. Example: TODO ### `lengthprefixed:` Internal name for --dump-spec: LengthPrefixed Turn stream of bytes to/from data packets with length-prefixed framing. [A] You can choose the number of header bytes (1 to 8) and endianness. Default is 4 bytes big endian. This affects both reading and writing - attach this overlay to stream specifier to turn it into a packet-orineted specifier. Mind the buffer size (-B). All packets should fit in there. Examples: websocat -u -b udp-l:127.0.0.1:1234 lengthprefixed:writefile:test.dat websocat -u -b lengthprefixed:readfile:test.dat udp:127.0.0.1:1235 This would save incoming UDP packets to a file, then replay the datagrams back to UDP socket websocat -b lengthprefixed:- ws://127.0.0.1:1234/ --binary-prefix=B --text-prefix=T This allows to mix and match text and binary WebSocket messages to and from stdio without the base64 overhead. ### `foreachmsg:` Internal name for --dump-spec: Foreachmsg Execute something for each incoming message. Somewhat the reverse of the `autoreconnect:`. Example: websocat -t -u ws://server/listen_for_updates foreachmsg:writefile:status.txt This keeps only recent incoming message in file and discards earlier messages. ### `log:` Internal name for --dump-spec: Log Log each buffer as it pass though the underlying connector. If you increase the logging level, you will also see hex buffers. Example: view WebSocket handshake and traffic on the way to echo.websocket.org websocat -t - ws-c:log:tcp:127.0.0.1:1080 --ws-c-uri ws://echo.websocket.org ### `jsonrpc:` Internal name for --dump-spec: JsonRpc [A] Turns messages like `abc 1,2` into `{"jsonrpc":"2.0","id":412, "method":"abc", "params":[1,2]}`. For simpler manual testing of websocket-based JSON-RPC services Example: TODO ### `timestamp:` Internal name for --dump-spec: Timestamp [A] Prepend timestamp to each incoming message. Example: TODO ### `socks5-connect:` Internal name for --dump-spec: SocksProxy SOCKS5 proxy client (raw) [A] Example: connect to a websocket using local `ssh -D` proxy websocat -t - ws-c:socks5-connect:tcp:127.0.0.1:1080 --socks5-destination echo.websocket.org:80 --ws-c-uri ws://echo.websocket.org For a user-friendly solution, see --socks5 command-line option ### `socks5-bind:` Internal name for --dump-spec: SocksBind SOCKS5 proxy client (raw, bind command) [A] Example: bind to a websocket using some remote SOCKS server websocat -v -t ws-u:socks5-bind:tcp:132.148.129.183:14124 - --socks5-destination 255.255.255.255:65535 Note that port is typically unpredictable. Use --socks5-bind-script option to know the port. See an example in moreexamples.md for more thorough example. ### `crypto:` Internal name for --dump-spec: Crypto [A] Encrypts written messages and decrypts (and verifies) read messages with a static key, using ChaCha20-Poly1305 algorithm. Do not not use in stream mode - packet boundaries are significant. Note that attacker may duplicate, drop or reorder messages, including between different Websocat sessions with the same key. Each encrypted message is 12 bytes bigger than original message. Associated --crypto-key option accepts the following prefixes: - `file:` prefix means that Websocat should read 32-byte file and use it as a key. - `base64:` prefix means the rest of the value is base64-encoded 32-byte buffer - `pwd:` means Websocat should use argon2 derivation from the specified password as a key Use `--crypto-reverse` option to swap encryption and decryption. Note that `crypto:` specifier is absent in usual Websocat builds. You may need to build Websocat from source code with `--features=crypto_peer` for it to be available. ### `prometheus:` Aliases: `metrics:` Internal name for --dump-spec: Prometheus [A] Account connections, messages, bytes and other data and expose Prometheus metrics on a separate port. Not included by default, build a crate with `--features=prometheus_peer` to have it. You can also use `--features=prometheus_peer,prometheus/process` to have additional metrics. ### `exit_on_specific_byte:` Internal name for --dump-spec: ExitOnSpecificByte [A] Turn specific byte into a EOF, allowing user to escape interactive Websocat session when terminal is set to raw mode. Works only bytes read from the overlay, not on the written bytes. Default byte is 1C which is typically triggered by Ctrl+\. Example: `(stty raw -echo; websocat -b exit_on_specific_byte:stdio tcp:127.0.0.1:23; stty sane)` ### `drop_on_backpressure:` Internal name for --dump-spec: DropOnBackpressure [A] Prevent writing from ever blocking, drop writes instead. Does not affect reading part. when terminal is set to raw mode. Works only bytes read from the overlay, not on the written bytes. Default byte is 1C which is typically triggered by Ctrl+\. Example (attachable log observer): some_program | websocat -b -u asyncstdio: drop_on_backpressure:autoreconnect:ws-l:127.0.0.1:1234 ### `waitfordata:` Aliases: `wait-for-data:` Internal name for --dump-spec: WaitForData Wait for some data to pending being written before starting connecting. [A] Example: Connect to the TCP server on the left side immediately, but connect to the TCP server on the right side only after some data gets written by the first connection websocat -b tcp:127.0.0.1:1234 waitfordata:tcp:127.0.0.1:1235 Example: Connect to first WebSocket server, wait for some incoming WebSocket message, then connect to the second WebSocket server and start exchanging text and binary WebSocket messages between them. websocat -b --binary-prefix=b --text-prefix=t ws://127.0.0.1:1234 waitfordata:ws://127.0.0.1:1235/ ### Address types or specifiers to be implemented later: `sctp:`, `speedlimit:`, `quic:` ### Final example Final example just for fun: wacky mode websocat ws-c:ws-l:ws-c:- tcp:127.0.0.1:5678 Connect to a websocket using stdin/stdout as a transport, then accept a websocket connection over the previous websocket used as a transport, then connect to a websocket using previous step as a transport, then forward resulting connection to the TCP port. (Exercise to the reader: manage to make it actually connect to 5678). ================================================ FILE: misc/prebuilt_release_settings.sh ================================================ targetsettings() { EXTRA_CARGO_FLAGS="--features=vendored_openssl,openssl-probe" case "$1" in i686-unknown-linux-musl) EXTRA_CARGO_FLAGS="--no-default-features --features=signal_handler,tokio-process,unix_stdio,compression" ;; arm-unknown-linux-musleabi) EXTRA_CARGO_FLAGS="--no-default-features --features=signal_handler,tokio-process,unix_stdio,compression" ;; loongarch64-unknown-linux-musl) EXTRA_CARGO_FLAGS="--no-default-features --features=signal_handler,tokio-process,unix_stdio,compression" ;; riscv64gc-unknown-linux-musl) EXTRA_CARGO_FLAGS="--no-default-features --features=signal_handler,tokio-process,unix_stdio,compression" ;; wasm32-wasip1) SKIP=1 ;; esac } ================================================ FILE: moreexamples.md ================================================ More examples to avoid bloating up README or specifier-specific docs. # SSL (TLS) and wss:// ## Connecting to wss:// without checking certificate Websocat has `-k` option to turn off checking of SSL certificate. As alternative (or when using older versions of Websocat) you can use external programs to provide SSL for websocat. With `socat`: ``` $ websocat -t --ws-c-uri=wss://echo.websocket.org/ - ws-c:cmd:'socat - ssl:echo.websocket.org:443,verify=0' sadf sadf dsafdsaf dsafdsaf ``` With `openssl s_client`, also showing the log output: ``` $ websocat -v -t --ws-c-uri=wss://echo.websocket.org/ - ws-c:cmd:'openssl s_client -connect echo.websocket.org:443 -quiet' INFO 2018-08-30T15:45:31Z: websocat::lints: Auto-inserting the line mode INFO 2018-08-30T15:45:31Z: websocat::sessionserve: Serving Line2Message(Stdio) to Message2Line(WsConnect(Cmd("openssl s_client -connect echo.websocket.org:443 -quiet"))) with Options { websocket_text_mode: true, websocket_protocol: None, udp_oneshot_mode: false, unidirectional: false, unidirectional_reverse: false, exit_on_eof: false, oneshot: false, unlink_unix_socket: false, exec_args: [], ws_c_uri: "wss://echo.websocket.org/", linemode_strip_newlines: false, linemode_strict: false, origin: None, custom_headers: [], websocket_version: None, websocket_dont_close: false, one_message: false, no_auto_linemode: false, buffer_size: 65536, broadcast_queue_len: 16, read_debt_handling: Warn, linemode_zero_terminated: false, restrict_uri: None, serve_static_files: [], exec_set_env: false, reuser_send_zero_msg_on_disconnect: false, process_zero_sighup: false, process_exit_sighup: false, socks_destination: None, auto_socks5: None, socks5_bind_script: None, tls_domain: None } INFO 2018-08-30T15:45:31Z: websocat::stdio_peer: get_stdio_peer (async) INFO 2018-08-30T15:45:31Z: websocat::stdio_peer: Setting stdin to nonblocking mode INFO 2018-08-30T15:45:31Z: websocat::stdio_peer: Installing signal handler INFO 2018-08-30T15:45:31Z: websocat::ws_client_peer: get_ws_client_peer_wrapped depth=2 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2 verify return:1 depth=1 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2 verify return:1 depth=0 OU = Domain Control Validated, CN = *.websocket.org verify return:1 INFO 2018-08-30T15:45:31Z: websocat::ws_client_peer: Connected to ws 123 123 qwer qwer INFO 2018-08-30T15:45:35Z: websocat::sessionserve: Forward finished INFO 2018-08-30T15:45:35Z: websocat::sessionserve: Forward shutdown finished INFO 2018-08-30T15:45:35Z: websocat::sessionserve: Reverse finished INFO 2018-08-30T15:45:35Z: websocat::sessionserve: Reverse shutdown finished INFO 2018-08-30T15:45:35Z: websocat::sessionserve: Finished INFO 2018-08-30T15:45:35Z: websocat::stdio_peer: Restoring blocking status for stdin INFO 2018-08-30T15:45:35Z: websocat::stdio_peer: Restoring blocking status for stdin ``` This approach can also be used in Websocat builds that do not support SSL. ## Listening wss:// for development purposes ``` $ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 Generating a 4096 bit RSA private key ..........++ .........................++ writing new private key to 'key.pem' Enter PEM pass phrase:1234 Verifying - Enter PEM pass phrase:1234 ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]: State or Province Name (full name) [Some-State]: Locality Name (eg, city) []: Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []: Email Address []: $ openssl pkcs12 -export -out q.pkcs12 -inkey key.pem -in cert.pem Enter pass phrase for key.pem:1234 Enter Export Password: Verifying - Enter Export Password: $ websocat --pkcs12-der=q.pkcs12 -s 1234 Listening on wss://127.0.0.1:1234/ ``` There is a pre-generated certificate `test.pkcs12` included in Git. Workaround method for creating a `wss://` server: ``` socat openssl-listen:1234,cert=cert.pem,key=key.pem,verify=0,fork,reuseaddr system:"websocat -t inetd-ws\\: open-fd\\:2" ``` # Proxy servers ## Connect to a WebSocket using a SOCKS5 proxy There is internal SOCKS5 client now, but sometimes external client is better: websocat -v -t - --ws-c-uri=ws://echo.websocket.org ws-c:cmd:'SOCKS5_PASSWORD=a connect-proxy -S a@127.0.0.1:9050 echo.websocket.org 80' ## Connect to a WebSocket using HTTP proxy websocat -v -t - --ws-c-uri=ws://echo.websocket.org ws-c:cmd:'connect-proxy -H 127.0.0.1:9051 echo.websocket.org 80' ## Listen WebSocket on SOCKS5 server side and connect to it ``` cat > port_obtained << \EOF #!/bin/sh echo Remote port opened: $1 websocat -t -1 literal:"Roundtrip using SOCKS server" ws://132.148.129.183:$1/ EOF chmod +x port_obtained websocat -E -t ws-u:socks5-bind:tcp:132.148.129.183:14124 - --socks5-destination 255.255.255.255:65535 --socks5-bind-script ./port_obtained Remote port opened: 53467 Roundtrip using SOCKS server websocat -v -E -t ws-u:socks5-bind:tcp:132.148.129.183:14124 - --socks5-destination 255.255.255.255:65535 --socks5-bind-script ./port_obtained INFO 2018-08-29T22:04:42Z: websocat::lints: Auto-inserting the line mode INFO 2018-08-29T22:04:42Z: websocat::sessionserve: Serving Message2Line(WsServer(SocksBind(TcpConnect(V4(132.148.129.183:14124))))) to Line2Message(Stdio) with Options { websocket_text_mode: true, websocket_protocol: None, udp_oneshot_mode: false, unidirectional: false, unidirectional_reverse: false, exit_on_eof: true, oneshot: false, unlink_unix_socket: false, exec_args: [], ws_c_uri: "ws://0.0.0.0/", linemode_strip_newlines: false, linemode_strict: false, origin: None, custom_headers: [], websocket_version: None, websocket_dont_close: false, one_message: false, no_auto_linemode: false, buffer_size: 65536, broadcast_queue_len: 16, read_debt_handling: Warn, linemode_zero_terminated: false, restrict_uri: None, serve_static_files: [], exec_set_env: false, reuser_send_zero_msg_on_disconnect: false, process_zero_sighup: false, process_exit_sighup: false, socks_destination: Some(SocksSocketAddr { host: Ip(V4(255.255.255.255)), port: 65535 }), auto_socks5: None, socks5_bind_script: Some("./port_obtained") } INFO 2018-08-29T22:04:43Z: websocat::net_peer: Connected to TCP INFO 2018-08-29T22:04:46Z: websocat::proxy_peer: SOCKS5 connect/bind: SocksSocketAddr { host: Ip(V4(0.0.0.0)), port: 34020 } Remote port opened: 34020 INFO 2018-08-29T22:04:46Z: websocat::proxy_peer: SOCKS5 remote connected: SocksSocketAddr { host: Ip(V4(104.131.203.210)), port: 58836 } INFO 2018-08-29T22:04:47Z: websocat::ws_server_peer: Incoming connection to websocket: / INFO 2018-08-29T22:04:47Z: websocat::ws_server_peer: Upgraded INFO 2018-08-29T22:04:47Z: websocat::stdio_peer: get_stdio_peer (async) INFO 2018-08-29T22:04:47Z: websocat::stdio_peer: Setting stdin to nonblocking mode INFO 2018-08-29T22:04:47Z: websocat::stdio_peer: Installing signal handler Roundtrip using SOCKS server INFO 2018-08-29T22:04:47Z: websocat::sessionserve: Forward finished INFO 2018-08-29T22:04:47Z: websocat::sessionserve: Forward shutdown finished INFO 2018-08-29T22:04:47Z: websocat::sessionserve: One of directions finished INFO 2018-08-29T22:04:47Z: websocat::stdio_peer: Restoring blocking status for stdin INFO 2018-08-29T22:04:47Z: websocat::stdio_peer: Restoring blocking status for stdin ``` # Persistent client connection Suppose there is WebSocket server which replies exactly one WebSocket text message for each received WebSocket request. You want to have a persistent WebSocket client connection to that server and issue multiple requests from a script. You can do something like this: ``` $ websocat -t -E tcp-l:127.0.0.1:1234 reuse-raw:ws://echo.websocket.org --max-messages-rev 1& [1] 864 $ WS_PID=$! $ echo 'Hello 1' | nc 127.0.0.1 1234 Hello 1 $ echo 'World 2' | nc 127.0.0.1 1234 World 2 $ kill $WS_PID ``` This scheme is unfortunately unreliable: if client is disconnected before it can receive a message, the message can get delivered to next connected client. # Configuring Nginx to forward Websocket connections Usual `proxy_pass` is not enough. You need something like this: ``` location /mywebsocket { proxy_read_timeout 1d; proxy_send_timeout 1d; proxy_pass http://localhost:8123; #proxy_pass http://unix:/tmp/unixsocket_websocat; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } ``` # Debugging connection issues by dumping traffic. If you want to log information that is passing though Websocat to console, you can use `log:` filter (overlay). This allows inspecting traffic at various levels. For Websocat versions earlier than 1.7, you can use `ws-c:cmd:` trick instead. Example sessions: ## At message level ``` $ websocat -t - --ws-c-uri=wss://echo.websocket.org log:ws-c:ssl:tcp:echo.websocket.org:443 [WARN websocat::ssl_peer] Connected to TLS without proper verification of certificate. Use --tls-domain option. asdf WRITE 5 "asdf\n" READ 5 "asdf\n" asdf 12345 WRITE 6 "12345\n" READ 6 "12345\n" 12345 ``` ## At HTTP and Websocket protocol level ``` $ websocat -t - --ws-c-uri=wss://echo.websocket.org ws-c:log:ssl:tcp:echo.websocket.org:443 [WARN websocat::ssl_peer] Connected to TLS without proper verification of certificate. Use --tls-domain option. WRITE 157 "GET / HTTP/1.1\r\nHost: echo.websocket.org\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: lnQ/ReYmkEBvBVajdASTHg==\r\n\r\n" READ 201 "HTTP/1.1 101 Web Socket Protocol Handshake\r\nConnection: Upgrade\r\nDate: Sun, 21 Feb 2021 20:59:59 GMT\r\nSec-WebSocket-Accept: uZ5OAMgIFtQE5WRsqRRlA+DM0UI=\r\nServer: Kaazing Gateway\r\nUpgrade: websocket\r\n\r\n" ABCDE WRITE 12 "\x81\x86\xd6\xa90\x97\x97\xebs\xd3\x93\xa3" READ 8 "\x81\x06ABCDE\n" ABCDE 56789 WRITE 12 "\x81\x86\x8aH\xe8b\xbf~\xdfZ\xb3B" READ 8 "\x81\x0656789\n" 56789 WRITE 6 "\x88\x80u\xbfoz" READ 2 "\x88\x00" ``` ## At TCP stream level ``` $ websocat -t - --ws-c-uri=wss://echo.websocket.org --tls-domain=echo.websocket.org ws-c:ssl:log:tcp:echo.websocket.org:443 WRITE 517 "\x16\x03\x01\x02\x00\x01\x00\x01\xfc\x03\x03\x0e(r\xec\xbbXr\xacb\xe2\x0b+\x0f\xdc\xfa\x17\xb7R`\x1e\xda\xb42e\xd7\xf2\xdd\xd24O;\xd9 Y\t2\xe9kI\xa5I\xd4\xee)p6\xd6\xbf\x8dE:`\xe8]\x7fVN\x9e\x10\xe4\x7f8\xa2:\x98\x00>\x13\x02\x13\x03\x13\x01\xc0,\xc00\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0+\xc0/\x00\x9e\xc0$\xc0(\x00k\xc0#\xc0\'\x00g\xc0\n\xc0\x14\x009\xc0\t\xc0\x13\x003\x00\x9d\x00\x9c\x00=\x00<\x005\x00/\x00\xff\x01\x00\x01u\x00\x00\x00\x17\x00\x15\x00\x00\x12echo.websocket.org\x00\x0b\x00\x04\x03\x00\x01\x02\x00\n\x00\x0c\x00\n\x00\x1d\x00\x17\x00\x1e\x00\x19\x00\x18\x00#\x00\x00\x00\x16\x00\x00\x00\x17\x00\x00\x00\r\x000\x00.\x04\x03\x05\x03\x06\x03\x08\x07\x08\x08\x08\t\x08\n\x08\x0b\x08\x04\x08\x05\x08\x06\x04\x01\x05\x01\x06\x01\x03\x03\x02\x03\x03\x01\x02\x01\x03\x02\x02\x02\x04\x02\x05\x02\x06\x02\x00+\x00\t\x08\x03\x04\x03\x03\x03\x02\x03\x01\x00-\x00\x02\x01\x01\x003\x00&\x00$\x00\x1d\x00 \xa0t\xa8\x9d\xd8t\x06E\x94\xba+\xa7\xcf\x90**W\x8dS\xd1\xf4\xd4\xf7\x06\xda\xa4B\x9e\xeb\xa4\xf3\x10\x00\x15\x00\xc1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" READ 5 "\x16\x03\x03\n\x00" READ 2560 "\x02\x00\x00M\x03\x03`2\xca\x95\x17H\xaa\x9f\x7fz[O>\x0e\xc8x\xd9\x8d\xe71\xac\xffJ\xb8\xd8\x8d\xe4LB2\xee\x80 `2\xca\x95\x93;2\xdc\x04@;T\xc23Ei\x8ax\xfa\xb8y\x12\'\x05(\xce8|^Fv\x97\x00/\x00\x00\x05\xff\x01\x00\x01\x00\x0b\x00\t\xa7\x00\t\xa4\x00\x0550\x82\x0510\x82\x04\x19\xa0\x03\x02\x01\x02\x02\x12\x04\x00\xd5\xa4Naq\xe3S\xeb\xc7\xc8\x82\xb3g\xc5\xdeq0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x00021\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x160\x14\x06\x03U\x04\n\x13\rLet\'s Encrypt1\x0b0\t\x06\x03U\x04\x03\x13\x02R30\x1e\x17\r210104172430Z\x17\r210404172430Z0\x181\x160\x14\x06\x03U\x04\x03\x13\rwebsocket.org0\x82\x01\"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x000\x82\x01\n\x02\x82\x01\x01\x00\xa56O\xbfj\xb6\'\x8a<#\xcf\xce=\x1d\xb4\x1d\x0fI\xffW\\\xd9\xdb\xack\xd3\xed;\xc4?7\x8d)\x1bOf\xf4\xfd\xefw\xdb\xbb\xf6\x9cN\xf4-\x1a.8Th\xa0q\xfe\xf5\xb0\xcf\x9f/\x14\xb4\xbb \xb8\xfd\xd24Yg?}\xd5L\xde\x07\x00\x04i0\x82\x04e0\x82\x03M\xa0\x03\x02\x01\x02\x02\x10@\x01u\x04\x83\x14\xa4\xc8!\x8c\x84\xa9\x0c\x16\xcd\xdf0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000?1$0\"\x06\x03U\x04\n\x13\x1bDigital Signature Trust Co.1\x170\x15\x06\x03U\x04\x03\x13\x0eDST Root CA X30\x1e\x17\r201007192140Z\x17\r210929192140Z021\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x160\x14\x06\x03U\x04\n\x13\rLet\'s Encrypt1\x0b0\t\x06\x03U\x04\x03\x13\x02R30\x82\x01\"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x000\x82\x01\n\x02\x82\x01\x01\x00\xbb\x02\x15(\xcc\xf6\xa0\x94\xd3\x0f\x12\xec\x8dU\x92\xc3\xf8\x82\xf1\x99\xa6zB\x88\xa7]&\xaa\xb5+\xb9\xc5L\xb1\xaf\x8ek\xf9u\xc8\xa3\xd7\x0fG\x94\x14U5W\x8c\x9e\xa8\xa29\x19\xf5\x820P\xd067\xdfBM\xf0\xd7r\x13`\x8e\x88=\xd0\x9a\xf8\xfe\x0c\x01~\xb8\x87I\xa5\xcc\x15s\x95\"\xa1\xdb>3m4\x92j&\xa6_r\x96\xa0v\xfa\x12\x14WH\xfd\xa6\xa5\x8dY\xfa\xd6(\xa3\xafT\x18\xca8\xb8E\xc2a\xb7l\x1e\xd75\x80_\xb3\x14}S\xa2\x1d\x98\xc4y\x80W\xf6\xb3&\xdf9\x84O9~\xbfd\xd2\xd7/\xb7U\xe1~\xcb6\x8a\xf2\x07\x07\xc1\xb3\xf1L\x14\xa3\x0c\xb2\xf0(\x99\xa3;\xff[\x1d\xa3p\xe2\xe9\x9cZ5\xaa\xe3\xf0\xa4\xb28\xac\x82e\xb9\x82\xfcz\xbf0\x1f\xce\x9f\x9d\xcb\x94\x7f\xc7\xb8\x8fH$\xdcj+\xfe\xb7z\x9b\xe0\xf0\xd4\xdct\xa7\xe7W\x92A\xa1e8\xc7\x14\x03\x03\x00\x01\x01\x16\x03\x03\x00@\xbb\x00\xba\x14\xcc\xa1\xa2@w mP=\xd3T\xaf\"\xe6M!\xa8\xe8A\xbb[\xafx\xbeDU}\xe1/\x8b\x0e\x8b\x8a\xe6\x82\x06\xe5bHy\xbcq{ \xdb\xb3o\xae\xb1\x03\x9e\xa6\xcf\x19\xee\xf9\x9b\xb3@0" READ 5 "\x14\x03\x03\x00\x01" READ 1 "\x01" READ 5 "\x16\x03\x03\x00@" READ 64 "\\0\xa6\xfb\x9fcks\xe2o\xd7L\xc2\xf7@\x0f\xaf\xc8\xb3\xd9F\xb0\x1b\xa3\xe7\xc6\xfb\xb4\x12\xf1\xac8\x9d0\x89\x96\x89H\xde\xda\n\xaaN\x90@\xa9<#\xe9_\xa1\xcex\x11\xe2\x0f\x87\x1c\x87 \xbf\'9\xa4" WRITE 213 "\x17\x03\x03\x00\xd0\x8b\xf1G\xdcB\x83\xbf\x0f\xb0\xf4\x97YXk\xe7ow\xd3\x81\x89\xa0\xa1\x8bG\x99\xeb\xe3\xfa\xda\xd4QW=\x1d\x06\xadn#)L\x08\xa6\x10\xa4\x03O\xdf\xd5\xe2a\xda\xce\x9b\xe9f\xdf\xf4\x14UzW\xd8\xc8\x1c\xa2\xbf\x00\xc5\x9c}\x7fW;\x9b\xe8\x9f\xb0\xaa\x8c\xd0\xf9\xe0\xf7\xf4\"\xb6\xb0\nY\xff\x05\x0bz\xbc\x9c\x81\xd0\xf5\xee\x0bK\xd4Q\xd5l\x81E\xfb\xdcW\x12\xe4\xaa\xe7\xaf\x1c\x7f.\xb1\xd78\x11\xe6\xfd\xb6\xb9\xc5\xd1\xec\x83\xe5\x16\xb2P\xbd\x97\xb5E\x95\'\xf2\xf8B\xee\x01S\xf61&@-\xa0\x95e,H\x07\xb1\x9c\xba`\xd9\xf3\x9b\xe8\xba\x8c\xaaqt_\xe2t&f\xd3\xba\xc3W\xcc\xb7;\xa7\xc9A\x16\x11$e\xfd\x1e\x8eKF\x0cO+\x84\x1e\xc7\x85\x1b\xf2\"Pou\xda" READ 5 "\x17\x03\x03\x00\xf0" READ 240 "3\x14\xe7g\x12\xc2I4\x96\x8f\r$\xb4\xa2(\xf2b\xf5]\x82\xa77\x88wJy\x84\xf4\x8e\xaf\xa6\x04\x8d\xed\x7f\xd6q\x80o\'0\x1e\x85\xa1\xf8\x9buk\xc2\x9bx>\xb4S\xe3\xa4\xa2\x88\x8a\xff\xac\x06\x81\xc4\x98g,\xb2\xa9\'\xe2C=\x90\x9b\xc4\xd4\x7f\xa3\xcffneE\xee\x86\xae\xd3\x17\xf3 (\xb7\xf8W\xcd(\x16\x82s2\xe7\x03y81\xe0r\xa0C\x82\xedK\x93u\x87x,\xf3\xca\xc7\x84\xa1\xf5x \x06Q5\xe7\x00%\xb4\x84\xfaw\x1f^R\xc9o\xb4\x96\xf0\xa7\x01\xc5h\x04.\xc8Q\xcfw\t\x8e\xa8\xf9\x98\x9a~q\xb9/\x9f.u\x023Q8\x95\x8b\x7f~\xccx\xa9_,\xc7*<.\x0e\'jH\x05\x8b\xe8\xc2\xee\xd8\xa4\x12\xe1\xb6\xd7FL\xe7\r\xc1\x07*\xcb+\xf0\xc2Pq\xed7\t\x1e\xa4,\x05\xd0\x0e\xba\xf1ABtL*\xba\xbb\xe7~\x1e\xefO\x85\xbd\xe6\x84S" qwert WRITE 69 "\x17\x03\x03\x00@\n\x8c\\\xa8b\xae\x05N\xb9O\x81\xaej\x9eM\xf7$\xf9\x8f\xc8\xefz!\x89.\xfe\x19\x12`\xbf{fJS\xd5\x15\x84\x0e\xa5\xf7\"4\xa1\xda[\x95\xe2#&\xe8\xd3\x96\xb8]\xa4+~\xb2Cc\xfc6\xb2|" READ 5 "\x17\x03\x03\x000" READ 48 "\xb8uj&\xda\xed\xf9\xbc\xbfn\x9eS`\xa3\x10q\x10[\x05}\xef\xd6\xbf\t\xea\x14f\xf9c\xe6$\xd1R:\xdc\xcc\x88[\x97\xca\x12\x9f\xc1pk\x0e\xf6o" qwert zxcvbn WRITE 69 "\x17\x03\x03\x00@\xe5\xf4\x99xS\xf6\xad\xf4\x89?\xbd\x99=`a\xab\x8e\xed\xd83\xa9-\xabU{\xfa~\xd9h\xf8\x1a\xfaS\xfdD-\xc4\x84=:\xae;\x0b\xccRHT\xef\x04E\x927\xbf~\x0e:\xb0\x16\x01\xf5\xd1\x0f\x1e\xc9" READ 5 "\x17\x03\x03\x000" READ 48 "\xa4\x9b=\x85\xe3\x8c\x0c-*\x08(Qu\xd8\x93\xbde,c\x1a\x9c\"\xc9o\xa7\xb5\xe9eH6\xa1\xed\xb9\xcb+\x13>nu^\x1c\x9d\x1b2\xc0\x98\x1d(" zxcvbn WRITE 53 "\x17\x03\x03\x000\x17\x19\xf4>\xddSG8\xdd\xcd\x00\xf2\xf58\x15n\xbaY\xbaU\xf0H\x8b\t}\xa5\xaa\xfbXy\xc7f\xc2r\x9e\x94dO\xdc\xaf\xad\xcc\xcd\x16\x87\xdd\x19\xb9" READ 5 "\x17\x03\x03\x000" READ 48 "j#\x9d\x17B\x89\xee\x92\x90\xcaH6\xf7PQe|p\xed\xf8,=\x0f+\x8a\x10)\xcf\x06\xb1\x06\xde\x9eA>B\xb7g\xde\xce\xcc\xfd\x88\x1c\xf1-\x96\x00" ``` # listening websockets from systemd Systemd units for WebSocket-to-localhost-SSH redirector. ## `Accept=yes` mode Each client connection gets separate Websocat process. `/etc/systemd/system/qqq.socket` ``` [Unit] Description="websocat" [Socket] ListenStream=/run/qqq.socket #ListenStream=127.0.0.1:1234 # also works for TCP Accept=yes [Install] WantedBy=sockets.target ``` `/etc/systemd/system/qqq@.service` (note the important `@` character). ``` [Unit] Description="websocat" Requires=qqq.socket [Service] Type=simple ExecStart=/opt/websocat -E -b ws-inetd: tcp:127.0.0.1:22 StandardInput=socket NonBlocking=true [Install] WantedBy=multi-user.target ``` ## `Accept=no` mode Websocat is socket-activated by systemd and keeps on listening for more connections Requires new enough Websocat version with `--accept-from-fd` option. `/etc/systemd/system/qqq.socket` ``` [Unit] Description="websocat" [Socket] ListenStream=/run/qqq.socket # does _not_ work with TCP socket currently Accept=no [Install] WantedBy=sockets.target ``` `/etc/systemd/system/qqq.service` (note the absence of `@`). ``` [Unit] Description="websocat" Requires=qqq.socket [Service] ExecStart=/opt/websocat -E -b --accept-from-fd l-ws-unix:3 tcp:127.0.0.1:22 [Install] WantedBy=multi-user.target ``` with `SocketUser=www-data` it can be combined with Nginx setup above. Example SSH client command: `ssh root@localhost -o 'ProxyCommand=/opt/websocat -E -b - ws-c:unix:/run/qqq.socket'` # Specifying distinct host names for resolving IP address, `Host: ` header and TLS. Example command line: websocat -t - --ws-c-uri=wss://domain-for-host-header/ ws-c:tls:tcp:domain-or-ip-address-for-resolving:443 --tls-domain domain-for-checking-certificate Or without TLS: websocat -t - --ws-c-uri=wss://domain-for-host-header/ ws-c:tcp:domain-or-ip-address-for-resolving:80 ================================================ FILE: src/all_peers.rs ================================================ // This is an X-Macro. #[macro_export] macro_rules! list_of_all_specifier_classes { ($your_macro:ident) => { $your_macro!($crate::ws_client_peer::WsClientClass); #[cfg(feature = "ssl")] $your_macro!($crate::ws_client_peer::WsClientSecureClass); $your_macro!($crate::ws_server_peer::WsTcpServerClass); $your_macro!($crate::ws_server_peer::WsInetdServerClass); $your_macro!($crate::ws_server_peer::WsUnixServerClass); $your_macro!($crate::ws_server_peer::WsAbstractUnixServerClass); $your_macro!($crate::ws_server_peer::WsServerClass); $your_macro!($crate::ws_lowlevel_peer::WsLlClientClass); $your_macro!($crate::ws_lowlevel_peer::WsLlServerClass); #[cfg(feature = "ssl")] $your_macro!($crate::ssl_peer::WssListenClass); $your_macro!($crate::http_peer::HttpRequestClass); $your_macro!($crate::http_peer::HttpClass); $your_macro!($crate::http_peer::HttpPostSseClass); #[cfg(all(unix, feature = "unix_stdio"))] $your_macro!($crate::stdio_peer::AsyncStdioClass); #[cfg(all(unix, feature = "unix_stdio"))] $your_macro!($crate::stdio_peer::InetdClass); #[cfg(not(all(unix, feature = "unix_stdio")))] $your_macro!($crate::stdio_threaded_peer::InetdClass); $your_macro!($crate::net_peer::TcpConnectClass); $your_macro!($crate::net_peer::TcpListenClass); #[cfg(feature = "ssl")] $your_macro!($crate::ssl_peer::TlsConnectClass); #[cfg(feature = "ssl")] $your_macro!($crate::ssl_peer::TlsAcceptClass); #[cfg(feature = "ssl")] $your_macro!($crate::ssl_peer::TlsListenClass); #[cfg(feature = "tokio-process")] $your_macro!($crate::process_peer::ShCClass); #[cfg(feature = "tokio-process")] $your_macro!($crate::process_peer::CmdClass); #[cfg(feature = "tokio-process")] $your_macro!($crate::process_peer::ExecClass); $your_macro!($crate::file_peer::ReadFileClass); $your_macro!($crate::file_peer::WriteFileClass); $your_macro!($crate::file_peer::AppendFileClass); $your_macro!($crate::primitive_reuse_peer::ReuserClass); $your_macro!($crate::broadcast_reuse_peer::BroadcastReuserClass); $your_macro!($crate::reconnect_peer::AutoReconnectClass); $your_macro!($crate::ws_client_peer::WsConnectClass); $your_macro!($crate::net_peer::UdpConnectClass); $your_macro!($crate::net_peer::UdpListenClass); #[cfg(all(unix, feature = "unix_stdio"))] $your_macro!($crate::stdio_peer::OpenAsyncClass); #[cfg(all(unix, feature = "unix_stdio"))] $your_macro!($crate::stdio_peer::OpenFdAsyncClass); $your_macro!($crate::stdio_threaded_peer::ThreadedStdioClass); $your_macro!($crate::stdio_threaded_peer::StdioClass); #[cfg(unix)] $your_macro!($crate::unix_peer::UnixConnectClass); #[cfg(unix)] $your_macro!($crate::unix_peer::UnixListenClass); #[cfg(unix)] $your_macro!($crate::unix_peer::UnixDgramClass); #[cfg(unix)] $your_macro!($crate::unix_peer::AbstractConnectClass); #[cfg(unix)] $your_macro!($crate::unix_peer::AbstractListenClass); #[cfg(unix)] $your_macro!($crate::unix_peer::AbstractDgramClass); #[cfg(all(windows,feature = "windows_named_pipes"))] $your_macro!($crate::windows_np_peer::NamedPipeConnectClass); $your_macro!($crate::line_peer::Message2LineClass); $your_macro!($crate::line_peer::Line2MessageClass); $your_macro!($crate::lengthprefixed_peer::LengthPrefixedClass); $your_macro!($crate::foreachmsg_peer::ForeachmsgClass); $your_macro!($crate::mirror_peer::MirrorClass); $your_macro!($crate::mirror_peer::LiteralReplyClass); $your_macro!($crate::trivial_peer::CloggedClass); $your_macro!($crate::trivial_peer::LiteralClass); $your_macro!($crate::trivial_peer::AssertClass); $your_macro!($crate::trivial_peer::Assert2Class); $your_macro!($crate::trivial_peer::LogClass); #[cfg(all(target_os = "linux", feature = "seqpacket"))] $your_macro!($crate::unix_peer::unix_seqpacket_peer::SeqpacketConnectClass); #[cfg(all(target_os = "linux", feature = "seqpacket"))] $your_macro!($crate::unix_peer::unix_seqpacket_peer::SeqpacketListenClass); $your_macro!($crate::jsonrpc_peer::JsonRpcClass); $your_macro!($crate::timestamp_peer::TimestampClass); $your_macro!($crate::socks5_peer::SocksProxyClass); $your_macro!($crate::socks5_peer::SocksBindClass); #[cfg(feature = "crypto_peer")] $your_macro!($crate::crypto_peer::CryptoClass); $your_macro!($crate::trivial_peer::RandomClass); #[cfg(feature = "prometheus_peer")] $your_macro!($crate::prometheus_peer::PrometheusClass); $your_macro!($crate::trivial_peer::ExitOnSpecificByteClass); $your_macro!($crate::trivial_peer::DropOnBackpressureClass); $your_macro!($crate::reconnect_peer::WaitForDataClass); }; } ================================================ FILE: src/broadcast_reuse_peer.rs ================================================ extern crate futures; extern crate tokio_io; use futures::future::ok; use std::cell::RefCell; use std::rc::Rc; use super::{brokenpipe, simple_err, wouldblock, BoxedNewPeerFuture, Peer}; use std::io::{Error as IoError, Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use super::{once, ConstructParams, PeerConstructor, Specifier}; use futures::Async; use futures::AsyncSink; use futures::Future; use futures::Sink; use futures::Stream; use crate::spawn_hack; use std::ops::DerefMut; use futures::unsync::mpsc; declare_slab_token!(BroadcastClientIndex); use slab_typesafe::Slab; #[derive(Debug)] pub struct BroadcastReuser(pub Rc); impl Specifier for BroadcastReuser { fn construct(&self, p: ConstructParams) -> PeerConstructor { let mut reuser = p.global(GlobalState::default).clone(); let bs = p.program_options.buffer_size; let ql = p.program_options.broadcast_queue_len; let l2r = p.left_to_right.clone(); let inner = || self.0.construct(p).get_only_first_conn(l2r); once(connection_reuser(&mut reuser, inner, bs, ql)) } specifier_boilerplate!(singleconnect has_subspec globalstate); self_0_is_subspecifier!(...); } specifier_class!( name = BroadcastReuserClass, target = BroadcastReuser, prefixes = [ "broadcast:", "reuse:", "reuse-broadcast:", "broadcast-reuse:" ], arg_handling = subspec, overlay = true, MessageBoundaryStatusDependsOnInnerType, SingleConnect, help = r#" Reuse this connection for serving multiple clients, sending replies to all clients. Messages from any connected client get directed to inner connection, replies from the inner connection get duplicated across all connected clients (and are dropped if there are none). If WebSocket client is too slow for accepting incoming data, messages get accumulated up to the configurable --broadcast-buffer, then dropped. Example: Simple data exchange between connected WebSocket clients websocat -E ws-l:0.0.0.0:8800 reuse-broadcast:mirror: "# ); type SailingBuffer = Rc>; type Clients = Slab>; pub struct Broadcaster { inner_peer: Peer, clients: Clients, } pub type HBroadCaster = Rc>>; pub type GlobalState = HBroadCaster; struct PeerHandleW(HBroadCaster); struct PeerHandleR( HBroadCaster, mpsc::Receiver, BroadcastClientIndex, ); struct InnerPeerReader(HBroadCaster, Vec); impl Future for InnerPeerReader { type Item = (); type Error = (); fn poll(&mut self) -> futures::Poll<(), ()> { loop { let mut meb = self.0.borrow_mut(); let me = meb.as_mut().expect("Assertion failed 16293"); match me.inner_peer.0.read(&mut self.1[..]) { Ok(0) => { info!("Underlying peer finished"); return Ok(futures::Async::Ready(())); } Ok(n) => { if me.clients.is_empty() { info!("Dropping broadcast due to no clients being connected"); continue; }; let sb = Rc::new(self.1[0..n].to_vec()); for (_, client) in me.clients.iter_mut() { match client.start_send(sb.clone()) { Ok(AsyncSink::Ready) => match client.poll_complete() { Ok(Async::Ready(())) => {} Ok(Async::NotReady) => { warn!("A client's sink is NotReady for poll_complete"); } Err(e) => { warn!("A client's sink is in error state: {}", e); } }, Ok(AsyncSink::NotReady(_)) => { warn!("A client's sink is NotReady for start_send"); } Err(e) => { warn!("A client's sink is in error state: {}", e); } }; } } Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Ok(Async::NotReady); } error!("Inner peer read failed: {}", e); return Err(()); } } } } } impl Drop for PeerHandleR { fn drop(&mut self) { self.0 .borrow_mut() .as_mut() .expect("Assertion failed 16292") .clients .remove(self.2); } } impl Read for PeerHandleR { fn read(&mut self, b: &mut [u8]) -> Result { loop { return match self.1.poll() { Ok(Async::Ready(Some(v))) => { if v.len() > b.len() { error!("Too big message dropped"); continue; } b[0..(v.len())].copy_from_slice(&v[..]); Ok(v.len()) } Ok(Async::Ready(None)) => brokenpipe(), Ok(Async::NotReady) => wouldblock(), Err(()) => Err(simple_err("Something unexpected".into())), }; } /*if let &mut Some(ref mut x) = self.0.borrow_mut().deref_mut() { x.inner_peer.0.read(b) // To be changed } else { unreachable!() }*/ } } impl AsyncRead for PeerHandleR {} impl Write for PeerHandleW { fn write(&mut self, b: &[u8]) -> Result { if let Some(ref mut x) = *self.0.borrow_mut().deref_mut() { x.inner_peer.1.write(b) } else { unreachable!() } } fn flush(&mut self) -> Result<(), IoError> { if let Some(ref mut x) = *self.0.borrow_mut().deref_mut() { x.inner_peer.1.flush() } else { unreachable!() } } } impl AsyncWrite for PeerHandleW { fn shutdown(&mut self) -> futures::Poll<(), IoError> { if let Some(ref mut _x) = *self.0.borrow_mut().deref_mut() { // Ignore shutdown attempts Ok(futures::Async::Ready(())) //_x.1.shutdown() } else { unreachable!() } } } fn makeclient(ps: HBroadCaster, queue_len: usize) -> Peer { let (send, recv) = mpsc::channel(queue_len); let k = ps .borrow_mut() .as_mut() .expect("Assertion failed 16291") .clients .insert(send); let ph1 = PeerHandleR(ps.clone(), recv, k); let ph2 = PeerHandleW(ps); Peer::new(ph1, ph2, None /* TODO */) } pub fn connection_reuser BoxedNewPeerFuture>( s: &mut GlobalState, inner_peer: F, buffer_size: usize, queue_len: usize, ) -> BoxedNewPeerFuture { let need_init = s.borrow().is_none(); let rc = s.clone(); if need_init { info!("Initializing"); Box::new(inner_peer().and_then(move |inner| { { let mut b = rc.borrow_mut(); let x: &mut Option = b.deref_mut(); *x = Some(Broadcaster { inner_peer: inner, clients: Clients::new(), }); spawn_hack(InnerPeerReader(rc.clone(), vec![0; buffer_size])); } let ps: HBroadCaster = rc.clone(); ok(makeclient(ps, queue_len)) })) as BoxedNewPeerFuture } else { info!("Reusing"); let ps: HBroadCaster = rc.clone(); Box::new(ok(makeclient(ps, queue_len))) as BoxedNewPeerFuture } } ================================================ FILE: src/crypto_peer.rs ================================================ use argon2::Argon2; use futures::Async; use futures::future::ok; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer}; use super::{ConstructParams, PeerConstructor, Specifier}; use std::io::{Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use std::io::Error as IoError; use chacha20poly1305::ChaCha20Poly1305; use chacha20poly1305::Nonce; use chacha20poly1305::aead::NewAead; use chacha20poly1305::aead::Aead; use rand::RngCore; #[derive(Debug)] pub struct Crypto(pub T); impl Specifier for Crypto { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); let mut key = [0u8; 32]; if let Some(k) = cp.program_options.crypto_key { key = k; } else { log::error!("You are using `crypto:` without `--crypto-key`. This uses a hard coded key and is insecure.") } inner.map(move |p, _| crypto_peer(p, key, cp.program_options.crypto_reverse)) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = CryptoClass, target = Crypto, prefixes = ["crypto:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" [A] Encrypts written messages and decrypts (and verifies) read messages with a static key, using ChaCha20-Poly1305 algorithm. Do not not use in stream mode - packet boundaries are significant. Note that attacker may duplicate, drop or reorder messages, including between different Websocat sessions with the same key. Each encrypted message is 12 bytes bigger than original message. Associated --crypto-key option accepts the following prefixes: - `file:` prefix means that Websocat should read 32-byte file and use it as a key. - `base64:` prefix means the rest of the value is base64-encoded 32-byte buffer - `pwd:` means Websocat should use argon2 derivation from the specified password as a key Use `--crypto-reverse` option to swap encryption and decryption. Note that `crypto:` specifier is absent in usual Websocat builds. You may need to build Websocat from source code with `--features=crypto_peer` for it to be available. "# ); #[derive(Clone, Copy)] enum Mode { Encrypt, Decrypt, } pub fn crypto_peer(inner_peer: Peer, key: [u8; 32], reverse: bool) -> BoxedNewPeerFuture { let (mode_r, mode_w) = if reverse { (Mode::Encrypt, Mode::Decrypt) } else { (Mode::Decrypt, Mode::Encrypt) }; let crypto = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&key)); let filtered_r = CryptoWrapperR(inner_peer.0, crypto.clone(), mode_r); let filtered_w = CryptoWrapperW(inner_peer.1, crypto, mode_w); let thepeer = Peer::new(filtered_r, filtered_w, inner_peer.2); Box::new(ok(thepeer)) as BoxedNewPeerFuture } struct CryptoWrapperR(Box, ChaCha20Poly1305, Mode); impl Read for CryptoWrapperR { fn read(&mut self, b: &mut [u8]) -> Result { let mut l = b.len(); assert!(l > 12); if matches!(self.2, Mode::Encrypt) { l -= 12; } let n = match self.0.read(&mut b[..l]) { Ok(x) => x, Err(e) => return Err(e), }; if n == 0 { return Ok(0) } let data = process_data(&b[..n], &self.1, self.2)?; let m = data.len(); b[..m].copy_from_slice(&data[..m]); Ok(m) } } impl AsyncRead for CryptoWrapperR {} struct CryptoWrapperW(Box, ChaCha20Poly1305, Mode); impl Write for CryptoWrapperW { fn write(&mut self, b: &[u8]) -> Result { let l = b.len(); let data = process_data(b, &self.1, self.2)?; let n = match self.0.write(&data[..]) { Ok(x) => x, Err(e) => return Err(e), }; if n != data.len() { log::error!("Short write when using `crypto:` specifier"); } Ok(l) } fn flush(&mut self) -> std::io::Result<()> { self.0.flush() } } impl AsyncWrite for CryptoWrapperW { fn shutdown(&mut self) -> std::result::Result, std::io::Error> { self.0.shutdown() } } fn process_data(buf: &[u8], crypto: &ChaCha20Poly1305, mode: Mode) -> Result, IoError> { let l = buf.len(); match mode { Mode::Encrypt => { let mut nonce = [0u8; 12]; rand::thread_rng().fill_bytes(&mut nonce[..]); let mut data: Vec = crypto .encrypt(Nonce::from_slice(&nonce), &buf[..]) .unwrap(); data.extend_from_slice(&nonce[..]); Ok(data) } Mode::Decrypt => { if l < 12 { log::error!("Insufficient packet length for `crypto:` specifier's decryption"); return Err(std::io::ErrorKind::Other.into()); } let mut nonce = [0u8; 12]; nonce.copy_from_slice(&buf[l-12..l]); match crypto.decrypt(Nonce::from_slice(&nonce), &buf[..(l-12)]) { Ok(x) => Ok(x), Err(_) => { log::error!("crypto: decryption failed"); return Err(std::io::ErrorKind::Other.into()) } } } } } pub fn interpret_opt(x: &str) -> crate::Result<[u8; 32]> { let mut key = [0u8; 32]; if x.starts_with("base64:") { let mut buf = Vec::with_capacity(32); base64::decode_config_buf(&x[7..], base64::STANDARD, &mut buf)?; if buf.len() != 32 { log::error!("Expected 32 bytes, got {} bytes", buf.len()); return Err("Non 32-byte buffer specified".into()); } key.copy_from_slice(&buf[..]); } else if x.starts_with("file:") { let buf = std::fs::read(&x[5..])?; if buf.len() != 32 { log::error!("Expected 32 bytes, got {} bytes", buf.len()); return Err("Non 32-byte buffer specified".into()); } key.copy_from_slice(&buf[..]) } else if x.starts_with("pwd:") { let argon2 = Argon2::default(); const SALT : &'static [u8] = &[0x81, 0x65, 0x0c, 0xc7, 0x09, 0x76, 0xc1, 0x12, 0x6b, 0x5b, 0x5f, 0x04, 0x08, 0x61, 0xf6, 0x1b, 0xd6, 0xab, 0x88, 0xa2, 0xee, 0x67, 0x47, 0xc1, 0xbe, 0x12, 0xd7, 0xd7, 0x2d, 0xb8, 0x39, 0xcf]; argon2.hash_password_into(x[4..].as_bytes(),SALT,&mut key[..]).unwrap(); } else { return Err("--crypto-key's value must start with `base64:`, `file:` or `pwd:`".into()); } Ok(key) } ================================================ FILE: src/file_peer.rs ================================================ use futures; use futures::Async; use std; use std::io::Result as IoResult; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use tokio_io::{AsyncRead, AsyncWrite}; use std::fs::{File, OpenOptions}; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer, Result}; use super::{once, ConstructParams, PeerConstructor, Specifier}; #[derive(Clone, Debug)] pub struct ReadFile(pub PathBuf); impl Specifier for ReadFile { fn construct(&self, _: ConstructParams) -> PeerConstructor { fn gp(p: &Path) -> Result { let f = File::open(p)?; Ok(Peer::new(ReadFileWrapper(f), super::trivial_peer::DevNull, None)) } once(Box::new(futures::future::result(gp(&self.0))) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = ReadFileClass, target = ReadFile, prefixes = ["readfile:"], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Synchronously read a file. Argument is a file path. Blocking on operations with the file pauses the whole process Example: Serve the file once per connection, ignore all replies. websocat ws-l:127.0.0.1:8000 readfile:hello.json "# ); #[derive(Clone, Debug)] pub struct WriteFile(pub PathBuf); impl Specifier for WriteFile { fn construct(&self, _: ConstructParams) -> PeerConstructor { fn gp(p: &Path) -> Result { let f = File::create(p)?; Ok(Peer::new(super::trivial_peer::DevNull, WriteFileWrapper(f), None)) } once(Box::new(futures::future::result(gp(&self.0))) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = WriteFileClass, target = WriteFile, prefixes = ["writefile:"], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Synchronously truncate and write a file. Blocking on operations with the file pauses the whole process Example: websocat ws-l:127.0.0.1:8000 writefile:data.txt "# ); #[derive(Clone, Debug)] pub struct AppendFile(pub PathBuf); impl Specifier for AppendFile { fn construct(&self, _: ConstructParams) -> PeerConstructor { fn gp(p: &Path) -> Result { let f = OpenOptions::new().create(true).append(true).open(p)?; Ok(Peer::new(super::trivial_peer::DevNull, WriteFileWrapper(f), None)) } once(Box::new(futures::future::result(gp(&self.0))) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = AppendFileClass, target = AppendFile, prefixes = ["appendfile:"], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Synchronously append a file. Blocking on operations with the file pauses the whole process Example: Logging all incoming data from WebSocket clients to one file websocat -u ws-l:127.0.0.1:8000 reuse:appendfile:log.txt "# ); pub struct ReadFileWrapper(pub File); impl AsyncRead for ReadFileWrapper {} impl Read for ReadFileWrapper { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { self.0.read(buf) } } struct WriteFileWrapper(File); impl AsyncWrite for WriteFileWrapper { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { Ok(Async::Ready(())) } } impl Write for WriteFileWrapper { fn write(&mut self, buf: &[u8]) -> IoResult { self.0.write(buf) } fn flush(&mut self) -> IoResult<()> { self.0.flush() } } ================================================ FILE: src/foreachmsg_peer.rs ================================================ use futures::future::ok; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer}; use super::{ConstructParams, PeerConstructor, Specifier}; use std::cell::RefCell; use std::io::{Error as IoError, Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use super::{once, simple_err, wouldblock}; use futures::{Async, Future, Poll}; #[derive(Debug)] pub struct Foreachmsg(pub Rc); impl Specifier for Foreachmsg { fn construct(&self, cp: ConstructParams) -> PeerConstructor { once(foreachmsg_peer(self.0.clone(), cp)) } specifier_boilerplate!(singleconnect noglobalstate has_subspec); self_0_is_subspecifier!(...); } specifier_class!( name = ForeachmsgClass, target = Foreachmsg, prefixes = ["foreachmsg:"], arg_handling = subspec, overlay = true, MessageBoundaryStatusDependsOnInnerType, SingleConnect, help = r#" Execute something for each incoming message. Somewhat the reverse of the `autoreconnect:`. Example: websocat -t -u ws://server/listen_for_updates foreachmsg:writefile:status.txt This keeps only recent incoming message in file and discards earlier messages. "# ); #[derive(Default)] struct State2 { already_warned: bool, } #[derive(Clone)] enum Phase { Idle, WriteDebt(Vec), Flushing, Closing, WaitingForReadToFinish, } struct State { s: Rc, p: Option, n: Option, cp: ConstructParams, aux: State2, ph: Phase, finished_reading: bool, read_waiter_tx: Option>, read_waiter_rx: Option>, wait_for_new_peer_tx: Option>, wait_for_new_peer_rx: Option>, need_wait_for_reading: bool, } /// This implementation's poll is to be reused many times, both after returning item and error impl State { //type Item = &'mut Peer; //type Error = Box<::std::error::Error>; fn poll(&mut self) -> Poll<&mut Peer, Box> { let pp = &mut self.p; let nn = &mut self.n; let aux = &mut self.aux; loop { let cp = self.cp.clone(); if let Some(ref mut p) = *pp { return Ok(Async::Ready(p)); } // Peer is not present: trying to create a new one if let Some(mut bnpf) = nn.take() { match bnpf.poll() { Ok(Async::Ready(p)) => { *pp = Some(p); if let Some(tx) = self.wait_for_new_peer_tx.take() { let _ = tx.send(()); } continue; } Ok(Async::NotReady) => { *nn = Some(bnpf); return Ok(Async::NotReady); } Err(_x) => { // Stop on error: //return Err(_x); // Just reconnect again on error if !aux.already_warned { aux.already_warned = true; error!("Reconnecting failed. Trying again in tight endless loop."); } } } } let l2r = cp.left_to_right.clone(); let pc: PeerConstructor = self.s.construct(cp); *nn = Some(pc.get_only_first_conn(l2r)); self.finished_reading = false; self.ph = Phase::Idle; self.read_waiter_tx = None; self.read_waiter_rx = None; } } } #[derive(Clone)] struct PeerHandle(Rc>); macro_rules! getpeer { ($state:ident -> $p:ident) => { let $p: &mut Peer = match $state.poll() { Ok(Async::Ready(p)) => p, Ok(Async::NotReady) => return wouldblock(), Err(e) => { return Err(simple_err(format!("{}", e))); } }; }; } impl State { fn reconnect(&mut self) { info!("Reconnect"); self.p = None; self.ph = Phase::Idle; self.finished_reading = false; self.read_waiter_tx = None; self.read_waiter_rx = None; } } impl Read for PeerHandle { fn read(&mut self, b: &mut [u8]) -> Result { let mut state = self.0.borrow_mut(); loop { if let Some(w) = state.wait_for_new_peer_rx.as_mut() { match w.poll() { Ok(Async::NotReady) => return wouldblock(), _ => { state.wait_for_new_peer_rx = None; } } } let p : &mut Peer = match state.poll() { Ok(Async::Ready(p)) => p, Ok(Async::NotReady) => return wouldblock(), Err(e) => { return Err(simple_err(format!("{}", e))); } }; #[allow(unused_assignments)] let mut finished_but_loop_around = false; match p.0.read(b) { Ok(0) => { state.finished_reading = true; if state.need_wait_for_reading { finished_but_loop_around = true; } else { return Ok(0); } } Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Err(e); } state.finished_reading = true; warn!("{}", e); if state.need_wait_for_reading { // Get a new peer to read from finished_but_loop_around = true; } else { return Err(e); } } Ok(x) => { return Ok(x); } } if finished_but_loop_around { state.finished_reading = true; let (tx,rx) = futures::sync::oneshot::channel(); state.wait_for_new_peer_tx = Some(tx); state.wait_for_new_peer_rx = Some(rx); if let Some(rw) = state.read_waiter_tx.take() { let _ = rw.send(()); } } } } } impl AsyncRead for PeerHandle {} impl Write for PeerHandle { fn write(&mut self, b: &[u8]) -> Result { let mut state = self.0.borrow_mut(); let mut do_reconnect = false; let mut finished = false; loop { if do_reconnect { state.reconnect(); do_reconnect = false; } else if finished { state.p = None; state.ph = Phase::Idle; return Ok(b.len()); } else { let mut ph = state.ph.clone(); { getpeer!(state -> p); match ph { Phase::Idle => { match p.1.write(b) { Ok(0) => { info!("End-of-file write?"); return Ok(0); } Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Err(e); } warn!("{}", e); return Err(e); } Ok(x) if x == b.len() => { debug!("Full write"); // A successful write. Flushing and closing the peer. ph = Phase::Flushing; }, Ok(x) => { debug!("Partial write of {} bytes", x); // A partial write. Creating write debt. let debt = b[x..b.len()].to_vec(); ph = Phase::WriteDebt(debt); } } }, Phase::WriteDebt(d) => { match p.1.write(&d[..]) { Ok(0) => { info!("End-of-file write v2?"); return Ok(0); } Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Err(e); } warn!("{}", e); return Err(e); } Ok(x) if x == d.len() => { debug!("Closing the debt"); // A successful write. Flushing and closing the peer. ph = Phase::Flushing; }, Ok(x) => { debug!("Partial write of {} debt bytes", x); // A partial write. Retaining the write debt. let debt = d[x..d.len()].to_vec(); ph = Phase::WriteDebt(debt); } } }, Phase::Flushing => { match p.1.flush() { Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Err(e); } warn!("{}", e); return Err(e); } Ok(()) => { debug!("Flushed"); ph = Phase::Closing; } } }, Phase::Closing => { match p.1.shutdown() { Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Err(e); } warn!("{}", e); return Err(e); }, Ok(Async::NotReady) => { return wouldblock(); }, Ok(Async::Ready(())) => { if state.need_wait_for_reading { if state.finished_reading { debug!("Closed and reading is also done"); finished=true; } else { debug!("Closed, but need to wait for other direction to finish"); ph = Phase::WaitingForReadToFinish; let (tx,rx) = futures::sync::oneshot::channel(); state.read_waiter_tx = Some(tx); state.read_waiter_rx = Some(rx); } } else { debug!("Closed"); finished=true; } } } }, Phase::WaitingForReadToFinish => { match state.read_waiter_rx.as_mut().unwrap().poll() { Ok(Async::NotReady) => { return wouldblock(); } _ => { debug!("Waited for read to finish"); finished=true; } } } } } state.ph = ph; } } } fn flush(&mut self) -> Result<(), IoError> { // No-op here: we flush and close after each write Ok(()) } } impl AsyncWrite for PeerHandle { fn shutdown(&mut self) -> futures::Poll<(), IoError> { // No-op here: we flush and close after each write Ok(Async::Ready(())) } } pub fn foreachmsg_peer(s: Rc, cp: ConstructParams) -> BoxedNewPeerFuture { let need_wait_for_reading = cp.program_options.foreachmsg_wait_reads; let s = Rc::new(RefCell::new(State { cp, s, p: None, n: None, aux: Default::default(), ph: Phase::Idle, finished_reading: false, read_waiter_tx: None, read_waiter_rx: None, wait_for_new_peer_rx: None, wait_for_new_peer_tx: None, need_wait_for_reading, })); let ph1 = PeerHandle(s.clone()); let ph2 = PeerHandle(s); let peer = Peer::new(ph1, ph2, None /* we handle hups ourselves */); Box::new(ok(peer)) as BoxedNewPeerFuture } ================================================ FILE: src/help.rs ================================================ use super::{Opt, SpecifierClass, StructOpt}; fn spechelp(sc: &dyn SpecifierClass, overlays: bool, advanced: bool) { if !advanced && sc.help().contains("[A]") { return; } if overlays ^ sc.is_overlay() { return; } let first_prefix = sc.get_prefixes()[0]; let mut first_help_line = None; for l in sc.help().lines() { if !l.trim().is_empty() { first_help_line = Some(l); break; } } if let Some(fhl) = first_help_line { println!("\t{:16}\t{}", first_prefix, fhl); } } // https://github.com/rust-lang/rust/issues/51942 #[allow(clippy::nonminimal_bool)] pub fn shorthelp() { //use std::io::Write; use std::io::{BufRead, BufReader}; let mut b = vec![]; if Opt::clap().write_help(&mut b).is_err() { eprintln!("Error displaying the help message"); } let mut lines_to_display = vec![]; let mut do_display = true; BufReader::new(&b[..]).lines().for_each(|l| { if let Ok(l) = l { { let lt = l.trim(); let new_paragraph_start = false || lt.starts_with('-') || l.is_empty(); if lt.starts_with("--help") { // Allowed to output [A] regardless do_display = true; } else if l.contains("[A]") { do_display = false; if l.trim().starts_with("[A]") { // Also retroactively retract the previous line let nl = lines_to_display.len() - 1; lines_to_display.truncate(nl); } } else if new_paragraph_start { do_display = true; }; } let mut additional_line = None; if l == "FLAGS:" { additional_line = Some(" (some flags are hidden, see --help=long)".to_string()); }; if l == "OPTIONS:" { additional_line = Some(" (some options are hidden, see --help=long)".to_string()); }; if do_display { lines_to_display.push(l); if let Some(x) = additional_line { lines_to_display.push(x); } }; } }); for l in lines_to_display { println!("{}", l); } println!("\nPartial list of address types:"); macro_rules! my { ($x:expr) => { spechelp(&$x, false, false); }; } list_of_all_specifier_classes!(my); println!("Partial list of overlays:"); macro_rules! my { ($x:expr) => { spechelp(&$x, true, false); }; } list_of_all_specifier_classes!(my); println!("See more address types with the --help=long option."); println!("See short examples and --dump-spec names for most address types and overlays with --help=doc option"); } pub fn longhelp() { //let q = Opt::from_iter(vec!["-"]); let mut a = Opt::clap(); let _ = a.print_help(); println!("\n\nFull list of address types:"); macro_rules! my { ($x:expr) => { spechelp(&$x, false, true); }; } list_of_all_specifier_classes!(my); println!("Full list of overlays:"); macro_rules! my { ($x:expr) => { spechelp(&$x, true, true); }; } list_of_all_specifier_classes!(my); } fn specdoc(sc: &dyn SpecifierClass, overlays: bool) { if sc.is_overlay() ^ overlays { return; } let first_prefix = sc.get_prefixes()[0]; let spec_name = sc.get_name().replace("Class", ""); let other_prefixes = sc.get_prefixes()[1..] .iter() .map(|x| format!("`{}`", x)) .collect::>() .join(", "); println!(r#"### `{}`"#, first_prefix); println!(); if !other_prefixes.is_empty() { println!("Aliases: {} ", other_prefixes); } println!("Internal name for --dump-spec: {}", spec_name); println!(); let help = sc .help() //.lines() //.map(|x|format!(" {}",x)) //.collect::>() //.join("\n") ; println!("{}\n", help); } pub fn dochelp() { println!(r#" # Websocat Reference (in progress) Websocat has many command-line options and special format for positional arguments. There are three main modes of websocat invocation: * Simple client mode: `websocat wss://your.server/url` * Simple server mode: `websocat -s 127.0.0.1:8080` * Advanced [socat][1]-like mode: `websocat -t ws-l:127.0.0.1:8080 mirror:` Ultimately in any of those modes websocat creates two connections and exchanges data between them. If one of the connections is bytestream-oriented (for example the terminal stdin/stdout or a TCP connection), but the other is message-oriented (for example, a WebSocket or UDP) then websocat operates in lines: each line correspond to a message. Details of this are configurable by various options. `ws-l:` or `mirror:` above are examples of address types. With the exception of special cases like WebSocket URL `ws://1.2.3.4/` or stdio `-`, websocat's positional argument is defined by this rule: ``` ::= ( ":" )* ":" [address] ``` Some address types may be "aliases" to other address types or combinations of overlays and address types. [1]:http://www.dest-unreach.org/socat/doc/socat.html # `--help=long` "Advanced" options and flags are denoted by `[A]` marker. ``` "#); let mut a = Opt::clap(); let _ = a.print_help(); println!( r#" ``` # Full list of address types "Advanced" address types are denoted by `[A]` marker. "# ); macro_rules! my { ($x:expr) => { specdoc(&$x, false); }; } list_of_all_specifier_classes!(my); println!( r#" # Full list of overlays "Advanced" overlays denoted by `[A]` marker. "# ); macro_rules! my { ($x:expr) => { specdoc(&$x, true); }; } list_of_all_specifier_classes!(my); println!( r#" ### Address types or specifiers to be implemented later: `sctp:`, `speedlimit:`, `quic:` ### Final example Final example just for fun: wacky mode websocat ws-c:ws-l:ws-c:- tcp:127.0.0.1:5678 Connect to a websocket using stdin/stdout as a transport, then accept a websocket connection over the previous websocket used as a transport, then connect to a websocket using previous step as a transport, then forward resulting connection to the TCP port. (Exercise to the reader: manage to make it actually connect to 5678). "# ); } ================================================ FILE: src/http_peer.rs ================================================ #![allow(unused)] #![allow(clippy::needless_pass_by_value,clippy::cast_lossless,clippy::identity_op)] use futures::future::{err, ok, Future}; use std::rc::Rc; use super::{box_up_err, peer_strerr, BoxedNewPeerFuture, Peer}; use super::{ConstructParams, L2rUser, PeerConstructor, Specifier}; use tokio_io::io::{read_exact, write_all}; use tokio_io::{AsyncRead,AsyncWrite}; use std::io::Write; use std::net::{IpAddr, Ipv4Addr}; use std::ffi::OsString; extern crate http_bytes; use http_bytes::http; use http_bytes::{Request,Response}; use crate::http::Uri; use crate::http::Method; use crate::util::peer_err2; #[derive(Debug)] pub struct HttpRequest(pub T); impl Specifier for HttpRequest { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, l2r| { let mut b = crate::http::request::Builder::default(); if let Some(uri) = cp.program_options.request_uri.as_ref() { b.uri(uri); } if let Some(method) = cp.program_options.request_method.as_ref() { b.method(method); } for (hn, hv) in &cp.program_options.request_headers { b.header(hn, hv); } let request = b.body(()).unwrap(); http_request_peer(&request, p, l2r) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = HttpRequestClass, target = HttpRequest, prefixes = ["http-request:"], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" [A] Issue HTTP request, receive a 1xx or 2xx reply, then pass the torch to outer peer, if any - lowlevel version. Content you write becomes body, content you read is body that server has sent. URI is specified using a separate command-line parameter Example: websocat -Ub - http-request:tcp:example.com:80 --request-uri=http://example.com/ --request-header 'Connection: close' "# ); /// Inner peer is a TCP peer configured to this host #[derive(Debug)] pub struct Http(pub T, pub Uri); impl Specifier for Http { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); let uri = self.1.clone(); inner.map(move |p, l2r| { let mut b = crate::http::request::Builder::default(); b.uri(uri.clone()); if let Some(method) = cp.program_options.request_method.as_ref() { b.method(method); } for (hn, hv) in &cp.program_options.request_headers { b.header(hn, hv); } let request = b.body(()).unwrap(); http_request_peer(&request, p, l2r) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = HttpClass, target = Http, prefixes = ["http:"], arg_handling = { fn construct(self: &HttpClass, arg: &str) -> super::Result> { let uri : Uri = format!("http:{}", arg).parse()?; let tcp_peer; { let auth = uri.authority_part().unwrap(); let host = auth.host(); let port = auth.port_part(); let addr = if let Some(p) = port { format!("tcp:{}:{}", host, p) } else { format!("tcp:{}:80", host) }; tcp_peer = crate::spec(addr.as_ref())?; } Ok(Rc::new(Http(tcp_peer, uri))) } fn construct_overlay( self: &HttpClass, _inner: Rc, ) -> super::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } }, overlay = false, StreamOriented, SingleConnect, help = r#" [A] Issue HTTP request, receive a 1xx or 2xx reply, then pass the torch to outer peer, if any - highlevel version. Content you write becomes body, content you read is body that server has sent. URI is specified inline. Example: websocat -b - http://example.com < /dev/null "# ); #[derive(Copy,Clone,PartialEq,Debug)] enum HttpHeaderEndDetectionState { Neutral, FirstCr, FirstLf, SecondCr, FoundHeaderEnd, } struct WaitForHttpHead { buf: Option>, offset : usize, state: HttpHeaderEndDetectionState, io : Option, } struct WaitForHttpHeadResult { buf: Vec, // Before the offset is header, after the offset is debt offset: usize, } impl WaitForHttpHead { pub fn new(r:R) -> WaitForHttpHead { WaitForHttpHead { buf: Some(Vec::with_capacity(512)), offset: 0, state: HttpHeaderEndDetectionState::Neutral, io: Some(r), } } } impl Future for WaitForHttpHead { type Item = (WaitForHttpHeadResult, R); type Error = Box; fn poll(&mut self) -> ::futures::Poll { loop { if self.buf.is_none() || self.io.is_none() { Err("WaitForHttpHeader future polled after completion")?; } let ret; { let buf = self.buf.as_mut().unwrap(); let io = self.io.as_mut().unwrap(); if buf.len() < self.offset + 1024 { buf.resize(self.offset + 1024, 0u8); } ret = try_nb!(io.read(&mut buf[self.offset..])); if ret == 0 { Err("Trimmed HTTP head")?; } } // parse for i in self.offset..(self.offset+ret) { let x = self.buf.as_ref().unwrap()[i]; use self::HttpHeaderEndDetectionState::*; //eprint!("{:?} -> ", self.state); self.state = match (self.state, x) { (Neutral, b'\r') => FirstCr, (FirstCr, b'\n') => FirstLf, (FirstLf, b'\r') => SecondCr, (SecondCr, b'\n') => FoundHeaderEnd, _ => Neutral, }; //eprintln!("{:?}", self.state); if self.state == FoundHeaderEnd { let io = self.io.take().unwrap(); let mut buf = self.buf.take().unwrap(); buf.resize(self.offset + ret, 0u8); return Ok(::futures::Async::Ready(( WaitForHttpHeadResult { buf, offset: i+1}, io, ))); } } self.offset += ret; if self.offset > 60_000 { Err("HTTP head too long")?; } } } } pub fn http_request_peer( request: &Request, inner_peer: Peer, _l2r: L2rUser, ) -> BoxedNewPeerFuture { let request = ::http_bytes::request_header_to_vec(request); let (r, w, hup) = (inner_peer.0, inner_peer.1, inner_peer.2); info!("Issuing HTTP request"); let f = ::tokio_io::io::write_all(w, request) .map_err(box_up_err) .and_then(move |(w, request)| { WaitForHttpHead::new(r).and_then(|(res, r)|{ debug!("Got HTTP response head"); let ret = (move||{ { let headbuf = &res.buf[0..res.offset]; trace!("{:?}",headbuf); let p = http_bytes::parse_response_header_easy(headbuf)?; if p.is_none() { Err("Something wrong with response HTTP head")?; } let p = p.unwrap(); if !p.1.is_empty() { Err("Something wrong with parsing HTTP")?; } let response = p.0; let status = response.status(); info!("HTTP response status: {}", status); debug!("{:#?}", response); if status.is_success() || status.is_informational() { // OK } else { Err("HTTP response indicates failure")?; } } let remaining = res.buf.len() - res.offset; if remaining == 0 { Ok(Peer::new(r,w,hup)) } else { debug!("{} bytes of debt to be read", remaining); let r = super::trivial_peer::PrependRead { inner: r, header: res.buf, remaining, }; Ok(Peer::new(r,w,hup)) } })(); ::futures::future::result(ret) }) }) ; Box::new(f) as BoxedNewPeerFuture } #[derive(Debug)] pub struct HttpPostSse(pub T); impl Specifier for HttpPostSse { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, l2r| { http_response_post_sse_peer(p, l2r) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = HttpPostSseClass, target = HttpPostSse, prefixes = ["http-post-sse:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" [A] Accept HTTP/1 request. Then, if it is GET, unidirectionally return incoming messages as server-sent events (SSE). If it is POST then, also unidirectionally, write body upstream. Example - turn SSE+POST pair into a client WebSocket connection: websocat -E -t http-post-sse:tcp-l:127.0.0.1:8080 reuse:ws://127.0.0.1:80/websock `curl -dQQQ http://127.0.0.1:8080/` would send into it and `curl -N http://127.0.0.1:8080/` would recv from it. "# ); #[derive(Debug)] enum ModeOfOperation { PostBody, GetSse, } pub fn http_response_post_sse_peer( inner_peer: Peer, _l2r: L2rUser, ) -> BoxedNewPeerFuture { let (r, w, hup) = (inner_peer.0, inner_peer.1, inner_peer.2); warn!("Note: http-post-see mode is not tested and may integrate poorly into current Websocat architecture. Expect it to be of lower quality than other Websocat modes."); info!("Incoming prospective HTTP request"); let f = WaitForHttpHead::new(r).and_then(|(res, r)|{ debug!("Got HTTP request head"); let ret : Result<_,Box> = (move||{ let mode; let request; { let headbuf = &res.buf[0..res.offset]; trace!("{:?}",headbuf); let p = http_bytes::parse_request_header_easy(headbuf)?; if p.is_none() { Err("Something wrong with request HTTP head")?; } let p = p.unwrap(); if !p.1.is_empty() { Err("Something wrong with parsing HTTP request")?; } request = p.0; let method = request.method(); mode = match *method { http::method::Method::GET => { info!("GET request. Serving a SSE stream"); ModeOfOperation::GetSse }, http::method::Method::POST => { info!("POST request. Writing body once"); ModeOfOperation::PostBody }, _ => { error!("HTTP request method is {}, but we expect only GET or POST in this mode", method); Err("Wrong HTTP request method")? } }; debug!("{:#?}", request); } // Now it's time to generate a successful reply. // (maybe actually we should read the full request first, but // let's do some thing at first and correct thing after that). use crate::http::header::{HOST,SERVER,CACHE_CONTROL, CONTENT_TYPE}; let mut reply = crate::http::response::Builder::default(); let status = match mode { ModeOfOperation::GetSse => 200, ModeOfOperation::PostBody => 204, }; reply.status(status); if let Some(x) = request.headers().get(HOST) { reply.header(HOST, x); } reply.header("Server", "websocat"); match mode { ModeOfOperation::GetSse => { reply.header(CACHE_CONTROL, "no-cache"); reply.header(CONTENT_TYPE, "text/event-stream"); } ModeOfOperation::PostBody => (), } let reply = reply.body(()).unwrap(); let reply = ::http_bytes::response_header_to_vec(&reply); Ok(::tokio_io::io::write_all(w, reply) .map_err(box_up_err) .and_then(move |(w, request)| { debug!("Response writing finished"); // Infinitely hang reading or writing // If use DevNull instead, connection may get closed prematurely let dummy = crate::trivial_peer::CloggedPeer; match mode { ModeOfOperation::GetSse => { // Will it call shutdown(2) on the socket? drop(r); let w = SseStream::new(w); Ok(Peer::new(dummy, w, hup)) }, ModeOfOperation::PostBody => { debug!("Start streaming POST body upstream, ignoring reverse data"); // Will it call shutdown(2) on the socket? drop(w); let remaining = res.buf.len() - res.offset; if remaining == 0 { Ok(Peer::new(r,dummy,hup)) } else { debug!("{} bytes of debt to be read", remaining); let r = super::trivial_peer::PrependRead { inner: r, header: res.buf, remaining, }; Ok(Peer::new(r,dummy,hup)) } }, } })) })(); match ret { Err(x) => peer_err2(x), Ok(x) => Box::new(x), } }); Box::new(f) as BoxedNewPeerFuture } #[derive(Clone,Copy,Debug)] enum SseState { BeforeLine(usize), InsideLine, AfterLine, Trailer, } struct SseStream { io : W, state: SseState, consumed_actual_buffer : usize, } impl SseStream { pub fn new(w: W) -> Self { SseStream { io: w, state: SseState::BeforeLine(0), consumed_actual_buffer: 0, } } } impl AsyncWrite for SseStream { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self.io.shutdown() } } impl Write for SseStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { // assumes buffer does not change on EAGAINs loop { let s = self.state; let mut need_write = 0; debug!("SSE state {:?}", s); let ret = match s { SseState::BeforeLine(x) => { self.io.write(&b"data: "[x..6]) } SseState::InsideLine => { let buf = &buf[self.consumed_actual_buffer..]; let max = buf.iter().position(|&x|x==b'\n').unwrap_or(buf.len()); let buf = &buf[0..max]; need_write = buf.len(); self.io.write(buf) } SseState::AfterLine => { self.io.write(b"\n") } SseState::Trailer => { self.io.write(b"\n") } }; let ll = ret?; self.state = match s { SseState::BeforeLine(x) => { let nl = ll + x; if nl == 6 { SseState::InsideLine } else { SseState::BeforeLine(nl) } } SseState::InsideLine => { self.consumed_actual_buffer += ll; if ll < need_write { SseState::InsideLine } else { SseState::AfterLine } } SseState::AfterLine => { if self.consumed_actual_buffer < buf.len() { if buf[self.consumed_actual_buffer] == b'\n' { self.consumed_actual_buffer += 1; } SseState::BeforeLine(0) } else { SseState::Trailer } } SseState::Trailer => { let r = self.consumed_actual_buffer; self.consumed_actual_buffer = 0; self.state = SseState::BeforeLine(0); debug!("r={} buflen={}", r, buf.len()); return Ok(r) } }; debug!(" new SSE state {:?}", self.state); } } fn flush(&mut self) -> std::io::Result<()> { self.io.flush() } } #[test] fn test_basic_sse_stream() { let mut v = vec![]; { let mut ss = SseStream::new(std::io::Cursor::new(&mut v)); } } ================================================ FILE: src/http_serve.rs ================================================ use self::hyper::http::h1::Incoming; use self::hyper::method::Method; use self::hyper::uri::RequestUri; use self::hyper::uri::RequestUri::AbsolutePath; use super::hyper; use futures::future::Future; use std::fs::File; use std::rc::Rc; use crate::options::StaticFile; use crate::trivial_peer::get_literal_peer_now; use crate::Peer; use crate::my_copy::{copy, CopyOptions}; const BAD_REQUEST :&[u8] = b"HTTP/1.1 400 Bad Request\r\nServer: websocat\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nOnly WebSocket connections are welcome here\n"; const NOT_FOUND: &[u8] = b"HTTP/1.1 404 Not Found\r\nServer: websocat\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nURI does not match any -F option and is not a WebSocket connection.\n"; const NOT_FOUND2: &[u8] = b"HTTP/1.1 500 Not Found\r\nServer: websocat\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nFailed to open the file on server side.\n"; const BAD_METHOD :&[u8] = b"HTTP/1.1 400 Bad Request\r\nServer: websocat\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nHTTP method should be GET\n"; const BAD_URI_FORMAT :&[u8] = b"HTTP/1.1 400 Bad Request\r\nServer: websocat\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nURI should be an absolute path\n"; pub fn get_static_file_reply(len: Option, ct: &str) -> Vec { let mut q = Vec::with_capacity(256); q.extend_from_slice(b"HTTP/1.1 200 OK\r\nServer: websocat\r\nContent-Type: "); q.extend_from_slice(ct.as_bytes()); q.extend_from_slice(b"\r\n"); if let Some(x) = len { q.extend_from_slice(b"Content-Length: "); q.extend_from_slice(format!("{}", x).as_bytes()); q.extend_from_slice(b"\r\n"); } q.extend_from_slice(b"\r\n"); q } #[allow(clippy::needless_pass_by_value)] pub fn http_serve( p: Peer, incoming: Option>, serve_static_files: Rc>, ) -> Box> { let mut serve_file = None; let content = if serve_static_files.is_empty() { BAD_REQUEST.to_vec() } else if let Some(inc) = incoming { info!("HTTP-serving {:?}", inc.subject); if inc.subject.0 == Method::Get { match inc.subject.1 { AbsolutePath(x) => { let mut reply = None; for sf in &*serve_static_files { if sf.uri == x { match File::open(&sf.file) { Ok(f) => { let fs = match f.metadata() { Err(_) => None, Ok(x) => Some(x.len()), }; reply = Some(get_static_file_reply(fs, &sf.content_type)); serve_file = Some(f); } Err(_) => { reply = Some(NOT_FOUND2.to_vec()); } } } } reply.unwrap_or_else(|| NOT_FOUND.to_vec()) } _ => BAD_URI_FORMAT.to_vec(), } } else { BAD_METHOD.to_vec() } } else { BAD_REQUEST.to_vec() }; let reply = get_literal_peer_now(content); let co = CopyOptions { buffer_size: 1024, once: false, stop_on_reader_zero_read: true, skip: false, max_ops: None, }; if let Some(f) = serve_file { Box::new( copy(reply, p.1, co, vec![]) .map_err(drop) .and_then(move |(_len, _, conn)| { let co2 = CopyOptions { buffer_size: 65536, once: false, stop_on_reader_zero_read: true, skip: false, max_ops: None, }; let wr = crate::file_peer::ReadFileWrapper(f); copy(wr, conn, co2, vec![]).map(|_| ()).map_err(drop) }), ) } else { Box::new(copy(reply, p.1, co, vec![]).map(|_| ()).map_err(drop)) } } ================================================ FILE: src/jsonrpc_peer.rs ================================================ use futures::future::ok; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer}; use super::{ConstructParams, PeerConstructor, Specifier}; use std::io::Read; use tokio_io::AsyncRead; use std::io::Error as IoError; #[derive(Debug)] pub struct JsonRpc(pub T); impl Specifier for JsonRpc { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, _| jsonrpc_peer(p, cp.program_options.jsonrpc_omit_jsonrpc)) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = JsonRpcClass, target = JsonRpc, prefixes = ["jsonrpc:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" [A] Turns messages like `abc 1,2` into `{"jsonrpc":"2.0","id":412, "method":"abc", "params":[1,2]}`. For simpler manual testing of websocket-based JSON-RPC services Example: TODO "# ); pub fn jsonrpc_peer(inner_peer: Peer, omit_jsonrpc: bool) -> BoxedNewPeerFuture { let filtered = JsonRpcWrapper(inner_peer.0, 1, omit_jsonrpc); let thepeer = Peer::new(filtered, inner_peer.1, inner_peer.2); Box::new(ok(thepeer)) as BoxedNewPeerFuture } struct JsonRpcWrapper(Box, u64, bool); impl Read for JsonRpcWrapper { fn read(&mut self, b: &mut [u8]) -> Result { let l = b.len(); assert!(l > 1); let n = self.0.read(&mut b[..l])?; if n == 0 { return Ok(0); } let mut method = Vec::with_capacity(20); let mut params = Vec::with_capacity(20); enum PS { BeforeMethodName, InsideMethodName, AfterMethodName, InsideParams, } let mut s = PS::BeforeMethodName; for &c in b[..n].iter() { match s { PS::BeforeMethodName => { if c == b' ' || c == b'\t' || c == b'\n' { // ignore } else { method.push(c); s = PS::InsideMethodName; } } PS::InsideMethodName => { if c == b' ' || c == b'\t' || c == b'\n' { s = PS::AfterMethodName; } else { method.push(c); } } PS::AfterMethodName => { if c == b' ' || c == b'\t' || c == b'\n' { // ignore } else { params.push(c); s = PS::InsideParams; } } PS::InsideParams => { params.push(c); } } } let mut bb = ::std::io::Cursor::new(b); use std::io::Write; //{"jsonrpc":"2.0","id":412, "method":"abc", "params":[1,2]} if self.2 { let _ = bb.write_all(b"{\"id\":"); } else { let _ = bb.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":"); } let _ = bb.write_all(format!("{}", self.1).as_bytes()); self.1 += 1; let _ = bb.write_all(b", \"method\":\""); let _ = bb.write_all(&method); let _ = bb.write_all(b"\", \"params\":"); let needs_brackets = params.is_empty() || params[0] != b'{' && params[0] != b'['; if !params.is_empty() && params[params.len() - 1] == b'\n' { let l = params.len() - 1; params.truncate(l); } if !params.is_empty() && params[params.len() - 1] == b'\r' { let l = params.len() - 1; params.truncate(l); } if needs_brackets { let _ = bb.write_all(b"["); } let _ = bb.write_all(¶ms); if needs_brackets { let _ = bb.write_all(b"]"); } let _ = bb.write_all(b"}\n"); if bb.position() as usize == l { warn!("Buffer too small, JSON RPC message may be truncated."); } Ok(bb.position() as usize) } } impl AsyncRead for JsonRpcWrapper {} ================================================ FILE: src/lengthprefixed_peer.rs ================================================ use futures::future::ok; use std::rc::Rc; use crate::{io_other_error, simple_err, peer_strerr}; use super::{BoxedNewPeerFuture, Peer}; use super::{ConstructParams, PeerConstructor, Specifier}; use std::io::{Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use std::io::Error as IoError; #[derive(Debug)] pub struct LengthPrefixed(pub T); impl Specifier for LengthPrefixed { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, _| { lengthprefixed_peer( p, cp.program_options.lengthprefixed_header_bytes, cp.program_options.lengthprefixed_little_endian, cp.program_options.lengthprefixed_skip_read_direction, cp.program_options.lengthprefixed_skip_write_direction, ) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = LengthPrefixedClass, target = LengthPrefixed, prefixes = ["lengthprefixed:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" Turn stream of bytes to/from data packets with length-prefixed framing. [A] You can choose the number of header bytes (1 to 8) and endianness. Default is 4 bytes big endian. This affects both reading and writing - attach this overlay to stream specifier to turn it into a packet-orineted specifier. Mind the buffer size (-B). All packets should fit in there. Examples: websocat -u -b udp-l:127.0.0.1:1234 lengthprefixed:writefile:test.dat websocat -u -b lengthprefixed:readfile:test.dat udp:127.0.0.1:1235 This would save incoming UDP packets to a file, then replay the datagrams back to UDP socket websocat -b lengthprefixed:- ws://127.0.0.1:1234/ --binary-prefix=B --text-prefix=T This allows to mix and match text and binary WebSocket messages to and from stdio without the base64 overhead. "# ); pub fn lengthprefixed_peer( inner_peer: Peer, num_bytes_in_length_prefix: usize, little_endian: bool, lengthprefixed_skip_read_direction: bool, lengthprefixed_skip_write_direction: bool, ) -> BoxedNewPeerFuture { if !(1..=8).contains(&num_bytes_in_length_prefix) { return peer_strerr("Number of header bytes for lengthprefixed overlay should be from 1 to 8"); } let (length_starting_pos, length_ending_pos) = if little_endian { (0, num_bytes_in_length_prefix) } else { (8 - num_bytes_in_length_prefix, 8) }; let reader = Lengthprefixed2PacketWrapper { inner: inner_peer.0, length_buffer: [0; 8], length_starting_pos, length_pos: length_starting_pos, length_ending_pos, little_endian, data_read_so_far: 0, }; let writer = Packet2LengthPrefixedWrapper { inner: inner_peer.1, length_buffer: [0; 8], length_starting_pos, length_pos: length_starting_pos, length_ending_pos, little_endian, data_written_so_far: 0, }; let thepeer = match (lengthprefixed_skip_read_direction, lengthprefixed_skip_write_direction) { (true, true) => Peer::new(reader.inner, writer.inner, inner_peer.2), (true, false) => Peer::new(reader.inner, writer, inner_peer.2), (false, true) => Peer::new(reader, writer.inner, inner_peer.2), (false, false) => Peer::new(reader, writer, inner_peer.2), }; Box::new(ok(thepeer)) as BoxedNewPeerFuture } struct Lengthprefixed2PacketWrapper { inner: Box, length_buffer: [u8; 8], length_starting_pos: usize, length_ending_pos: usize, length_pos: usize, little_endian: bool, data_read_so_far: usize, } impl Read for Lengthprefixed2PacketWrapper { fn read(&mut self, buf: &mut [u8]) -> Result { loop { assert!(self.length_pos <= self.length_ending_pos); assert!(self.length_pos >= self.length_starting_pos); if self.length_ending_pos != self.length_pos { match self .inner .read(&mut self.length_buffer[self.length_pos..self.length_ending_pos]) { Err(e) => return Err(e), Ok(0) => { if self.length_pos != self.length_starting_pos { error!("Possibly trimmed length-prefixed data.") } return Ok(0); } Ok(n) => { self.length_pos += n; continue; } } } else { let packet_len = if self.little_endian { u64::from_le_bytes(self.length_buffer) } else { u64::from_be_bytes(self.length_buffer) }; if packet_len >= (buf.len() as u64) { error!("Failed to process too big packet. You may need to increase the -B buffer size."); return Err(io_other_error(simple_err("Packet length overflow".into()))); } let packet_len = packet_len as usize; if packet_len == 0 { return Ok(0); } if self.data_read_so_far == packet_len { self.data_read_so_far = 0; self.length_buffer = [0; 8]; self.length_pos = self.length_starting_pos; return Ok(packet_len); } // Assume we are called with the same buffer until we return success, so we // can use buffer as a persistent scratch space match self.inner.read(&mut buf[self.data_read_so_far..packet_len]) { Err(e) => return Err(e), Ok(0) => { return Err(io_other_error(simple_err("Data trimmed".into()))); } Ok(n) => { self.data_read_so_far += n; continue; } } } } } } impl AsyncRead for Lengthprefixed2PacketWrapper {} struct Packet2LengthPrefixedWrapper { inner: Box, length_buffer: [u8; 8], length_starting_pos: usize, length_ending_pos: usize, length_pos: usize, little_endian: bool, data_written_so_far: usize, } impl Write for Packet2LengthPrefixedWrapper { fn write(&mut self, buf: &[u8]) -> std::io::Result { // Assuming `write` is retried with the same buffer when we return WouldBlock loop { if self.length_pos == self.length_starting_pos { if self.little_endian { self.length_buffer = (buf.len() as u64).to_le_bytes() } else { self.length_buffer = (buf.len() as u64).to_be_bytes() } } if self.length_pos < self.length_ending_pos { match self .inner .write(&self.length_buffer[self.length_pos..self.length_ending_pos]) { Err(x) => return Err(x), Ok(n) => self.length_pos += n, } continue; } if self.data_written_so_far == buf.len() { self.data_written_so_far = 0; self.length_pos = self.length_starting_pos; self.length_buffer = [0; 8]; return Ok(buf.len()); } match self.inner.write(&buf[self.data_written_so_far..]) { Err(e) => return Err(e), Ok(n) => self.data_written_so_far += n, } } } fn flush(&mut self) -> std::io::Result<()> { self.inner.flush() } } impl AsyncWrite for Packet2LengthPrefixedWrapper { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self.inner.shutdown() } } ================================================ FILE: src/lib.rs ================================================ //! Note: library usage is not semver/API-stable //! //! Type evolution of a websocat run: //! //! 1. `&str` - string as passed to command line. When it meets the list of `SpecifierClass`es, there appears: //! 2. `SpecifierStack` - specifier class, final string argument and vector of overlays. //! 3. `Specifier` - more rigid version of SpecifierStack, with everything parsable parsed. May be nested. When `construct` is called, we get: //! 4. `PeerConstructor` - a future or stream that returns one or more connections. After completion, we get one or more of: //! 5. `Peer` - an active connection. Once we have two of them, we can start a: //! 6. `Session` with two `Transfer`s - forward and reverse. #![allow(renamed_and_removed_lints)] extern crate futures; #[macro_use] extern crate tokio_io; extern crate tokio_current_thread; extern crate tokio_reactor; extern crate tokio_tcp; extern crate tokio_udp; extern crate tokio_codec; extern crate tokio_timer; extern crate websocket; extern crate websocket_base; extern crate http_bytes; extern crate anymap; pub use http_bytes::http; extern crate tk_listen; extern crate net2; #[macro_use] extern crate log; #[macro_use] extern crate slab_typesafe; #[macro_use] extern crate smart_default; #[macro_use] extern crate derivative; use futures::future::Future; use tokio_io::{AsyncRead, AsyncWrite}; use futures::Stream; use std::cell::RefCell; use std::rc::Rc; use std::str::FromStr; type Result = std::result::Result>; /// First representation of websocat command-line, partially parsed. pub struct WebsocatConfiguration1 { pub opts: Options, pub addr1: String, pub addr2: String, } impl WebsocatConfiguration1 { /// Is allowed to call blocking calls /// happens only at start of websocat pub fn parse1(self) -> Result { Ok(WebsocatConfiguration2 { opts: self.opts, s1: SpecifierStack::from_str(self.addr1.as_str())?, s2: SpecifierStack::from_str(self.addr2.as_str())?, }) } } /// Second representation of websocat configuration: everything /// (e.g. socket addresses) should already be parsed and verified /// A structural form: two chains of specifier nodes. /// Futures/async is not yet involved at this stage, but everything /// should be checked and ready to do to start it (apart from OS errors) /// /// This form is designed to be editable by lints and command-line options. pub struct WebsocatConfiguration2 { pub opts: Options, pub s1: SpecifierStack, pub s2: SpecifierStack, } impl WebsocatConfiguration2 { pub fn parse2(self) -> Result { Ok(WebsocatConfiguration3 { opts: self.opts, s1: ::from_stack(&self.s1)?, s2: ::from_stack(&self.s2)?, }) } } /// An immutable chain of functions that results in a `Future`s or `Streams` that rely on each other. /// This is somewhat like a frozen form of `WebsocatConfiguration2`. pub struct WebsocatConfiguration3 { pub opts: Options, pub s1: Rc, pub s2: Rc, } impl WebsocatConfiguration3 { pub fn serve(self, onerror: std::rc::Rc) -> impl Future where OE: Fn(Box) + 'static, { serve(self.s1, self.s2, self.opts, onerror) } } pub mod options; pub use crate::options::Options; #[derive(SmartDefault)] pub struct ProgramState( #[default(anymap::AnyMap::with_capacity(2))] anymap::AnyMap ); /// Some information passed from the left specifier Peer to the right #[derive(Default, Clone)] pub struct LeftSpecToRightSpec { /// URI the client requested when connecting to WebSocket uri: Option, /// Address:port of connecting client, if it is TCP client_addr: Option, /// All incoming HTTP headers headers: Vec<(String, String)>, } pub type L2rWriter = Rc>; pub type L2rReader = Rc; #[derive(Clone)] pub enum L2rUser { FillIn(L2rWriter), ReadFrom(L2rReader), } /// Resolves if/when TCP socket gets reset pub type HupToken = Box>>; pub struct Peer(Box, Box, Option); pub type BoxedNewPeerFuture = Box>>; pub type BoxedNewPeerStream = Box>>; #[macro_use] pub mod specifier; pub use crate::specifier::{ ClassMessageBoundaryStatus, ClassMulticonnectStatus, ConstructParams, Specifier, SpecifierClass, SpecifierStack, }; #[macro_use] pub mod all_peers; pub mod lints; mod my_copy; pub use crate::util::{brokenpipe, io_other_error, simple_err2, wouldblock}; #[cfg(all(unix, feature = "unix_stdio"))] pub mod stdio_peer; pub mod file_peer; pub mod mirror_peer; pub mod net_peer; pub mod stdio_threaded_peer; pub mod trivial_peer; pub mod ws_client_peer; pub mod ws_peer; pub mod ws_server_peer; pub mod ws_lowlevel_peer; pub mod http_peer; #[cfg(feature = "tokio-process")] pub mod process_peer; #[cfg(all(windows,feature = "windows_named_pipes"))] pub mod windows_np_peer; #[cfg(unix)] pub mod unix_peer; pub mod broadcast_reuse_peer; pub mod jsonrpc_peer; pub mod timestamp_peer; pub mod line_peer; pub mod lengthprefixed_peer; pub mod foreachmsg_peer; pub mod primitive_reuse_peer; pub mod reconnect_peer; pub mod socks5_peer; #[cfg(feature = "ssl")] pub mod ssl_peer; #[cfg(feature = "crypto_peer")] pub mod crypto_peer; #[cfg(feature = "prometheus_peer")] pub mod prometheus_peer; pub mod specparse; pub type PeerOverlay = Rc BoxedNewPeerFuture>; pub enum PeerConstructor { ServeOnce(BoxedNewPeerFuture), ServeMultipleTimes(BoxedNewPeerStream), Overlay1(BoxedNewPeerFuture, PeerOverlay), OverlayM(BoxedNewPeerStream, PeerOverlay), Error(Box), } /// A remnant of the hack pub fn spawn_hack(f: T) where T: Future + 'static, { tokio_current_thread::TaskExecutor::current() .spawn_local(Box::new(f)) .unwrap() } pub mod util; pub use crate::util::{box_up_err, multi, once, peer_err, peer_err_s, peer_strerr, simple_err}; pub mod readdebt; pub use crate::specparse::spec; pub struct Transfer { from: Box, to: Box, } pub struct Session { t1: Transfer, t2: Transfer, opts: Rc, hup1: Option, hup2: Option, } pub mod sessionserve; pub use crate::sessionserve::serve; ================================================ FILE: src/line_peer.rs ================================================ use futures::future::ok; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer}; use super::{ConstructParams, PeerConstructor, Specifier}; use std::io::Read; use tokio_io::AsyncRead; use std::io::Error as IoError; #[derive(Debug)] pub struct Message2Line(pub T); impl Specifier for Message2Line { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); let zt = cp.program_options.linemode_zero_terminated; inner.map(move |p, _| packet2line_peer(p, zt)) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = Message2LineClass, target = Message2Line, prefixes = ["msg2line:"], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" Line filter: Turns messages from packet stream into lines of byte stream. [A] Ensure each message (a chunk from one read call from underlying connection) contains no inner newlines (or zero bytes) and terminates with one newline. Reverse of the `line2msg:`. Unless --null-terminated, replaces both newlines (\x0A) and carriage returns (\x0D) with spaces (\x20) for each read. Does not affect writing at all. Use this specifier on both ends to get bi-directional behaviour. Automatically inserted by --line option on top of the stack containing a websocket. Example: TODO "# ); #[derive(Debug)] pub struct Line2Message(pub T); impl Specifier for Line2Message { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let retain_newlines = !cp.program_options.linemode_strip_newlines; let strict = cp.program_options.linemode_strict; let nullt = cp.program_options.linemode_zero_terminated; let inner = self.0.construct(cp.clone()); inner.map(move |p, _| line2packet_peer(p, retain_newlines, strict, nullt)) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name=Line2MessageClass, target=Line2Message, prefixes=["line2msg:"], arg_handling=subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help=r#" Line filter: turn lines from byte stream into messages as delimited by '\\n' or '\\0' [A] Ensure that each message (a successful read call) is obtained from a line [A] coming from underlying specifier, buffering up or splitting content as needed. Reverse of the `msg2line:`. Does not affect writing at all. Use this specifier on both ends to get bi-directional behaviour. Automatically inserted by --line option at the top of the stack opposite to websocket-containing stack. Example: TODO "# ); pub fn packet2line_peer(inner_peer: Peer, null_terminated: bool) -> BoxedNewPeerFuture { let filtered = Packet2LineWrapper(inner_peer.0, null_terminated); let thepeer = Peer::new(filtered, inner_peer.1, inner_peer.2); Box::new(ok(thepeer)) as BoxedNewPeerFuture } struct Packet2LineWrapper(Box, bool); impl Read for Packet2LineWrapper { fn read(&mut self, b: &mut [u8]) -> Result { let l = b.len(); assert!(l > 1); let mut n = self.0.read(&mut b[..(l - 1)])?; if n == 0 { return Ok(n); } if !self.1 { // newline-terminated // chomp away \n or \r\n if n > 0 && b[n - 1] == b'\n' { n -= 1; } if n > 0 && b[n - 1] == b'\r' { n -= 1; } // replace those with spaces for c in b.iter_mut().take(n) { if *c == b'\n' || *c == b'\r' { *c = b' '; } } // add back one \n b[n] = b'\n'; n += 1; } else { // null-terminated if n > 0 && b[n - 1] == b'\x00' { n -= 1; } for c in b.iter_mut().take(n) { if *c == b'\x00' { warn!("zero byte in a message in null-terminated mode"); } } b[n] = b'\x00'; n += 1; } Ok(n) } } impl AsyncRead for Packet2LineWrapper {} pub fn line2packet_peer( inner_peer: Peer, retain_newlines: bool, strict: bool, null_terminated: bool, ) -> BoxedNewPeerFuture { let filtered = Line2PacketWrapper { inner: inner_peer.0, queue: vec![], retain_newlines, allow_incomplete_lines: !strict, drop_too_long_lines: strict, eof: false, null_terminated, }; let thepeer = Peer::new(filtered, inner_peer.1, inner_peer.2); Box::new(ok(thepeer)) as BoxedNewPeerFuture } struct Line2PacketWrapper { inner: Box, queue: Vec, retain_newlines: bool, allow_incomplete_lines: bool, drop_too_long_lines: bool, eof: bool, null_terminated: bool, } impl Line2PacketWrapper { #[allow(clippy::collapsible_if)] fn deliver_the_line(&mut self, buf: &mut [u8], mut n: usize) -> Option { if n > buf.len() { if self.drop_too_long_lines { error!("Dropping too long line of {} bytes because of buffer (-B option) is only {} bytes", n, buf.len()); drop(self.queue.drain(0..n)); return None; } else { warn!("Splitting too long line of {} bytes because of buffer (-B option) is only {} bytes", n, buf.len()); n = buf.len(); } } else { if !self.retain_newlines && !self.null_terminated { if n > 0 && (buf[n - 1] == b'\n') { n -= 1 } if n > 0 && (buf[n - 1] == b'\r') { n -= 1 } } if self.null_terminated { if n > 0 && (buf[n - 1] == b'\x00') { n -= 1 } } } buf[0..n].copy_from_slice(&self.queue[0..n]); drop(self.queue.drain(0..n)); Some(n) } } impl Read for Line2PacketWrapper { #[allow(clippy::collapsible_if)] fn read(&mut self, buf: &mut [u8]) -> Result { //eprint!("ql={} ", self.queue.len()); if self.eof { return Ok(0); } let char_to_look_at = if self.null_terminated { b'\x00' } else { b'\n' }; let mut queued_line_len = None; for i in 0..self.queue.len() { if self.queue[i] == char_to_look_at { queued_line_len = Some(i); break; } } //eprint!("qll={:?} ", queued_line_len); if let Some(mut n) = queued_line_len { n += 1; if let Some(nn) = self.deliver_the_line(buf, n) { Ok(nn) } else { // line dropped, recursing self.read(buf) } } else { let mut n = self.inner.read(buf)?; if n == 0 { self.eof = true; if !self.queue.is_empty() { if self.allow_incomplete_lines { warn!("Sending possibly incomplete line."); let bl = self.queue.len(); if let Some(nn) = self.deliver_the_line(buf, bl) { return Ok(nn); } } else { warn!( "Throwing away {} bytes of incomplete line", self.queue.len() ); } } return Ok(0); } let happy_case = if !self.null_terminated { self.queue.is_empty() && (!buf[0..(n - 1)].contains(&b'\n')) && buf[n - 1] == b'\n' } else { self.queue.is_empty() && (!buf[0..(n - 1)].contains(&b'\x00')) && buf[n - 1] == b'\x00' }; if happy_case { // Specifically to avoid allocations when data is already nice if !self.retain_newlines && !self.null_terminated { if n > 0 && (buf[n - 1] == b'\n') { n -= 1 } if n > 0 && (buf[n - 1] == b'\r') { n -= 1 } } if self.null_terminated { if n > 0 && (buf[n - 1] == b'\x00') { n -= 1 } } //eprintln!("happy n={}", n); Ok(n) } else { // Just queue up and recurse self.queue.extend_from_slice(&buf[0..n]); //eprintln!(" recurse"); self.read(buf) } } } } impl AsyncRead for Line2PacketWrapper {} ================================================ FILE: src/lints.rs ================================================ #![allow(clippy::collapsible_if,clippy::needless_pass_by_value)] use super::{Options, Result, SpecifierClass, SpecifierStack, WebsocatConfiguration2}; use super::specifier::SpecifierNode; use std::rc::Rc; use std::str::FromStr; use std::ops::Not; extern crate hyper; extern crate url; use std::net::{IpAddr, SocketAddr}; use super::socks5_peer::{SocksHostAddr, SocksSocketAddr}; #[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] pub enum StdioUsageStatus { /// Does not use standard input or output at all None, /// Uses a reuser for connecting multiple peers at stdio, not distinguishing between IsItself and Indirectly WithReuser, /// Stdio wrapped into something (but not the reuser) Indirectly, /// Is the `-` or `stdio:` or `threadedstdio:` itself. IsItself, } trait ClassExt { fn is_stdio(&self) -> bool; fn is_reuser(&self) -> bool; } pub type OnWarning = Box Fn(&'a str) + 'static>; #[rustfmt::skip] impl ClassExt for Rc { fn is_stdio(&self) -> bool { [ "StdioClass", "ThreadedStdioClass", "ThreadedStdioSubstituteClass", ].contains(&self.get_name()) } fn is_reuser(&self) -> bool { [ "ReuserClass", "BroadcastReuserClass", ].contains(&self.get_name()) } } pub trait SpecifierStackExt { fn stdio_usage_status(&self) -> StdioUsageStatus; fn reuser_count(&self) -> usize; fn contains(&self, t: &'static str) -> bool; fn is_multiconnect(&self) -> bool; fn is_stream_oriented(&self) -> bool; fn autotoreconn_misuse(&self) -> bool; fn insert_line_class_in_proper_place(&mut self, x: Rc); } impl SpecifierStackExt for SpecifierStack { fn stdio_usage_status(&self) -> StdioUsageStatus { use self::StdioUsageStatus::*; if !self.addrtype.cls.is_stdio() { return None; } let mut sus: StdioUsageStatus = IsItself; for overlay in self.overlays.iter().rev() { if overlay.cls.is_reuser() { sus = WithReuser; } else if sus == IsItself { sus = Indirectly; } } sus } fn reuser_count(&self) -> usize { let mut c = 0; for overlay in &self.overlays { if overlay.cls.is_reuser() { c += 1; } } c } fn contains(&self, t: &'static str) -> bool { for overlay in &self.overlays { if overlay.cls.get_name() == t { return true; } } self.addrtype.cls.get_name() == t } fn autotoreconn_misuse(&self) -> bool { let mut autoreconnect_found = false; for overlay in &self.overlays { if overlay.cls.get_name() == "AutoReconnectClass" { autoreconnect_found = true; } if overlay.cls.get_name() == "BroadcastReuserClass" || overlay.cls.get_name() == "ReuserClass" { if autoreconnect_found { return true; } } } false } fn is_multiconnect(&self) -> bool { use super::ClassMulticonnectStatus::*; match self.addrtype.cls.multiconnect_status() { MultiConnect => (), SingleConnect => return false, MulticonnectnessDependsOnInnerType => unreachable!(), } for overlay in self.overlays.iter().rev() { match overlay.cls.multiconnect_status() { MultiConnect => (), SingleConnect => return false, MulticonnectnessDependsOnInnerType => (), } } true } fn is_stream_oriented(&self) -> bool { use super::ClassMessageBoundaryStatus::*; let mut q = match self.addrtype.cls.message_boundary_status() { StreamOriented => true, MessageOriented => false, MessageBoundaryStatusDependsOnInnerType => unreachable!(), }; for overlay in self.overlays.iter().rev() { match overlay.cls.message_boundary_status() { StreamOriented => q = true, MessageOriented => q = false, MessageBoundaryStatusDependsOnInnerType => (), } } q } fn insert_line_class_in_proper_place(&mut self, x: Rc) { use super::ClassMessageBoundaryStatus::*; let mut insert_idx = 0; for overlay in &self.overlays { match overlay.cls.message_boundary_status() { StreamOriented => break, MessageOriented => break, MessageBoundaryStatusDependsOnInnerType => insert_idx += 1, } } self.overlays.insert(insert_idx, SpecifierNode{cls: x}); } } impl WebsocatConfiguration2 { pub fn inetd_mode(&self) -> bool { self.contains_class("InetdClass") } #[rustfmt::skip] #[allow(clippy::nonminimal_bool)] pub fn websocket_used(&self) -> bool { false || self.contains_class("WsConnectClass") || self.contains_class("WsClientClass") || self.contains_class("WsClientSecureClass") || self.contains_class("WsServerClass") } #[rustfmt::skip] #[allow(clippy::nonminimal_bool)] pub fn exec_used(&self) -> bool { false || self.contains_class("ExecClass") || self.contains_class("CmdClass") || self.contains_class("ShCClass") } pub fn contains_class(&self, x: &'static str) -> bool { self.s1.contains(x) || self.s2.contains(x) } pub fn get_exec_parameter(&self) -> Option<&str> { if self.s1.addrtype.cls.get_name() == "ExecClass" { return Some(self.s1.addr.as_str()); } if self.s2.addrtype.cls.get_name() == "ExecClass" { return Some(self.s2.addr.as_str()); } None } fn l_stdio(&mut self, multiconnect: bool, reuser_has_been_inserted: &mut bool, r#async: bool) -> Result<()> { use self::StdioUsageStatus::{Indirectly, IsItself, None, WithReuser}; match (self.s1.stdio_usage_status(), self.s2.stdio_usage_status()) { (_, None) => (), (None, WithReuser) => (), (None, IsItself) | (None, Indirectly) => { if multiconnect { self.s2.overlays.insert( 0, SpecifierNode{cls:Rc::new(super::broadcast_reuse_peer::BroadcastReuserClass)}, ); *reuser_has_been_inserted = true; } } (IsItself, IsItself) => { info!("Special mode, exception from usual one-stdio rule. Acting like `cat(1)`"); self.s2 = SpecifierStack::from_str("mirror:")?; if self.opts.unidirectional ^ self.opts.unidirectional_reverse { self.opts.unidirectional = false; self.opts.unidirectional_reverse = false; } return Ok(()); } (_, _) => { Err("Too many usages of stdin/stdout. Specify it either on left or right address, not on both.")?; } } #[cfg(all(unix, feature = "unix_stdio"))] { if r#async { if self.s1.addrtype.cls.get_name() == "StdioClass" { debug!("Substituting StdioClass with AsyncStdioClass at the left"); self.s1.addrtype = SpecifierNode{cls:Rc::new(crate::stdio_peer::AsyncStdioClass)}; } if self.s2.addrtype.cls.get_name() == "StdioClass" { debug!("Substituting StdioClass with AsyncStdioClass at the right"); self.s2.addrtype = SpecifierNode{cls:Rc::new(crate::stdio_peer::AsyncStdioClass)}; } } } Ok(()) } fn l_reuser(&mut self, reuser_has_been_inserted: bool) -> Result<()> { if self.s1.reuser_count() + self.s2.reuser_count() > 1 { if reuser_has_been_inserted { error!("The reuser you specified conflicts with automatically inserted reuser based on usage of stdin/stdout in multiconnect mode."); } Err("Too many usages of connection reuser. Please limit to only one instance.")?; } Ok(()) } fn l_linemode(&mut self) -> Result<()> { if !self.opts.no_auto_linemode && self.opts.websocket_text_mode { match (self.s1.is_stream_oriented(), self.s2.is_stream_oriented()) { (false, false) => {} (true, true) => {} (true, false) => { info!("Auto-inserting the line mode"); self.s1.insert_line_class_in_proper_place(Rc::new( super::line_peer::Line2MessageClass, )); self.s2.insert_line_class_in_proper_place(Rc::new( super::line_peer::Message2LineClass, )); } (false, true) => { info!("Auto-inserting the line mode"); self.s2.insert_line_class_in_proper_place(Rc::new( super::line_peer::Line2MessageClass, )); self.s1.insert_line_class_in_proper_place(Rc::new( super::line_peer::Message2LineClass, )); } } }; Ok(()) } fn l_listener_on_the_right(&mut self, on_warning: &OnWarning) -> Result<()> { if !self.opts.oneshot && self.s2.is_multiconnect() && !self.s1.is_multiconnect() { on_warning("You have specified a listener on the right (as the second positional argument) instead of on the left. It will only serve one connection.\nChange arguments order to enable multiple parallel connections or use --oneshot argument to make single connection explicit."); } Ok(()) } fn l_reuser_for_append(&mut self, multiconnect: bool) -> Result<()> { if multiconnect && (self.s2.addrtype.cls.get_name() == "WriteFileClass" || self.s2.addrtype.cls.get_name() == "AppendFileClass") && self.s2.reuser_count() == 0 { info!("Auto-inserting the reuser"); self.s2 .overlays .push(SpecifierNode{cls:Rc::new(super::primitive_reuse_peer::ReuserClass)}); }; Ok(()) } fn l_exec(&mut self, on_warning: &OnWarning) -> Result<()> { if self.s1.addrtype.cls.get_name() == "ExecClass" && self.s2.addrtype.cls.get_name() == "ExecClass" { Err("Can't use exec: more than one time. Replace one of them with sh-c: or cmd:.")?; } if let Some(x) = self.get_exec_parameter() { if self.opts.exec_args.is_empty() && x.contains(' ') { on_warning("Warning: you specified exec: without the corresponding --exec-args at the end of command line. Unlike in cmd: or sh-c:, spaces inside exec:'s direct parameter are interpreted as part of program name, not as separator."); } } Ok(()) } fn l_uri_staticfiles(&mut self, on_warning: &OnWarning) -> Result<()> { if self.opts.restrict_uri.is_some() && !self.contains_class("WsServerClass") { on_warning("--restrict-uri is meaningless without a WebSocket server"); } if !self.opts.serve_static_files.is_empty() && !self.contains_class("WsServerClass") { on_warning("--static-file (-F) is meaningless without a WebSocket server"); } for sf in &self.opts.serve_static_files { if !sf.uri.starts_with('/') { on_warning(&format!( "Static file's URI `{}` should begin with `/`?", sf.uri )); } if !sf.file.exists() { on_warning(&format!("File {:?} does not exist", sf.file)); } if !sf.content_type.contains('/') { on_warning(&format!( "Content-Type `{}` lacks `/` character", sf.content_type )); } } Ok(()) } fn l_environ(&mut self, on_warning: &OnWarning) -> Result<()> { if self.opts.exec_set_env { if !self.exec_used() { on_warning("-e (--set-environment) is meaningless without a exec: or sh-c: or cmd: address"); } if !self.contains_class("TcpListenClass") && !self.contains_class("WsServerClass") { on_warning("-e (--set-environment) is currently meaningless without a websocket server and/or TCP listener"); } } if !self.opts.headers_to_env.is_empty() && !self.opts.exec_set_env { on_warning("--header-to-env is meaningless without -e (--set-environment)"); } Ok(()) } fn l_closebug(&mut self, on_warning: &OnWarning) -> Result<()> { if !self.opts.oneshot && self.s1.is_multiconnect() { if self.s1.contains("TcpListenClass") || self.s1.contains("UnixListenClass") || self.s1.contains("SeqpacketListenClass") { if !self.opts.unidirectional && (self.opts.unidirectional_reverse || !self.opts.exit_on_eof) { on_warning("Unfortunately, serving multiple clients without --exit-on-eof (-E) or with -U option is prone to socket leak in this websocat version"); } } } Ok(()) } fn l_socks5_c( s: &mut SpecifierStack, opts: &mut Options, on_warning: &OnWarning, secure: bool, ) -> Result<()> { let url = if secure { #[cfg(not(feature = "ssl"))] { Err("SSL support not compiled in")?; } format!("wss://{}", s.addr) } else { format!("ws://{}", s.addr) }; // Overwrite WsClientClass s.addrtype = SpecifierNode{cls: Rc::new(super::net_peer::TcpConnectClass) }; match opts.auto_socks5.unwrap() { SocketAddr::V4(sa4) => { s.addr = format!("{}:{}", sa4.ip(), sa4.port()); } SocketAddr::V6(sa6) => { s.addr = format!("[{}]:{}", sa6.ip(), sa6.port()); } } use self::hyper::Url; use self::url::Host; let u = Url::parse(&url)?; if !u.has_host() { Err("WebSocket URL has no host")?; } let port = u.port_or_known_default().unwrap_or(80); let host = u.host().unwrap(); let host = match host { Host::Domain(dom) => SocksHostAddr::Name(dom.to_string()), Host::Ipv4(ip4) => SocksHostAddr::Ip(IpAddr::V4(ip4)), Host::Ipv6(ip6) => SocksHostAddr::Ip(IpAddr::V6(ip6)), }; if opts.socks_destination.is_none() { opts.socks_destination = Some(SocksSocketAddr { host, port }); } if secure && opts.tls_domain.is_none() { opts.tls_domain = u.host_str().map(|x| x.to_string()); } if opts.ws_c_uri != "ws://0.0.0.0/" { on_warning( "Looks like you've overridden ws-c-uri. We are overwriting it for --socks5 option.", ); } opts.ws_c_uri = url; s.overlays .push(SpecifierNode{cls: Rc::new(super::ws_client_peer::WsConnectClass)}); if secure { #[cfg(feature = "ssl")] s.overlays.push(SpecifierNode{cls: Rc::new(super::ssl_peer::TlsConnectClass)}); } s.overlays.push(SpecifierNode{cls: Rc::new(super::socks5_peer::SocksProxyClass)}); Ok(()) } fn l_socks5(&mut self, on_warning: &OnWarning) -> Result<()> { if self.opts.socks_destination.is_some() ^ (self.contains_class("SocksProxyClass") || self.contains_class("SocksBindClass")) { on_warning( "--socks5-destination option and socks5-connect: overlay should go together", ); } if self.opts.socks5_bind_script.is_some() ^ self.contains_class("SocksBindClass") { on_warning("--socks5-bind-script option and socks5-bind: overlay should go together"); } if self.opts.auto_socks5.is_some() { if !((self.s1.addrtype.cls.get_name() == "WsClientClass" || self.s1.addrtype.cls.get_name() == "WsClientSecureClass") ^ (self.s2.addrtype.cls.get_name() == "WsClientClass" || self.s2.addrtype.cls.get_name() == "WsClientSecureClass")) { Err("User-friendly --socks5 option supports socksifying exactly one non-raw websocket client connection. You are using two or none.")?; } if self.s1.addrtype.cls.get_name() == "WsClientClass" { WebsocatConfiguration2::l_socks5_c( &mut self.s1, &mut self.opts, on_warning, false, )?; } if self.s1.addrtype.cls.get_name() == "WsClientSecureClass" { WebsocatConfiguration2::l_socks5_c(&mut self.s1, &mut self.opts, on_warning, true)?; } if self.s2.addrtype.cls.get_name() == "WsClientClass" { WebsocatConfiguration2::l_socks5_c( &mut self.s2, &mut self.opts, on_warning, false, )?; } if self.s2.addrtype.cls.get_name() == "WsClientSecureClass" { WebsocatConfiguration2::l_socks5_c(&mut self.s2, &mut self.opts, on_warning, true)?; } } Ok(()) } #[cfg(feature = "ssl")] fn l_ssl(&mut self, _on_warning: &OnWarning) -> Result<()> { if self.opts.pkcs12_der.is_some() && !self.contains_class("TlsAcceptClass") { Err("--pkcs12-der makes no sense without an TLS connections acceptor")?; } if self.opts.pkcs12_der.is_none() && self.contains_class("TlsAcceptClass") { Err("You need to specify server key and certificate using the --pkcs12-der option to use the TLS connections acceptor")?; } if self.opts.client_pkcs12_der.is_some() && !self.contains_class("WsClientSecureClass") && !self.contains_class("TlsConnectClass") { Err("--client-pkcs12-der makes no sense without wss:// or ssl: connectors")?; } #[cfg(target_os = "macos")] { if (self.opts.pkcs12_der.is_some() && self.opts.pkcs12_passwd.is_none()) || (self.opts.client_pkcs12_der.is_some() && self.opts.client_pkcs12_passwd.is_none()) { _on_warning("PKCS12 archives without password may be unsupported on Mac"); for x in ::std::env::args() { if x.contains("test.pkcs12") { _on_warning("If you want a pre-made test certificate, use other file: `--pkcs12-der 1234.pkcs12 --pkcs12-passwd 1234`"); break; } } } } Ok(()) } fn l_ping(&mut self, _on_warning: &OnWarning) -> Result<()> { if self.opts.ws_ping_interval.is_some() || self.opts.ws_ping_timeout.is_some() { if !self.websocket_used() { _on_warning("--ping-interval or --ping-timeout options are not effective if no WebSocket usage is specified") } } if self.opts.ws_ping_timeout.is_some() && self.opts.ws_ping_interval.is_none() { _on_warning("--ping-timeout specified without --ping-interval. This will probably lead to unconditional disconnection after that interval.") } if let (Some(t), Some(i)) = (self.opts.ws_ping_timeout, self.opts.ws_ping_interval) { if t <= i { _on_warning("--ping-timeout's value is not more than --ping-interval. Expect spurious disconnections."); } } if self.opts.ws_ping_timeout.is_some() { if self.opts.unidirectional_reverse || self.opts.exit_on_eof { // OK } else { _on_warning("--ping-interval is currently not very effective without -E or -U") } } if self.opts.print_ping_rtts && self.opts.ws_ping_interval.is_none() { _on_warning("--print-ping-rtts is not effective without --ping-interval"); } Ok(()) } fn l_proto(&mut self, _on_warning: &OnWarning) -> Result<()> { if self.opts.websocket_protocol.is_some() { if !(self.contains_class("WsConnectClass") || self.contains_class("WsClientClass") || self.contains_class("WsClientSecureClass")) { if self.contains_class("WsServerClass") { _on_warning("--protocol option is unused. Maybe you want --server-protocol?") } else { _on_warning("--protocol option is unused.") } } } if self.opts.websocket_reply_protocol.is_some() { if !self.contains_class("WsServerClass") { _on_warning("--server-protocol option is unused.") } } Ok(()) } fn l_eeof_unidir(&mut self, _on_warning: &OnWarning) -> Result<()> { if self.opts.exit_on_eof { if self.opts.unidirectional || self.opts.unidirectional_reverse { _on_warning("--exit-on-eof and --unidirectional[-reverse] options are now useless together"); _on_warning("You may want to remove --exit-on-eof. If you are happy with what happens, consider `-uU` instead of `-uE`."); } } Ok(()) } fn l_udp(&mut self, _on_warning: &OnWarning) -> Result<()> { if self.opts.udp_join_multicast_addr.is_empty().not() { if self.opts.udp_broadcast { _on_warning("Both --udp-broadcast and a multicast address is set. This is strange."); } let ifs = self.opts.udp_join_multicast_iface_v4.len() + self.opts.udp_join_multicast_iface_v6.len(); if ifs != 0 { let mut v4_multicasts = 0; let mut v6_multicasts = 0; for i in &self.opts.udp_join_multicast_addr { match i { std::net::IpAddr::V4(_) => v4_multicasts += 1, std::net::IpAddr::V6(_) => v6_multicasts += 1, } } if v4_multicasts != self.opts.udp_join_multicast_iface_v4.len() { return Err("--udp-multicast-iface-v4 option mush be specified the same number of times as IPv4 addresses for --udp-multicast (alternatively --udp-multicast-iface-* options should be not specified at all)")?; } if v6_multicasts != self.opts.udp_join_multicast_iface_v6.len() { return Err("--udp-multicast-iface-v6 option mush be specified the same number of times as IPv6 addresses for --udp-multicast (alternatively --udp-multicast-iface-* options should be not specified at all)")?; } } } else if self.opts.udp_multicast_loop { return Err("--udp-multicast-loop is not applicable without --udp-multicast")?; } Ok(()) } fn l_crypto(&mut self, _on_warning: &OnWarning) -> Result<()> { #[cfg(feature="crypto_peer")] if self.opts.crypto_key.is_some() { if !self.contains_class("CryptoClass") { _on_warning("--crypto-key option is meaningless without a `crypto:` overlay"); } } Ok(()) } fn l_prometheus(&mut self, _on_warning: &OnWarning) -> Result<()> { #[cfg(feature="prometheus_peer")] if self.opts.prometheus.is_some() { if !self.contains_class("PrometheusClass") { self.s2.overlays.insert(0, SpecifierNode { cls: Rc::new(crate::prometheus_peer::PrometheusClass) }); } } else if self.contains_class("PrometheusClass") { _on_warning("Using `prometheus:` overlay without `--prometheus` option is meaningless"); } Ok(()) } fn l_sizelimits(&mut self, _on_warning: &OnWarning) -> Result<()> { if self.opts.max_ws_message_length < self.opts.max_ws_frame_length { _on_warning("Lowering --max-ws-message-length without also lowering --max-ws-frame-length may be meaningless, as the former only affects whether to begin accept a new frame or not, given accumulated message size. Succesfully accepted frames within the frame size limit may exceed the message size.") } Ok(()) } fn l_compress(&mut self, _on_warning: &OnWarning) -> Result<()> { let mut cn = 0; let mut un = 0; if self.opts.compress_deflate { cn += 1 } if self.opts.compress_gzip { cn += 1 } if self.opts.compress_zlib { cn += 1 } if self.opts.uncompress_deflate { un += 1 } if self.opts.uncompress_gzip { un += 1 } if self.opts.uncompress_zlib { un += 1 } if cn > 1 { return Err("Multiple --compress-* options specifed")?; } if un > 1 { return Err("Multiple --uncompress-* options specifed")?; } #[cfg(not(feature="compression"))] { if cn > 0 || un > 0 { return Err("Compression support is not selected during Websocat compilation")?; } } Ok(()) } fn l_autoreconn_reuse(&mut self, _on_warning: &OnWarning) -> Result<()> { if self.s1.autotoreconn_misuse() || self.s2.autotoreconn_misuse() { _on_warning("Warning: `autoreconnect:reuse:` is a bad overlay combination. Maybe you want `reuse:autoreconnect:"); } Ok(()) } pub fn lint_and_fixup(&mut self, on_warning: OnWarning) -> Result<()> { let multiconnect = !self.opts.oneshot && self.s1.is_multiconnect(); let mut reuser_has_been_inserted = false; self.l_prometheus(&on_warning)?; self.l_stdio(multiconnect, &mut reuser_has_been_inserted, self.opts.asyncstdio)?; self.l_reuser(reuser_has_been_inserted)?; self.l_linemode()?; self.l_listener_on_the_right(&on_warning)?; self.l_reuser_for_append(multiconnect)?; self.l_exec(&on_warning)?; self.l_uri_staticfiles(&on_warning)?; self.l_environ(&on_warning)?; self.l_closebug(&on_warning)?; self.l_socks5(&on_warning)?; #[cfg(feature = "ssl")] self.l_ssl(&on_warning)?; self.l_ping(&on_warning)?; self.l_proto(&on_warning)?; self.l_eeof_unidir(&on_warning)?; self.l_udp(&on_warning)?; self.l_crypto(&on_warning)?; self.l_sizelimits(&on_warning)?; self.l_compress(&on_warning)?; self.l_autoreconn_reuse(&on_warning)?; // TODO: UDP connect oneshot mode // TODO: tests for the linter Ok(()) } } ================================================ FILE: src/main.rs ================================================ #![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![allow(clippy::deprecated_cfg_attr)] #[macro_use] extern crate websocat; extern crate futures; extern crate tokio; extern crate tokio_stdin_stdout; extern crate websocket_base; extern crate env_logger; #[macro_use] extern crate log; #[cfg(feature = "openssl-probe")] extern crate openssl_probe; #[allow(unused_imports)] #[macro_use] extern crate structopt; extern crate atty; extern crate http_bytes; use http_bytes::http; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use structopt::StructOpt; use websocat::options::StaticFile; use websocat::socks5_peer::{SocksHostAddr, SocksSocketAddr}; use websocat::{Options, SpecifierClass, WebsocatConfiguration1}; type Result = std::result::Result>; use std::ffi::OsString; #[derive(StructOpt, Debug)] #[structopt( after_help = " Basic examples: Command-line websocket client: websocat ws://ws.vi-server.org/mirror/ WebSocket server websocat -s 8080 WebSocket-to-TCP proxy: websocat --binary ws-l:127.0.0.1:8080 tcp:127.0.0.1:5678 ", usage = "websocat ws://URL | wss://URL (simple client)\n websocat -s port (simple server)\n websocat [FLAGS] [OPTIONS] (advanced mode)" )] struct Opt { /// In simple mode, WebSocket URL to connect. /// In advanced mode first address (there are many kinds of addresses) to use. /// See --help=types for info about address types. /// If this is an address for listening, it will try serving multiple connections. addr1: Option, /// In advanced mode, second address to connect. /// If this is an address for listening, it will accept only one connection. addr2: Option, #[structopt( short = "u", long = "unidirectional", help = "Inhibit copying data in one direction" )] unidirectional: bool, #[structopt( short = "U", long = "unidirectional-reverse", help = "Inhibit copying data in the other direction (or maybe in both directions if combined with -u)" )] unidirectional_reverse: bool, #[structopt( long = "exit-on-eof", short = "E", help = "Close a data transfer direction if the other one reached EOF" )] exit_on_eof: bool, #[structopt( short = "t", long = "text", help = "Send message to WebSockets as text messages" )] websocket_text_mode: bool, #[structopt( short = "b", long = "binary", help = "Send message to WebSockets as binary messages" )] websocket_binary_mode: bool, #[structopt( long = "oneshot", help = "Serve only once. Not to be confused with -1 (--one-message)" )] oneshot: bool, #[structopt( short = "h", long = "help", help = "See the help.\n--help=short is the list of easy options and address types\n--help=long lists all options and types (see [A] markers)\n--help=doc also shows longer description and examples." )] help: Option, #[structopt( long = "dump-spec", help = "[A] Instead of running, dump the specifiers representation to stdout" )] dumpspec: bool, /// Specify this Sec-WebSocket-Protocol: header when connecting #[structopt(long = "protocol")] websocket_protocol: Option, /// Force this Sec-WebSocket-Protocol: header when accepting a connection #[structopt(long = "server-protocol")] websocket_reply_protocol: Option, #[structopt( long = "udp-oneshot", help = "[A] udp-listen: replies only one packet per client" )] udp_oneshot_mode: bool, /// [A] Set SO_BROADCAST #[structopt(long="udp-broadcast")] udp_broadcast: bool, /// [A] Set IP[V6]_MULTICAST_LOOP #[structopt(long="udp-multicast-loop")] udp_multicast_loop: bool, /// [A] Set IP_TTL, also IP_MULTICAST_TTL if applicable #[structopt(long="udp-ttl")] udp_ttl: Option, /// [A] Issue IP[V6]_ADD_MEMBERSHIP for specified multicast address. /// Can be specified multiple times. #[structopt(long="udp-multicast")] udp_join_multicast_addr: Vec, /// [A] IPv4 address of multicast network interface. /// Has to be either not specified or specified the same number of times as multicast IPv4 addresses. Order matters. #[structopt(long="udp-multicast-iface-v4")] udp_join_multicast_iface_v4: Vec, /// [A] Index of network interface for IPv6 multicast. /// Has to be either not specified or specified the same number of times as multicast IPv6 addresses. Order matters. #[structopt(long="udp-multicast-iface-v6")] udp_join_multicast_iface_v6: Vec, /// [A] Set SO_REUSEADDR for UDP socket. Listening TCP sockets are always reuseaddr. #[structopt(long="udp-reuseaddr")] udp_reuseaddr: bool, #[structopt( long = "unlink", help = "[A] Unlink listening UNIX socket before binding to it" )] unlink_unix_socket: bool, #[structopt( long = "accept-from-fd", help = "[A] Do not call `socket(2)` in UNIX socket listener peer, start with `accept(2)` using specified file descriptor number as argument instead of filename" )] unix_socket_accept_from_fd: bool, #[structopt( long = "exec-args", raw(allow_hyphen_values = r#"true"#), help = "[A] Arguments for the `exec:` specifier. Must be the last option, everything after it gets into the exec args list." )] exec_args: Vec, #[structopt( long = "ws-c-uri", help = "[A] URI to use for ws-c: overlay", default_value = "ws://0.0.0.0/" )] ws_c_uri: String, #[structopt( long = "linemode-strip-newlines", help = "[A] Don't include trailing \\n or \\r\\n coming from streams in WebSocket messages" )] linemode_strip_newlines: bool, #[structopt( long = "--no-line", help = "[A] Don't automatically insert line-to-message transformation" )] no_auto_linemode: bool, #[structopt( long = "origin", help = "Add Origin HTTP header to websocket client request" )] origin: Option, #[structopt( long = "header", short = "H", help = "Add custom HTTP header to websocket client request. Separate header name and value with a colon and optionally a single space. Can be used multiple times. Note that single -H may eat multiple further arguments, leading to confusing errors. Specify headers at the end or with equal sign like -H='X: y'.", parse(try_from_str = "interpret_custom_header") )] custom_headers: Vec<(String, Vec)>, #[structopt( long = "server-header", help = "Add custom HTTP header to websocket upgrade reply. Separate header name and value with a colon and optionally a single space. Can be used multiple times. Note that single -H may eat multiple further arguments, leading to confusing errors.", parse(try_from_str = "interpret_custom_header") )] custom_reply_headers: Vec<(String, Vec)>, /// Forward specified incoming request header to /// H_* environment variable for `exec:`-like specifiers. #[structopt( long = "header-to-env", )] headers_to_env: Vec, #[structopt( long = "websocket-version", help = "Override the Sec-WebSocket-Version value" )] websocket_version: Option, #[structopt( long = "no-close", short = "n", help = "Don't send Close message to websocket on EOF" )] websocket_dont_close: bool, #[structopt( short = "1", long = "one-message", help = "Send and/or receive only one message. Use with --no-close and/or -u/-U." )] one_message: bool, #[structopt( short = "s", long = "server-mode", help = "Simple server mode: specify TCP port or addr:port as single argument" )] server_mode: bool, #[structopt( long = "no-fixups", help = "[A] Don't perform automatic command-line fixups. May destabilize websocat operation. Use --dump-spec without --no-fixups to discover what is being inserted automatically and read the full manual about Websocat internal workings." )] no_lints: bool, #[structopt( short = "B", long = "buffer-size", help = "Maximum message size, in bytes", default_value = "65536" )] buffer_size: usize, #[structopt( short = "v", parse(from_occurrences), help = "Increase verbosity level to info or further" )] verbosity: u8, #[structopt( short = "q", help = "Suppress all diagnostic messages, except of startup errors" )] quiet: bool, #[structopt( long = "queue-len", help = "[A] Number of pending queued messages for broadcast reuser", default_value = "16" )] broadcast_queue_len: usize, #[structopt( short = "S", long = "strict", help = "strict line/message mode: drop too long messages instead of splitting them, drop incomplete lines." )] strict_mode: bool, #[structopt( short = "0", long = "null-terminated", help = "Use \\0 instead of \\n for linemode" )] linemode_zero_terminated: bool, #[structopt( long = "restrict-uri", help = "When serving a websocket, only accept the given URI, like `/ws`\nThis liberates other URIs for things like serving static files or proxying." )] restrict_uri: Option, #[structopt( short = "F", long = "static-file", help = "Serve a named static file for non-websocket connections.\nArgument syntax: ::\nArgument example: /index.html:text/html:index.html\nDirectories are not and will not be supported for security reasons.\nCan be specified multiple times. Recommended to specify them at the end or with equal sign like `-F=...`, otherwise this option may eat positional arguments", parse(try_from_str = "interpret_static_file") )] serve_static_files: Vec, #[structopt( short = "e", long = "set-environment", help = "Set WEBSOCAT_* environment variables when doing exec:/cmd:/sh-c:\nCurrently it's WEBSOCAT_URI and WEBSOCAT_CLIENT for\nrequest URI and client address (if TCP)\nBeware of ShellShock or similar security problems." )] exec_set_env: bool, #[structopt( long = "reuser-send-zero-msg-on-disconnect", help = "[A] Make reuse-raw: send a zero-length message to the peer when some clients disconnects." )] reuser_send_zero_msg_on_disconnect: bool, #[structopt( long = "exec-sighup-on-zero-msg", help = "[A] Make exec: or sh-c: or cmd: send SIGHUP on UNIX when facing incoming zero-length message." )] process_zero_sighup: bool, #[structopt( long = "exec-sighup-on-stdin-close", help = "[A] Make exec: or sh-c: or cmd: send SIGHUP on UNIX when input is closed." )] process_exit_sighup: bool, #[structopt( long = "exec-exit-on-disconnect", help = "[A] Make exec: or sh-c: or cmd: immediately exit when connection is closed, don't wait for termination." )] process_exit_on_disconnect: bool, #[structopt( long = "jsonrpc", help = "Format messages you type as JSON RPC 2.0 method calls. First word becomes method name, the rest becomes parameters, possibly automatically wrapped in []." )] jsonrpc: bool, #[structopt( long = "socks5-destination", help = "[A] Examples: 1.2.3.4:5678 2600:::80 hostname:5678", parse(try_from_str = "interpret_socks_destination") )] socks_destination: Option, #[structopt( long = "socks5", help = "Use specified address:port as a SOCKS5 proxy. Example: --socks5 127.0.0.1:9050" )] auto_socks5: Option, #[structopt( long = "socks5-bind-script", help = "[A] Execute specified script in `socks5-bind:` mode when remote port number becomes known.", parse(from_os_str) )] socks5_bind_script: Option, #[structopt( long = "socks5-user-pass", help = "[A] Specify username:password for SOCKS5 proxy. If not specified, the default is to use no authentication." )] socks5_user_pass: Option, #[structopt( long = "tls-domain", alias = "ssl-domain", help = "[A] Specify domain for SNI or certificate verification when using tls-connect: overlay" )] tls_domain: Option, #[cfg(feature = "ssl")] #[structopt( long = "pkcs12-der", help = "Pkcs12 archive needed to accept SSL connections, certificate and key.\nA command to output it: openssl pkcs12 -export -out output.pkcs12 -inkey key.pem -in cert.pem\nUse with -s (--server-mode) option or with manually specified TLS overlays.\nSee moreexamples.md for more info.", parse(try_from_os_str = "websocat::ssl_peer::interpret_pkcs12") )] pkcs12_der: Option>, #[cfg(feature = "ssl")] #[structopt( long = "pkcs12-passwd", help = "Password for --pkcs12-der pkcs12 archive. Required on Mac." )] pkcs12_passwd: Option, #[cfg(feature = "ssl")] #[structopt( long = "client-pkcs12-der", help = "[A] Client identity TLS certificate", parse(try_from_os_str = "websocat::ssl_peer::interpret_pkcs12") )] client_pkcs12_der: Option>, #[cfg(feature = "ssl")] #[structopt( long = "client-pkcs12-passwd", help = "[A] Password for --client-pkcs12-der pkcs12 archive. Required on Mac." )] client_pkcs12_passwd: Option, #[cfg(feature = "ssl")] #[structopt( long = "insecure", short = "k", help = "Accept invalid certificates and hostnames while connecting to TLS" )] tls_insecure: bool, /// Maximum number of simultaneous connections for listening mode #[structopt(long = "conncap")] max_parallel_conns: Option, /// Send WebSocket pings each this number of seconds #[structopt(long = "ping-interval")] ws_ping_interval: Option, /// Drop WebSocket connection if Pong message not received for this number of seconds #[structopt(long = "ping-timeout")] ws_ping_timeout: Option, /// [A] Just a Sec-WebSocket-Key value without running main Websocat #[structopt(long = "just-generate-key")] just_generate_key: bool, /// [A] Just a Sec-WebSocket-Accept value based on supplied /// Sec-WebSocket-Key value without running main Websocat #[structopt(long = "just-generate-accept")] just_generate_accept: Option, /// [A] URI to use for `http-request:` specifier #[structopt(long = "request-uri")] request_uri: Option, /// [A] Method to use for `http-request:` specifier #[structopt(long = "request-method", short="X")] request_method: Option, /// [A] Specify HTTP request headers for `http-request:` specifier. #[structopt( long = "request-header", parse(try_from_str = "interpret_custom_header2"), )] request_headers: Vec<(http::header::HeaderName, http::header::HeaderValue)>, /// [A] Don't exit when encountered a zero message. /// Zero messages are used internally in Websocat, /// so it may fail to close connection at all. #[structopt(long = "no-exit-on-zeromsg")] no_exit_on_zeromsg: bool, /// [A] Silently drop incoming zero-length WebSocket messages. /// They may cause connection close due to /// usage of zero-len message as EOF flag inside Websocat. #[structopt(long = "websocket-ignore-zeromsg")] websocket_ignore_zeromsg: bool, /// Maximum number of messages to copy in one direction. #[structopt(long = "max-messages")] max_messages: Option, /// Maximum number of messages to copy in the other direction. #[structopt(long = "max-messages-rev")] max_messages_rev: Option, /// [A] Delay before reconnect attempt for `autoreconnect:` overlay. #[structopt(long = "--autoreconnect-delay-millis", default_value="20")] autoreconnect_delay_millis: u64, /// [A] Prepend specified text to each received WebSocket text message. /// Also strip this prefix from outgoing messages, explicitly marking /// them as text even if `--binary` is specified #[structopt(long = "--text-prefix")] pub ws_text_prefix: Option, /// [A] Prepend specified text to each received WebSocket binary message. /// Also strip this prefix from outgoing messages, explicitly marking /// them as binary even if `--text` is specified #[structopt(long = "--binary-prefix")] pub ws_binary_prefix: Option, /// Encode incoming binary WebSocket messages in one-line Base64 /// If `--binary-prefix` (see `--help=full`) is set, outgoing WebSocket messages /// that start with the prefix are decoded from base64 prior to sending. #[structopt(long = "--base64")] pub ws_binary_base64: bool, /// [A] Encode incoming text WebSocket messages in one-line Base64. /// I don't know whether it can be ever useful, but it's for symmetry with `--base64`. #[structopt(long = "--base64-text")] pub ws_text_base64: bool, /// Close connection with a status code. #[structopt(long = "--close-status-code")] pub close_status_code: Option, /// Close connection with a reason message. This option only takes effect if /// --close-status-code option is provided as well. #[structopt(long = "--close-reason")] pub close_reason: Option, /// [A] On UNIX, set stdin and stdout to nonblocking mode instead of spawning a thread. /// This should improve performance, but may break other programs running on the same console. #[structopt(long = "--async-stdio")] pub asyncstdio: bool, /// [A] Inhibit using stdin/stdout in a nonblocking way if it is not a tty #[structopt(long = "--no-async-stdio")] pub noasyncstdio: bool, /// Add `Authorization: Basic` HTTP request header with this base64-encoded parameter. /// Also available as `WEBSOCAT_BASIC_AUTH` environment variable #[structopt(long = "--basic-auth")] pub basic_auth: Option, /// Add `Authorization: Basic` HTTP request header base64-encoded content of the specified file #[structopt(long = "--basic-auth-file")] pub basic_auth_file: Option, /// [A] Wait for reading to finish before closing foreachmsg:'s peer #[structopt(long = "--foreachmsg-wait-read")] pub foreachmsg_wait_reads: bool, /// [A] Print a line to stdout for each port being listened #[structopt(long = "--stdout-announce-listening-ports")] pub announce_listens: bool, /// [A] Use monotonic clock for `timestamp:` overlay #[structopt(long = "--timestamp-monotonic")] pub timestamp_monotonic: bool, /// Print measured round-trip-time to stderr after each received WebSocket pong. #[structopt(long = "print-ping-rtts")] pub print_ping_rtts: bool, /// [A] Specify encryption/decryption key for `crypto:` specifier. Requires `base64:`, `file:` or `pwd:` prefix. #[cfg(feature = "crypto_peer")] #[structopt(long = "crypto-key", parse(try_from_str = "websocat::crypto_peer::interpret_opt"))] pub crypto_key: Option<[u8; 32]>, /// [A] Swap encryption and decryption operations in `crypto:` specifier - encrypt on read, decrypto on write. #[cfg(feature = "crypto_peer")] #[structopt(long = "crypto-reverse")] pub crypto_reverse: bool, /// Expose Prometheus metrics on specified IP address and port in addition to running usual Websocat session #[cfg(feature = "prometheus_peer")] #[structopt(long = "prometheus")] pub prometheus: Option, /// [A] Override the byte which byte_to_exit_on: overlay looks for #[structopt(long = "byte-to-exit-on", default_value = "28")] byte_to_exit_on: u8, /// [A] Maximum size of incoming WebSocket messages (sans of one data frame), to prevent memory overflow #[structopt(long = "max-ws-message-length", default_value = "209715200")] pub max_ws_message_length: usize, /// [A] Maximum size of incoming WebSocket frames, to prevent memory overflow #[structopt(long = "max-ws-frame-length", default_value = "104857600")] pub max_ws_frame_length: usize, /// Prepend copied data with a specified string. Can be specified multiple times. #[structopt(long = "preamble", short="p")] pub preamble: Vec, /// Prepend copied data with a specified string (reverse direction). Can be specified multiple times. #[structopt(long = "preamble-reverse", short="P")] pub preamble_reverse: Vec, /// [A] Compress data coming to a WebSocket using deflate method. Affects only binary WebSocket messages. #[structopt(long = "compress-deflate")] pub compress_deflate: bool, /// [A] Compress data coming to a WebSocket using zlib method. Affects only binary WebSocket messages. #[structopt(long = "compress-zlib")] pub compress_zlib: bool, /// [A] Compress data coming to a WebSocket using gzip method. Affects only binary WebSocket messages. #[structopt(long = "compress-gzip")] pub compress_gzip: bool, /// [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. #[structopt(long = "uncompress-deflate")] pub uncompress_deflate: bool, /// [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. #[structopt(long = "uncompress-zlib")] pub uncompress_zlib: bool, /// [A] Uncompress data coming from a WebSocket using deflate method. Affects only binary WebSocket messages. #[structopt(long = "uncompress-gzip")] pub uncompress_gzip: bool, /// [A] Omit `jsonrpc` field when using `--jsonrpc`, e.g. for Chromium #[structopt(long = "jsonrpc-omit-jsonrpc")] pub jsonrpc_omit_jsonrpc: bool, /// [A] Stop replying to incoming WebSocket pings after specified number of replies #[structopt(long = "inhibit-pongs")] pub inhibit_pongs: Option, /// [A] Stop sending pings after this number of sent pings #[structopt(long = "max-sent-pings")] pub max_sent_pings: Option, /// [A] Use this number of length header bytes for `lengthprefixed:` overlay. #[structopt(long = "--lengthprefixed-nbytes", default_value = "4")] pub lengthprefixed_header_bytes: usize, /// [A] Use little-endian framing headers instead of big-endian for `lengthprefixed:` overlay. #[structopt(long = "--lengthprefixed-little-endian")] pub lengthprefixed_little_endian: bool, /// [A] Only affect one direction of the `lengthprefixed:` overlay, bypass tranformation for the other one. #[structopt(long = "--lengthprefixed-skip-read-direction")] pub lengthprefixed_skip_read_direction: bool, /// [A] Only affect one direction of the `lengthprefixed:` overlay, bypass tranformation for the other one. #[structopt(long = "--lengthprefixed-skip-write-direction")] pub lengthprefixed_skip_write_direction: bool, /// Set `User-Agent` request header to this value. Similar to setting it with `-H`. #[structopt(long = "--ua")] pub useragent: Option, } // TODO: make it byte-oriented/OsStr? fn interpret_custom_header(x: &str) -> Result<(String, Vec)> { let colon = x.find(':'); let colon = if let Some(colon) = colon { colon } else { Err("Argument to --header must contain `:` character")? }; let hn = &x[0..colon]; let mut hv = &x[colon + 1..]; if hv.starts_with(' ') { hv = &x[colon + 2..]; } Ok((hn.to_owned(), hv.as_bytes().to_vec())) } fn interpret_custom_header2(x: &str) -> Result<(http::header::HeaderName, http::header::HeaderValue)> { let colon = x.find(':'); let colon = if let Some(colon) = colon { colon } else { Err("Specified header must contain `:` character")? }; let hn = &x[0..colon]; let mut hv = &x[colon + 1..]; if hv.starts_with(' ') { hv = &x[colon + 2..]; } use std::str::FromStr; let hn = http::header::HeaderName::from_str(hn)?; let hv = http::header::HeaderValue::from_str(hv)?; Ok((hn,hv)) } fn interpret_static_file(x: &str) -> Result { let colon1 = match x.find(':') { Some(x) => x, None => Err("Argument to --static-file must contain two colons (`:`)")?, }; let uri = &x[0..colon1]; let rest = &x[colon1 + 1..]; let colon2 = match rest.find(':') { Some(x) => x, None => Err("Argument to --static-file must contain two colons (`:`)")?, }; let ct = &rest[0..colon2]; let fp = &rest[colon2 + 1..]; if uri.is_empty() || ct.is_empty() || fp.is_empty() { Err("Empty URI, content-type or path in --static-file parameter")? } Ok(StaticFile { uri: uri.to_string(), content_type: ct.to_string(), file: fp.into(), }) } fn interpret_socks_destination(x: &str) -> Result { let colon = x.rfind(':'); let colon = if let Some(colon) = colon { colon } else { Err("Argument to --socks5-destination must contain a `:` character")? }; let h = &x[0..colon]; let p = &x[colon + 1..]; let port: u16 = p.parse()?; let host = if let Ok(ip4) = h.parse() { SocksHostAddr::Ip(IpAddr::V4(ip4)) } else if let Ok(ip6) = h.parse() { SocksHostAddr::Ip(IpAddr::V6(ip6)) } else { SocksHostAddr::Name(h.to_string()) }; Ok(SocksSocketAddr { host, port }) } pub mod help; // Based on https://github.com/rust-clique/clap-verbosity-flag/blob/master/src/lib.rs mod logging { extern crate env_logger; extern crate log; use self::env_logger::Builder as LoggerBuilder; use self::log::Level; pub fn setup_env_logger(ll: u8) -> Result<(), Box> { if ::std::env::var("RUST_LOG").is_ok() { if ll > 0 { eprintln!("websocat: RUST_LOG environment variable overrides any -v"); } env_logger::init(); return Ok(()); } let lf = match ll { //0 => Level::Error, 0 => Level::Warn, 1 => Level::Info, 2 => Level::Debug, _ => Level::Trace, } .to_level_filter(); LoggerBuilder::new() .filter(Some("websocat"), lf) .filter(None, Level::Warn.to_level_filter()) .try_init()?; Ok(()) } } fn run() -> Result<()> { if std::env::args().nth(1).unwrap_or_default() == "--long-help" { help::longhelp(); return Ok(()); } if ["-?", "-h", "--help"].contains(&std::env::args().nth(1).unwrap_or_default().as_str()) { help::shorthelp(); return Ok(()); } let mut logging_already_set = false; if std::env::var("WEBSOCAT_EARLY_LOG").is_ok() { logging::setup_env_logger(0)?; logging_already_set = true; } let mut cmd = Opt::from_args(); let mut quiet = cmd.quiet; if let Some(h) = cmd.help { if &h == "long" || &h == "full" || &h == "all" { help::longhelp(); return Ok(()); } else if &h == "doc" { help::dochelp(); return Ok(()); } help::shorthelp(); return Ok(()); } if cmd.just_generate_key { println!("{}", websocket_base::header::WebSocketKey::new().serialize()); return Ok(()); } if let Some(key) = cmd.just_generate_accept { use std::str::FromStr; let k = websocket_base::header::WebSocketKey::from_str(&key)?; println!("{}", websocket_base::header::WebSocketAccept::new(&k).serialize()); return Ok(()); } let mut recommend_explicit_text_or_bin = false; if cmd.websocket_binary_mode && cmd.websocket_text_mode { Err("--binary and --text are mutually exclusive")?; } if !cmd.websocket_binary_mode && !cmd.websocket_text_mode { cmd.websocket_text_mode = true; recommend_explicit_text_or_bin = true; } if cmd.noasyncstdio && cmd.asyncstdio { Err("--no-async-stdio and --async-stdio are not meaningful together")?; } if !cmd.noasyncstdio && atty::isnt(atty::Stream::Stdin) && atty::isnt(atty::Stream::Stdout) { cmd.asyncstdio = true; } //if !cmd.serve_static_files.is_empty() && cmd.restrict_uri.is_none() { // Err("Specify --static-file is not supported without --restrict-uri")? //} if false // || cmd.oneshot { Err("This mode is not implemented")? } #[cfg(feature = "openssl-probe")] { openssl_probe::init_ssl_cert_env_vars(); } let mut opts: Options = Default::default(); { macro_rules! opts { ($($o:ident)*) => {{ $(opts.$o = cmd.$o;)* }}; } opts!( websocket_text_mode websocket_protocol websocket_reply_protocol udp_oneshot_mode udp_broadcast udp_multicast_loop udp_ttl udp_join_multicast_addr udp_join_multicast_iface_v4 udp_join_multicast_iface_v6 udp_reuseaddr unidirectional unidirectional_reverse exit_on_eof oneshot unlink_unix_socket unix_socket_accept_from_fd exec_args ws_c_uri linemode_strip_newlines origin custom_headers custom_reply_headers headers_to_env websocket_version websocket_dont_close one_message no_auto_linemode buffer_size linemode_zero_terminated broadcast_queue_len restrict_uri serve_static_files exec_set_env reuser_send_zero_msg_on_disconnect process_zero_sighup process_exit_sighup process_exit_on_disconnect socks_destination auto_socks5 socks5_bind_script socks5_user_pass tls_domain max_parallel_conns ws_ping_interval ws_ping_timeout request_uri request_method request_headers websocket_ignore_zeromsg no_exit_on_zeromsg max_messages max_messages_rev autoreconnect_delay_millis ws_text_prefix ws_binary_prefix ws_binary_base64 ws_text_base64 close_status_code close_reason asyncstdio foreachmsg_wait_reads announce_listens timestamp_monotonic print_ping_rtts byte_to_exit_on max_ws_message_length max_ws_frame_length preamble preamble_reverse compress_deflate compress_zlib compress_gzip uncompress_deflate uncompress_zlib uncompress_gzip jsonrpc_omit_jsonrpc inhibit_pongs max_sent_pings lengthprefixed_header_bytes lengthprefixed_little_endian lengthprefixed_skip_read_direction lengthprefixed_skip_write_direction ); #[cfg(feature = "ssl")] { opts! { pkcs12_der pkcs12_passwd client_pkcs12_der client_pkcs12_passwd tls_insecure } } #[cfg(feature = "crypto_peer")] { opts! { crypto_key crypto_reverse } } #[cfg(feature = "prometheus_peer")] { opts! { prometheus } } }; if let Some(x) = cmd.useragent { opts.custom_headers.push(("User-Agent".to_owned(), x.as_bytes().to_vec())); opts.request_headers.push((http::header::USER_AGENT, http::header::HeaderValue::from_bytes(x.as_bytes()).unwrap())); } let mut basic_auth_content : Option = None; if let Some(ba) = cmd.basic_auth { basic_auth_content = Some(ba); } if let Ok(ba) = std::env::var("WEBSOCAT_BASIC_AUTH") { if basic_auth_content.is_some() { return Err("Multiple request basic auth options specified simultaneously".into()); } basic_auth_content = Some(ba); } if let Some(baf) = cmd.basic_auth_file { if basic_auth_content.is_some() { return Err("Multiple request basic auth options specified simultaneously".into()); } let x = std::fs::read_to_string(&baf).inspect_err(|_|{error!("Failed to read `{:?}`", baf);})?; basic_auth_content = Some(x.trim().to_owned()); } if let Some(ba) = basic_auth_content { let x = base64::encode(&ba); let q = format!("Basic {}", x); opts.custom_headers.push(("Authorization".to_owned(), q.as_bytes().to_vec())); opts.request_headers.push((http::header::AUTHORIZATION, http::header::HeaderValue::from_bytes(q.as_bytes()).unwrap())); } let (s1, s2): (String, String) = match (cmd.addr1, cmd.addr2) { (None, None) => { for x in std::env::args() { if x == "-p" || x == "-P" || x == "--preamble" || x == "--preamble-reverse" { eprintln!("Warning: all dashless arguments after -p or -P are considered part of the preamble. You may want to move -p/-P to the end of the command line.") } } return Err("No URL specified. Use `websocat --help` to show the help message.")?; } (Some(cmds1), Some(cmds2)) => { // Advanced mode if cmd.jsonrpc { Err("--jsonrpc option is only for simple (single-argument) mode.\nUse `jsonrpc:` specifier manually if you want it in advanced mode.")? } if cmd.server_mode { Err("--server and two positional arguments are incompatible.\nBuild server command line without -s option, but with `listen` address types")? } (cmds1, cmds2) } (Some(cmds1), None) => { // Easy mode recommend_explicit_text_or_bin = false; if cmd.server_mode { #[allow(unused)] let mut secure = false; #[cfg(feature = "ssl")] { if opts.pkcs12_der.is_some() { secure = true; } } opts.exit_on_eof = true; if !secure { if cmds1.contains(':') { if !quiet { eprintln!("Listening on ws://{}/", cmds1); } (format!("ws-l:{}", cmds1), "-".to_string()) } else { if !quiet { eprintln!("Listening on ws://127.0.0.1:{}/", cmds1); } (format!("ws-l:127.0.0.1:{}", cmds1), "-".to_string()) } } else if cmds1.contains(':') { if !quiet { eprintln!("Listening on wss://{}/", cmds1); } (format!("wss-l:{}", cmds1), "-".to_string()) } else { if !quiet { eprintln!("Listening on wss://127.0.0.1:{}/", cmds1); } (format!("wss-l:127.0.0.1:{}", cmds1), "-".to_string()) } } else { if !(cmds1.starts_with("ws://") || cmds1.starts_with("wss://")) { if !quiet { eprintln!("Specify ws:// or wss:// URI to connect to a websocket"); } Err("Invalid command-line parameters")?; } ("-".to_string(), cmds1) } } (None, Some(_)) => unreachable!(), }; if opts.websocket_text_mode { opts.read_debt_handling = websocat::readdebt::DebtHandling::Warn; } if cmd.strict_mode { opts.read_debt_handling = websocat::readdebt::DebtHandling::DropMessage; opts.linemode_strict = true; } debug!("Done first phase of interpreting options."); let websocat1 = WebsocatConfiguration1 { opts, addr1: s1, addr2: s2, }; let mut websocat2 = websocat1.parse1()?; debug!("Done second phase of interpreting options."); if websocat2.inetd_mode() { quiet = true; } if !quiet && recommend_explicit_text_or_bin { eprintln!("websocat: It is recommended to either set --binary or --text explicitly"); } if !quiet && !logging_already_set { logging::setup_env_logger(cmd.verbosity)?; } if !cmd.no_lints { websocat2.lint_and_fixup(Box::new(move |e: &str| { if !quiet { eprintln!("websocat: {}", e); } }))?; } if cmd.jsonrpc { websocat2 .s1 .overlays .insert(0, websocat::specifier::SpecifierNode{cls: ::std::rc::Rc::new(websocat::jsonrpc_peer::JsonRpcClass)}); } debug!("Done third phase of interpreting options."); let websocat = websocat2.parse2()?; debug!("Done fourth phase of interpreting options."); if cmd.dumpspec { println!("{:?}", websocat.s1); println!("{:?}", websocat.s2); println!("{:?}", websocat.opts); return Ok(()); } let mut core = tokio::runtime::current_thread::Runtime::new()?; let error_handler = std::rc::Rc::new(move |e| { if !quiet { eprintln!("websocat: {}", e); } }); let prog = websocat.serve(error_handler); debug!("Preparation done. Now actually starting."); core.block_on(prog) .map_err(|()| "error running".to_string())?; Ok(()) } fn main() { let r = run(); if let Err(e) = r { eprintln!("websocat: {}", e); ::std::process::exit(1); } } ================================================ FILE: src/mirror_peer.rs ================================================ use super::{BoxedNewPeerFuture, Peer}; use super::{brokenpipe, io_other_error, wouldblock}; use futures; use futures::sink::Sink; use futures::stream::Stream; use std; use std::io::Result as IoResult; use std::io::{Read, Write}; use futures::Async::{NotReady, Ready}; use std::rc::Rc; use futures::sync::mpsc; use tokio_io::{AsyncRead, AsyncWrite}; use super::readdebt::{DebtHandling, ProcessMessageResult, ReadDebt, ZeroMessagesHandling}; use super::{once, ConstructParams, PeerConstructor, Specifier}; #[derive(Debug, Clone)] pub struct Mirror; impl Specifier for Mirror { fn construct(&self, cp: ConstructParams) -> PeerConstructor { once(get_mirror_peer(cp.program_options.read_debt_handling)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = MirrorClass, target = Mirror, prefixes = ["mirror:"], arg_handling = noarg, overlay = false, MessageOriented, SingleConnect, help = r#" Simply copy output to input. No arguments needed. Example: emulate echo.websocket.org websocat -t ws-l:127.0.0.1:1234 mirror: "# ); // TODO: doc example, mention echo.websocket.org #[derive(Clone)] pub struct LiteralReply(pub Vec); impl Specifier for LiteralReply { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(get_literal_reply_peer(self.0.clone())) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } impl std::fmt::Debug for LiteralReply { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { write!(f, "LiteralReply") } } specifier_class!( name = LiteralReplyClass, target = LiteralReply, prefixes = ["literalreply:"], arg_handling = into, overlay = false, MessageOriented, SingleConnect, help = r#" Reply with a specified string for each input packet. Example: websocat ws-l:0.0.0.0:1234 literalreply:'{"status":"OK"}' "# ); struct MirrorWrite(mpsc::Sender>); struct MirrorRead { debt: ReadDebt, ch: mpsc::Receiver>, } pub fn get_mirror_peer(debt_handling: DebtHandling) -> BoxedNewPeerFuture { let (sender, receiver) = mpsc::channel::>(0); let r = MirrorRead { debt: ReadDebt(Default::default(), debt_handling, ZeroMessagesHandling::Deliver), ch: receiver, }; let w = MirrorWrite(sender); let p = Peer::new(r, w, None); Box::new(futures::future::ok(p)) as BoxedNewPeerFuture } pub fn get_literal_reply_peer(content: Vec) -> BoxedNewPeerFuture { let (sender, receiver) = mpsc::channel::<()>(0); let r = LiteralReplyRead { debt: ReadDebt(Default::default(), DebtHandling::Silent, ZeroMessagesHandling::Deliver), ch: receiver, content, }; let w = LiteralReplyHandle(sender); let p = Peer::new(r, w, None); Box::new(futures::future::ok(p)) as BoxedNewPeerFuture } impl AsyncRead for MirrorRead {} impl Read for MirrorRead { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { if let Some(ret) = self.debt.check_debt(buf) { return ret; } loop { let r = self.ch.poll(); return match r { Ok(Ready(Some(x))) => match self.debt.process_message(buf, x.as_slice()) { ProcessMessageResult::Return(x) => x, ProcessMessageResult::Recurse => continue, }, Ok(Ready(None)) => brokenpipe(), Ok(NotReady) => wouldblock(), Err(_) => brokenpipe(), }; } } } impl AsyncWrite for MirrorWrite { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { Ok(Ready(())) } } impl Write for MirrorWrite { fn write(&mut self, buf: &[u8]) -> IoResult { let om = buf.to_vec(); match self.0.start_send(om).map_err(io_other_error)? { futures::AsyncSink::NotReady(_) => wouldblock(), futures::AsyncSink::Ready => Ok(buf.len()), } } fn flush(&mut self) -> IoResult<()> { match self.0.poll_complete().map_err(io_other_error)? { NotReady => wouldblock(), Ready(()) => Ok(()), } } } impl Drop for MirrorWrite { fn drop(&mut self) { info!("MirrorWrite drop"); let _ = self.0.start_send(vec![]).map_err(|_| ()).map(|_| ()); let _ = self.0.poll_complete().map_err(|_| ()).map(|_| ()); } } struct LiteralReplyHandle(mpsc::Sender<()>); struct LiteralReplyRead { debt: ReadDebt, ch: mpsc::Receiver<()>, content: Vec, } impl AsyncWrite for LiteralReplyHandle { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { Ok(Ready(())) } } impl Write for LiteralReplyHandle { fn write(&mut self, buf: &[u8]) -> IoResult { match self.0.start_send(()).map_err(io_other_error)? { futures::AsyncSink::NotReady(_) => wouldblock(), futures::AsyncSink::Ready => Ok(buf.len()), } } fn flush(&mut self) -> IoResult<()> { match self.0.poll_complete().map_err(io_other_error)? { NotReady => wouldblock(), Ready(()) => Ok(()), } } } impl AsyncRead for LiteralReplyRead {} impl Read for LiteralReplyRead { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { if let Some(ret) = self.debt.check_debt(buf) { return ret; } loop { let r = self.ch.poll(); return match r { Ok(Ready(Some(()))) => match self.debt.process_message(buf, &self.content) { ProcessMessageResult::Return(x) => x, ProcessMessageResult::Recurse => continue, }, Ok(Ready(None)) => brokenpipe(), Ok(NotReady) => wouldblock(), Err(_) => brokenpipe(), }; } } } ================================================ FILE: src/my_copy.rs ================================================ use std::io; use futures::{Future, Poll}; use crate::{AsyncRead, AsyncWrite}; #[derive(Debug, Copy, Clone)] pub struct CopyOptions { pub stop_on_reader_zero_read: bool, pub once: bool, pub buffer_size: usize, /// Because of -u or -U pub skip: bool, pub max_ops: Option, } /// A future which will copy all data from a reader into a writer. /// A modified version of tokio_io::copy::Copy. /// /// Created by the [`copy`] function, this future will resolve to the number of /// bytes copied or an error if one happens. /// /// [`copy`]: fn.copy.html #[derive(Debug)] pub struct Copy { reader: Option, read_done: bool, writer: Option, pos: usize, cap: usize, amt: u64, buf: Box<[u8]>, opts: CopyOptions, read_occurred: bool, remaining_ops: Option, preamble: Vec, preamble_index: usize, } /// Creates a future which represents copying all the bytes from one object to /// another. /// /// The returned future will copy all the bytes read from `reader` into the /// `writer` specified. This future will only complete once the `reader` has hit /// EOF and all bytes have been written to and flushed from the `writer` /// provided. /// /// On success the number of bytes is returned and the `reader` and `writer` are /// consumed. On error the error is returned and the I/O objects are consumed as /// well. /// /// Unlike original tokio_io::copy::copy, it does not always stop on zero length reads /// , handles BrokenPipe error kind as EOF and flushes after every write pub fn copy(reader: R, writer: W, opts: CopyOptions, preamble: Vec) -> Copy where R: AsyncRead, W: AsyncWrite, { Copy { reader: Some(reader), read_done: false, writer: Some(writer), amt: 0, pos: 0, cap: 0, // TODO - de-hardcode buffer size buf: vec![0; opts.buffer_size].into_boxed_slice(), opts, read_occurred: false, remaining_ops: opts.max_ops, preamble, preamble_index: 0, } } impl Future for Copy where R: AsyncRead, W: AsyncWrite, { type Item = (u64, R, W); type Error = io::Error; fn poll(&mut self) -> Poll<(u64, R, W), io::Error> { loop { // First ensure that preamble messages got drained if self.preamble_index < self.preamble.len() { let writer = self.writer.as_mut().unwrap(); let i = try_nb!(writer.write(self.preamble[self.preamble_index].as_bytes())); if i == 0 { return Err(io::Error::new( io::ErrorKind::WriteZero, "write zero byte into writer", )); } else { trace!("preamble write {}", i); if i != self.preamble[self.preamble_index].len() { warn!("Short write of a preamble. Expect trimmed data.") } self.preamble_index += 1; } try_nb!(writer.flush()); continue; } // Handle inhibiting options only after preamble is drained. if self.opts.skip { debug!("copy skipped"); let reader = self.reader.take().unwrap(); let writer = self.writer.take().unwrap(); return Ok((0, reader, writer).into()); } // If our buffer is empty, then we need to read some data to // continue. trace!("poll"); if self.pos == self.cap && !self.read_done { if self.read_occurred && self.opts.once { debug!("Once mode requested, so aborting copy"); self.read_done = true; continue; } if self.remaining_ops == Some(0) { debug!("Maximum number of messages to copy exceed, so aborting copy"); self.read_done = true; continue; } let reader = self.reader.as_mut().unwrap(); let rr = reader.read(&mut self.buf); if let Err(ref e) = rr { if e.kind() == io::ErrorKind::BrokenPipe { debug!("BrokenPipe: read_done"); self.read_done = true; continue; } } let n = try_nb!(rr); trace!("read {}", n); if let Some(ref mut maxops) = self.remaining_ops { *maxops -= 1; } if n == 0 { debug!("zero len"); if self.opts.stop_on_reader_zero_read { debug!("read_done"); self.read_done = true; } continue; } else { self.pos = 0; self.cap = n; self.read_occurred = true; } } // If our buffer has some data, let's write it out! while self.pos < self.cap { let writer = self.writer.as_mut().unwrap(); let i = try_nb!(writer.write(&self.buf[self.pos..self.cap])); if i == 0 { return Err(io::Error::new( io::ErrorKind::WriteZero, "write zero byte into writer", )); } else { trace!("write {}", i); self.pos += i; self.amt += i as u64; } try_nb!(writer.flush()); } // If we've written al the data and we've seen EOF, flush out the // data and finish the transfer. // done with the entire transfer. if self.pos == self.cap && self.read_done { try_nb!(self.writer.as_mut().unwrap().flush()); let reader = self.reader.take().unwrap(); let writer = self.writer.take().unwrap(); debug!("done"); return Ok((self.amt, reader, writer).into()); } } } } ================================================ FILE: src/net_peer.rs ================================================ extern crate net2; use futures; use futures::future::Future; use futures::stream::Stream; use futures::unsync::oneshot::{channel, Receiver, Sender}; use std; use std::io::Result as IoResult; use std::io::{Read, Write}; use std::net::SocketAddr; use tokio_io::{AsyncRead, AsyncWrite}; use std::cell::RefCell; use std::rc::Rc; use tokio_tcp::{TcpListener, TcpStream}; use tokio_udp::UdpSocket; use super::L2rUser; use super::{box_up_err, peer_err_s, wouldblock, BoxedNewPeerFuture, BoxedNewPeerStream, Peer}; use super::{multi, once, ConstructParams, Options, PeerConstructor, Specifier}; #[derive(Debug, Clone)] pub struct TcpConnect(pub Vec); impl Specifier for TcpConnect { fn construct(&self, _: ConstructParams) -> PeerConstructor { // FIXME: connect to multiple things once(tcp_connect_peer(&self.0[..])) } specifier_boilerplate!(noglobalstate singleconnect no_subspec ); } specifier_class!( name = TcpConnectClass, target = TcpConnect, prefixes = ["tcp:", "tcp-connect:", "connect-tcp:", "tcp-c:", "c-tcp:"], arg_handling = parseresolve, overlay = false, StreamOriented, SingleConnect, help = r#" Connect to specified TCP host and port. Argument is a socket address. Example: simulate netcat netcat websocat - tcp:127.0.0.1:22 Example: redirect websocket connections to local SSH server over IPv6 websocat ws-l:0.0.0.0:8084 tcp:[::1]:22 "# ); #[derive(Debug, Clone)] pub struct TcpListen(pub SocketAddr); impl Specifier for TcpListen { fn construct(&self, p: ConstructParams) -> PeerConstructor { multi(tcp_listen_peer(&self.0, p.left_to_right, p.program_options.announce_listens)) } specifier_boilerplate!(noglobalstate multiconnect no_subspec ); } specifier_class!( name = TcpListenClass, target = TcpListen, prefixes = ["tcp-listen:", "listen-tcp:", "tcp-l:", "l-tcp:"], arg_handling = parse, overlay = false, StreamOriented, MultiConnect, help = r#" Listen TCP port on specified address. Example: echo server websocat tcp-l:0.0.0.0:1441 mirror: Example: redirect TCP to a websocket websocat tcp-l:0.0.0.0:8088 ws://echo.websocket.org "# ); #[derive(Debug, Clone)] pub struct UdpConnect(pub SocketAddr); impl Specifier for UdpConnect { fn construct(&self, p: ConstructParams) -> PeerConstructor { once(udp_connect_peer(&self.0, &p.program_options)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec ); } specifier_class!( name = UdpConnectClass, target = UdpConnect, prefixes = ["udp:", "udp-connect:", "connect-udp:", "udp-c:", "c-udp:"], arg_handling = parse, overlay = false, MessageOriented, SingleConnect, help = r#" Send and receive packets to specified UDP socket, from random UDP port "# ); #[derive(Debug, Clone)] pub struct UdpListen(pub SocketAddr); impl Specifier for UdpListen { fn construct(&self, p: ConstructParams) -> PeerConstructor { once(udp_listen_peer(&self.0, &p.program_options)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec ); } specifier_class!( name = UdpListenClass, target = UdpListen, prefixes = ["udp-listen:", "listen-udp:", "udp-l:", "l-udp:"], arg_handling = parse, overlay = false, MessageOriented, SingleConnect, help = r#" Bind an UDP socket to specified host:port, receive packet from any remote UDP socket, send replies to recently observed remote UDP socket. Note that it is not a multiconnect specifier like e.g. `tcp-listen`: entire lifecycle of the UDP socket is the same connection. File a feature request on Github if you want proper DNS-like request-reply UDP mode here. "# ); /* struct RcReadProxy(Rc) where for<'a> &'a R : AsyncRead; impl AsyncRead for RcReadProxy where for<'a> &'a R : AsyncRead{} impl Read for RcReadProxy where for<'a> &'a R : AsyncRead { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { (&*self.0).read(buf) } } struct RcWriteProxy(Rc) where for<'a> &'a W : AsyncWrite; impl AsyncWrite for RcWriteProxy where for<'a> &'a W : AsyncWrite { fn shutdown(&mut self) -> futures::Poll<(),std::io::Error> { (&*self.0).shutdown() } } impl Write for RcWriteProxy where for<'a> &'a W : AsyncWrite { fn write(&mut self, buf: &[u8]) -> IoResult { (&*self.0).write(buf) } fn flush(&mut self) -> IoResult<()> { (&*self.0).flush() } } */ // based on https://github.com/tokio-rs/tokio-core/blob/master/examples/proxy.rs #[derive(Clone)] struct MyTcpStream(Rc, bool); impl Read for MyTcpStream { fn read(&mut self, buf: &mut [u8]) -> IoResult { (&*self.0).read(buf) } } impl Write for MyTcpStream { fn write(&mut self, buf: &[u8]) -> IoResult { (&*self.0).write(buf) } fn flush(&mut self) -> IoResult<()> { Ok(()) } } impl AsyncRead for MyTcpStream {} impl AsyncWrite for MyTcpStream { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self.0.shutdown(std::net::Shutdown::Write)?; Ok(().into()) } } impl Drop for MyTcpStream { fn drop(&mut self) { let i_am_read_part = self.1; if i_am_read_part { let _ = self.0.shutdown(std::net::Shutdown::Read); } } } pub fn tcp_race(addrs: &[SocketAddr]) -> Box> + Send> { // Apply Happy Eyeballs in case of multiple proposed addresses. if addrs.len() > 1 { debug!("Setting up a race between multiple TCP client sockets. Who connects the first?"); } use futures::stream::futures_unordered::FuturesUnordered; let mut fu = FuturesUnordered::new(); for addr in addrs { let addr = *addr; fu.push( TcpStream::connect(&addr) .map(move |x| { info!("Connected to TCP {}", addr); x }) .map_err(|e|Box::new(e) as Box) ); } // reverse Ok and Err variants so that `fold` would exit early on a successful connection, but accumulate errors. let p = fu.then(|x| { let reversed = match x { Ok(a) => Err(a), Err(a) => Ok(a), }; futures::future::done(reversed) }).fold(None, |_accum, e|{ log::info!("Failure during connecting TCP: {}", e); futures::future::ok(Some(e)) }).then(|x| { match x { Ok(a) => Err(a), Err(a) => Ok(a), } }).map_err(|e : Option<_>| e.unwrap()); Box::new(p) } pub fn tcp_connect_peer(addrs: &[SocketAddr]) -> BoxedNewPeerFuture { let p = tcp_race(addrs) .map(|x : TcpStream| { let x = Rc::new(x); Peer::new( MyTcpStream(x.clone(), true), MyTcpStream(x.clone(), false), None /* TODO */ ) }).map_err(|e|{let e : Box = e; e}); /*let p = fu.into_future().and_then(|(x, _losers)| { let peer = x.unwrap(); debug!("We have a winner. Disconnecting losers."); futures::future::ok(peer) });*/ //Box::new(p.map_err(|(e,_)|e)) as BoxedNewPeerFuture Box::new(p) as BoxedNewPeerFuture } pub fn tcp_listen_peer(addr: &SocketAddr, l2r: L2rUser, announce: bool) -> BoxedNewPeerStream { let bound = match TcpListener::bind(addr) { Ok(x) => x, Err(e) => return peer_err_s(e), }; debug!("Listening TCP socket"); if announce { println!("LISTEN proto=tcp,ip={},port={}", addr.ip(), addr.port()); } use tk_listen::ListenExt; Box::new( bound .incoming() .sleep_on_error(::std::time::Duration::from_millis(500)) .map(move |x| { let addr = x.peer_addr().ok(); info!("Incoming TCP connection from {:?}", addr); match l2r { L2rUser::FillIn(ref y) => { let mut z = y.borrow_mut(); z.client_addr = addr.map(|a| format!("{}", a)); } L2rUser::ReadFrom(_) => {} } let x = Rc::new(x); Peer::new( MyTcpStream(x.clone(), true), MyTcpStream(x.clone(), false), None, /* TODO */ ) }) .map_err(|()| crate::simple_err2("unreachable error?")), ) as BoxedNewPeerStream } #[derive(Debug)] enum UdpPeerState { ConnectMode, WaitingForAddress((Sender<()>, Receiver<()>)), HasAddress(SocketAddr), } struct UdpPeer { s: UdpSocket, state: Option, oneshot_mode: bool, } #[derive(Clone)] struct UdpPeerHandle(Rc>); fn get_zero_address(addr: &SocketAddr) -> SocketAddr { use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; let ip = match addr.ip() { IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), }; SocketAddr::new(ip, 0) } fn apply_udp_options(s: &UdpSocket, opts:&Rc) -> IoResult<()> { if opts.udp_broadcast { s.set_broadcast(true)?; } let mut multicast_v4 = false; let mut multicast_v6 = false; let mut v4ai = opts.udp_join_multicast_iface_v4.iter(); let mut v6ai = opts.udp_join_multicast_iface_v6.iter(); let use_ai = opts.udp_join_multicast_iface_v4.len() + opts.udp_join_multicast_iface_v6.len() > 0; for multicast_address in opts.udp_join_multicast_addr.iter() { match multicast_address { std::net::IpAddr::V4(a) => { multicast_v4 = true; let interface_address = if use_ai { *v4ai.next().unwrap() } else { std::net::Ipv4Addr::UNSPECIFIED }; s.join_multicast_v4(a, &interface_address)?; }, std::net::IpAddr::V6(a) => { multicast_v6 = true; let interface_index = if use_ai { *v6ai.next().unwrap() } else { 0 }; s.join_multicast_v6(a, interface_index)?; } } } if opts.udp_multicast_loop { if multicast_v4 { s.set_multicast_loop_v4(true)?; } if multicast_v6 { s.set_multicast_loop_v6(true)?; } } if let Some(ttl) = opts.udp_ttl { s.set_ttl(ttl)?; if multicast_v4 { s.set_multicast_ttl_v4(ttl)?; } } Ok(()) } pub fn get_udp(addr: &SocketAddr, opts: &Rc) -> IoResult { let u = match addr { SocketAddr::V4(_) => net2::UdpBuilder::new_v4()?, SocketAddr::V6(_) => net2::UdpBuilder::new_v6()?, }; if opts.udp_reuseaddr { u.reuse_address(true)?; } //u.only_v6(true); let u = u.bind(addr)?; UdpSocket::from_std(u, &tokio_reactor::Handle::default()) } pub fn udp_connect_peer(addr: &SocketAddr, opts: &Rc) -> BoxedNewPeerFuture { let za = get_zero_address(addr); Box::new(futures::future::result( get_udp(&za, opts) .and_then(|x| { x.connect(addr)?; apply_udp_options(&x, opts)?; let h1 = UdpPeerHandle(Rc::new(RefCell::new(UdpPeer { s: x, state: Some(UdpPeerState::ConnectMode), oneshot_mode: opts.udp_oneshot_mode, }))); let h2 = h1.clone(); Ok(Peer::new(h1, h2, None)) }) .map_err(box_up_err), )) as BoxedNewPeerFuture } pub fn udp_listen_peer(addr: &SocketAddr, opts: &Rc) -> BoxedNewPeerFuture { Box::new(futures::future::result( get_udp(addr, opts) .and_then(|x| { apply_udp_options(&x, opts)?; debug!("Ready for serving UDP"); if opts.announce_listens { println!("LISTEN proto=udp,ip={},port={}", addr.ip(), addr.port()); } let h1 = UdpPeerHandle(Rc::new(RefCell::new(UdpPeer { s: x, state: Some(UdpPeerState::WaitingForAddress(channel())), oneshot_mode: opts.udp_oneshot_mode, }))); let h2 = h1.clone(); Ok(Peer::new(h1, h2, None)) }) .map_err(box_up_err), )) as BoxedNewPeerFuture } impl Read for UdpPeerHandle { fn read(&mut self, buf: &mut [u8]) -> IoResult { let mut p = self.0.borrow_mut(); match p.state.take().expect("Assertion failed 193912") { UdpPeerState::ConnectMode => { p.state = Some(UdpPeerState::ConnectMode); p.s.recv2(buf) } UdpPeerState::HasAddress(oldaddr) => { p.s.recv_from2(buf) .map(|(ret, addr)| { if addr != oldaddr { warn!("New client for the same listening UDP socket"); } p.state = Some(UdpPeerState::HasAddress(addr)); ret }) .inspect_err(|_| { p.state = Some(UdpPeerState::HasAddress(oldaddr)); }) } UdpPeerState::WaitingForAddress((cmpl, pollster)) => match p.s.recv_from2(buf) { Ok((ret, addr)) => { p.state = Some(UdpPeerState::HasAddress(addr)); let _ = cmpl.send(()); Ok(ret) } Err(e) => { p.state = Some(UdpPeerState::WaitingForAddress((cmpl, pollster))); Err(e) } }, } } } impl Write for UdpPeerHandle { fn write(&mut self, buf: &[u8]) -> IoResult { let mut p = self.0.borrow_mut(); match p.state.take().expect("Assertion failed 193913") { UdpPeerState::ConnectMode => { p.state = Some(UdpPeerState::ConnectMode); p.s.send2(buf) } UdpPeerState::HasAddress(a) => { if p.oneshot_mode { p.state = Some(UdpPeerState::WaitingForAddress(channel())); } else { p.state = Some(UdpPeerState::HasAddress(a)); } p.s.send_to2(buf, &a) } UdpPeerState::WaitingForAddress((cmpl, mut pollster)) => { let _ = pollster.poll(); // register wakeup p.state = Some(UdpPeerState::WaitingForAddress((cmpl, pollster))); wouldblock() } } } fn flush(&mut self) -> IoResult<()> { Ok(()) } } impl AsyncRead for UdpPeerHandle {} impl AsyncWrite for UdpPeerHandle { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { Ok(().into()) } } /// Squirreled await from deprecated UdpSocket functions trait UndeprecateNonpollSendRecv { fn recv2(&mut self, buf: &mut [u8]) -> std::io::Result; fn recv_from2(&mut self, buf: &mut [u8]) -> std::io::Result<(usize, SocketAddr)>; fn send2(&mut self, buf: &[u8]) -> std::io::Result; fn send_to2(&mut self, buf: &[u8], target: &SocketAddr) -> std::io::Result; } impl UndeprecateNonpollSendRecv for UdpSocket { fn recv2(&mut self, buf: &mut [u8]) -> std::io::Result { match self.poll_recv(buf)? { futures::Async::Ready(n) => Ok(n), futures::Async::NotReady => Err(std::io::ErrorKind::WouldBlock.into()), } } fn recv_from2(&mut self, buf: &mut [u8]) -> std::io::Result<(usize, SocketAddr)> { match self.poll_recv_from(buf)? { futures::Async::Ready(ret) => Ok(ret), futures::Async::NotReady => Err(std::io::ErrorKind::WouldBlock.into()), } } fn send2(&mut self, buf: &[u8]) -> std::io::Result { match self.poll_send(buf)? { futures::Async::Ready(n) => Ok(n), futures::Async::NotReady => Err(std::io::ErrorKind::WouldBlock.into()), } } fn send_to2(&mut self, buf: &[u8], target: &SocketAddr) -> std::io::Result { match self.poll_send_to(buf, target)? { futures::Async::Ready(n) => Ok(n), futures::Async::NotReady => Err(std::io::ErrorKind::WouldBlock.into()), } } } ================================================ FILE: src/options.rs ================================================ pub use super::socks5_peer::SocksSocketAddr; use super::readdebt::DebtHandling; use std::ffi::OsString; use std::net::SocketAddr; #[derive(Debug, Clone)] pub struct StaticFile { pub uri: String, pub file: ::std::path::PathBuf, pub content_type: String, } extern crate http_bytes; use http_bytes::http; #[derive(SmartDefault, Derivative)] #[derivative(Debug)] pub struct Options { pub websocket_text_mode: bool, pub websocket_protocol: Option, pub websocket_reply_protocol: Option, pub udp_oneshot_mode: bool, pub udp_broadcast: bool, pub udp_multicast_loop: bool, pub udp_ttl: Option, pub udp_join_multicast_addr: Vec, pub udp_join_multicast_iface_v4: Vec, pub udp_join_multicast_iface_v6: Vec, pub udp_reuseaddr: bool, pub unidirectional: bool, pub unidirectional_reverse: bool, pub max_messages: Option, pub max_messages_rev: Option, pub exit_on_eof: bool, pub oneshot: bool, pub unlink_unix_socket: bool, pub unix_socket_accept_from_fd: bool, pub exec_args: Vec, pub ws_c_uri: String, // TODO: delete this pub linemode_strip_newlines: bool, pub linemode_strict: bool, pub origin: Option, pub custom_headers: Vec<(String, Vec)>, pub custom_reply_headers: Vec<(String, Vec)>, pub websocket_version: Option, pub websocket_dont_close: bool, pub websocket_ignore_zeromsg: bool, pub one_message: bool, pub no_auto_linemode: bool, #[default = 65536] pub buffer_size: usize, #[default = 16] pub broadcast_queue_len: usize, #[default(DebtHandling::Silent)] pub read_debt_handling: DebtHandling, pub linemode_zero_terminated: bool, pub restrict_uri: Option, pub serve_static_files: Vec, pub exec_set_env: bool, pub no_exit_on_zeromsg: bool, pub reuser_send_zero_msg_on_disconnect: bool, pub process_zero_sighup: bool, pub process_exit_sighup: bool, pub process_exit_on_disconnect: bool, pub socks_destination: Option, pub auto_socks5: Option, pub socks5_bind_script: Option, pub socks5_user_pass: Option, pub tls_domain: Option, #[derivative(Debug = "ignore")] pub pkcs12_der: Option>, #[derivative(Debug = "ignore")] pub pkcs12_passwd: Option, #[derivative(Debug = "ignore")] pub client_pkcs12_der: Option>, #[derivative(Debug = "ignore")] pub client_pkcs12_passwd: Option, pub tls_insecure: bool, pub headers_to_env: Vec, pub max_parallel_conns: Option, pub ws_ping_interval: Option, pub ws_ping_timeout: Option, pub request_uri: Option, pub request_method: Option, pub request_headers: Vec<(http::header::HeaderName, http::header::HeaderValue)>, pub autoreconnect_delay_millis: u64, pub ws_text_prefix: Option, pub ws_binary_prefix: Option, pub ws_binary_base64: bool, pub ws_text_base64: bool, pub close_status_code: Option, pub close_reason: Option, /// Only affects linter pub asyncstdio: bool, pub foreachmsg_wait_reads: bool, pub announce_listens: bool, pub timestamp_monotonic: bool, pub print_ping_rtts: bool, #[cfg(feature = "crypto_peer")] pub crypto_key: Option<[u8; 32]>, #[cfg(feature = "crypto_peer")] pub crypto_reverse: bool, #[cfg(feature = "prometheus_peer")] pub prometheus: Option, #[default = 0x1c] pub byte_to_exit_on: u8, #[default = 209715200] pub max_ws_message_length: usize, #[default = 104857600] pub max_ws_frame_length: usize, pub preamble: Vec, pub preamble_reverse: Vec, pub compress_deflate: bool, pub compress_zlib: bool, pub compress_gzip: bool, pub uncompress_deflate: bool, pub uncompress_zlib: bool, pub uncompress_gzip: bool, pub jsonrpc_omit_jsonrpc: bool, pub inhibit_pongs: Option, pub max_sent_pings: Option, pub lengthprefixed_header_bytes: usize, pub lengthprefixed_little_endian: bool, pub lengthprefixed_skip_read_direction: bool, pub lengthprefixed_skip_write_direction: bool, } ================================================ FILE: src/primitive_reuse_peer.rs ================================================ extern crate futures; extern crate tokio_io; use futures::future::ok; use std::cell::RefCell; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer}; use std::io::{Error as IoError, Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use super::{once, ConstructParams, PeerConstructor, Specifier}; use futures::Future; use std::ops::DerefMut; #[derive(Debug)] pub struct Reuser(pub Rc); impl Specifier for Reuser { fn construct(&self, p: ConstructParams) -> PeerConstructor { let send_zero_msg_on_disconnect = p.program_options.reuser_send_zero_msg_on_disconnect; let reuser = p.global(GlobalState::default).clone(); let mut reuser = reuser.clone(); let l2r = p.left_to_right.clone(); let inner = || self.0.construct(p).get_only_first_conn(l2r); once(connection_reuser( &mut reuser, inner, send_zero_msg_on_disconnect, )) } specifier_boilerplate!(singleconnect has_subspec globalstate); self_0_is_subspecifier!(...); } specifier_class!( name = ReuserClass, target = Reuser, prefixes = ["reuse-raw:", "raw-reuse:"], arg_handling = subspec, overlay = true, MessageBoundaryStatusDependsOnInnerType, SingleConnect, help = r#" Reuse subspecifier for serving multiple clients: unpredictable mode. [A] Better used with --unidirectional, otherwise replies get directed to random connected client. Example: Forward multiple parallel WebSocket connections to a single persistent TCP connection websocat -u ws-l:0.0.0.0:8800 reuse:tcp:127.0.0.1:4567 Example (unreliable): don't disconnect SSH when websocket reconnects websocat ws-l:[::]:8088 reuse:tcp:127.0.0.1:22 "# ); type PeerSlot = Rc>>; #[derive(Default, Clone)] pub struct GlobalState(PeerSlot); #[derive(Clone)] struct PeerHandle(PeerSlot, bool); impl Read for PeerHandle { fn read(&mut self, b: &mut [u8]) -> Result { if let Some(ref mut x) = *self.0.borrow_mut().deref_mut() { x.0.read(b) } else { unreachable!() } } } impl AsyncRead for PeerHandle {} impl Write for PeerHandle { fn write(&mut self, b: &[u8]) -> Result { if let Some(ref mut x) = *self.0.borrow_mut().deref_mut() { x.1.write(b) } else { unreachable!() } } fn flush(&mut self) -> Result<(), IoError> { if let Some(ref mut x) = *self.0.borrow_mut().deref_mut() { x.1.flush() } else { unreachable!() } } } impl AsyncWrite for PeerHandle { fn shutdown(&mut self) -> futures::Poll<(), IoError> { if self.1 { let _ = self.write(b""); } if let Some(ref mut _x) = *self.0.borrow_mut().deref_mut() { // Ignore shutdown attempts Ok(futures::Async::Ready(())) //_x.1.shutdown() } else { unreachable!() } } } pub fn connection_reuser BoxedNewPeerFuture>( s: &mut GlobalState, inner_peer: F, send_zero_msg_on_disconnect: bool, ) -> BoxedNewPeerFuture { let need_init = s.0.borrow().is_none(); let rc = s.0.clone(); if need_init { info!("Initializing"); Box::new(inner_peer().and_then(move |inner| { { let mut b = rc.borrow_mut(); let x: &mut Option = b.deref_mut(); *x = Some(inner); } let ps: PeerSlot = rc.clone(); let ph1 = PeerHandle(ps, send_zero_msg_on_disconnect); let ph2 = ph1.clone(); let peer = Peer::new(ph1, ph2, None /* TODO */); ok(peer) })) as BoxedNewPeerFuture } else { info!("Reusing"); let ps: PeerSlot = rc.clone(); let ph1 = PeerHandle(ps, send_zero_msg_on_disconnect); let ph2 = ph1.clone(); let peer = Peer::new(ph1, ph2, None /* TODO */); Box::new(ok(peer)) as BoxedNewPeerFuture } } ================================================ FILE: src/process_peer.rs ================================================ extern crate tokio_process; use futures; use std::io::Result as IoResult; use std::io::{Read, Write}; use std::{self, process::ExitStatus}; use tokio_io::{AsyncRead, AsyncWrite}; use super::{L2rUser, LeftSpecToRightSpec}; use std::cell::RefCell; use std::rc::Rc; use std::process::Command; use self::tokio_process::{Child, CommandExt}; use super::{once, ConstructParams, PeerConstructor, Specifier}; use super::{BoxedNewPeerFuture, Peer}; use std::process::Stdio; fn needenv(p: &ConstructParams) -> Option<&LeftSpecToRightSpec> { match (p.program_options.exec_set_env, &p.left_to_right) { (true, L2rUser::ReadFrom(ref x)) => Some(&**x), _ => None, } } #[derive(Debug, Clone)] pub struct Cmd(pub String); impl Specifier for Cmd { fn construct(&self, p: ConstructParams) -> PeerConstructor { let zero_sighup = p.program_options.process_zero_sighup; let exit_sighup = p.program_options.process_exit_sighup; let exit_on_disconnect = p.program_options.process_exit_on_disconnect; let args = if cfg!(target_os = "windows") { let mut args = Command::new("cmd"); args.arg("/C").arg(self.0.clone()); args } else { let mut args = Command::new("sh"); args.arg("-c").arg(self.0.clone()); args }; let env = needenv(&p); once(Box::new(futures::future::result(process_connect_peer( args, env, zero_sighup, exit_sighup, exit_on_disconnect, ))) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec ); } specifier_class!( name = CmdClass, target = Cmd, prefixes = ["cmd:"], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Start specified command line using `sh -c` or `cmd /C` (depending on platform) Otherwise should be the the same as `sh-c:` (see examples from there). "# ); // TODO: client and example output for each server example // TODO: chromium-based examples #[derive(Debug, Clone)] pub struct ShC(pub String); impl Specifier for ShC { fn construct(&self, p: ConstructParams) -> PeerConstructor { let zero_sighup = p.program_options.process_zero_sighup; let exit_sighup = p.program_options.process_exit_sighup; let exit_on_disconnect = p.program_options.process_exit_on_disconnect; let mut args = Command::new("sh"); args.arg("-c").arg(self.0.clone()); let env = needenv(&p); once(Box::new(futures::future::result(process_connect_peer( args, env, zero_sighup, exit_sighup, exit_on_disconnect, ))) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec ); } specifier_class!( name = ShCClass, target = ShC, prefixes = ["sh-c:"], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Start specified command line using `sh -c` (even on Windows) Example: serve a counter websocat -U ws-l:127.0.0.1:8008 sh-c:'for i in 0 1 2 3 4 5 6 7 8 9 10; do echo $i; sleep 1; done' Example: unauthenticated shell websocat --exit-on-eof ws-l:127.0.0.1:5667 sh-c:'bash -i 2>&1' "# ); #[derive(Debug, Clone)] pub struct Exec(pub String); impl Specifier for Exec { fn construct(&self, p: ConstructParams) -> PeerConstructor { let zero_sighup = p.program_options.process_zero_sighup; let exit_sighup = p.program_options.process_exit_sighup; let exit_on_disconnect = p.program_options.process_exit_on_disconnect; let mut args = Command::new(self.0.clone()); args.args(p.program_options.exec_args.clone()); let env = needenv(&p); once(Box::new(futures::future::result(process_connect_peer( args, env, zero_sighup, exit_sighup, exit_on_disconnect, ))) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec ); } specifier_class!( name = ExecClass, target = Exec, prefixes = ["exec:"], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Execute a program directly (without a subshell), providing array of arguments on Unix [A] Example: Serve current date websocat -U ws-l:127.0.0.1:5667 exec:date Example: pinger websocat -U ws-l:127.0.0.1:5667 exec:ping --exec-args 127.0.0.1 -c 1 "# ); fn process_connect_peer( mut cmd: Command, l2r: Option<&LeftSpecToRightSpec>, zero_sighup: bool, close_sighup: bool, exit_on_disconnect: bool, ) -> Result> { if let Some(x) = l2r { if let Some(ref z) = x.client_addr { cmd.env("WEBSOCAT_CLIENT", z); }; if let Some(ref z) = x.uri { cmd.env("WEBSOCAT_URI", z); }; for (hn, hv) in &x.headers { cmd.env(format!("H_{}", hn), hv); } } cmd.stdin(Stdio::piped()).stdout(Stdio::piped()); let child = cmd.spawn_async()?; let ph = ProcessPeer { chld: Rc::new(RefCell::new(ForgetfulProcess { chld: Some(child), exit_on_disconnect, })), sighup_on_zero: zero_sighup, sighup_on_close: close_sighup, }; Ok(Peer::new(ph.clone(), ph, None /* TODO */)) } struct ForgetfulProcess { chld: Option, exit_on_disconnect: bool, } #[derive(Clone)] struct ProcessPeer { chld: Rc>, sighup_on_zero: bool, sighup_on_close: bool, } impl Read for ProcessPeer { fn read(&mut self, buf: &mut [u8]) -> IoResult { self.chld .borrow_mut() .chld .as_mut() .unwrap() .stdout() .as_mut() .expect("assertion failed 1425") .read(buf) } } impl Write for ProcessPeer { fn write(&mut self, buf: &[u8]) -> IoResult { #[cfg(unix)] { if self.sighup_on_zero && buf.is_empty() { // TODO use nix crate? if let Some(ref chld) = self.chld.borrow().chld { unsafe { extern crate libc; libc::kill(chld.id() as libc::pid_t, libc::SIGHUP); } } } } self.chld .borrow_mut() .chld .as_mut() .unwrap() .stdin() .as_mut() .expect("assertion failed 1425") .write(buf) } fn flush(&mut self) -> IoResult<()> { self.chld .borrow_mut() .chld .as_mut() .unwrap() .stdin() .as_mut() .expect("assertion failed 1425") .flush() } } impl AsyncRead for ProcessPeer {} impl AsyncWrite for ProcessPeer { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { #[cfg(unix)] { if self.sighup_on_close { // TODO use nix crate? if let Some(ref chld) = self.chld.borrow().chld { unsafe { extern crate libc; libc::kill(chld.id() as libc::pid_t, libc::SIGHUP); } } } } debug!("Shutdown of process peer's writer"); let mut c: tokio_process::ChildStdin = self .chld .borrow_mut() .chld .as_mut() .unwrap() .stdin() .take() .expect("assertion failed 1425"); c.shutdown() } } impl Drop for ForgetfulProcess { fn drop(&mut self) { use futures::Future; let mut chld = self.chld.take().unwrap(); if self.exit_on_disconnect { debug!("Forcing child process to exit"); match chld.kill() { Ok(()) => (), Err(e) => { warn!("Error terminating child process: {}", e); } } tokio::spawn( chld.map(|_exc: ExitStatus| { }).map_err(|_|()) ); } else { tokio::spawn( chld.map(|exc: ExitStatus| { if exc.success() { debug!("Child process exited") } else { warn!("Child process exited unsuccessfully: {:?}", exc.code()); } }) .map_err(|e| { error!("Error waiting for child process termination: {}", e); }), ); } } } ================================================ FILE: src/prometheus_peer.rs ================================================ use futures::future::ok; use futures::{Async}; use prometheus::core::{AtomicU64, Atomic}; use std::cell::RefCell; use std::net::{SocketAddr, TcpListener}; use std::rc::Rc; use std::time::Duration; use crate::ws_server_peer::http_serve::get_static_file_reply; use super::{BoxedNewPeerFuture, Peer}; use super::{ConstructParams, PeerConstructor, Specifier}; use std::io::{Read, Write, ErrorKind}; use tokio_io::{AsyncRead, AsyncWrite}; use std::io::Error as IoError; use prometheus::{Encoder, IntCounter, Histogram}; #[derive(prometheus_metric_storage::MetricStorage)] #[metric(subsystem = "websocat1")] pub struct GlobalStats { /// Number of times write function was called w_msgs: IntCounter, /// Number of times read function was called r_msgs: IntCounter, /// Total number of written bytes to the `prometheus:` node w_bytes: IntCounter, /// Total number of read bytes from the `prometheus:` node r_bytes: IntCounter, /// Number of times `prometheus:` overlay was instantiated connects: IntCounter, /// Number of times `prometheus:` overlay's destructor was called disconnects: IntCounter, /// Distribution of times between `prometheus:` overlay initiation and destruction #[metric(buckets(0.1, 1, 10, 60, 300, 3600))] session_durations: Histogram, /// Distribution of times between one `prometheus:` overlay initiation and the next initiation #[metric(buckets(0.1, 1, 10, 60, 300, 3600))] between_connects: Histogram, /// Distribution of the number of total bytes written to the overlay per connection #[metric(buckets(0, 32, 1024, 65536, 1048576, 33554432, 1073741824))] session_w_bytes: Histogram, /// Distribution of the number of total bytes read to the overlay per connection #[metric(buckets(0, 32, 1024, 65536, 1048576, 33554432, 1073741824))] session_r_bytes: Histogram, /// Distribution of the number of total count of write function calls to the overlay per connection #[metric(buckets(0, 1, 2, 8, 64, 2048, 65536, 2097152))] session_w_msgs: Histogram, /// Distribution of the number of total count of read function calls to the overlay per connection #[metric(buckets(0, 1, 2, 8, 64, 2048, 65536, 2097152))] session_r_msgs: Histogram, /// Distribution of the `session_r_bytes` divided by `session_durations` values #[metric(buckets(0.5, 10, 100, 1000, 1000_0, 1000_00, 1000_000, 10_000_000, 100_000_000))] session_avg_r_bps: Histogram, /// Distribution of the `session_w_bytes` divided by `session_durations` values #[metric(buckets(0.5, 10, 100, 1000, 1000_0, 1000_00, 1000_000, 10_000_000, 100_000_000))] session_avg_w_bps: Histogram, /// Distribution of byte lengths underlying `read` function successfully returned #[metric(buckets(0, 1, 10, 50, 300, 1024, 8192, 65536, 4194304))] read_lengths: Histogram, /// Number of times `read` function of the underlying specifier returned error (besides EAGAIN) read_errors: IntCounter, /// Number of times `read` function of the underlying specifier returned EAGAIN read_wouldblocks: IntCounter, /// Number of times `write` function of the underlying specifier returned error (besides EAGAIN) write_errors: IntCounter, /// Number of times `write` function of the underlying specifier returned EAGAIN write_wouldblocks: IntCounter, /// Distribution of byte lengths underlying `write` function successfully returned #[metric(buckets(0, 1, 10, 50, 300, 1024, 8192, 65536, 4194304))] write_lengths: Histogram, /// durations it took to make a function call to underlying node for reading #[metric(buckets(0.1e-3,1e-3,0.01,0.1,1,10))] read_timings: Histogram, /// durations it took to make a function call to underlying node for writing #[metric(buckets(0.1e-3,1e-3,0.01,0.1,1,10))] write_timings: Histogram, } pub type HGlobalStats = Rc; pub type GlobalState = (HGlobalStats, Rc>>); struct Droppie { w_msgs: AtomicU64, r_msgs: AtomicU64, w_bytes: AtomicU64, r_bytes: AtomicU64, session_timing: Option, handle: HGlobalStats, } impl Droppie { fn new(handle: HGlobalStats) -> Droppie { handle.connects.inc(); Droppie { session_timing: Some(handle.session_durations.start_timer()), handle: handle, w_msgs: AtomicU64::new(0), r_msgs: AtomicU64::new(0), w_bytes: AtomicU64::new(0), r_bytes: AtomicU64::new(0), } } } impl Drop for Droppie { fn drop(&mut self) { let t = self.session_timing.take().unwrap().stop_and_record(); self.handle.session_r_bytes.observe(self.r_bytes.get() as f64); self.handle.session_w_bytes.observe(self.w_bytes.get() as f64); self.handle.session_r_msgs.observe(self.r_msgs.get() as f64); self.handle.session_w_msgs.observe(self.w_msgs.get() as f64); let r_avg_bps = self.r_bytes.get() as f64 / t; let w_avg_bps = self.w_bytes.get() as f64 / t; self.handle.session_avg_r_bps.observe(r_avg_bps); self.handle.session_avg_w_bps.observe(w_avg_bps); self.handle.disconnects.inc(); } } pub fn new_global_stats() -> GlobalState { (Rc::new(GlobalStats::new(prometheus::default_registry()).unwrap()), Rc::new(RefCell::new(None))) } pub fn serve(psa: SocketAddr) -> crate::Result<()> { let tcp = TcpListener::bind(&psa)?; debug!("Listening TCP socket for Prometheus metrics"); std::thread::spawn(move || { for s in tcp.incoming() { if let Ok(s) = s { let mut s = std::io::BufWriter::new(s); let stats = prometheus::default_registry().gather(); let header = get_static_file_reply(None, "text/plain; version=0.0.4"); let _ = s.write_all(&header[..]); let _ = prometheus::TextEncoder::default().encode(&stats[..], &mut s); } std::thread::sleep(Duration::from_millis(5)); } }); Ok(()) } #[derive(Debug)] pub struct Prometheus(pub T); impl Specifier for Prometheus { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let stats: GlobalState = cp.global(new_global_stats).clone(); let inner = self.0.construct(cp.clone()); inner.map(move |p, _| prometheus_peer(p, stats.clone())) } specifier_boilerplate!(globalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = PrometheusClass, target = Prometheus, prefixes = ["prometheus:", "metrics:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" [A] Account connections, messages, bytes and other data and expose Prometheus metrics on a separate port. Not included by default, build a crate with `--features=prometheus_peer` to have it. You can also use `--features=prometheus_peer,prometheus/process` to have additional metrics. "# ); pub fn prometheus_peer(inner_peer: Peer, stats: GlobalState) -> BoxedNewPeerFuture { let droppie = Droppie::new(stats.0); // stops previous and start new timer *stats.1.borrow_mut() = Some(droppie.handle.between_connects.start_timer()); let droppie = Rc::new(droppie); let r = StatsWrapperR(inner_peer.0, droppie.clone()); let w = StatsWrapperW(inner_peer.1, droppie); let thepeer = Peer::new(r, w, inner_peer.2); Box::new(ok(thepeer)) as BoxedNewPeerFuture } struct StatsWrapperR(Box, Rc); impl Read for StatsWrapperR { fn read(&mut self, b: &mut [u8]) -> Result { let timer = self.1.handle.read_timings.start_timer(); let ret = self.0.read(b); timer.stop_and_record(); match &ret { Ok(x) => { self.1.handle.read_lengths.observe(*x as f64); self.1.handle.r_msgs.inc(); self.1.handle.r_bytes.inc_by(*x as u64); self.1.r_msgs.inc_by(1); self.1.r_bytes.inc_by(*x as u64); }, Err(e) if e.kind() == ErrorKind::WouldBlock => { self.1.handle.read_wouldblocks.inc(); }, Err(_) => { self.1.handle.read_errors.inc(); }, }; ret } } impl AsyncRead for StatsWrapperR {} struct StatsWrapperW(Box, Rc); impl Write for StatsWrapperW { fn write(&mut self, b: &[u8]) -> Result { let timer = self.1.handle.write_timings.start_timer(); let ret = self.0.write(b); timer.stop_and_record(); match &ret { Ok(x) => { self.1.handle.write_lengths.observe(*x as f64); self.1.handle.w_msgs.inc(); self.1.handle.w_bytes.inc_by(*x as u64); self.1.w_msgs.inc_by(1); self.1.w_bytes.inc_by(*x as u64); }, Err(e) if e.kind() == ErrorKind::WouldBlock => { self.1.handle.write_wouldblocks.inc(); }, Err(_) => { self.1.handle.write_errors.inc(); }, }; ret } fn flush(&mut self) -> std::io::Result<()> { self.0.flush() } } impl AsyncWrite for StatsWrapperW { fn shutdown(&mut self) -> std::result::Result, std::io::Error> { self.0.shutdown() } } ================================================ FILE: src/readdebt.rs ================================================ use std; #[derive(Debug, Clone, Copy)] pub enum DebtHandling { Silent, Warn, DropMessage, } pub enum ZeroMessagesHandling { Drop, Deliver, } pub enum ProcessMessageResult { Return(std::result::Result), Recurse, } /// A `Read` utility to deal with partial reads pub struct ReadDebt(pub Option>, pub DebtHandling, pub ZeroMessagesHandling); impl ReadDebt { pub fn process_message(&mut self, buf: &mut [u8], buf_in: &[u8]) -> ProcessMessageResult { assert_eq!(self.0, None); let mut l = buf_in.len(); if l > buf.len() { match self.1 { DebtHandling::Silent => (), DebtHandling::Warn => { warn!("Incoming message too long ({} > {}): splitting it to parts.\nUse -B option to increase buffer size or -S option to drop messages instead of splitting.", l, buf.len()); } DebtHandling::DropMessage => { error!("Dropping too large message ({} > {}). Use -B option to increase buffer size.", l, buf.len()); return ProcessMessageResult::Recurse; } } l = buf.len(); } buf[..l].copy_from_slice(&buf_in[..l]); if l < buf_in.len() { self.0 = Some(buf_in[l..].to_vec()); } debug!("Fulfilling the debt of {} bytes", l); if l == 0 { match self.2 { ZeroMessagesHandling::Deliver => (), ZeroMessagesHandling::Drop => { info!("Dropping incoming zero-length message"); return ProcessMessageResult::Recurse; } } } ProcessMessageResult::Return(Ok(l)) } pub fn check_debt( &mut self, buf: &mut [u8], ) -> Option> { if let Some(debt) = self.0.take() { match self.process_message(buf, debt.as_slice()) { ProcessMessageResult::Return(x) => Some(x), ProcessMessageResult::Recurse => unreachable!(), } } else { None } } } ================================================ FILE: src/reconnect_peer.rs ================================================ extern crate futures; extern crate tokio_io; use futures::future::ok; use std::cell::RefCell; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer}; use std::io::{Error as IoError, Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use super::{once, simple_err, wouldblock, ConstructParams, PeerConstructor, Specifier}; use futures::{Async, Future, Poll}; // TODO: shutdown write part if out writing part is shut down // TODO: stop if writing part and reading parts are closed (shutdown)? #[derive(Debug)] pub struct AutoReconnect(pub Rc); impl Specifier for AutoReconnect { fn construct(&self, cp: ConstructParams) -> PeerConstructor { once(autoreconnector(self.0.clone(), cp)) } specifier_boilerplate!(singleconnect noglobalstate has_subspec ); self_0_is_subspecifier!(...); } specifier_class!( name = AutoReconnectClass, target = AutoReconnect, prefixes = ["autoreconnect:"], arg_handling = subspec, overlay = true, MessageBoundaryStatusDependsOnInnerType, SingleConnect, help = r#" Re-establish underlying connection on any error or EOF Example: keep connecting to the port or spin 100% CPU trying if it is closed. websocat - autoreconnect:tcp:127.0.0.1:5445 Example: keep remote logging connection open (or flood the host if port is closed): websocat -u ws-l:0.0.0.0:8080 reuse:autoreconnect:tcp:192.168.0.3:1025 TODO: implement delays between reconnect attempts "# ); #[derive(Default)] struct State2 { already_warned: bool, } struct State { s: Rc, p: Option, n: Option, cp: ConstructParams, aux: State2, reconnect_delay: std::time::Duration, ratelimiter: Option, reconnect_count_limit: Option, /// Do not initiate connection now, return not ready outcome instead pegged_until_write: bool, } /// This implementation's poll is to be reused many times, both after returning item and error impl State { //type Item = &'mut Peer; //type Error = Box<::std::error::Error>; fn poll(&mut self) -> Poll<&mut Peer, Box> { let pp = &mut self.p; let nn = &mut self.n; let aux = &mut self.aux; loop { if let Some(delay) = self.ratelimiter.as_mut() { match delay.poll() { Ok(Async::Ready(_)) => { debug!("Waited for reconnect"); self.ratelimiter = None; } Err(e) => error!("tokio-timer's Delay: {}", e), Ok(Async::NotReady) => return Ok(Async::NotReady), } } if let Some(ref mut p) = *pp { return Ok(Async::Ready(p)); } let cp = self.cp.clone(); // Peer is not present: trying to create a new one if self.pegged_until_write { return Ok(Async::NotReady); } if self.reconnect_count_limit == Some(0) { info!("autoreconnector reconnect limit reached. Failing connection."); return Err(Box::new(simple_err("No more connections allowed".to_owned()))); } if let Some(mut bnpf) = nn.take() { match bnpf.poll() { Ok(Async::Ready(p)) => { *pp = Some(p); if let Some(ref mut cl) = self.reconnect_count_limit { *cl -= 1; } continue; } Ok(Async::NotReady) => { *nn = Some(bnpf); return Ok(Async::NotReady); } Err(_x) => { // Stop on error: //return Err(_x); if let Some(ref mut cl) = self.reconnect_count_limit { *cl -= 1; } // Just reconnect again on error if !aux.already_warned { aux.already_warned = true; warn!("Reconnecting failed. Further failed reconnects announcements will have lower log severity."); } else { info!("Reconnecting failed."); } self.ratelimiter = Some(tokio_timer::Delay::new(std::time::Instant::now() + self.reconnect_delay)); continue; } } } let l2r = cp.left_to_right.clone(); let pc: PeerConstructor = self.s.construct(cp); *nn = Some(pc.get_only_first_conn(l2r)); } } } #[derive(Clone)] struct PeerHandle(Rc>); macro_rules! getpeer { ($state:ident -> $p:ident) => { let $p: &mut Peer = match $state.poll() { Ok(Async::Ready(p)) => p, Ok(Async::NotReady) => return wouldblock(), Err(e) => { return Err(simple_err(format!("{}", e))); } }; }; } impl State { fn reconnect(&mut self) { info!("Reconnect"); self.p = None; } } macro_rules! main_loop { ($state:ident, $p:ident,bytes $e:expr) => { main_loop!(qqq $state, $p, do_reconnect, { match $e { Ok(0) => { do_reconnect = true; } Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Err(e); } warn!("{}", e); do_reconnect = true; } Ok(x) => return Ok(x), } }); }; ($state:ident, $p:ident,none $e:expr) => { main_loop!(qqq $state, $p, do_reconnect, { match $e { Err(e) => { if e.kind() == ::std::io::ErrorKind::WouldBlock { return Err(e); } warn!("{}", e); do_reconnect = true; } Ok(()) => return Ok(()), } }); }; (qqq $state:ident, $p:ident, $do_reconnect:ident, $the_match:expr) => { let mut $do_reconnect = false; loop { if $do_reconnect { $state.reconnect(); $do_reconnect = false; } else { getpeer!($state -> $p); $the_match } } }; } impl Read for PeerHandle { fn read(&mut self, b: &mut [u8]) -> Result { let mut state = self.0.borrow_mut(); main_loop!(state, p, bytes p.0.read(b)); } } impl AsyncRead for PeerHandle {} impl Write for PeerHandle { fn write(&mut self, b: &[u8]) -> Result { let mut state = self.0.borrow_mut(); state.pegged_until_write = false; main_loop!(state, p, bytes p.1.write(b)); } fn flush(&mut self) -> Result<(), IoError> { let mut state = self.0.borrow_mut(); main_loop!(state, p, none p.1.flush()); } } impl AsyncWrite for PeerHandle { fn shutdown(&mut self) -> futures::Poll<(), IoError> { let mut state = self.0.borrow_mut(); state.p = None; Ok(Async::Ready(())) } } pub fn autoreconnector(s: Rc, cp: ConstructParams) -> BoxedNewPeerFuture { let reconnect_delay = std::time::Duration::from_millis(cp.program_options.autoreconnect_delay_millis); let s = Rc::new(RefCell::new(State { cp, s, p: None, n: None, aux: Default::default(), reconnect_delay, ratelimiter: None, reconnect_count_limit: None, pegged_until_write: false, })); let ph1 = PeerHandle(s.clone()); let ph2 = PeerHandle(s); let peer = Peer::new(ph1, ph2, None /* we handle hups ourselves */); Box::new(ok(peer)) as BoxedNewPeerFuture } pub fn waitfordata(s: Rc, cp: ConstructParams) -> BoxedNewPeerFuture { let reconnect_delay = std::time::Duration::from_millis(cp.program_options.autoreconnect_delay_millis); let s = Rc::new(RefCell::new(State { cp, s, p: None, n: None, aux: Default::default(), reconnect_delay, // unused ratelimiter: None, reconnect_count_limit: Some(1), pegged_until_write: true, })); let ph1 = PeerHandle(s.clone()); let ph2 = PeerHandle(s); let peer = Peer::new(ph1, ph2, None /* we handle hups ourselves, though shouldn't probably */); Box::new(ok(peer)) as BoxedNewPeerFuture } #[derive(Debug)] pub struct WaitForData(pub Rc); impl Specifier for WaitForData { fn construct(&self, cp: ConstructParams) -> PeerConstructor { once(waitfordata(self.0.clone(), cp)) } specifier_boilerplate!(singleconnect has_subspec globalstate); self_0_is_subspecifier!(...); } specifier_class!( name = WaitForDataClass, target = WaitForData, prefixes = ["waitfordata:", "wait-for-data:"], arg_handling = subspec, overlay = true, MessageBoundaryStatusDependsOnInnerType, SingleConnect, help = r#" Wait for some data to pending being written before starting connecting. [A] Example: Connect to the TCP server on the left side immediately, but connect to the TCP server on the right side only after some data gets written by the first connection websocat -b tcp:127.0.0.1:1234 waitfordata:tcp:127.0.0.1:1235 Example: Connect to first WebSocket server, wait for some incoming WebSocket message, then connect to the second WebSocket server and start exchanging text and binary WebSocket messages between them. websocat -b --binary-prefix=b --text-prefix=t ws://127.0.0.1:1234 waitfordata:ws://127.0.0.1:1235/ "# ); ================================================ FILE: src/sessionserve.rs ================================================ use super::futures::{Future, Stream}; use super::{ futures, my_copy, ConstructParams, L2rUser, L2rWriter, Options, Peer, PeerConstructor, ProgramState, Session, Specifier, Transfer, }; use crate::spawn_hack; use std; use std::cell::RefCell; use std::rc::Rc; use tokio_io; impl Session { pub fn run(self) -> Box>> { let once = self.opts.one_message; let mut co1 = my_copy::CopyOptions { stop_on_reader_zero_read: !self.opts.no_exit_on_zeromsg, once, buffer_size: self.opts.buffer_size, skip: false, max_ops: self.opts.max_messages, }; let mut co2 = co1; co2.max_ops = self.opts.max_messages_rev; if self.opts.unidirectional { co2.skip=true; } if self.opts.unidirectional_reverse { co1.skip=true; } let f1 = my_copy::copy(self.t1.from, self.t1.to, co1, self.opts.preamble.clone()); let f2 = my_copy::copy(self.t2.from, self.t2.to, co2, self.opts.preamble_reverse.clone()); let f1 = f1.and_then(|(_, r, w)| { info!("Forward finished"); std::mem::drop(r); tokio_io::io::shutdown(w).map(|w| { debug!("Forward shutdown finished"); std::mem::drop(w); }) }); let f2 = f2.and_then(|(_, r, w)| { info!("Reverse finished"); std::mem::drop(r); tokio_io::io::shutdown(w).map(|w| { debug!("Reverse shutdown finished"); std::mem::drop(w); }) }); type Ret = Box>>; let tmp = if !self.opts.exit_on_eof { Box::new( f1.join(f2) .map(|(_, _)| { info!("Both directions finished"); }) .map_err(|x| Box::new(x) as Box), ) as Ret } else { Box::new( f1.select(f2) .map(|(_, _)| { info!("One of directions finished"); }) .map_err(|(x, _)| Box::new(x) as Box), ) as Ret }; // tmp is now everything except of HUP handling if self.hup1.is_none() && self.hup2.is_none() { tmp // no need for complications } else { let mut s = futures::stream::futures_unordered::FuturesUnordered::new(); s.push(tmp); if let Some(hup) = self.hup1 { s.push(hup); } if let Some(hup) = self.hup2 { s.push(hup); } Box::new( s.into_future() .map(|(x, _)|x.unwrap()) .map_err(|(e,_)|e) ) as Ret } } pub fn new(peer1: Peer, peer2: Peer, opts: Rc) -> Self { Session{ t1: Transfer { from: peer1.0, to: peer2.1, }, t2: Transfer { from: peer2.0, to: peer1.1, }, opts, hup1: peer1.2, hup2: peer2.2, } } } fn l2r_new() -> L2rWriter { Rc::new(RefCell::new(Default::default())) } pub fn serve( s1: Rc, s2: Rc, opts: Options, onerror: std::rc::Rc, ) -> impl Future where OE: Fn(Box) + 'static, { futures::future::ok(()).and_then(|()| serve_impl(s1, s2, opts, onerror)) } #[allow(clippy::needless_pass_by_value)] fn serve_impl( s1: Rc, s2: Rc, opts: Options, onerror: std::rc::Rc, ) -> Box> where OE: Fn(Box) + 'static, { debug!("Serving {:?} to {:?} with {:?}", s1, s2, opts); let ps = Rc::new(RefCell::new(ProgramState::default())); use crate::PeerConstructor::{Overlay1, OverlayM, ServeMultipleTimes, ServeOnce}; let e1 = onerror.clone(); let e2 = onerror.clone(); let e3 = onerror.clone(); let opts1 = Rc::new(opts); let opts2 = opts1.clone(); let l2r = l2r_new(); let cp = Rc::new(RefCell::new(ConstructParams { program_options: opts1.clone(), global_state: ps.clone(), left_to_right: L2rUser::FillIn(l2r.clone()), })); #[cfg(feature = "prometheus_peer")] { if let Some(psa) = opts1.prometheus { let _ /*: crate::prometheus_peer::GlobalState*/ = cp.as_ref().borrow().global(crate::prometheus_peer::new_global_stats); if let Err(e) = crate::prometheus_peer::serve(psa) { error!("Error listening Prometheus exposer socket: {}", e); } } } let mut left = s1.construct(cp.borrow().clone()); if opts2.oneshot { left = PeerConstructor::ServeOnce(left.get_only_first_conn(cp.borrow().left_to_right.clone())); } let max_parallel_conns = opts1.max_parallel_conns; let current_parallel_conns = Rc::new(::std::cell::Cell::new(0usize)); match left { PeerConstructor::Error(e) => { e1(e); Box::new(futures::future::ok(())) as Box> }, ServeMultipleTimes(stream) => { let runner = stream .map(move |peer1| { let mut cpc = current_parallel_conns.get(); let cpc2 = current_parallel_conns.clone(); cpc += 1; if let Some(cap) = max_parallel_conns { if cpc > cap { warn!("Dropping connection because of connection cap"); return; } } info!("Serving {} ongoing connections", cpc); current_parallel_conns.set(cpc); let opts3 = opts2.clone(); let e1_1 = e1.clone(); let cp2 = cp.borrow().reply(); cp.borrow_mut().reset_l2r(); let l2rc = cp2.left_to_right.clone(); spawn_hack( s2.construct(cp2) .get_only_first_conn(l2rc) .and_then(move |peer2| { let s = Session::new(peer1, peer2, opts3); s.run() }) .map_err(move |e| e1_1(e)) .then(move |r| { cpc2.set(cpc2.get() - 1); futures::future::result(r) }), ) }) .for_each(|()| futures::future::ok(())); Box::new(runner.map_err(move |e| e2(e))) as Box> } OverlayM(stream, mapper) => { let runner = stream .map(move |peer1_| { debug!("Underlying connection established"); let mut cpc = current_parallel_conns.get(); let cpc2 = current_parallel_conns.clone(); cpc += 1; if let Some(cap) = max_parallel_conns { if cpc > cap { warn!("Dropping connection because of connection cap"); return; } } info!("Serving {} ongoing connections", cpc); current_parallel_conns.set(cpc); let cp_ = cp.borrow().deep_clone(); cp.borrow_mut().reset_l2r(); let opts3 = opts2.clone(); let e1_1 = e1.clone(); let s2 = s2.clone(); let l2rc = cp_.left_to_right.clone(); spawn_hack( mapper(peer1_, l2rc) .and_then(move |peer1| { let cp2 = cp_.reply(); let l2rc = cp2.left_to_right.clone(); s2.construct(cp2) .get_only_first_conn(l2rc) .and_then(move |peer2| { let s = Session::new(peer1, peer2, opts3); s.run() }) }) .map_err(move |e| e1_1(e)) .then(move |r| { cpc2.set(cpc2.get() - 1); futures::future::result(r) }), ) }) .for_each(|()| futures::future::ok(())); Box::new(runner.map_err(move |e| e2(e))) as Box> } ServeOnce(peer1c) => { let runner = peer1c.and_then(move |peer1| { let cp2 = cp.borrow().reply(); let l2rc = cp2.left_to_right.clone(); let right = s2.construct(cp2); let fut = right.get_only_first_conn(l2rc); fut.and_then(move |peer2| { let s = Session::new(peer1, peer2, opts2); s.run().map(|()| { ::std::mem::drop(ps) // otherwise ps will be dropped sooner // and stdin/stdout may become blocking sooner }) }) }); Box::new(runner.map_err(move |e| e3(e))) as Box> } Overlay1(peer1c, mapper) => { let runner = peer1c.and_then(move |peer1_| { let l2rc = cp.borrow().left_to_right.clone(); debug!("Underlying connection established"); mapper(peer1_, l2rc).and_then(move |peer1| { let cp2 = cp.borrow().reply(); let l2rc = cp2.left_to_right.clone(); let right = s2.construct(cp2); let fut = right.get_only_first_conn(l2rc); fut.and_then(move |peer2| { let s = Session::new(peer1, peer2, opts2); s.run().map(|()| { ::std::mem::drop(ps) // otherwise ps will be dropped sooner // and stdin/stdout may become blocking sooner }) }) }) }); Box::new(runner.map_err(move |e| e3(e))) as Box> } } } ================================================ FILE: src/socks5_peer.rs ================================================ #![allow(clippy::needless_pass_by_value,clippy::cast_lossless,clippy::identity_op)] use futures::future::{err, ok, Future}; use std::rc::Rc; use super::{box_up_err, peer_strerr, BoxedNewPeerFuture, Peer}; use super::{ConstructParams, L2rUser, PeerConstructor, Specifier}; use tokio_io::io::{read_exact, write_all}; use std::io::Write; use std::net::{IpAddr, Ipv4Addr}; use tokio_io::{AsyncRead, AsyncWrite}; use std::ffi::OsString; #[derive(Debug, Clone)] pub enum SocksHostAddr { Ip(IpAddr), Name(String), } #[derive(Debug, Clone)] pub struct SocksSocketAddr { pub host: SocksHostAddr, pub port: u16, } #[derive(Debug)] pub struct SocksProxy(pub T); impl Specifier for SocksProxy { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, l2r| { socks5_peer(p, l2r, false, None, &cp.program_options.socks_destination, cp.program_options.socks5_user_pass.clone(), false) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = SocksProxyClass, target = SocksProxy, prefixes = ["socks5-connect:"], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" SOCKS5 proxy client (raw) [A] Example: connect to a websocket using local `ssh -D` proxy websocat -t - ws-c:socks5-connect:tcp:127.0.0.1:1080 --socks5-destination echo.websocket.org:80 --ws-c-uri ws://echo.websocket.org For a user-friendly solution, see --socks5 command-line option "# ); #[derive(Debug)] pub struct SocksBind(pub T); impl Specifier for SocksBind { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, l2r| { socks5_peer( p, l2r, true, cp.program_options.socks5_bind_script.clone(), &cp.program_options.socks_destination, cp.program_options.socks5_user_pass.clone(), cp.program_options.announce_listens, ) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = SocksBindClass, target = SocksBind, prefixes = ["socks5-bind:"], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" SOCKS5 proxy client (raw, bind command) [A] Example: bind to a websocket using some remote SOCKS server websocat -v -t ws-u:socks5-bind:tcp:132.148.129.183:14124 - --socks5-destination 255.255.255.255:65535 Note that port is typically unpredictable. Use --socks5-bind-script option to know the port. See an example in moreexamples.md for more thorough example. "# ); type RSRRet = Box>>; fn read_socks_reply(p: Peer) -> RSRRet { let (r, w, hup) = (p.0, p.1, p.2); let reply = [0; 4]; fn myerr(x: &'static str) -> RSRRet { Box::new(err(x.to_string().into())) } Box::new( read_exact(r, reply) .map_err(box_up_err) .and_then(move |(r, reply)| { if reply[0] != b'\x05' { return myerr("Not a SOCKS5 reply 2"); } if reply[1] != b'\x00' { let msg = match reply[1] { 1 => "SOCKS: General SOCKS server failure", 2 => "SOCKS connection not allowed", 3 => "SOCKS: network unreachable", 4 => "SOCKS: host unreachable", 5 => "SOCKS: connection refused", 6 => "SOCKS: TTL expired", 7 => "SOCKS: Command not supported", 8 => "SOCKS: Address type not supported", _ => "SOCKS: Unknown failure", }; return myerr(msg); } let ret: RSRRet = match reply[3] { b'\x01' => { // ipv4 let addrport = [0; 4 + 2]; Box::new(read_exact(r, addrport).map_err(box_up_err).and_then( move |(r, addrport)| { let port = (addrport[4] as u16) * 256 + (addrport[5] as u16); let ip = Ipv4Addr::new( addrport[0], addrport[1], addrport[2], addrport[3], ); let host = SocksHostAddr::Ip(IpAddr::V4(ip)); ok((SocksSocketAddr { host, port }, Peer(r, w, hup))) }, )) } b'\x04' => { // ipv6 let addrport = [0; 16 + 2]; Box::new(read_exact(r, addrport).map_err(box_up_err).and_then( move |(r, addrport)| { let port = (addrport[16] as u16) * 256 + (addrport[17] as u16); // still not worth to switch to Cargo.toml to add // "bytes" dependency, then scroll up for "extern crate", // then look up docs again to find out where to get that BE thing. let mut ip = [0u8; 16]; ip.copy_from_slice(&addrport[0..16]); let host = SocksHostAddr::Ip(IpAddr::V6(ip.into())); ok((SocksSocketAddr { host, port }, Peer(r, w, hup))) }, )) } b'\x03' => { let alen = [0; 1]; Box::new(read_exact(r, alen).map_err(box_up_err).and_then( move |(r, alen)| { let alen = alen[0] as usize; let addrport = vec![0; alen + 2]; read_exact(r, addrport).map_err(box_up_err).and_then( move |(r, addrport)| { let port = (addrport[alen] as u16) * 256 + (addrport[alen + 1] as u16); let host = SocksHostAddr::Name( ::std::str::from_utf8(&addrport[0..alen]) .unwrap_or("(invalid hostname)") .to_string(), ); ok((SocksSocketAddr { host, port }, Peer(r, w, hup))) }, ) }, )) } _ => { return myerr("SOCKS: bound address type is unknown"); } }; ret }), ) } pub fn socks5_peer( inner_peer: Peer, _l2r: L2rUser, do_bind: bool, bind_script: Option, socks_destination: &Option, socks5_user_pass: Option, announce_listen: bool, ) -> BoxedNewPeerFuture { let (desthost, destport) = if let Some(ref sd) = *socks_destination { (sd.host.clone(), sd.port) } else { return peer_strerr( "--socks5-destination is required for socks5-connect: or socks5-bind: overlays", ); }; if let SocksHostAddr::Name(ref n) = desthost { if n.len() > 255 { return peer_strerr("Destination host name too long for SOCKS5"); } }; info!("Connecting to SOCKS server"); let (r, w, hup) = (inner_peer.0, inner_peer.1, inner_peer.2); let f = write_all(w, b"\x05\x02\x00\x02") .map_err(box_up_err) .and_then(move |(w, _)| { read_exact(r, [0; 2]) .map_err(box_up_err) .and_then(move |(r, reply)| { if reply[0] != 0x05 { return peer_strerr("Not a SOCKS5 reply"); } let auth_future: BoxedNewPeerFuture = match reply[1] { 0x00 => { info!("SOCKS5: Auth method 0, no auth"); authenticate_no_auth(r, w) } 0x02 => { authenticate_username_password(r, w, &socks5_user_pass) } _ => { return peer_strerr("SOCKS5: Unknown authentication method"); } }; Box::new(auth_future.and_then(move |Peer(r, w, _)| { let request = build_socks5_request(do_bind, &desthost, destport).unwrap(); Box::new( write_all(w, request) .map_err(box_up_err) .and_then(move |(w, _)| { read_socks_reply(Peer(r, w, hup)).and_then(move |(addr, Peer(r, w, hup))| { info!("SOCKS5 connect/bind: {:?}", addr); if do_bind { if announce_listen { println!("LISTEN proto=tcp,port={}", addr.port); } if let Some(bs) = bind_script { let _ = ::std::process::Command::new(bs) .arg(format!("{}", addr.port)) .spawn(); } Box::new(read_socks_reply(Peer(r, w, hup)).and_then(move |(addr, Peer(r, w, hup))| { info!("SOCKS5 remote connected: {:?}", addr); Box::new(ok(Peer(r, w, hup))) })) as BoxedNewPeerFuture } else { Box::new(ok(Peer(r, w, hup))) as BoxedNewPeerFuture } }) }), ) })) }) }); Box::new(f) as BoxedNewPeerFuture } fn authenticate_no_auth( r: Box, w: Box ) -> BoxedNewPeerFuture { Box::new(ok(Peer(r, w, None))) } fn authenticate_username_password( r: Box, w: Box, socks5_user_pass: &Option, ) -> BoxedNewPeerFuture { info!("SOCKS5: Auth method 2, sending username/password"); let (user, pass) = match socks5_user_pass { Some(ref user_pass) => { let parts: Vec<&str> = user_pass.splitn(2, ':').collect(); if parts.len() != 2 { return peer_strerr("SOCKS5: Invalid username:password format"); } (parts[0].as_bytes(), parts[1].as_bytes()) }, None => return peer_strerr("SOCKS5: Username and password required but not provided"), }; let mut buffer = Vec::with_capacity(1 + 2 + 1 + user.len() + 1 + pass.len()); buffer.write_all(&[0x01]).unwrap(); // Version buffer.write_all(&[user.len() as u8]).unwrap(); buffer.write_all(user).unwrap(); buffer.write_all(&[pass.len() as u8]).unwrap(); buffer.write_all(pass).unwrap(); Box::new( write_all(w, buffer) .map_err(box_up_err) .and_then(move |(w, _)| { read_exact(r, [0; 2]) .map_err(box_up_err) .and_then(move |(r, reply)| { if reply[0] != 0x01 || reply[1] != 0x00 { return peer_strerr("SOCKS5: Authentication failed"); } Box::new(ok(Peer( Box::new(r) as Box, Box::new(w) as Box, None, ))) }) }), ) } fn build_socks5_request( do_bind: bool, desthost: &SocksHostAddr, destport: u16, ) -> Result, Box> { let mut request = Vec::with_capacity(20); if do_bind { request.write_all(&[0x05, 0x02, 0x00])?; // SOCKS5, CMD BIND } else { request.write_all(&[0x05, 0x01, 0x00])?; // SOCKS5, CMD CONNECT } match desthost { SocksHostAddr::Ip(IpAddr::V4(ip4)) => { request.write_all(&[0x01])?; // ATYP IPv4 request.write_all(&ip4.octets())?; } SocksHostAddr::Ip(IpAddr::V6(ip6)) => { request.write_all(&[0x04])?; // ATYP IPv6 request.write_all(&ip6.octets())?; } SocksHostAddr::Name(name) => { request.write_all(&[0x03])?; // ATYP DOMAINNAME request.write_all(&[name.len() as u8])?; request.write_all(name.as_bytes())?; } } request.write_all(&[(destport >> 8) as u8, (destport & 0xFF) as u8])?; Ok(request) } ================================================ FILE: src/specifier.rs ================================================ use super::{L2rUser, Options, Result}; use super::{PeerConstructor, ProgramState}; use std; use std::cell::RefCell; use std::rc::Rc; pub enum ClassMessageBoundaryStatus { StreamOriented, MessageOriented, MessageBoundaryStatusDependsOnInnerType, } pub enum ClassMulticonnectStatus { MultiConnect, SingleConnect, MulticonnectnessDependsOnInnerType, } /// A trait for a each specified type's accompanying object /// /// Don't forget to register each instance at the `list_of_all_specifier_classes` macro. pub trait SpecifierClass: std::fmt::Debug { /// The primary name of the class fn get_name(&self) -> &'static str; /// Names to match command line parameters against, with a `:` colon if needed fn get_prefixes(&self) -> Vec<&'static str>; /// --long-help snippet about this specifier fn help(&self) -> &'static str; /// Given the command line text, construct the specifier /// arg is what comes after the colon (e.g. `//echo.websocket.org` in `ws://echo.websocket.org`) fn construct(&self, arg: &str) -> Result>; /// Given the inner specifier, construct this specifier. fn construct_overlay(&self, inner: Rc) -> Result>; /// Returns if this specifier is an overlay fn is_overlay(&self) -> bool; /// True if it is not expected to preserve message boundaries on reads fn message_boundary_status(&self) -> ClassMessageBoundaryStatus; fn multiconnect_status(&self) -> ClassMulticonnectStatus; /// If it is Some then is_overlay, construct and most other things are ignored and prefix get replaced... fn alias_info(&self) -> Option<&'static str>; } macro_rules! specifier_alias { (name=$n:ident, prefixes=[$($p:expr),*], alias=$x:expr, help=$h:expr) => { #[derive(Debug,Default)] pub struct $n; impl $crate::SpecifierClass for $n { fn get_name(&self) -> &'static str { stringify!($n) } fn get_prefixes(&self) -> Vec<&'static str> { vec![$($p),*] } fn help(&self) -> &'static str { $h } fn message_boundary_status(&self) -> $crate::ClassMessageBoundaryStatus { panic!("Error: message_boundary_status called on alias class") } fn multiconnect_status(&self) -> $crate::ClassMulticonnectStatus { panic!("Error: multiconnect_status called on alias class") } fn is_overlay(&self) -> bool { false } fn construct(&self, _arg:&str) -> $crate::Result> { panic!("Error: construct called on alias class") } fn construct_overlay(&self, _inner : Rc) -> $crate::Result> { panic!("Error: construct_overlay called on alias class") } fn alias_info(&self) -> Option<&'static str> { Some($x) } } }; } macro_rules! specifier_class { (name=$n:ident, target=$t:ident, prefixes=[$($p:expr),*], arg_handling=$c:tt, overlay=$o:expr, $so:expr, $ms:expr, help=$h:expr) => { #[derive(Debug,Default)] pub struct $n; impl $crate::SpecifierClass for $n { fn get_name(&self) -> &'static str { stringify!($n) } fn get_prefixes(&self) -> Vec<&'static str> { vec![$($p),*] } fn help(&self) -> &'static str { $h } fn message_boundary_status(&self) -> $crate::ClassMessageBoundaryStatus { use $crate::ClassMessageBoundaryStatus::*; $so } fn multiconnect_status(&self) -> $crate::ClassMulticonnectStatus { use $crate::ClassMulticonnectStatus::*; $ms } fn is_overlay(&self) -> bool { $o } specifier_class!(construct target=$t $c); } }; (construct target=$t:ident noarg) => { fn construct(&self, just_arg:&str) -> $crate::Result> { if just_arg != "" { Err(format!("{}-specifer requires no parameters. `{}` is not needed", self.get_name(), just_arg))?; } Ok(Rc::new($t)) } fn construct_overlay(&self, _inner : Rc) -> $crate::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } fn alias_info(&self) -> Option<&'static str> { None } }; (construct target=$t:ident into) => { fn construct(&self, just_arg:&str) -> $crate::Result> { Ok(Rc::new($t(just_arg.into()))) } fn construct_overlay(&self, _inner : Rc) -> $crate::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } fn alias_info(&self) -> Option<&'static str> { None } }; (construct target=$t:ident parse) => { fn construct(&self, just_arg:&str) -> $crate::Result> { Ok(Rc::new($t(just_arg.parse()?))) } fn construct_overlay(&self, _inner : Rc) -> $crate::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } fn alias_info(&self) -> Option<&'static str> { None } }; (construct target=$t:ident parseresolve) => { fn construct(&self, just_arg:&str) -> $crate::Result> { use std::net::ToSocketAddrs; info!("Resolving hostname to IP addresses"); let addrs : Vec = just_arg.to_socket_addrs()?.collect(); if addrs.is_empty() { Err("Failed to resolve this hostname to IP")?; } for addr in &addrs { info!("Got IP: {}", addr); } Ok(Rc::new($t(addrs))) } fn construct_overlay(&self, _inner : Rc) -> $crate::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } fn alias_info(&self) -> Option<&'static str> { None } }; (construct target=$t:ident subspec) => { fn construct(&self, just_arg:&str) -> $crate::Result> { Ok(Rc::new($t($crate::spec(just_arg)?))) } fn construct_overlay(&self, _inner : Rc) -> $crate::Result> { Ok(Rc::new($t(_inner))) } fn alias_info(&self) -> Option<&'static str> { None } }; (construct target=$t:ident {$($x:tt)*}) => { $($x)* fn alias_info(&self) -> Option<&'static str> { None } }; } #[derive(Debug)] pub struct SpecifierNode { pub cls: Rc, //pub opt: Rc, } #[derive(Debug)] pub struct SpecifierStack { pub addr: String, pub addrtype: SpecifierNode, pub overlays: Vec, } #[derive(Clone)] pub struct ConstructParams { pub global_state: Rc>, pub program_options: Rc, pub left_to_right: L2rUser, } /// All of those methods are about left_to_right mechanism impl ConstructParams { /// Reset left_to_right to default value. pub fn reset_l2r(&mut self) { match self.left_to_right { L2rUser::FillIn(ref mut x) => { *x.borrow_mut() = Default::default(); //*x = Rc::new(RefCell::new(Default::default())); } L2rUser::ReadFrom(_) => panic!("ConstructParams::reset_l2r called wrong"), } } /// Clones ConstructParams, changing FillIn to ReadFrom in left_to_right field /// and also disassociating it from the original RefCell. /// /// Panics when called on object with left_to_right set to ReadFrom. pub fn reply(&self) -> Self { let l2r = match self.left_to_right { L2rUser::FillIn(ref x) => Rc::new(x.borrow().clone()), L2rUser::ReadFrom(_) => panic!("ConstructParams::reply called wrong"), }; ConstructParams { global_state: self.global_state.clone(), program_options: self.program_options.clone(), left_to_right: L2rUser::ReadFrom(l2r), } } pub fn deep_clone(&self) -> Self { let l2r = match self.left_to_right { L2rUser::FillIn(ref x) => L2rUser::FillIn(Rc::new(RefCell::new(x.borrow().clone()))), L2rUser::ReadFrom(_) => { panic!( "You are not supposed to use ConstructParams::deep_clone on ReadFrom things" ); } }; ConstructParams { global_state: self.global_state.clone(), program_options: self.program_options.clone(), left_to_right: l2r, } } /// Access specified-specific global (singleton) data pub fn global(&self, def:F) -> std::cell::RefMut where F : FnOnce()->T { std::cell::RefMut::map( self.global_state.borrow_mut(), |x|{ x.0.entry::().or_insert_with(def) } ) } } /// A parsed command line argument. /// For example, `ws-listen:tcp-l:127.0.0.1:8080` gets parsed into /// a `WsUpgrade(TcpListen(SocketAddr))`. pub trait Specifier: std::fmt::Debug { /// Apply the specifier for constructing a "socket" or other connecting device. fn construct(&self, p: ConstructParams) -> PeerConstructor; // Specified by `specifier_boilerplate!`: fn is_multiconnect(&self) -> bool; fn uses_global_state(&self) -> bool; } impl Specifier for Rc { fn construct(&self, p: ConstructParams) -> PeerConstructor { (**self).construct(p) } fn is_multiconnect(&self) -> bool { (**self).is_multiconnect() } fn uses_global_state(&self) -> bool { (**self).uses_global_state() } } macro_rules! specifier_boilerplate { (singleconnect $($e:tt)*) => { fn is_multiconnect(&self) -> bool { false } specifier_boilerplate!($($e)*); }; (multiconnect $($e:tt)*) => { fn is_multiconnect(&self) -> bool { true } specifier_boilerplate!($($e)*); }; (no_subspec $($e:tt)*) => { specifier_boilerplate!($($e)*); }; (has_subspec $($e:tt)*) => { specifier_boilerplate!($($e)*); }; () => { }; (globalstate $($e:tt)*) => { fn uses_global_state(&self) -> bool { true } specifier_boilerplate!($($e)*); }; (noglobalstate $($e:tt)*) => { fn uses_global_state(&self) -> bool { false } specifier_boilerplate!($($e)*); }; } macro_rules! self_0_is_subspecifier { (...) => { // removed with old linter }; (proxy_is_multiconnect) => { self_0_is_subspecifier!(...); fn is_multiconnect(&self) -> bool { self.0.is_multiconnect() } }; } ================================================ FILE: src/specparse.rs ================================================ use super::{Result}; use super::specifier::{Specifier, SpecifierClass, SpecifierStack, SpecifierNode}; use std::rc::Rc; use std::str::FromStr; pub fn spec(s: &str) -> Result> { ::from_stack(&SpecifierStack::from_str(s)?) } fn some_checks(s: &str) -> Result<()> { #[cfg(not(feature = "ssl"))] { if s.starts_with("wss://") { Err("SSL is not compiled in. Use ws:// or get/make another Websocat build.\nYou can also try to workaround missing SSL by using ws-c:cmd:socat trick (see some ws-c: example)")? } } #[cfg(not(any(target_os = "linux", target_os = "android")))] { if s.starts_with("abstract") { warn!("Abstract-namespaced UNIX sockets are unlikely to be supported here"); } } if s.starts_with("open:") { return Err("There is no `open:` address type. Consider `open-async:` or `readfile:` or `writefile:` or `appendfile:`".into()); } #[cfg(not(unix))] { if s.starts_with("unix") || s.starts_with("abstract") { Err("`unix*:` or `abstract*:` are not supported in this Websocat build")? } } #[cfg(not(feature = "tokio-process"))] { if s.starts_with("sh-c:") { Err("`sh-c:` is not supported in this Websocat build")? } else if s.starts_with("exec:") { Err("`exec:` is not supported in this Websocat build")? } } #[cfg(not(feature = "crypto_peer"))] { if s.starts_with("crypto:") { Err("`crypto:` support is not compiled in")? } } #[cfg(not(feature = "prometheus_peer"))] { if s.starts_with("metrics:") || s.starts_with("prometheus:") { Err("`prometheus:` support is not compiled in")? } } Ok(()) } impl FromStr for SpecifierStack { type Err = Box; #[allow(clippy::cyclomatic_complexity)] fn from_str(s: &str) -> Result { some_checks(s)?; let mut s = s.to_string(); let mut overlays = vec![]; let addrtype; let addr; let mut found = false; 'a: loop { macro_rules! my { ($x:expr) => { for pre in $x.get_prefixes() { if s.starts_with(pre) { let rest = &s[pre.len()..].to_string(); if let Some(a) = $x.alias_info() { s = format!("{}{}", a, rest); continue 'a; } else if $x.is_overlay() { let cls = Rc::new($x) as Rc; overlays.push(SpecifierNode{cls}); s = rest.to_string(); continue 'a; } else { addr = rest.to_string(); let cls = Rc::new($x) as Rc; addrtype = SpecifierNode{cls}; #[allow(unused_assignments)] { found = true; } break 'a; } } } }; } list_of_all_specifier_classes!(my); if !found { if let Some(colon) = s.find(':') { Err(format!( "Unknown address or overlay type of `{}:`", &s[..colon] ))?; } else { Err(format!("Unknown address or overlay type of `{}`\nMaybe you forgot the `:` character?", s))?; } } } Ok(SpecifierStack { addr, addrtype, overlays, }) } } impl dyn Specifier { pub fn from_stack(st: &SpecifierStack) -> Result> { let mut x = st.addrtype.cls.construct(st.addr.as_str())?; for overlay in st.overlays.iter().rev() { x = overlay.cls.construct_overlay(x)?; } Ok(x) } } ================================================ FILE: src/ssl_peer.rs ================================================ use futures::future::{ok, Future}; use std::rc::Rc; use super::{box_up_err, peer_err, BoxedNewPeerFuture, Peer}; use super::{ConstructParams, L2rUser, Options, PeerConstructor, Specifier}; pub extern crate native_tls; extern crate readwrite; extern crate tokio_tls; use self::native_tls::{Identity as Pkcs12, TlsAcceptor, TlsConnector}; use self::tokio_tls::{TlsAcceptor as TlsAcceptorExt, TlsConnector as TlsConnectorExt}; use std::ffi::{OsStr, OsString}; pub fn interpret_pkcs12(x: &OsStr) -> ::std::result::Result, OsString> { match (|| { use std::io::Read; let mut f = ::std::fs::File::open(x)?; let mut v = Vec::with_capacity(2048); f.read_to_end(&mut v)?; Ok(v) })() { Err(e) => { let e: Box = e; let o: OsString = format!("{}", e).into(); Err(o) } Ok(x) => Ok(x), } } #[derive(Debug)] pub struct TlsConnect(pub T); impl Specifier for TlsConnect { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, l2r| { ssl_connect( p, l2r, cp.program_options.tls_domain.clone(), cp.program_options.tls_insecure, cp.program_options.client_pkcs12_der.clone(), cp.program_options.client_pkcs12_passwd.clone(), ) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = TlsConnectClass, target = TlsConnect, prefixes = ["ssl-connect:","ssl-c:","ssl:","tls:","tls-connect:","tls-c:","c-ssl:","connect-ssl:","c-tls:","connect-tls:"], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" Overlay to add TLS encryption atop of existing connection [A] Example: manually connect to a secure websocket websocat -t - ws-c:tls-c:tcp:174.129.224.73:1080 --ws-c-uri ws://echo.websocket.org --tls-domain echo.websocket.org For a user-friendly solution, see --socks5 command-line option "# ); #[derive(Debug)] pub struct TlsAccept(pub T); impl Specifier for TlsAccept { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, l2r| ssl_accept(p, l2r, cp.program_options.clone())) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = TlsAcceptClass, target = TlsAccept, prefixes = [ "ssl-accept:", "ssl-a:", "tls-a:", "tls-accept:", "a-ssl:", "accept-ssl:", "accept-tls:", "accept-tls:" ], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" Accept an TLS connection using arbitrary backing stream. [A] Example: The same as in TlsListenClass's example, but with manual acceptor websocat -E -b --pkcs12-der=q.pkcs12 tls-a:tcp-l:127.0.0.1:1234 mirror: "# ); specifier_alias!( name = TlsListenClass, prefixes = [ "ssl-listen:", "ssl-l:", "tls-l:", "tls-listen:", "l-ssl:", "listen-ssl:", "listen-tls:", "listen-tls:" ], alias = "tls-accept:tcp-l:", help = r#" Listen for SSL connections on a TCP port Example: Non-websocket SSL echo server websocat -E -b --pkcs12-der=q.pkcs12 ssl-listen:127.0.0.1:1234 mirror: socat - ssl:127.0.0.1:1234,verify=0 "# ); specifier_alias!( name = WssListenClass, prefixes = ["wss-listen:", "wss-l:", "l-wss:", "wss-listen:"], alias = "ws-u:tls-accept:tcp-l:", help = r#" Listen for secure WebSocket connections on a TCP port Example: wss:// echo server + client for testing websocat -E -t --pkcs12-der=q.pkcs12 wss-listen:127.0.0.1:1234 mirror: websocat --ws-c-uri=wss://localhost/ -t - ws-c:cmd:'socat - ssl:127.0.0.1:1234,verify=0' See [moreexamples.md](./moreexamples.md) for info about generation of `q.pkcs12`. "# ); use tokio_io::AsyncRead; pub fn ssl_connect( inner_peer: Peer, _l2r: L2rUser, dom: Option, tls_insecure: bool, client_identity : Option>, client_identity_password : Option, ) -> BoxedNewPeerFuture { let hup = inner_peer.2; let squashed_peer = readwrite::ReadWriteAsync::new(inner_peer.0, inner_peer.1); fn gettlsc(nohost: bool, noverify: bool, client_identity : Option>, client_identity_password : Option) -> native_tls::Result { let mut b = TlsConnector::builder(); if nohost { b.danger_accept_invalid_hostnames(true); } if noverify { b.danger_accept_invalid_hostnames(true); b.danger_accept_invalid_certs(true); } if let Some(client_ident) = client_identity { let identity = super::ssl_peer::native_tls::Identity::from_pkcs12( &client_ident, &client_identity_password.unwrap_or("".to_string()), ) .map_err(|e| { error!( "Unable to parse client identity: {}\nContinuing without a client identity", e ) }) .ok(); if let Some(x) = identity { b.identity(x); } } let tlsc: TlsConnector = b.build()?; Ok(TlsConnectorExt::from(tlsc)) } let tls = match gettlsc(dom.is_none(), tls_insecure, client_identity, client_identity_password) { Ok(x) => x, Err(e) => return peer_err(e), }; info!("Connecting to TLS"); if let Some(dom) = dom { Box::new( tls.connect(dom.as_str(), squashed_peer) .map_err(box_up_err) .and_then(move |tls_stream| { info!("Connected to TLS"); let (r, w) = tls_stream.split(); ok(Peer::new(r, w, hup)) }), ) } else { Box::new(tls.connect("domainverificationdisabled", squashed_peer).map_err(box_up_err).and_then(move |tls_stream| { warn!("Connected to TLS without proper verification of certificate. Use --tls-domain option."); let (r,w) = tls_stream.split(); ok(Peer::new(r,w, hup)) })) } } pub fn ssl_accept(inner_peer: Peer, _l2r: L2rUser, progopt: Rc) -> BoxedNewPeerFuture { let hup = inner_peer.2; let squashed_peer = readwrite::ReadWriteAsync::new(inner_peer.0, inner_peer.1); fn gettlsa(cert: &[u8], passwd: &str) -> native_tls::Result { let pkcs12 = Pkcs12::from_pkcs12(cert, passwd)?; Ok(TlsAcceptorExt::from(TlsAcceptor::builder(pkcs12).build()?)) } let der = progopt .pkcs12_der .as_ref() .expect("lint should have caught the missing pkcs12_der option"); let passwd = progopt .pkcs12_passwd.as_deref() .unwrap_or(""); let tls = match gettlsa(der, passwd) { Ok(x) => x, Err(e) => return peer_err(e), }; debug!("Accepting a TLS connection"); Box::new( tls.accept(squashed_peer) .map_err(box_up_err) .and_then(move |tls_stream| { info!("Accepted TLS connection"); match tls_stream.get_ref().peer_certificate() { Ok(Some(_cert)) => { // Does not actually work with native-tls info!(" the client presented an identity certificate."); } Ok(None) => { debug!(" no identity certificate from the client. But Websocat may have failed to request it."); } Err(e) => { warn!("Error getting identity certificate from client: {}", e); } } let (r, w) = tls_stream.split(); ok(Peer::new(r, w, hup)) }), ) } ================================================ FILE: src/stdio_peer.rs ================================================ #[cfg(unix)] extern crate tokio_file_unix; extern crate tokio_reactor; #[cfg(all(unix, feature = "signal_handler"))] extern crate tokio_signal; extern crate tokio_stdin_stdout; use futures; use futures::future::Future; use std; use std::cell::RefCell; use std::io::Result as IoResult; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::rc::Rc; use tokio_io::{AsyncRead, AsyncWrite}; #[cfg(unix)] use self::tokio_file_unix::File as UnixFile; use std::fs::{File as FsFile, OpenOptions}; use super::{BoxedNewPeerFuture, Peer, Result}; use futures::Stream; use super::{once, spawn_hack, ConstructParams, PeerConstructor, Specifier}; #[derive(Clone, Debug)] pub struct AsyncStdio; impl Specifier for AsyncStdio { fn construct(&self, p: ConstructParams) -> PeerConstructor { let ret = get_stdio_peer(&mut p.global(GlobalState::default)); once(ret) } specifier_boilerplate!(globalstate singleconnect no_subspec); } specifier_class!( name = AsyncStdioClass, target = AsyncStdio, prefixes = ["asyncstdio:"], arg_handling = noarg, overlay = false, StreamOriented, SingleConnect, help = r#" [A] Set stdin and stdout to nonblocking mode, then use it as a communication counterpart. UNIX-only. May cause problems with programs running at the same terminal. This specifier backs the `--async-stdio` CLI option. Typically this specifier can be specified only one time. Example: simulate `cat(1)`. This is an exception from "only one time" rule above: websocat - - Example: SSH transport ssh -c ProxyCommand='websocat asyncstdio: ws://myserver/mywebsocket' user@myserver "# ); specifier_class!( name = InetdClass, target = AsyncStdio, prefixes = ["inetd:"], arg_handling = noarg, overlay = false, StreamOriented, SingleConnect, help = r#" Like `asyncstdio:`, but intended for inetd(8) usage. [A] Automatically enables `-q` (`--quiet`) mode. `inetd-ws:` - is of `ws-l:inetd:` Example of inetd.conf line that makes it listen for websocket connections on port 1234 and redirect the data to local SSH server. 1234 stream tcp nowait myuser /opt/websocat websocat inetd-ws: tcp:127.0.0.1:22 "# ); #[derive(Clone, Debug)] pub struct OpenAsync(pub PathBuf); impl Specifier for OpenAsync { fn construct(&self, _: ConstructParams) -> PeerConstructor { let ret = get_file_peer(&self.0); once(ret) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = OpenAsyncClass, target = OpenAsync, prefixes = ["open-async:"], arg_handling = into, overlay = false, MessageOriented, // ? SingleConnect, help = r#" Open file for read and write and use it like a socket. [A] Not for regular files, see readfile/writefile instead. Example: Serve big blobs of random data to clients websocat -U ws-l:127.0.0.1:8088 open-async:/dev/urandom "# ); #[derive(Clone, Debug)] pub struct OpenFdAsync(pub i32); impl Specifier for OpenFdAsync { fn construct(&self, _: ConstructParams) -> PeerConstructor { let ret = get_fd_peer(self.0); once(ret) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = OpenFdAsyncClass, target = OpenFdAsync, prefixes = ["open-fd:"], arg_handling = parse, overlay = false, MessageOriented, // ? SingleConnect, help = r#" Use specified file descriptor like a socket. [A] Example: Serve random data to clients v2 websocat -U ws-l:127.0.0.1:8088 reuse:open-fd:55 55< /dev/urandom "# ); fn get_stdio_peer_impl(s: &mut GlobalState) -> Result { let si; let so; { if !UnixFile::raw_new(std::io::stdin()).get_nonblocking()? { debug!("Setting stdin to nonblocking mode"); s.need_to_restore_stdin_blocking_status = true; } let stdin = self::UnixFile::new_nb(std::io::stdin())?; if !UnixFile::raw_new(std::io::stdout()).get_nonblocking()? { debug!("Setting stdout to nonblocking mode"); s.need_to_restore_stdout_blocking_status = true; } let stdout = self::UnixFile::new_nb(std::io::stdout())?; si = stdin.into_reader(&tokio_reactor::Handle::default())?; so = stdout.into_io(&tokio_reactor::Handle::default())?; let s_clone = s.clone(); #[cfg(all(unix, feature = "signal_handler"))] { debug!("Installing signal handler"); let ctrl_c = tokio_signal::ctrl_c().flatten_stream(); let prog = ctrl_c.for_each(move |()| { restore_blocking_status(&s_clone); ::std::process::exit(0); #[allow(unreachable_code)] Ok(()) }); spawn_hack(Box::new(prog.map_err(|_| ()))); } } Ok(Peer::new(si, so, None)) } pub fn get_stdio_peer(s: &mut GlobalState) -> BoxedNewPeerFuture { debug!("get_stdio_peer (async)"); Box::new(futures::future::result(get_stdio_peer_impl(s))) as BoxedNewPeerFuture } #[derive(Default, Clone)] pub struct GlobalState { need_to_restore_stdin_blocking_status: bool, need_to_restore_stdout_blocking_status: bool, } impl Drop for GlobalState { fn drop(&mut self) { restore_blocking_status(self); } } fn restore_blocking_status(s: &GlobalState) { { debug!("restore_blocking_status"); if s.need_to_restore_stdin_blocking_status { debug!("Restoring blocking status for stdin"); let _ = UnixFile::raw_new(std::io::stdin()).set_nonblocking(false); } if s.need_to_restore_stdout_blocking_status { debug!("Restoring blocking status for stdout"); let _ = UnixFile::raw_new(std::io::stdout()).set_nonblocking(false); } } } type ImplPollEvented = ::tokio_reactor::PollEvented>; #[derive(Clone)] struct FileWrapper(Rc>); impl AsyncRead for FileWrapper {} impl Read for FileWrapper { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { self.0.borrow_mut().read(buf) } } impl AsyncWrite for FileWrapper { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self.0.borrow_mut().shutdown() } } impl Write for FileWrapper { fn write(&mut self, buf: &[u8]) -> IoResult { self.0.borrow_mut().write(buf) } fn flush(&mut self) -> IoResult<()> { self.0.borrow_mut().flush() } } fn get_file_peer_impl(p: &Path) -> Result { let oo = OpenOptions::new() .read(true) .write(true) .create(false) .open(p)?; let f = self::UnixFile::new_nb(oo)?; let s = f.into_io(&tokio_reactor::Handle::default())?; let ss = FileWrapper(Rc::new(RefCell::new(s))); Ok(Peer::new(ss.clone(), ss, None)) } pub fn get_file_peer(p: &Path) -> BoxedNewPeerFuture { debug!("get_file_peer"); Box::new(futures::future::result(get_file_peer_impl(p))) as BoxedNewPeerFuture } fn get_fd_peer_impl(fd: i32) -> Result { let ff: FsFile = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) }; let f = self::UnixFile::new_nb(ff)?; let s = f.into_io(&tokio_reactor::Handle::default())?; let ss = FileWrapper(Rc::new(RefCell::new(s))); Ok(Peer::new(ss.clone(), ss, None)) } pub fn get_fd_peer(fd: i32) -> BoxedNewPeerFuture { debug!("get_fd_peer"); Box::new(futures::future::result(get_fd_peer_impl(fd))) as BoxedNewPeerFuture } ================================================ FILE: src/stdio_threaded_peer.rs ================================================ extern crate tokio_stdin_stdout; use super::{BoxedNewPeerFuture, Peer}; use super::{once, ConstructParams, PeerConstructor, Specifier}; use std::rc::Rc; #[derive(Debug, Clone)] pub struct ThreadedStdio; impl Specifier for ThreadedStdio { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(get_stdio_peer()) } specifier_boilerplate!(globalstate singleconnect no_subspec); } specifier_class!( name = ThreadedStdioClass, target = ThreadedStdio, prefixes = ["threadedstdio:"], arg_handling = noarg, overlay = false, StreamOriented, SingleConnect, help = r#" [A] Stdin/stdout, spawning a thread (threaded version). Like `-`, but forces threaded mode instead of async mode Use when standard input is not `epoll(7)`-able or you want to avoid setting it to nonblocking mode. "# ); specifier_class!( name = StdioClass, target = ThreadedStdio, prefixes = ["-", "stdio:"], arg_handling = noarg, overlay = false, StreamOriented, SingleConnect, help = r#" Read input from console, print to console. Uses threaded implementation even on UNIX unless requested by `--async-stdio` CLI option. Typically this specifier can be specified only one time. Example: simulate `cat(1)`. This is an exception from "only one time" rule above: websocat - - Example: SSH transport ssh -c ProxyCommand='websocat - ws://myserver/mywebsocket' user@myserver "# ); #[cfg(not(all(unix, feature = "unix_stdio")))] specifier_class!( name = InetdClass, target = ThreadedStdio, prefixes = ["inetd:"], arg_handling = noarg, overlay = false, StreamOriented, SingleConnect, help = r#" Alias of stdio: (threaded version). "# ); pub fn get_stdio_peer() -> BoxedNewPeerFuture { info!("get_stdio_peer (threaded)"); Box::new(::futures::future::ok(Peer::new( tokio_stdin_stdout::stdin(0), tokio_stdin_stdout::stdout(0), None, ))) as BoxedNewPeerFuture } ================================================ FILE: src/timestamp_peer.rs ================================================ use futures::future::ok; use std::rc::Rc; use super::{BoxedNewPeerFuture, Peer}; use super::{ConstructParams, PeerConstructor, Specifier}; use std::time::{SystemTime, UNIX_EPOCH, Instant}; use std::io::Read; use tokio_io::AsyncRead; use std::io::Error as IoError; #[derive(Debug)] pub struct TimestampPeer(pub T); impl Specifier for TimestampPeer { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, _| timestamp_peer(p, cp.program_options.timestamp_monotonic)) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = TimestampClass, target = TimestampPeer, prefixes = ["timestamp:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" [A] Prepend timestamp to each incoming message. Example: TODO "# ); pub fn timestamp_peer(inner_peer: Peer, monotonic: bool) -> BoxedNewPeerFuture { let instant = if monotonic { Some(Instant::now() )} else { None }; let filtered = TimestampWrapper(inner_peer.0, instant); let thepeer = Peer::new(filtered, inner_peer.1, inner_peer.2); Box::new(ok(thepeer)) as BoxedNewPeerFuture } struct TimestampWrapper(Box, Option); impl Read for TimestampWrapper { fn read(&mut self, b: &mut [u8]) -> Result { let l = b.len(); assert!(l > 1); let n = self.0.read(&mut b[..l])?; if n == 0 { return Ok(0); } let mut v: Vec = Vec::with_capacity(n + 50); { let mut vv = ::std::io::Cursor::new(&mut v); use std::io::Write; let x = if let Some(basetime) = self.1 { Instant::now().duration_since(basetime).as_secs_f64() } else { (SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards")).as_secs_f64() }; let _ = write!(vv, "{} ", x); let _ = vv.write_all(&b[..n]); } if v.len() > l { warn!("Buffer too small, timstamp-prepended message may be truncated."); } let ll = v.len().min(l); b[..ll].copy_from_slice(&v[..ll]); Ok(ll) } } impl AsyncRead for TimestampWrapper {} ================================================ FILE: src/trivial_peer.rs ================================================ use super::{BoxedNewPeerFuture, Peer}; use futures; use rand::RngCore; use std; use std::io::Result as IoResult; use std::io::{Read, Write}; use futures::Async::Ready; use std::rc::Rc; use tokio_io::{AsyncRead, AsyncWrite}; use super::readdebt::{DebtHandling, ReadDebt, ZeroMessagesHandling}; use super::wouldblock; use super::{once, simple_err, ConstructParams, PeerConstructor, Specifier}; #[derive(Clone)] pub struct Literal(pub Vec); impl Specifier for Literal { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(get_literal_peer(self.0.clone())) } specifier_boilerplate!(singleconnect no_subspec noglobalstate); } impl std::fmt::Debug for Literal { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { write!(f, "Literal") } } specifier_class!( name = LiteralClass, target = Literal, prefixes = ["literal:"], arg_handling = into, overlay = false, MessageOriented, SingleConnect, help = r#" Output a string, discard input. Example: websocat ws-l:127.0.0.1:8080 literal:'{ "hello":"world"} ' "# ); // TODO: better doc #[derive(Clone)] pub struct Assert(pub Vec); impl Specifier for Assert { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(get_assert_peer(self.0.clone())) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } impl std::fmt::Debug for Assert { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { write!(f, "Assert") } } specifier_class!( name = AssertClass, target = Assert, prefixes = ["assert:"], arg_handling = into, overlay = false, MessageOriented, SingleConnect, help = r#" Check the input. [A] Read entire input and panic the program if the input is not equal to the specified string. Used in tests. "# ); #[derive(Clone)] pub struct Assert2(pub Vec); impl Specifier for Assert2 { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(get_assert2_peer(self.0.clone())) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } impl std::fmt::Debug for Assert2 { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { write!(f, "Assert2") } } specifier_class!( name = Assert2Class, target = Assert2, prefixes = ["assert2:"], arg_handling = into, overlay = false, MessageOriented, SingleConnect, help = r#" Check the input. [A] Read entire input and emit an error if the input is not equal to the specified string. "# ); #[derive(Debug, Clone)] pub struct Clogged; impl Specifier for Clogged { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(get_clogged_peer()) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = CloggedClass, target = Clogged, prefixes = ["clogged:"], arg_handling = noarg, overlay = false, MessageOriented, SingleConnect, help = r#" Do nothing. Don't read or write any bytes. Keep connections in "hung" state. [A] "# ); pub struct LiteralPeer { debt: ReadDebt, } pub fn get_literal_peer_now(b: Vec) -> LiteralPeer { LiteralPeer { debt: ReadDebt(Some(b), DebtHandling::Silent, ZeroMessagesHandling::Deliver), } } pub fn get_literal_peer(b: Vec) -> BoxedNewPeerFuture { let r = get_literal_peer_now(b); let w = DevNull; let p = Peer::new(r, w, None); Box::new(futures::future::ok(p)) as BoxedNewPeerFuture } pub fn get_assert_peer(b: Vec) -> BoxedNewPeerFuture { let r = DevNull; let w = AssertPeer(vec![], b, true); let p = Peer::new(r, w, None); Box::new(futures::future::ok(p)) as BoxedNewPeerFuture } pub fn get_assert2_peer(b: Vec) -> BoxedNewPeerFuture { let r = DevNull; let w = AssertPeer(vec![], b, false); let p = Peer::new(r, w, None); Box::new(futures::future::ok(p)) as BoxedNewPeerFuture } /// A special peer that returns NotReady without registering for any wakeup, deliberately hanging all connections forever. pub fn get_clogged_peer() -> BoxedNewPeerFuture { let r = CloggedPeer; let w = CloggedPeer; let p = Peer::new(r, w, None); Box::new(futures::future::ok(p)) as BoxedNewPeerFuture } impl AsyncRead for LiteralPeer {} impl Read for LiteralPeer { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { if let Some(ret) = self.debt.check_debt(buf) { debug!("LiteralPeer debt"); return ret; } debug!("LiteralPeer finished"); Ok(0) } } pub struct DevNull; impl AsyncWrite for DevNull { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { Ok(Ready(())) } } impl Write for DevNull { fn write(&mut self, buf: &[u8]) -> IoResult { Ok(buf.len()) } fn flush(&mut self) -> IoResult<()> { Ok(()) } } impl AsyncRead for DevNull {} impl Read for DevNull { fn read(&mut self, _buf: &mut [u8]) -> std::result::Result { Ok(0) } } struct AssertPeer(Vec, Vec, bool); impl AsyncWrite for AssertPeer { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { if self.2 { assert_eq!(self.0, self.1); } else if self.0 != self.1 { error!("Assertion failed"); return Err(simple_err("Assertion failed".into())); } info!("Assertion succeed"); Ok(Ready(())) } } impl Write for AssertPeer { fn write(&mut self, buf: &[u8]) -> IoResult { self.0.extend_from_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> IoResult<()> { Ok(()) } } pub struct CloggedPeer; impl AsyncWrite for CloggedPeer { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { wouldblock() } } impl Write for CloggedPeer { fn write(&mut self, _buf: &[u8]) -> IoResult { wouldblock() } fn flush(&mut self) -> IoResult<()> { wouldblock() } } impl AsyncRead for CloggedPeer {} impl Read for CloggedPeer { fn read(&mut self, _buf: &mut [u8]) -> std::result::Result { wouldblock() } } // TODO: make Prepend{Read,Write} available from command line /// First read content of `header`, then start relaying from `inner`. pub struct PrependRead { pub header: Vec, pub remaining: usize, pub inner: Box, } impl AsyncRead for PrependRead {} impl Read for PrependRead { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { if self.remaining == 0 { trace!("PrependRead relay"); return self.inner.read(buf); } let l = buf.len().min(self.remaining); debug!("PrependRead read debt {}", l); let offset = self.header.len() - self.remaining; buf[..l].copy_from_slice(&self.header[offset..(offset+l)]); let ret = l; self.remaining -= ret; if self.remaining == 0 { self.header.clear(); self.header.shrink_to_fit(); } Ok(l) } } /// First write `header` to `inner`, then start copying data directly to it. pub struct PrependWrite { pub header: Vec, pub remaining: usize, pub inner: Box, } impl AsyncWrite for PrependWrite { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self.inner.shutdown() } } impl Write for PrependWrite { fn write(&mut self, buf: &[u8]) -> IoResult { loop { if self.remaining == 0 { return self.inner.write(buf); } let offset = self.header.len() - self.remaining; let ret = self.inner.write(&self.header[offset..])?; self.remaining -= ret; if self.remaining == 0 { self.header.clear(); self.header.shrink_to_fit(); } } } fn flush(&mut self) -> IoResult<()> { self.inner.flush() } } #[derive(Debug)] pub struct Log(pub T); impl Specifier for Log { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, _l2r| { Box::new(futures::future::ok(Peer(Box::new(LogRead(p.0)), Box::new(LogWrite(p.1)), p.2))) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = LogClass, target = Log, prefixes = ["log:"], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" Log each buffer as it pass though the underlying connector. If you increase the logging level, you will also see hex buffers. Example: view WebSocket handshake and traffic on the way to echo.websocket.org websocat -t - ws-c:log:tcp:127.0.0.1:1080 --ws-c-uri ws://echo.websocket.org "# ); pub struct LogRead (pub Box); fn log_buffer(tag: &'static str, buf: &[u8]) { let mut s = String::with_capacity(buf.len()*2); for x in buf.iter().cloned().map(std::ascii::escape_default) { s.push_str(String::from_utf8_lossy(&x.collect::>()).as_ref() ); } eprintln!("{} {} \"{}\"", tag, buf.len(), s ); debug!("{}", hex::encode(buf)); } impl AsyncRead for LogRead {} impl Read for LogRead { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { let ret = self.0.read(buf); if let Ok(ref sz) = ret { let buf = &buf[..*sz]; log_buffer("READ", buf); } else { //eprintln!("FAILED_READ"); } ret } } pub struct LogWrite(pub Box); impl AsyncWrite for LogWrite { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self.0.shutdown() } } impl Write for LogWrite { fn write(&mut self, buf: &[u8]) -> IoResult { let ret = self.0.write(buf); if let Ok(ref sz) = ret { let buf = &buf[..*sz]; log_buffer("WRITE", buf); } else { //eprintln!("FAILED_WRITE"); } ret } fn flush(&mut self) -> IoResult<()> { self.0.flush() } } #[derive(Debug)] pub struct Random; impl Specifier for Random { fn construct(&self, _cp: ConstructParams) -> PeerConstructor { let r = RandomReader(); let w = DevNull; let p = Peer::new(r, w, None); once(Box::new(futures::future::ok(p)) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = RandomClass, target = Random, prefixes = ["random:"], arg_handling = noarg, overlay = false, MessageOriented, SingleConnect, help = r#" Generate random bytes when being read from, discard written bytes. websocat -b random: ws://127.0.0.1/flood "# ); pub struct RandomReader (); impl AsyncRead for RandomReader {} impl Read for RandomReader { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { rand::thread_rng().fill_bytes(buf); Ok(buf.len()) } } #[derive(Debug)] pub struct ExitOnSpecificByte(pub T); impl Specifier for ExitOnSpecificByte { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, _l2r| { Box::new(futures::future::ok(Peer(Box::new(ExitOnSpecificByteReader { inner: p.0, the_byte: cp.program_options.byte_to_exit_on, eof_triggered: false, }), p.1, p.2))) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = ExitOnSpecificByteClass, target = ExitOnSpecificByte, prefixes = ["exit_on_specific_byte:"], arg_handling = subspec, overlay = true, StreamOriented, MulticonnectnessDependsOnInnerType, help = r#" [A] Turn specific byte into a EOF, allowing user to escape interactive Websocat session when terminal is set to raw mode. Works only bytes read from the overlay, not on the written bytes. Default byte is 1C which is typically triggered by Ctrl+\. Example: `(stty raw -echo; websocat -b exit_on_specific_byte:stdio tcp:127.0.0.1:23; stty sane)` "# ); pub struct ExitOnSpecificByteReader { inner: Box, the_byte: u8, eof_triggered: bool, } impl AsyncRead for ExitOnSpecificByteReader {} impl Read for ExitOnSpecificByteReader { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { if self.eof_triggered { return Ok(0); } let ret = self.inner.read(buf); if let Ok(ref sz) = ret { let buf = &buf[..*sz]; if let Some((pos,_)) = buf.iter().enumerate().find(|x|*x.1==self.the_byte) { log::info!("Special byte detected. Triggering EOF."); self.eof_triggered = true; return Ok(pos); } } ret } } #[derive(Debug)] pub struct DropOnBackpressure(pub T); impl Specifier for DropOnBackpressure { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let inner = self.0.construct(cp.clone()); inner.map(move |p, _l2r| { Box::new(futures::future::ok(Peer(p.0, Box::new(DropOnBackpressureWriter { inner: p.1 }), p.2))) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = DropOnBackpressureClass, target = DropOnBackpressure, prefixes = ["drop_on_backpressure:"], arg_handling = subspec, overlay = true, MessageBoundaryStatusDependsOnInnerType, MulticonnectnessDependsOnInnerType, help = r#" [A] Prevent writing from ever blocking, drop writes instead. Does not affect reading part. when terminal is set to raw mode. Works only bytes read from the overlay, not on the written bytes. Default byte is 1C which is typically triggered by Ctrl+\. Example (attachable log observer): some_program | websocat -b -u asyncstdio: drop_on_backpressure:autoreconnect:ws-l:127.0.0.1:1234 "# ); pub struct DropOnBackpressureWriter { inner: Box, } impl Write for DropOnBackpressureWriter { fn write(&mut self, buf: &[u8]) -> IoResult { match self.inner.write(buf) { Ok(n) => Ok(n), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { log::debug!("Dropping {} bytes due to backpressure", buf.len()); Ok(buf.len()) } Err(e) => Err(e), } } fn flush(&mut self) -> IoResult<()> { match self.inner.flush() { Ok(()) => Ok(()), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { log::debug!("Dropping flush due to backpressure"); Ok(()) } Err(e) => Err(e), } } } impl AsyncWrite for DropOnBackpressureWriter { fn shutdown(&mut self) -> tokio::prelude::Poll<(), std::io::Error> { self.inner.shutdown() } } ================================================ FILE: src/unix_peer.rs ================================================ extern crate tokio_reactor; extern crate tokio_uds; extern crate libc; use futures; use futures::stream::Stream; use std; use std::io::Result as IoResult; use std::io::{Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use std::cell::RefCell; use std::rc::Rc; use std::path::{Path, PathBuf}; use self::tokio_uds::{UnixDatagram, UnixListener, UnixStream}; //#[cfg_attr(feature="cargo-clippy",allow(unused_imports))] #[allow(unused_imports)] use super::simple_err; use super::{box_up_err, peer_err_s, util::peer_err_sb, BoxedNewPeerFuture, BoxedNewPeerStream, Peer}; use super::{multi, once, ConstructParams, Options, PeerConstructor, Specifier}; #[derive(Debug, Clone)] pub struct UnixConnect(pub PathBuf); impl Specifier for UnixConnect { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(unix_connect_peer(&self.0)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = UnixConnectClass, target = UnixConnect, prefixes = [ "unix:", "unix-connect:", "connect-unix:", "unix-c:", "c-unix:" ], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Connect to UNIX socket. Argument is filesystem path. [A] Example: forward connections from websockets to a UNIX stream socket websocat ws-l:127.0.0.1:8088 unix:the_socket "# ); #[derive(Debug, Clone)] pub struct UnixListen(pub PathBuf); impl Specifier for UnixListen { fn construct(&self, p: ConstructParams) -> PeerConstructor { multi(unix_listen_peer(&self.0, &p.program_options)) } specifier_boilerplate!(noglobalstate multiconnect no_subspec); } specifier_class!( name = UnixListenClass, target = UnixListen, prefixes = ["unix-listen:", "listen-unix:", "unix-l:", "l-unix:"], arg_handling = into, overlay = false, StreamOriented, MultiConnect, help = r#" Listen for connections on a specified UNIX socket [A] Example: forward connections from a UNIX socket to a WebSocket websocat --unlink unix-l:the_socket ws://127.0.0.1:8089 Example: Accept forwarded WebSocket connections from Nginx umask 0000 websocat --unlink -b -E ws-u:unix-l:/tmp/wstest tcp:[::]:22 Nginx config: location /ws { proxy_read_timeout 7d; proxy_send_timeout 7d; #proxy_pass http://localhost:3012; proxy_pass http://unix:/tmp/wstest; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } This configuration allows to make Nginx responsible for SSL and also it can choose which connections to forward to websocat based on URLs. Obviously, Nginx can also redirect to TCP-listening websocat just as well - UNIX sockets are not a requirement for this feature. See `moreexamples.md` for SystemD usage (untested). TODO: --chmod option? "# ); #[derive(Debug, Clone)] pub struct UnixDgram(pub PathBuf, pub PathBuf); impl Specifier for UnixDgram { fn construct(&self, p: ConstructParams) -> PeerConstructor { once(dgram_peer(&self.0, &self.1, &p.program_options)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = UnixDgramClass, target = UnixDgram, prefixes = ["unix-dgram:"], arg_handling = { fn construct(self: &UnixDgramClass, just_arg: &str) -> super::Result> { let splits: Vec<&str> = just_arg.split(':').collect(); if splits.len() != 2 { Err("Expected two colon-separated paths")?; } Ok(Rc::new(UnixDgram(splits[0].into(), splits[1].into()))) } fn construct_overlay( self: &UnixDgramClass, _inner: Rc, ) -> super::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } }, overlay = false, MessageOriented, SingleConnect, help = r#" Send packets to one path, receive from the other. [A] A socket for sending must be already opened. I don't know if this mode has any use, it is here just for completeness. Example: socat unix-recv:./sender -& websocat - unix-dgram:./receiver:./sender "# ); fn to_abstract(x: &str) -> PathBuf { format!("\x00{}", x).into() } #[derive(Debug, Clone)] pub struct AbstractConnect(pub String); impl Specifier for AbstractConnect { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(unix_connect_peer(&to_abstract(&self.0))) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = AbstractConnectClass, target = AbstractConnect, prefixes = [ "abstract:", "abstract-connect:", "connect-abstract:", "abstract-c:", "c-abstract:" ], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Connect to UNIX abstract-namespaced socket. Argument is some string used as address. [A] Too long addresses may be silently chopped off. Example: forward connections from websockets to an abstract stream socket websocat ws-l:127.0.0.1:8088 abstract:the_socket Note that abstract-namespaced Linux sockets may not be normally supported by Rust, so non-prebuilt versions may have problems with them. "# ); #[derive(Debug, Clone)] pub struct AbstractListen(pub String); impl Specifier for AbstractListen { fn construct(&self, cp: ConstructParams) -> PeerConstructor { multi(unix_listen_peer( &to_abstract(&self.0), &cp.program_options, )) } specifier_boilerplate!(noglobalstate multiconnect no_subspec); } specifier_class!( name = AbstractListenClass, target = AbstractListen, prefixes = [ "abstract-listen:", "listen-abstract:", "abstract-l:", "l-abstract:" ], arg_handling = into, overlay = false, StreamOriented, MultiConnect, help = r#" Listen for connections on a specified abstract UNIX socket [A] Example: forward connections from an abstract UNIX socket to a WebSocket websocat abstract-l:the_socket ws://127.0.0.1:8089 Note that abstract-namespaced Linux sockets may not be normally supported by Rust, so non-prebuilt versions may have problems with them. "# ); #[derive(Debug, Clone)] pub struct AbstractDgram(pub String, pub String); impl Specifier for AbstractDgram { fn construct(&self, p: ConstructParams) -> PeerConstructor { #[cfg(not(all(target_os = "linux", feature = "workaround1")))] { once(dgram_peer( &to_abstract(&self.0), &to_abstract(&self.1), &p.program_options, )) } #[cfg(all(target_os = "linux", feature = "workaround1"))] { once(dgram_peer_workaround( &to_abstract(&self.0), &to_abstract(&self.1), &p.program_options, )) } } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = AbstractDgramClass, target = AbstractDgram, prefixes = ["abstract-dgram:"], arg_handling = { fn construct(self: &AbstractDgramClass, just_arg: &str) -> super::Result> { let splits: Vec<&str> = just_arg.split(':').collect(); if splits.len() != 2 { Err("Expected two colon-separated addresses")?; } Ok(Rc::new(UnixDgram(splits[0].into(), splits[1].into()))) } fn construct_overlay( self: &AbstractDgramClass, _inner: Rc, ) -> super::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } }, overlay = false, MessageOriented, SingleConnect, help = r#" Send packets to one address, receive from the other. [A] A socket for sending must be already opened. I don't know if this mode has any use, it is here just for completeness. Example (untested): websocat - abstract-dgram:receiver_addr:sender_addr Note that abstract-namespaced Linux sockets may not be normally supported by Rust, so non-prebuilt versions may have problems with them. In particular, this mode may fail to work without `workaround1` Cargo feature. "# ); #[cfg(all(target_os = "linux", feature = "seqpacket"))] #[path = "unix_seqpacket_peer.rs"] pub mod unix_seqpacket_peer; // based on https://github.com/tokio-rs/tokio-core/blob/master/examples/proxy.rs #[derive(Clone)] struct MyUnixStream(Rc, bool); impl Read for MyUnixStream { fn read(&mut self, buf: &mut [u8]) -> IoResult { (&*self.0).read(buf) } } impl Write for MyUnixStream { fn write(&mut self, buf: &[u8]) -> IoResult { (&*self.0).write(buf) } fn flush(&mut self) -> IoResult<()> { Ok(()) } } impl AsyncRead for MyUnixStream {} impl AsyncWrite for MyUnixStream { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self.0.shutdown(std::net::Shutdown::Write)?; Ok(().into()) } } impl Drop for MyUnixStream { fn drop(&mut self) { let i_am_read_part = self.1; if i_am_read_part { let _ = self.0.shutdown(std::net::Shutdown::Read); } } } pub fn unix_connect_peer(addr: &Path) -> BoxedNewPeerFuture { use futures::Future; Box::new( UnixStream::connect(addr) .map(|x| { info!("Connected to a unix socket"); let x = Rc::new(x); Peer::new( MyUnixStream(x.clone(), true), MyUnixStream(x.clone(), false), None /* TODO */, ) }) .map_err(box_up_err), ) as BoxedNewPeerFuture } pub fn unix_listen_peer(addr: &Path, opts: &Rc) -> BoxedNewPeerStream { let bound = if opts.unix_socket_accept_from_fd { // Special mode for SystemD (untested yet) let fdnum: libc::c_int = match addr.to_str().map(|x|x.parse()) { Some(Ok(x)) => x, _ => { let e: Box = From::from("Specify numeric argument instead of path in --accept-from-fd mode"); return peer_err_sb(e); } }; use std::os::unix::io::FromRawFd; let l = unsafe { std::os::unix::net::UnixListener::from_raw_fd(fdnum) } ; let _ = l.set_nonblocking(true); UnixListener::from_std(l, &tokio_reactor::Handle::default()) } else { if opts.unlink_unix_socket { let _ = ::std::fs::remove_file(addr); }; let bound = UnixListener::bind(addr); if opts.announce_listens { let poss = addr.as_os_str(); use std::os::unix::ffi::OsStrExt; if !poss.is_empty() && poss.as_bytes()[0] == b'\0' { println!("LISTEN proto=abstract,path_hex={}", hex::encode(&poss.as_bytes()[1..])); } else { println!("LISTEN proto=unix,path={:?}", addr); } } bound }; let bound = match bound { Ok(x) => x, Err(e) => return peer_err_s(e), }; debug!("UNIX listening socket should be ready"); // TODO: chmod use tk_listen::ListenExt; Box::new( bound .incoming() .sleep_on_error(::std::time::Duration::from_millis(500)) .map(|x| { info!("Incoming unix socket connection"); let x = Rc::new(x); Peer::new( MyUnixStream(x.clone(), true), MyUnixStream(x.clone(), false), None /* TODO */, ) }) .map_err(|()| crate::simple_err2("unreachable error?")), ) as BoxedNewPeerStream } struct DgramPeer { s: UnixDatagram, #[allow(unused)] oneshot_mode: bool, // TODO } #[derive(Clone)] struct DgramPeerHandle(Rc>); pub fn dgram_peer(bindaddr: &Path, connectaddr: &Path, opts: &Rc) -> BoxedNewPeerFuture { Box::new(futures::future::result( UnixDatagram::bind(bindaddr) .and_then(|x| { x.connect(connectaddr)?; let h1 = DgramPeerHandle(Rc::new(RefCell::new(DgramPeer { s: x, oneshot_mode: opts.udp_oneshot_mode, }))); let h2 = h1.clone(); Ok(Peer::new(h1, h2, None)) }) .map_err(box_up_err), )) as BoxedNewPeerFuture } #[cfg(all(target_os = "linux", feature = "workaround1"))] pub fn dgram_peer_workaround( bindaddr: &Path, connectaddr: &Path, opts: &Rc, ) -> BoxedNewPeerFuture { info!("Workaround method for getting abstract datagram socket"); fn getfd(bindaddr: &Path, connectaddr: &Path) -> Option { use self::libc::{ bind, c_char, close, connect, sa_family_t, sockaddr_un, socket, socklen_t, AF_UNIX, SOCK_DGRAM, }; use std::mem::size_of; use std::os::unix::ffi::OsStrExt; unsafe { let s = socket(AF_UNIX, SOCK_DGRAM, 0); if s == -1 { return None; } { let mut sa = sockaddr_un { sun_family: AF_UNIX as sa_family_t, sun_path: [0; 108], }; let bp: &[c_char] = &*(bindaddr.as_os_str().as_bytes() as *const [u8] as *const [c_char]); let l = 108.min(bp.len()); sa.sun_path[..l].copy_from_slice(&bp[..l]); let sa_len = l + size_of::(); let sa_ = &sa as *const self::libc::sockaddr_un as *const self::libc::sockaddr; let ret = bind(s, sa_, sa_len as socklen_t); if ret == -1 { close(s); return None; } } { let mut sa = sockaddr_un { sun_family: AF_UNIX as sa_family_t, sun_path: [0; 108], }; let bp: &[c_char] = &*(connectaddr.as_os_str().as_bytes() as *const [u8] as *const [c_char]); let l = 108.min(bp.len()); sa.sun_path[..l].copy_from_slice(&bp[..l]); let sa_len = l + size_of::(); let sa_ = &sa as *const self::libc::sockaddr_un as *const self::libc::sockaddr; let ret = connect(s, sa_, sa_len as socklen_t); if ret == -1 { close(s); return None; } } Some(s) } } fn getpeer( bindaddr: &Path, connectaddr: &Path, opts: &Rc, ) -> Result> { if let Some(fd) = getfd(bindaddr, connectaddr) { let s: ::std::os::unix::net::UnixDatagram = unsafe { ::std::os::unix::io::FromRawFd::from_raw_fd(fd) }; let ss = UnixDatagram::from_std(s, &tokio_reactor::Handle::default())?; let h1 = DgramPeerHandle(Rc::new(RefCell::new(DgramPeer { s: ss, oneshot_mode: opts.udp_oneshot_mode, }))); let h2 = h1.clone(); Ok(Peer::new(h1, h2, None)) } else { Err("Failed to get, bind or connect socket")? } } Box::new(futures::future::result({ getpeer(bindaddr, connectaddr, opts) })) as BoxedNewPeerFuture } impl Read for DgramPeerHandle { fn read(&mut self, buf: &mut [u8]) -> IoResult { let p = self.0.borrow_mut(); p.s.recv(buf) } } impl Write for DgramPeerHandle { fn write(&mut self, buf: &[u8]) -> IoResult { let p = self.0.borrow_mut(); p.s.send(buf) } fn flush(&mut self) -> IoResult<()> { Ok(()) } } impl AsyncRead for DgramPeerHandle {} impl AsyncWrite for DgramPeerHandle { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { Ok(().into()) } } trait HacksForMigratingFromTokioCore { fn recv(&self, buf: &mut [u8]) -> std::io::Result; fn send(&self, buf: &[u8]) -> std::io::Result; } impl HacksForMigratingFromTokioCore for UnixDatagram { fn recv(&self, buf: &mut [u8]) -> std::io::Result { match self.poll_recv(buf)? { futures::Async::Ready(n) => Ok(n), futures::Async::NotReady => Err(std::io::ErrorKind::WouldBlock.into()), } } fn send(&self, buf: &[u8]) -> std::io::Result { match self.poll_send(buf)? { futures::Async::Ready(n) => Ok(n), futures::Async::NotReady => Err(std::io::ErrorKind::WouldBlock.into()), } } } ================================================ FILE: src/unix_seqpacket_peer.rs ================================================ extern crate tokio_reactor; use super::{ futures, libc, multi, once, peer_err_s, simple_err, BoxedNewPeerFuture, BoxedNewPeerStream, ConstructParams, MyUnixStream, Options, Peer, PeerConstructor, Specifier, UnixListener, UnixStream, }; use futures::Stream; use std::path::{Path, PathBuf}; use std::rc::Rc; #[derive(Debug, Clone)] pub struct SeqpacketConnect(pub PathBuf); impl Specifier for SeqpacketConnect { fn construct(&self, _: ConstructParams) -> PeerConstructor { once(seqpacket_connect_peer(&self.0)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = SeqpacketConnectClass, target = SeqpacketConnect, prefixes = [ "seqpacket:", "seqpacket-connect:", "connect-seqpacket:", "seqpacket-c:", "c-seqpacket:" ], arg_handling = into, overlay = false, MessageOriented, SingleConnect, help = r#" Connect to AF_UNIX SOCK_SEQPACKET socket. Argument is a filesystem path. [A] Start the path with `@` character to make it connect to abstract-namespaced socket instead. Too long paths are silently truncated. Example: forward connections from websockets to a UNIX seqpacket abstract socket websocat ws-l:127.0.0.1:1234 seqpacket:@test "# ); #[derive(Debug, Clone)] pub struct SeqpacketListen(pub PathBuf); impl Specifier for SeqpacketListen { fn construct(&self, p: ConstructParams) -> PeerConstructor { multi(seqpacket_listen_peer(&self.0, &p.program_options)) } specifier_boilerplate!(noglobalstate multiconnect no_subspec); } specifier_class!( name = SeqpacketListenClass, target = SeqpacketListen, prefixes = [ "seqpacket-listen:", "listen-seqpacket:", "seqpacket-l:", "l-seqpacket:" ], arg_handling = into, overlay = false, MessageOriented, MultiConnect, help = r#" Listen for connections on a specified AF_UNIX SOCK_SEQPACKET socket [A] Start the path with `@` character to make it connect to abstract-namespaced socket instead. Too long (>=108 bytes) paths are silently truncated. Example: forward connections from a UNIX seqpacket socket to a WebSocket websocat --unlink seqpacket-l:the_socket ws://127.0.0.1:8089 "# ); pub fn seqpacket_connect_peer(addr: &Path) -> BoxedNewPeerFuture { fn getfd(addr: &Path) -> Option { use self::libc::{ c_char, close, connect, sa_family_t, sockaddr_un, socket, socklen_t, AF_UNIX, SOCK_SEQPACKET, }; use std::mem::size_of; use std::os::unix::ffi::OsStrExt; unsafe { let s = socket(AF_UNIX, SOCK_SEQPACKET, 0); if s == -1 { return None; } { let mut sa = sockaddr_un { sun_family: AF_UNIX as sa_family_t, sun_path: [0; 108], }; let bp: &[c_char] = &*(addr.as_os_str().as_bytes() as *const [u8] as *const [c_char]); let l = 108.min(bp.len()); sa.sun_path[..l].copy_from_slice(&bp[..l]); if sa.sun_path[0] == b'@' as c_char { sa.sun_path[0] = b'\x00' as c_char; } let sa_len = l + size_of::(); let sa_ = &sa as *const self::libc::sockaddr_un as *const self::libc::sockaddr; let ret = connect(s, sa_, sa_len as socklen_t); if ret == -1 { close(s); return None; } } Some(s) } } fn getpeer(addr: &Path) -> Result> { if let Some(fd) = getfd(addr) { let s: ::std::os::unix::net::UnixStream = unsafe { ::std::os::unix::io::FromRawFd::from_raw_fd(fd) }; let ss = UnixStream::from_std(s, &tokio_reactor::Handle::default())?; let x = Rc::new(ss); Ok(Peer::new( MyUnixStream(x.clone(), true), MyUnixStream(x.clone(), false), None /* TODO*/ , )) } else { Err("Failed to get or connect socket")? } } Box::new(futures::future::result(getpeer(addr))) as BoxedNewPeerFuture } pub fn seqpacket_listen_peer(addr: &Path, opts: &Rc) -> BoxedNewPeerStream { fn getfd(addr: &Path, opts: &Rc) -> Option { use self::libc::{ bind, c_char, close, listen, sa_family_t, sockaddr_un, socket, socklen_t, unlink, AF_UNIX, SOCK_SEQPACKET, }; use std::mem::size_of; use std::os::unix::ffi::OsStrExt; unsafe { let s = socket(AF_UNIX, SOCK_SEQPACKET, 0); if s == -1 { return None; } { let mut sa = sockaddr_un { sun_family: AF_UNIX as sa_family_t, sun_path: [0; 108], }; let bp: &[c_char] = &*(addr.as_os_str().as_bytes() as *const [u8] as *const [c_char]); let l = 108.min(bp.len()); sa.sun_path[..l].copy_from_slice(&bp[..l]); if sa.sun_path[0] == b'@' as c_char { sa.sun_path[0] = b'\x00' as c_char; } else if opts.unlink_unix_socket { sa.sun_path[107] = 0; unlink(&sa.sun_path as *const c_char); } let sa_len = l + size_of::(); let sa_ = &sa as *const self::libc::sockaddr_un as *const self::libc::sockaddr; let ret = bind(s, sa_, sa_len as socklen_t); if ret == -1 { close(s); return None; } } { let ret = listen(s, 50); if ret == -1 { close(s); return None; } } if opts.announce_listens { // too lazy to actually handle '"@' vs '@"' here - is seqpacket even used by somebody around? let s = format!("LISTEN proto=unix_seqpacket,path={:?}", addr); if s.contains("path=\"@") { warn!("that particular LISTEN line format should be changed in future Websocat version"); } println!("{}", s); } Some(s) } } let fd = match getfd(addr, opts) { Some(x) => x, None => return peer_err_s(simple_err("Failed to get or bind socket".into())), }; let l1: ::std::os::unix::net::UnixListener = unsafe { ::std::os::unix::io::FromRawFd::from_raw_fd(fd) }; let bound = match UnixListener::from_std(l1, &tokio_reactor::Handle::default()) { Ok(x) => x, Err(e) => return peer_err_s(e), }; use tk_listen::ListenExt; Box::new( bound .incoming() .sleep_on_error(::std::time::Duration::from_millis(500)) .map(|x| { info!("Incoming unix socket connection"); let x = Rc::new(x); Peer::new( MyUnixStream(x.clone(), true), MyUnixStream(x.clone(), false), None /* TODO*/ , ) }) .map_err(|()| crate::simple_err2("unreachable error?")), ) as BoxedNewPeerStream } ================================================ FILE: src/util.rs ================================================ use super::{ futures, AsyncRead, AsyncWrite, BoxedNewPeerFuture, BoxedNewPeerStream, L2rUser, Peer, PeerConstructor, Rc, HupToken, }; use super::{Future, Stream}; pub fn wouldblock() -> std::io::Result { Err(std::io::Error::new(std::io::ErrorKind::WouldBlock, "")) } pub fn brokenpipe() -> std::io::Result { Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "")) } pub fn io_other_error(e: E) -> std::io::Error { std::io::Error::new(std::io::ErrorKind::Other, e) } #[allow(clippy::redundant_closure)] impl PeerConstructor { pub fn map(self, func: F) -> Self where F: Fn(Peer, L2rUser) -> BoxedNewPeerFuture + 'static, { let f = Rc::new(func); use crate::PeerConstructor::*; match self { Error(e) => Error(e), ServeOnce(x) => Overlay1(x, f), ServeMultipleTimes(s) => OverlayM(s, f), Overlay1(x, mapper) => Overlay1( x, Rc::new(move |p, l2r| { let ff = f.clone(); let l2rc = l2r.clone(); Box::new(mapper(p, l2r).and_then(move |x| ff(x, l2rc))) }), ), OverlayM(x, mapper) => OverlayM( x, Rc::new(move |p, l2r| { let ff = f.clone(); let l2rc = l2r.clone(); Box::new(mapper(p, l2r).and_then(move |x| ff(x, l2rc))) }), ), // This implementation (without Overlay{1,M} cases) // causes task to be spawned too late (before establishing ws upgrade) // when serving clients: //ServeOnce(x) => ServeOnce(Box::new(x.and_then(f)) as BoxedNewPeerFuture), //ServeMultipleTimes(s) => { // ServeMultipleTimes(Box::new(s.and_then(f)) as BoxedNewPeerStream) //} } } pub fn get_only_first_conn(self, l2r: L2rUser) -> BoxedNewPeerFuture { use crate::PeerConstructor::*; match self { Error(e) => Box::new(futures::future::err(e)) as BoxedNewPeerFuture, ServeMultipleTimes(stre) => Box::new( stre.into_future() .map(move |(std_peer, _)| std_peer.expect("Nowhere to connect it")) .map_err(|(e, _)| e), ) as BoxedNewPeerFuture, ServeOnce(futur) => futur, Overlay1(futur, mapper) => { Box::new(futur.and_then(move |p| mapper(p, l2r))) as BoxedNewPeerFuture } OverlayM(stre, mapper) => Box::new( stre.into_future() .map(move |(std_peer, _)| std_peer.expect("Nowhere to connect it")) .map_err(|(e, _)| e) .and_then(move |p| mapper(p, l2r)), ) as BoxedNewPeerFuture, } } } pub fn once(x: BoxedNewPeerFuture) -> PeerConstructor { PeerConstructor::ServeOnce(x) } pub fn multi(x: BoxedNewPeerStream) -> PeerConstructor { PeerConstructor::ServeMultipleTimes(x) } pub fn peer_err(e: E) -> BoxedNewPeerFuture { Box::new(futures::future::err( Box::new(e) as Box )) as BoxedNewPeerFuture } pub fn peer_err2(e: Box) -> BoxedNewPeerFuture { Box::new(futures::future::err( e )) as BoxedNewPeerFuture } pub fn peer_err_s(e: E) -> BoxedNewPeerStream { Box::new(futures::stream::iter_result(vec![Err( Box::new(e) as Box )])) as BoxedNewPeerStream } pub fn peer_err_sb(e: Box) -> BoxedNewPeerStream { Box::new(futures::stream::iter_result(vec![Err( e )])) as BoxedNewPeerStream } pub fn peer_strerr(e: &str) -> BoxedNewPeerFuture { let q: Box = From::from(e); Box::new(futures::future::err(q)) as BoxedNewPeerFuture } pub fn simple_err(e: String) -> std::io::Error { let e1: Box = e.into(); ::std::io::Error::new(::std::io::ErrorKind::Other, e1) } pub fn simple_err2(e: &'static str) -> Box { let e1: Box = e.to_string().into(); e1 as Box } pub fn box_up_err(e: E) -> Box { Box::new(e) as Box } impl Peer { pub fn new(r: R, w: W, hup: Option) -> Self { Peer( Box::new(r) as Box, Box::new(w) as Box, hup, ) } } ================================================ FILE: src/windows_np_peer.rs ================================================ extern crate tokio_named_pipes; use futures; use std; use std::io::Result as IoResult; use std::io::{Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use std::path::{Path, PathBuf}; //use super::{L2rUser, LeftSpecToRightSpec}; use std::cell::RefCell; use std::rc::Rc; use tokio_named_pipes::NamedPipe; use super::{once, ConstructParams, PeerConstructor, Specifier}; use super::{BoxedNewPeerFuture, Peer, Result}; #[derive(Debug, Clone)] pub struct NamedPipeConnect(pub PathBuf); impl Specifier for NamedPipeConnect { fn construct(&self, _p: ConstructParams) -> PeerConstructor { once(Box::new(futures::future::result(named_pipe_connect_peer(&self.0))) as BoxedNewPeerFuture) } specifier_boilerplate!(noglobalstate singleconnect no_subspec ); } specifier_class!( name = NamedPipeConnectClass, target = NamedPipeConnect, prefixes = ["namedpipeconnect:"], arg_handling = into, overlay = false, StreamOriented, SingleConnect, help = r#" Connect to a named pipe on Windows Example: websocat ws-l:127.0.0.1:8000 namedpipeconnect:\\.\pipe\Pipe "# ); fn named_pipe_connect_peer( path: &Path, ) -> Result { let pipe = NamedPipe::new(path, &tokio::reactor::Handle::default())?; let ph = NamedPipeConnectPeer(Rc::new(RefCell::new(pipe))); Ok(Peer::new(ph.clone(), ph, None)) } #[derive(Clone)] struct NamedPipeConnectPeer(Rc>); impl Read for NamedPipeConnectPeer { fn read(&mut self, buf: &mut [u8]) -> IoResult { self.0 .borrow_mut() .read(buf) } } impl Write for NamedPipeConnectPeer { fn write(&mut self, buf: &[u8]) -> IoResult { self.0 .borrow_mut() .write(buf) } fn flush(&mut self) -> IoResult<()> { self.0 .borrow_mut() .flush() } } impl AsyncRead for NamedPipeConnectPeer {} impl AsyncWrite for NamedPipeConnectPeer { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { self .0 .borrow_mut() .shutdown() } } ================================================ FILE: src/ws_client_peer.rs ================================================ extern crate hyper; extern crate websocket; use self::websocket::client::r#async::ClientNew; use self::websocket::stream::r#async::Stream as WsStream; use self::websocket::ClientBuilder; use futures::future::Future; use std::net::SocketAddr; use std::rc::Rc; use self::websocket::client::Url; use super::{box_up_err, peer_err, peer_strerr, BoxedNewPeerFuture, Peer, Result}; use super::ws_peer::PeerForWs; use super::{once, ConstructParams, Options, PeerConstructor, Specifier}; use self::hyper::header::Headers; #[derive(Debug, Clone)] pub struct WsClient(pub Url); impl Specifier for WsClient { fn construct(&self, p: ConstructParams) -> PeerConstructor { let url = self.0.clone(); once(get_ws_client_peer(&url, p.program_options)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } specifier_class!( name = WsClientClass, target = WsClient, prefixes = ["ws://"], arg_handling = { fn construct(self: &WsClientClass, arg: &str) -> super::Result> { Ok(Rc::new(WsClient(format!("ws:{}", arg).parse()?))) } fn construct_overlay( self: &WsClientClass, _inner: Rc, ) -> super::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } }, overlay = false, MessageOriented, SingleConnect, help = r#" Insecure (ws://) WebSocket client. Argument is host and URL. Example: connect to public WebSocket loopback and copy binary chunks from stdin to the websocket. websocat - ws://echo.websocket.org/ "# ); #[cfg(feature = "ssl")] #[derive(Debug, Clone)] pub struct WsClientSecure(pub Url); #[cfg(feature = "ssl")] impl Specifier for WsClientSecure { fn construct(&self, p: ConstructParams) -> PeerConstructor { let url = self.0.clone(); once(get_ws_client_peer(&url, p.program_options)) } specifier_boilerplate!(noglobalstate singleconnect no_subspec); } #[cfg(feature = "ssl")] specifier_class!( name = WsClientSecureClass, target = WsClientSecure, prefixes = ["wss://"], arg_handling = { fn construct(self: &WsClientSecureClass, arg: &str) -> super::Result> { Ok(Rc::new(WsClient(format!("wss:{}", arg).parse()?))) } fn construct_overlay( self: &WsClientSecureClass, _inner: Rc, ) -> super::Result> { panic!("Error: construct_overlay called on non-overlay specifier class") } }, overlay = false, MessageOriented, SingleConnect, help = r#" Secure (wss://) WebSocket client. Argument is host and URL. Example: forward TCP port 4554 to a websocket websocat tcp-l:127.0.0.1:4554 wss://127.0.0.1/some_websocket"# ); #[derive(Debug)] pub struct WsConnect(pub T); impl Specifier for WsConnect { fn construct(&self, p: ConstructParams) -> PeerConstructor { let inner = self.0.construct(p.clone()); let url: Url = match p.program_options.ws_c_uri.parse() { Ok(x) => x, Err(e) => return PeerConstructor::ServeOnce(peer_err(e)), }; let opts = p.program_options; inner.map(move |q, _| get_ws_client_peer_wrapped(&url, q, opts.clone())) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = WsConnectClass, target = WsConnect, prefixes = ["ws-c:", "c-ws:", "ws-connect:", "connect-ws:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" Low-level WebSocket connector. Argument is a some another address. [A] URL and Host: header being sent are independent from the underlying connection. Example: connect to echo server in more explicit way websocat --ws-c-uri=ws://echo.websocket.org/ - ws-c:tcp:174.129.224.73:80 Example: connect to echo server, observing WebSocket TCP packet exchange websocat --ws-c-uri=ws://echo.websocket.org/ - ws-c:cmd:"socat -v -x - tcp:174.129.224.73:80" "# ); fn get_ws_client_peer_impl(uri: &Url, opts: Rc, f: F) -> BoxedNewPeerFuture where S: WsStream + Send + 'static, F: FnOnce(ClientBuilder) -> Result>, { let stage1 = ClientBuilder::from_url(uri); let stage2 = if opts.custom_headers.is_empty() { stage1 } else { let mut h = Headers::new(); for (hn, hv) in opts.custom_headers.clone() { h.append_raw(hn, hv); } stage1.custom_headers(&h) }; let stage3 = if let Some(ref x) = opts.origin { stage2.origin(x.clone()) } else { stage2 }; let stage4 = if let Some(ref p) = opts.websocket_protocol { stage3.add_protocol(p.to_owned()) } else { stage3 }; let stage5 = if let Some(ref v) = opts.websocket_version { stage4.version(websocket::header::WebSocketVersion::Unknown(v.clone())) } else { stage4 }; let stage6 = stage5.max_dataframe_size(opts.max_ws_frame_length).max_message_size(opts.max_ws_message_length); let after_connect = match f(stage6) { Ok(x) => x, Err(_) => return peer_strerr("Failed to make TLS connector"), }; Box::new( after_connect .map(move |(duplex, headers)| { info!("Connected to ws, response headers: {:?}", headers); let close_on_shutdown = !opts.websocket_dont_close; super::ws_peer::finish_building_ws_peer(&opts, duplex, close_on_shutdown, None) }) .map_err(box_up_err), ) as BoxedNewPeerFuture } pub fn get_ws_client_peer(uri: &Url, opts: Rc) -> BoxedNewPeerFuture { info!("get_ws_client_peer"); #[allow(unused)] let tls_insecure = opts.tls_insecure; #[allow(unused)] let client_ident = opts.client_pkcs12_der.clone(); #[allow(unused)] let client_ident_passwd = opts.client_pkcs12_passwd.clone(); get_ws_client_peer_impl(uri, opts, |before_connect| { #[cfg(feature = "ssl")] let mut builder_ = super::ssl_peer::native_tls::TlsConnector::builder(); #[cfg(feature = "ssl")] let builder = builder_ .danger_accept_invalid_certs(tls_insecure) .danger_accept_invalid_hostnames(tls_insecure); #[cfg(feature = "ssl")] let after_connect = { let identity = if let Some(client_ident) = client_ident { super::ssl_peer::native_tls::Identity::from_pkcs12( &client_ident, &client_ident_passwd.unwrap_or("".to_string()), ) .map_err(|e| { error!( "Unable to parse client identity: {}\nContinuing without a client identity", e ) }) .ok() } else { None }; let tls_opts = if let Some(client_ident) = identity { debug!("Adding client identity to the TLS connection"); Some(builder.identity(client_ident).build()?) } else { Some(builder.build()?) }; before_connect.async_connect_with_cb(tls_opts, |addrs| { let addrs : Vec = addrs.collect(); let r = crate::net_peer::tcp_race(&addrs); let r = r.map_err(|e|{ websocket::WebSocketError::Other(e) }); Box::new(r) }) }; /// FIXME: happy eyeballs without TLS support #[cfg(not(feature = "ssl"))] let after_connect = before_connect.async_connect_insecure(); Ok(after_connect) }) } unsafe impl Send for PeerForWs { //! https://github.com/cyderize/rust-websocket/issues/168 } pub fn get_ws_client_peer_wrapped(uri: &Url, inner: Peer, opts: Rc) -> BoxedNewPeerFuture { info!("get_ws_client_peer_wrapped"); get_ws_client_peer_impl(uri, opts, |before_connect| { Ok(before_connect.async_connect_on(PeerForWs(inner))) }) } ================================================ FILE: src/ws_lowlevel_peer.rs ================================================ #![allow(unused)] extern crate websocket_base; use futures::future::Future; use futures::stream::Stream; use std::cell::RefCell; use std::rc::Rc; use super::{box_up_err, peer_err, peer_strerr, BoxedNewPeerFuture, Peer, Result}; use super::ws_peer::{Mode1, PeerForWs, WsReadWrapper, WsWriteWrapper}; use super::{once, ConstructParams, Options, PeerConstructor, Specifier}; use self::websocket_base::codec::ws::Context as WsLlContext; #[derive(Debug, Clone)] pub struct WsLlClient(pub T); impl Specifier for WsLlClient { fn construct(&self, p: ConstructParams) -> PeerConstructor { let inner = self.0.construct(p.clone()); let opts = p.program_options; inner.map(move |q, _| get_ws_lowlevel_peer( WsLlContext::Client, q, opts.clone(), )) } specifier_boilerplate!(noglobalstate singleconnect has_subspec); } specifier_class!( name = WsLlClientClass, target = WsLlClient, prefixes = ["ws-lowlevel-client:","ws-ll-client:","ws-ll-c:"], arg_handling = subspec, overlay = false, MessageOriented, SingleConnect, help = r#" [A] Low-level HTTP-independent WebSocket client connection without associated HTTP upgrade. Example: TODO "# ); #[derive(Debug, Clone)] pub struct WsLlServer(pub T); impl Specifier for WsLlServer { fn construct(&self, p: ConstructParams) -> PeerConstructor { let inner = self.0.construct(p.clone()); let opts = p.program_options; inner.map(move |q, _| get_ws_lowlevel_peer( WsLlContext::Server, q, opts.clone(), )) } specifier_boilerplate!(noglobalstate singleconnect has_subspec); } specifier_class!( name = WsLlServerClass, target = WsLlServer, prefixes = ["ws-lowlevel-server:","ws-ll-server:","ws-ll-s:"], arg_handling = subspec, overlay = false, MessageOriented, SingleConnect, help = r#" [A] Low-level HTTP-independent WebSocket server connection without associated HTTP upgrade. Example: TODO "# ); pub fn get_ws_lowlevel_peer(mode: WsLlContext, mut inner: Peer, opts: Rc) -> BoxedNewPeerFuture { info!("get_ws_lowlevel_peer"); use ::tokio_codec::Decoder; let c = websocket_base::codec::ws::MessageCodec::new_with_limits(mode, opts.max_ws_frame_length, opts.max_ws_message_length); let hup = inner.2; inner.2 = None; let duplex = c.framed(PeerForWs(inner)); let close_on_shutdown = !opts.websocket_dont_close; let p = super::ws_peer::finish_building_ws_peer(&opts, duplex, close_on_shutdown, hup); Box::new( ::futures::future::ok(p) ) as BoxedNewPeerFuture } ================================================ FILE: src/ws_peer.rs ================================================ extern crate tokio_codec; extern crate websocket; extern crate base64; use self::websocket::stream::r#async::Stream as WsStream; use self::websocket::OwnedMessage; use futures; use futures::sink::Sink; use futures::stream::Stream; use std; use std::io::Result as IoResult; use std::io::{Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; use std::cell::RefCell; use std::rc::Rc; use futures::Async::{NotReady, Ready}; use super::{brokenpipe, io_other_error, wouldblock, Peer, HupToken}; use super::readdebt::{ProcessMessageResult, ReadDebt}; type MultiProducerWsSink = Rc< RefCell< WsSinkWithOneBufferedMessage, >, >; type WsSource = futures::stream::SplitStream< tokio_codec::Framed>, >; pub struct WsSinkWithOneBufferedMessage { sink: futures::stream::SplitSink>>, pong_debt: Option, ping_debt: Option, } #[derive(Copy,Clone,PartialEq, Eq)] pub enum CompressionMethod { None, Deflate, Zlib, Gzip, } impl CompressionMethod { #[cfg(feature="compression")] fn uncompress(&self, x: Vec) -> Vec { if self == &CompressionMethod::None { return x; } let l = x.len(); let mut y = Vec::with_capacity(l*2); match self { CompressionMethod::None => unreachable!(), CompressionMethod::Gzip => { let mut t = flate2::read::GzDecoder::new(std::io::Cursor::new(x)); match t.read_to_end(&mut y) { Ok(_) => (), Err(e) => { error!("Error uncompressing data: {}", e); } } } CompressionMethod::Deflate => { let mut t = flate2::read::DeflateDecoder::new(std::io::Cursor::new(x)); match t.read_to_end(&mut y) { Ok(_) => (), Err(e) => { error!("Error uncompressing data: {}", e); } } } CompressionMethod::Zlib =>{ let mut t = flate2::read::ZlibDecoder::new(std::io::Cursor::new(x)); match t.read_to_end(&mut y) { Ok(_) => (), Err(e) => { error!("Error uncompressing data: {}", e); } } } } debug!("Uncompressed {} bytes into {} bytes", l, y.len()); y } #[cfg(feature="compression")] fn compress(&self, x: Vec) -> Vec { if self == &CompressionMethod::None { return x; } let l = x.len(); let mut y = Vec::with_capacity(l+64); let c = flate2::Compression::new(6); match self { CompressionMethod::None => unreachable!(), CompressionMethod::Gzip => { let mut t = flate2::read::GzEncoder::new(std::io::Cursor::new(x), c); match t.read_to_end(&mut y) { Ok(_) => (), Err(e) => { error!("Error compressing data: {}", e); } } } CompressionMethod::Deflate => { let mut t = flate2::read::DeflateEncoder::new(std::io::Cursor::new(x), c); match t.read_to_end(&mut y) { Ok(_) => (), Err(e) => { error!("Error compressing data: {}", e); } } } CompressionMethod::Zlib =>{ let mut t = flate2::read::ZlibEncoder::new(std::io::Cursor::new(x), c); match t.read_to_end(&mut y) { Ok(_) => (), Err(e) => { error!("Error compressing data: {}", e); } } } } debug!("Compressed {} bytes into {} bytes", l, y.len()); y } #[cfg(not(feature="compression"))] fn uncompress(&self, x: Vec) -> Vec { if self == &CompressionMethod::None { return x; } error!("Compression support is not selected during Websocat compilation"); vec![] } #[cfg(not(feature="compression"))] fn compress(&self, x: Vec) -> Vec { if self == &CompressionMethod::None { return x; } error!("Compression support is not selected during Websocat compilation"); vec![] } } pub struct WsReadWrapper { pub s: WsSource, pub pingreply: MultiProducerWsSink, pub debt: ReadDebt, pub pong_timeout: Option<(::tokio_timer::Delay, ::std::time::Duration)>, pub ping_aborter: Option<::futures::unsync::oneshot::Sender<()>>, pub text_prefix: Option, pub binary_prefix: Option, pub binary_base64: bool, pub text_base64: bool, pub creation_time: ::std::time::Instant, // for measuring ping RTTs pub print_rtts: bool, pub inhibit_pongs: Option, pub uncompress : CompressionMethod, } impl AsyncRead for WsReadWrapper {} impl Read for WsReadWrapper { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { if let Some(ret) = self.debt.check_debt(buf) { return ret; } macro_rules! abort_and_broken_pipe { () => {{ if let Some(abt) = self.ping_aborter.take() { let _ = abt.send(()); } brokenpipe() }}; } fn process_prefixes_and_base64<'a>(qbuf :&'a mut Vec, q: &mut &'a [u8], prefix: &Option, base64: bool) { match (prefix, base64) { (None, false) => (), (Some(pr), false) => { debug!("prepending prefix"); qbuf.reserve_exact(pr.len() + q.len()); qbuf.extend_from_slice(pr.as_bytes()); qbuf.extend_from_slice(q); *q = &mut qbuf[..]; } (None, true) => { debug!("encoding to base64"); qbuf.resize(q.len() * 3 / 2 + 3, 0); let r = base64::encode_config_slice(q, base64::STANDARD, &mut qbuf[..]); qbuf.resize(r, 0); qbuf.push(b'\n'); *q = &mut qbuf[..]; }, (Some(pr), true) => { debug!("prepending prefix and encoding to base64"); qbuf.extend_from_slice(pr.as_bytes()); qbuf.resize(pr.len() + q.len() * 3 / 2 + 3, 0); let r = base64::encode_config_slice(q, base64::STANDARD, &mut qbuf[pr.len()..]); qbuf.resize(pr.len()+r, 0); qbuf.push(b'\n'); *q = &mut qbuf[..]; }, } } loop { return match self.s.poll().map_err(io_other_error)? { Ready(Some(OwnedMessage::Close(x))) => { info!("Received WebSocket close message"); debug!("The close message is {:?}", x); abort_and_broken_pipe!() } Ready(None) => { info!("incoming None"); abort_and_broken_pipe!() } Ready(Some(OwnedMessage::Ping(_x))) if self.inhibit_pongs == Some(0) => { info!("Received and ignored WebSocket ping"); continue; } Ready(Some(OwnedMessage::Ping(x))) => { info!("Received WebSocket ping"); if let Some(ref mut ip) = self.inhibit_pongs { *ip = ip.wrapping_sub(1); } let om = OwnedMessage::Pong(x); let mut sink = self.pingreply.borrow_mut(); let mut proceed = false; // If case of when we cannot sing pong write away (send window full), we try to cache one of pong replies. // But this scheme is not foolproof - there can be cases where pongs would still get lost match sink.sink.start_send(om).map_err(io_other_error)? { futures::AsyncSink::NotReady(om) => { if sink.pong_debt.is_some() { warn!( "dropped a ping request from websocket due to channel contention" ); } debug!("WebSocket write contenction: buffering pong instead of sending immediately"); sink.pong_debt = Some(om); } futures::AsyncSink::Ready => { proceed = true; } } if proceed { let _ = sink.sink.poll_complete().map_err(io_other_error)?; } continue; } Ready(Some(OwnedMessage::Pong(buf))) => { if buf.len() == 12 { let (mut origts1, mut origts2) = ([0u8; 8], [0u8; 4]); origts1.copy_from_slice(&buf[0..8]); origts2.copy_from_slice(&buf[8..12]); let (origts1, origts2) = (u64::from_be_bytes(origts1), u32::from_be_bytes(origts2)); let origts = ::std::time::Duration::new(origts1, origts2); let newts = ::std::time::Instant::now() - self.creation_time; let delta = newts.checked_sub(origts).unwrap_or_default(); info!("Received a pong from websocket; RTT = {:?}", delta); if self.print_rtts { eprintln!("RTT {}.{:06} s", delta.as_secs(), delta.subsec_micros()); } } else { warn!("Received a pong with a strange content from websocket"); } if let Some((de, intvl)) = self.pong_timeout.as_mut() { de.reset(::std::time::Instant::now() + *intvl); } continue; } Ready(Some(OwnedMessage::Text(x))) => { debug!("incoming text"); let mut qbuf : Vec = vec![]; let mut q : &[u8] = x.as_bytes(); process_prefixes_and_base64(&mut qbuf, &mut q, &self.text_prefix, self.text_base64); match self.debt.process_message(buf, q) { ProcessMessageResult::Return(x) => x, ProcessMessageResult::Recurse => continue, } } Ready(Some(OwnedMessage::Binary(mut x))) => { x = self.uncompress.uncompress(x); debug!("incoming binary"); let mut qbuf : Vec = vec![]; let mut q : &[u8] = x.as_slice(); process_prefixes_and_base64(&mut qbuf, &mut q, &self.binary_prefix, self.binary_base64); match self.debt.process_message(buf, q) { ProcessMessageResult::Return(x) => x, ProcessMessageResult::Recurse => continue, } } NotReady => { use futures::Async; use futures::Future; if let Some((de, _intvl)) = self.pong_timeout.as_mut() { match de.poll() { Err(e) => error!("tokio-timer's Delay: {}", e), Ok(Async::NotReady) => (), Ok(Async::Ready(_inst)) => { warn!("Closing WebSocket connection due to ping timeout"); return abort_and_broken_pipe!(); } } } wouldblock() } }; } } } #[derive(Debug, Copy, Clone)] pub enum Mode1 { Text, Binary, } pub struct WsWriteWrapper { pub sink: MultiProducerWsSink, pub mode: Mode1, pub close_on_shutdown: bool, pub text_prefix: Option, pub binary_prefix: Option, pub binary_base64: bool, pub text_base64: bool, pub close_status_code: Option, pub close_reason: Option, pub compress : CompressionMethod, } impl AsyncWrite for WsWriteWrapper { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { if !self.close_on_shutdown { return Ok(Ready(())); } let close_data = self.close_status_code.map(|code| websocket::CloseData{ status_code: code, reason: self.close_reason.clone().unwrap_or_default() } ); let mut sink = self.sink.borrow_mut(); match sink.sink .start_send(OwnedMessage::Close(close_data)) .map_err(io_other_error)? { futures::AsyncSink::NotReady(_) => wouldblock(), futures::AsyncSink::Ready => { // Too lazy to implement a state machine here just for // properly handling this. // And shutdown result is ignored here anyway. let _ = sink.sink.poll_complete().map_err(|_| ()).map(|_| ()); Ok(Ready(())) } } } } impl Write for WsWriteWrapper { fn write(&mut self, buf_: &[u8]) -> IoResult { if self.sink.borrow().pong_debt.is_some() { let mut sink = self.sink.borrow_mut(); let debt = sink.pong_debt.take().unwrap(); match sink.sink.start_send(debt).map_err(io_other_error)? { futures::AsyncSink::NotReady(debt) => { sink.pong_debt = Some(debt); return wouldblock(); }, futures::AsyncSink::Ready => { debug!("Finished sending cached Pong reply message"); } } } if self.sink.borrow().ping_debt.is_some() { let mut sink = self.sink.borrow_mut(); let debt = sink.ping_debt.take().unwrap(); match sink.sink.start_send(debt).map_err(io_other_error)? { futures::AsyncSink::NotReady(debt) => { sink.ping_debt = Some(debt); return wouldblock(); }, futures::AsyncSink::Ready => { debug!("Finished sending cached Ping message"); } } } let bufv; let mut effective_mode = self.mode; let mut buf : &[u8] = buf_; let origlen = buf.len(); if let Some(pr) = &self.text_prefix { if buf.starts_with(pr.as_bytes()) { effective_mode = Mode1::Text; buf = &buf[pr.len()..]; } } if let Some(pr) = &self.binary_prefix { if buf.starts_with(pr.as_bytes()) { effective_mode = Mode1::Binary; buf = &buf[pr.len()..]; } } let decode_base64 = match effective_mode { Mode1::Binary => self.binary_base64, Mode1::Text => self.text_base64, }; if decode_base64 { if buf.last() == Some(&b'\n') { buf = &buf[..(buf.len()-1)]; } if buf.last() == Some(&b'\r') { buf = &buf[..(buf.len()-1)]; } if let Ok(v) = base64::decode(buf) { bufv = v; buf = &bufv[..]; } else { error!("Failed to decode user-supplised base64 buffer. Sending message as is."); } } let om = match effective_mode { Mode1::Binary => { let x = buf.to_vec(); let x = self.compress.compress(x); OwnedMessage::Binary(x) }, Mode1::Text => { let text_tmp; let text = match ::std::str::from_utf8(buf) { Ok(x) => x, Err(_) => { error!( "Invalid UTF-8 in a text WebSocket message. Sending lossy data. May be \ caused by unlucky buffer splits." ); text_tmp = String::from_utf8_lossy(buf); text_tmp.as_ref() } }; OwnedMessage::Text(text.to_string()) } }; match self.sink.borrow_mut().sink.start_send(om).map_err(io_other_error)? { futures::AsyncSink::NotReady(_) => wouldblock(), futures::AsyncSink::Ready => Ok(origlen), } } fn flush(&mut self) -> IoResult<()> { match self .sink .borrow_mut() .sink .poll_complete() .map_err(io_other_error)? { NotReady => wouldblock(), Ready(()) => Ok(()), } } } impl Drop for WsWriteWrapper { fn drop(&mut self) { debug!("drop WsWriteWrapper",); // moved to shutdown() } } pub struct PeerForWs(pub Peer); //implicit impl websocket::stream::async::Stream for PeerForWs {} impl AsyncRead for PeerForWs {} impl Read for PeerForWs { fn read(&mut self, buf: &mut [u8]) -> std::result::Result { (self.0).0.read(buf) } } impl AsyncWrite for PeerForWs { fn shutdown(&mut self) -> futures::Poll<(), std::io::Error> { (self.0).1.shutdown() } } impl Write for PeerForWs { fn write(&mut self, buf: &[u8]) -> IoResult { (self.0).1.write(buf) } fn flush(&mut self) -> IoResult<()> { (self.0).1.flush() } } enum WsPingerState { WaitingForTimer, StartSend, PollComplete, } /// Periodically sends WebSocket pings pub struct WsPinger { st: WsPingerState, si: MultiProducerWsSink, t: ::tokio_timer::Interval, origin: ::std::time::Instant, aborter: ::futures::unsync::oneshot::Receiver<()>, max_sent_pings: Option, } impl WsPinger { pub fn new( sink: MultiProducerWsSink, interval: ::std::time::Duration, origin: ::std::time::Instant, aborter: ::futures::unsync::oneshot::Receiver<()>, max_sent_pings: Option, ) -> Self { WsPinger { st: WsPingerState::WaitingForTimer, t: ::tokio_timer::Interval::new_interval(interval), si: sink, origin, aborter, max_sent_pings, } } } impl ::futures::Future for WsPinger { type Item = (); type Error = (); fn poll(&mut self) -> ::futures::Poll<(), ()> { use self::WsPingerState::*; use futures::Async; use futures::AsyncSink; loop { match self.aborter.poll() { Err(e) => warn!("unsync/oneshot: {}", e), Ok(Async::NotReady) => (), Ok(Async::Ready(())) => { debug!("Pinger aborted"); return Ok(Async::Ready(())); } } match self.st { WaitingForTimer => match self.t.poll() { Err(e) => warn!("wspinger: {}", e), Ok(Async::Ready(None)) => warn!("tokio-timer's interval stream ended?"), Ok(Async::NotReady) => return Ok(Async::NotReady), Ok(Async::Ready(Some(_instant))) => { if let Some(ref mut maxnum) = self.max_sent_pings { if *maxnum > 0 { *maxnum -= 1; } else { info!("Not sending WebSocket pings anymore"); self.st = WaitingForTimer; continue; } } self.st = StartSend; info!("Sending WebSocket ping"); continue; } }, StartSend => { let ts = ::std::time::Instant::now().duration_since(self.origin); let (ts1, ts2) = (ts.as_secs(), ts.subsec_nanos()); let mut ts = [0; 12]; ts[0..8].copy_from_slice(&ts1.to_be_bytes()); ts[8..12].copy_from_slice(&ts2.to_be_bytes()); let om = OwnedMessage::Ping(ts.to_vec()); let mut sink = self.si.borrow_mut(); match sink.sink.start_send(om) { Err(e) => info!("wsping: {}", e), Ok(AsyncSink::NotReady(om)) => { if sink.ping_debt.is_some() { warn!( "dropped a ping request to websocket due to channel contention" ); } else { debug!("WebSocket write contenction: buffering ping instead of sending immediately"); sink.ping_debt = Some(om); } self.st = WaitingForTimer; continue; } Ok(AsyncSink::Ready) => { self.st = PollComplete; continue; } } } PollComplete => match self.si.borrow_mut().sink.poll_complete() { Err(e) => info!("wsping: {}", e), Ok(Async::NotReady) => { self.st = WaitingForTimer; continue; //return Ok(Async::NotReady); } Ok(Async::Ready(())) => { self.st = WaitingForTimer; continue; } }, } return Ok(Async::Ready(())); } } } pub type Duplex = ::tokio_codec::Framed>; pub fn finish_building_ws_peer(opts: &super::Options, duplex: Duplex, close_on_shutdown: bool, hup: Option) -> Peer where S : tokio_io::AsyncRead + tokio_io::AsyncWrite + 'static + Send { let (sink, stream) = duplex.split(); let wrappedsink = WsSinkWithOneBufferedMessage { sink, pong_debt: None, ping_debt: None, }; let mpsink = Rc::new(RefCell::new(wrappedsink)); let mode1 = if opts.websocket_text_mode { Mode1::Text } else { Mode1::Binary }; let now = ::std::time::Instant::now(); let ping_aborter = if let Some(d) = opts.ws_ping_interval { debug!("Starting pinger"); let (tx, rx) = ::futures::unsync::oneshot::channel(); let intv = ::std::time::Duration::from_secs(d); let pinger = super::ws_peer::WsPinger::new(mpsink.clone(), intv,now, rx, opts.max_sent_pings); ::tokio_current_thread::spawn(pinger); Some(tx) } else { None }; let pong_timeout = if let Some(d) = opts.ws_ping_timeout { let to = ::std::time::Duration::from_secs(d); let de = ::tokio_timer::Delay::new(std::time::Instant::now() + to); Some((de, to)) } else { None }; let zmsgh = if opts.no_exit_on_zeromsg { super::readdebt::ZeroMessagesHandling::Drop } else { super::readdebt::ZeroMessagesHandling::Deliver }; let compress = match (opts.compress_deflate, opts.compress_gzip, opts.compress_zlib) { (false, false, false) => CompressionMethod::None, (true, false, false) => CompressionMethod::Deflate, (false, true, false) => CompressionMethod::Gzip, (false, false, true) => CompressionMethod::Zlib, _ => { error!("Multiple compression methods specified"); CompressionMethod::None } }; let uncompress = match (opts.uncompress_deflate, opts.uncompress_gzip, opts.uncompress_zlib) { (false, false, false) => CompressionMethod::None, (true, false, false) => CompressionMethod::Deflate, (false, true, false) => CompressionMethod::Gzip, (false, false, true) => CompressionMethod::Zlib, _ => { error!("Multiple uncompression methos specified"); CompressionMethod::None } }; let ws_str = WsReadWrapper { s: stream, pingreply: mpsink.clone(), debt: super::readdebt::ReadDebt(Default::default(), opts.read_debt_handling, zmsgh), pong_timeout, ping_aborter, text_prefix: opts.ws_text_prefix.clone(), binary_prefix: opts.ws_binary_prefix.clone(), binary_base64: opts.ws_binary_base64, text_base64: opts.ws_text_base64, creation_time: now, print_rtts: opts.print_ping_rtts, inhibit_pongs: opts.inhibit_pongs, uncompress, }; let ws_sin = WsWriteWrapper{ sink: mpsink, mode: mode1, close_on_shutdown, text_prefix: opts.ws_text_prefix.clone(), binary_prefix: opts.ws_binary_prefix.clone(), binary_base64: opts.ws_binary_base64, text_base64: opts.ws_text_base64, close_status_code: opts.close_status_code, close_reason: opts.close_reason.clone(), compress, }; Peer::new(ws_str, ws_sin, hup) } ================================================ FILE: src/ws_server_peer.rs ================================================ extern crate hyper; extern crate websocket; use self::hyper::uri::RequestUri::AbsolutePath; use self::websocket::WebSocketError; use futures::future::{err, Future}; use std::rc::Rc; use crate::options::StaticFile; use self::websocket::server::upgrade::r#async::IntoWs; use super::ws_peer::{PeerForWs}; use super::{box_up_err, io_other_error, BoxedNewPeerFuture, Peer}; use super::{ConstructParams, L2rUser, PeerConstructor, Specifier}; #[derive(Debug)] pub struct WsServer(pub T); impl Specifier for WsServer { fn construct(&self, cp: ConstructParams) -> PeerConstructor { let restrict_uri = Rc::new(cp.program_options.restrict_uri.clone()); let serve_static_files = Rc::new(cp.program_options.serve_static_files.clone()); let inner = self.0.construct(cp.clone()); //let l2r = cp.left_to_right; inner.map(move |p, l2r| { // FIXME: attack of `Vec::clone`s. ws_upgrade_peer( p, restrict_uri.clone(), serve_static_files.clone(), cp.program_options.websocket_reply_protocol.clone(), cp.program_options.custom_reply_headers.clone(), cp.program_options.clone(), l2r, ) }) } specifier_boilerplate!(noglobalstate has_subspec); self_0_is_subspecifier!(proxy_is_multiconnect); } specifier_class!( name = WsServerClass, target = WsServer, prefixes = ["ws-upgrade:", "upgrade-ws:", "ws-u:", "u-ws:"], arg_handling = subspec, overlay = true, MessageOriented, MulticonnectnessDependsOnInnerType, help = r#" WebSocket upgrader / raw server. Specify your own protocol instead of usual TCP. [A] All other WebSocket server modes actually use this overlay under the hood. Example: serve incoming connection from socat socat tcp-l:1234,fork,reuseaddr exec:'websocat -t ws-u\:stdio\: mirror\:' "# ); specifier_alias!( name = WsTcpServerClass, prefixes = ["ws-listen:", "ws-l:", "l-ws:", "listen-ws:"], alias = "ws-u:tcp-l:", help = r#" WebSocket server. Argument is host and port to listen. Example: Dump all incoming websocket data to console websocat ws-l:127.0.0.1:8808 - Example: the same, but more verbose: websocat ws-l:tcp-l:127.0.0.1:8808 reuse:- "# ); specifier_alias!( name = WsInetdServerClass, prefixes = ["inetd-ws:", "ws-inetd:"], alias = "ws-u:inetd:", help = r#" WebSocket inetd server. [A] TODO: transfer the example here "# ); specifier_alias!( name = WsUnixServerClass, prefixes = ["l-ws-unix:"], alias = "ws-u:unix-l:", help = r#" WebSocket UNIX socket-based server. [A] "# ); specifier_alias!( name = WsAbstractUnixServerClass, prefixes = ["l-ws-abstract:"], alias = "ws-l:abstract-l:", help = r#" WebSocket abstract-namespaced UNIX socket server. [A] "# ); #[path = "http_serve.rs"] pub mod http_serve; pub fn ws_upgrade_peer( inner_peer: Peer, restrict_uri: Rc>, serve_static_files: Rc>, websocket_protocol: Option, custom_reply_headers: Vec<(String, Vec)>, opts: Rc, l2r: L2rUser, ) -> BoxedNewPeerFuture { let step1 = PeerForWs(inner_peer); let step2: Box< dyn Future, Error = _>, > = step1.into_ws(); let step3 = step2 .or_else(|(innerpeer, hyper_incoming, _bytesmut, e)| { http_serve::http_serve(innerpeer.0, hyper_incoming, serve_static_files) .then(|_| err(WebSocketError::IoError(io_other_error(e))) ) }) .and_then( move |mut x| -> Box> { info!("Incoming connection to websocket: {}", x.request.subject.1); use ::websocket::header::WebSocketProtocol; let mut protocol_check = true; { let pp : Option<&WebSocketProtocol> = x.request.headers.get(); if let Some(rp) = websocket_protocol { // Unconditionally set this protocol x.headers.set_raw("Sec-WebSocket-Protocol", vec![rp.as_bytes().to_vec()], ); // Warn if not present in client protocols let mut present = false; if let Some(pp) = pp { if let Some(pp) = pp.iter().next() { if pp == &rp { present = true; } } } if !present { if pp.is_none() { warn!("Client failed to specify Sec-WebSocket-Protocol header. Replying with it anyway, against the RFC."); } else { protocol_check = false; } } } else { // No protocol specified, just choosing the first if any. if let Some(pp) = pp { if pp.len() > 1 { warn!("Multiple `Sec-WebSocket-Protocol`s specified in the request. Choosing the first one. Use --server-protocol to make it explicit.") } if let Some(pp) = pp.iter().next() { x.headers.set_raw( "Sec-WebSocket-Protocol", vec![pp.as_bytes().to_vec()], ); } } } } for (hn, hv) in custom_reply_headers { x.headers.append_raw(hn, hv); } debug!("{:?}", x.request); debug!("{:?}", x.headers); if !protocol_check { return Box::new( x.reject() .and_then(|_| { warn!("Requested Sec-WebSocket-Protocol does not match --server-protocol option"); ::futures::future::err(crate::util::simple_err( "Requested Sec-WebSocket-Protocol does not match --server-protocol option" .to_string(), )) }) .map_err(|e| websocket::WebSocketError::IoError(io_other_error(e))), ) as Box>; } match l2r { L2rUser::FillIn(ref y) => { let uri = &x.request.subject.1; let mut z = y.borrow_mut(); z.uri = Some(format!("{}", uri)); let h : &websocket::header::Headers = &x.request.headers; for q in opts.headers_to_env.iter() { if let Some(v) = h.get_raw(q) { if v.is_empty() { continue } if v.len() > 1 { warn!("Extra request header for {} ignored", q); } if let Ok(val) = String::from_utf8(v[0].clone()) { z.headers.push(( q.clone(), val, )); } else { warn!("Header {} value contains invalid UTF-8", q); } } else { warn!("No request header {}, so no envvar H_{}", q, q); } } }, L2rUser::ReadFrom(_) => {}, } if let Some(ref restrict_uri) = *restrict_uri { let check_passed = matches!(x.request.subject.1, AbsolutePath(ref x) if x == restrict_uri); if !check_passed { return Box::new( x.reject() .and_then(|_| { warn!("Incoming request URI doesn't match the --restrict-uri value"); ::futures::future::err(crate::util::simple_err( "Request URI doesn't match --restrict-uri parameter" .to_string(), )) }) .map_err(|e| websocket::WebSocketError::IoError(io_other_error(e))), ) as Box>; } }; Box::new(x.accept_with_limits(opts.max_ws_frame_length, opts.max_ws_message_length).map(move |(y, headers)| { debug!("{:?}", headers); info!("Upgraded"); let close_on_shutdown = !opts.websocket_dont_close; super::ws_peer::finish_building_ws_peer(&opts, y, close_on_shutdown, None) })) as Box> }, ); let step4 = step3.map_err(box_up_err); Box::new(step4) as BoxedNewPeerFuture } ================================================ FILE: test.sh ================================================ #!/bin/bash if [ "$TRAVIS_OS_NAME" = "osx" ]; then echo "Not supported on Mac"; exit 0 fi set -ex PATH=target/debug:$PATH websocat -q -t ws-l:127.0.0.1:19923 mirror:& MIR=$! trap 'kill $MIR' EXIT trap 'echo test failed' ERR function ensurenonblock() { perl -we 'use Fcntl qw(F_GETFL O_NONBLOCK); open F, "<&=", 0; my $flags = fcntl(F, F_GETFL, 0); if ($flags & O_NONBLOCK) { exit 1; } else { exit 0; }' } sleep 1 ensurenonblock C1=$(find /proc/$MIR/fd -type l -printf '\n' | wc -l) { echo 123 sleep 1 echo ABC } | websocat ws://127.0.0.1:19923 | { TS1=$(date +%s.%N) read A TS2=$(date +%s.%N) read B TS3=$(date +%s.%N) echo TS1=$TS1 A=$A TS2=$TS2 B=$B TS3=$TS3 > lol } perl -ne ' use POSIX; if(m!TS1=(\S+) A=123 TS2=(\S+) B=ABC TS3=(\S+)!) { if ($2-$1 > 0.1) { print STDERR "Err 1\n"; exit 1; } if ($3-$2 < 0.7 || $3-$2 > 1.4) { print STDERR "Err 2\n"; exit 1; } print STDERR "Timing OK\n"; POSIX::_exit 0; } END { exit 1; } ' lol rm -f lol ensurenonblock C2=$(find /proc/$MIR/fd -type l -printf '\n' | wc -l) test "$C1" -eq "$C2" websocat -b literal:qwe ws://127.0.0.1:19923 websocat -b literal:qwe -u ws://127.0.0.1:19923 websocat -b literal:qwe -1uU ws://127.0.0.1:19923 C2=$(find /proc/$MIR/fd -type l -printf '\n' | wc -l) test "$C1" -eq "$C2" echo Test OK ================================================ FILE: test_help.sh ================================================ #!/bin/bash # This test tracks how much of Websocat functionality is implemented by inspecting the help message true ${WEBSOCAT:=target/debug/websocat} T=$($WEBSOCAT --help=full) CTR=0 SUCC=0 tt() { PAT="$1" true $((CTR+=1)) if echo "$T" | cut -c 1-50 | tr ' \t' '\n\n' | grep -q -- "^$PAT\$"; then true $((SUCC+=1)) printf '%40s [ OK ]\n' "$PAT" else printf '%40s [FAIL]\n' "$PAT" fi } tt --async-stdio tt --dump-spec tt --set-environment tt --exit-on-eof tt --foreachmsg-wait-read tt --jsonrpc tt --just-generate-key tt --linemode-strip-newlines tt --null-terminated tt --no-line tt --no-exit-on-zeromsg tt --no-fixups tt --no-async-stdio tt --one-message tt --oneshot tt --exec-sighup-on-stdin-close tt --exec-sighup-on-zero-msg tt --reuser-send-zero-msg-on-disconnect tt --server-mode tt --strict tt --insecure tt --udp-broadcast tt --udp-multicast-loop tt --udp-oneshot tt --udp-reuseaddr tt --unidirectional tt --unidirectional-reverse tt --accept-from-fd tt --unlink tt --version tt -v tt --binary tt --no-close tt --websocket-ignore-zeromsg tt --text tt --base64 tt --base64-text tt --socks5 tt --autoreconnect-delay-millis tt --basic-auth tt --queue-len tt --buffer-size tt --header tt --server-header tt --exec-args tt --header-to-env tt --help tt --just-generate-accept tt --max-messages tt --max-messages-rev tt --conncap tt --origin tt --pkcs12-der tt --pkcs12-passwd tt --request-header tt --request-method tt --request-uri tt --restrict-uri tt --static-file tt --socks5-bind-script tt --socks5-destination tt --tls-domain tt --udp-multicast tt --udp-multicast-iface-v4 tt --udp-multicast-iface-v6 tt --udp-ttl tt --protocol tt --server-protocol tt --websocket-version tt --binary-prefix tt --ws-c-uri tt --ping-interval tt --ping-timeout tt --text-prefix tt ws-listen: tt inetd-ws: tt l-ws-unix: tt l-ws-abstract: tt ws-lowlevel-client: tt ws-lowlevel-server: tt wss-listen: tt http: tt asyncstdio: tt inetd: tt tcp: tt tcp-listen: tt ssl-listen: tt sh-c: tt cmd: tt exec: tt readfile: tt writefile: tt appendfile: tt udp: tt udp-listen: tt open-async: tt open-fd: tt threadedstdio: tt unix: tt unix-listen: tt unix-dgram: tt abstract: tt abstract-listen: tt abstract-dgram: tt mirror: tt literalreply: tt clogged: tt literal: tt assert: tt assert2: tt seqpacket: tt seqpacket-listen: tt ws-upgrade: tt http-request: tt http-post-sse: tt ssl-connect: tt ssl-accept: tt reuse-raw: tt broadcast: tt autoreconnect: tt ws-c: tt msg2line: tt line2msg: tt foreachmsg: tt log: tt jsonrpc: tt socks5-connect: tt socks5-bind: echo "$SUCC of $CTR" ================================================ FILE: tests/test.rs ================================================ extern crate websocat; extern crate env_logger; extern crate futures; extern crate tokio; extern crate tokio_timer; use futures::future::Future; use websocat::{spec, Options, WebsocatConfiguration3}; fn dflt() -> Options { Default::default() } macro_rules! wt { ($core:ident, $s1:expr, $s2:expr,delay = $ms:expr, $($rest:tt)*) => {{ let s1 = spec($s1).unwrap(); let s2 = spec($s2).unwrap(); let delay = tokio_timer::Delay::new(std::time::Instant::now() + std::time::Duration::new(0, $ms * 1_000_000)).map_err(|_|()); delay.and_then(|()| wt!(stage2, h2, s1, s2, $($rest)*) ) }}; ($core:ident, $s1:expr, $s2:expr,nodelay,$($rest:tt)*) => {{ let s1 = spec($s1).unwrap(); let s2 = spec($s2).unwrap(); wt!(stage2, h2, s1, s2, $($rest)*) }}; (stage2, $h2:ident, $s1:ident, $s2:ident, noopts,$($rest:tt)*) => { wt!(stage2, $h2, $s1, $s2, opts=Default::default(),$($rest)*) }; (stage2, $h2:ident, $s1:ident, $s2:ident, opts=$opts:expr,$($rest:tt)*) => {{ let websocat = WebsocatConfiguration3 { opts: $opts, $s1, $s2, }; websocat.serve( wt!(stage3, $($rest)*), ) }}; (stage3, errpanic,) => { std::rc::Rc::new(|e| { eprintln!("{}", e); panic!(); }) }; (stage3, errignore,) => { std::rc::Rc::new(|_| { }) }; } macro_rules! prepare { ($core:ident) => { let _ = env_logger::try_init(); let mut $core = tokio::runtime::current_thread::Runtime::new().unwrap(); }; } macro_rules! run { ($core:ident, $prog:expr) => { $core.block_on($prog).map_err(|()| panic!()).unwrap(); }; } #[test] fn trivial() { prepare!(core); let prog = wt!( core, "literal:qwerty", "assert:qwerty", nodelay, noopts, errpanic, ); run!(core, prog); } #[test] fn tcp() { prepare!(core); let prog1 = wt!( core, "literal:qwert2y", "tcp-l:127.0.0.1:45912", nodelay, noopts, errpanic, ); let prog2 = wt!( core, "tcp:127.0.0.1:45912", "assert:qwert2y", delay = 200, noopts, errpanic, ); let prog = prog1.join(prog2); run!(core, prog); } #[test] fn ws() { prepare!(core); let prog1 = wt!( core, "literal:qwert3y", "ws-l:127.0.0.1:45913", nodelay, noopts, errpanic, ); let prog2 = wt!( core, "ws://127.0.0.1:45913/ololo", "assert:qwert3y", delay = 200, noopts, errpanic, ); let prog = prog1.join(prog2); run!(core, prog); } #[test] fn ws_ll() { prepare!(core); let prog1 = wt!( core, "literal:qwert3y", "ws-ll-s:tcp-l:127.0.0.1:45915", nodelay, noopts, errpanic, ); let prog2 = wt!( core, "ws-ll-c:tcp:127.0.0.1:45915", "assert:qwert3y", delay = 200, noopts, errpanic, ); let prog = prog1.join(prog2); run!(core, prog); } #[test] fn ws_persist() { prepare!(core); let prog1 = wt!( core, "ws-l:127.0.0.1:45914", "literal:qwert4y", nodelay, noopts, errignore, ); let prog2 = wt!( core, "literal:invalid_connection_request", "tcp:127.0.0.1:45914", delay = 200, noopts, errpanic, ); let prog3 = wt!( core, "ws://127.0.0.1:45914/ololo", "assert:qwert4y", delay = 400, noopts, errpanic, ); core.spawn(prog1); core.spawn(prog2); let prog = prog3; run!(core, prog); } #[test] #[cfg(unix)] fn unix() { prepare!(core); let prog1 = wt!( core, "literal:qwert3y", "unix-l:zxc", nodelay, opts = Options { unlink_unix_socket: true, ..dflt() }, errpanic, ); let prog2 = wt!( core, "unix-c:zxc", "assert:qwert3y", delay = 200, noopts, errpanic, ); let prog = prog1.join(prog2); run!(core, prog); let _ = ::std::fs::remove_file("zxc"); } #[test] #[cfg(any(target_os = "linux", target_os = "android"))] fn abstract_() { prepare!(core); let prog1 = wt!( core, "literal:qwert4y", "abstract-l:zxc", nodelay, noopts, errpanic, ); let prog2 = wt!( core, "abstract-c:zxc", "assert:qwert4y", delay = 200, noopts, errpanic, ); let prog = prog1.join(prog2); run!(core, prog); }